Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a3f06bab1 | ||
|
|
db59d5b558 | ||
|
|
2fcbf3ab62 | ||
|
|
fa9b10be72 | ||
|
|
c495cef529 | ||
|
|
c0c8dff6ce | ||
|
|
80b00cc89f | ||
|
|
934db3d179 | ||
|
|
6a040a0405 | ||
|
|
2821ef0f69 | ||
|
|
343d931ddb | ||
|
|
3fc257f71b | ||
|
|
6dbb0a17e9 | ||
|
|
ae5ad6a4ac | ||
|
|
549af6c584 | ||
|
|
9a799eb4e6 | ||
|
|
f260b873b6 | ||
|
|
782a90b584 | ||
|
|
7df903dc4d | ||
|
|
8fc5e91ec7 | ||
|
|
9ca1a2c273 | ||
|
|
86265c1d7c | ||
|
|
a057c9a323 | ||
|
|
2e63bb6dcb | ||
|
|
1b5db80b32 | ||
|
|
3f20cea402 | ||
|
|
389fe1ff64 | ||
|
|
bad2d7ba85 | ||
|
|
416aa298ac | ||
|
|
a535b1adbf | ||
|
|
05fbd1a283 | ||
|
|
63552cbc8e | ||
|
|
c00bd489f1 | ||
|
|
16c2e3a995 | ||
|
|
650b025181 | ||
|
|
8fe46f7400 | ||
|
|
a9bcc15797 | ||
|
|
b6c856bd07 | ||
|
|
4beea0484a | ||
|
|
2679db1d10 | ||
|
|
3e73462e04 | ||
|
|
f63a87737a | ||
|
|
e7472eac1c | ||
|
|
db3c847771 | ||
|
|
4adbfc24a5 | ||
|
|
8f734a6562 | ||
|
|
bcf7519eb3 | ||
|
|
66089052ee | ||
|
|
d50cc8ff65 | ||
|
|
b75ea94f58 | ||
|
|
2c24e2fd28 | ||
|
|
f8dc208665 | ||
|
|
c72b8e8d1e | ||
|
|
b108c693fa | ||
|
|
aac1912ea7 | ||
|
|
3dcd26aac3 | ||
|
|
e53b9807f6 | ||
|
|
36fe8b76d4 | ||
|
|
f832f88c75 | ||
|
|
659a28de02 | ||
|
|
583149a472 | ||
|
|
206e6463be | ||
|
|
118a9feec8 | ||
|
|
1f2ccb059a | ||
|
|
f4d9d6c858 | ||
|
|
3477f5664a | ||
|
|
edefa5219c | ||
|
|
cf0d198365 | ||
|
|
08b14ed77e | ||
|
|
6ee3075867 | ||
|
|
cc8a945cbf | ||
|
|
1a1922b3fc | ||
|
|
48bd766298 | ||
|
|
1ece3873cd | ||
|
|
472d902486 | ||
|
|
4ac6a18d40 | ||
|
|
8a47ea91ba | ||
|
|
bd4394aa89 | ||
|
|
56f69ce71e | ||
|
|
248a6cd50b | ||
|
|
d9e659d817 | ||
|
|
77e5564f4b | ||
|
|
e79766d5ec | ||
|
|
c55fd76743 |
@@ -1,8 +1,8 @@
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.40
|
||||
rev: v0.0.90
|
||||
hooks:
|
||||
- id: lint
|
||||
- id: ruff
|
||||
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.10.1
|
||||
|
||||
20
Cargo.lock
generated
20
Cargo.lock
generated
@@ -466,6 +466,15 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codegen"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff61280aed771c3070e7dcc9e050c66f1eb1e3b96431ba66f9f74641d02fc41d"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.11.1"
|
||||
@@ -2045,7 +2054,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.76"
|
||||
version = "0.0.90"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
@@ -2054,6 +2063,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"clearscreen",
|
||||
"codegen",
|
||||
"colored",
|
||||
"common-path",
|
||||
"dirs 4.0.0",
|
||||
@@ -2101,7 +2111,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-ast"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/charliermarsh/RustPython.git?rev=778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f#778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f"
|
||||
source = "git+https://github.com/charliermarsh/RustPython.git?rev=1b253a12705f84972cd76e8dc1cdaaccb233e5a5#1b253a12705f84972cd76e8dc1cdaaccb233e5a5"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"rustpython-common",
|
||||
@@ -2111,7 +2121,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-common"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/charliermarsh/RustPython.git?rev=778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f#778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f"
|
||||
source = "git+https://github.com/charliermarsh/RustPython.git?rev=1b253a12705f84972cd76e8dc1cdaaccb233e5a5#1b253a12705f84972cd76e8dc1cdaaccb233e5a5"
|
||||
dependencies = [
|
||||
"ascii",
|
||||
"cfg-if 1.0.0",
|
||||
@@ -2134,7 +2144,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-compiler-core"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/charliermarsh/RustPython.git?rev=778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f#778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f"
|
||||
source = "git+https://github.com/charliermarsh/RustPython.git?rev=1b253a12705f84972cd76e8dc1cdaaccb233e5a5#1b253a12705f84972cd76e8dc1cdaaccb233e5a5"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bitflags",
|
||||
@@ -2151,7 +2161,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-parser"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/charliermarsh/RustPython.git?rev=778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f#778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f"
|
||||
source = "git+https://github.com/charliermarsh/RustPython.git?rev=1b253a12705f84972cd76e8dc1cdaaccb233e5a5#1b253a12705f84972cd76e8dc1cdaaccb233e5a5"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"anyhow",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.76"
|
||||
version = "0.0.90"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
@@ -26,9 +26,9 @@ once_cell = { version = "1.13.1" }
|
||||
path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix_paths_on_wasm"] }
|
||||
rayon = { version = "1.5.3" }
|
||||
regex = { version = "1.6.0" }
|
||||
rustpython-ast = { features = ["unparse"], git = "https://github.com/charliermarsh/RustPython.git", rev = "778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f" }
|
||||
rustpython-common = { git = "https://github.com/charliermarsh/RustPython.git", rev = "778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f" }
|
||||
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f" }
|
||||
rustpython-ast = { features = ["unparse"], git = "https://github.com/charliermarsh/RustPython.git", rev = "1b253a12705f84972cd76e8dc1cdaaccb233e5a5" }
|
||||
rustpython-common = { git = "https://github.com/charliermarsh/RustPython.git", rev = "1b253a12705f84972cd76e8dc1cdaaccb233e5a5" }
|
||||
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "1b253a12705f84972cd76e8dc1cdaaccb233e5a5" }
|
||||
serde = { version = "1.0.143", features = ["derive"] }
|
||||
serde_json = { version = "1.0.83" }
|
||||
strum = { version = "0.24.1", features = ["strum_macros"] }
|
||||
@@ -50,6 +50,7 @@ getrandom = { version = "0.2.7", features = ["js"] }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = { version = "2.0.4" }
|
||||
codegen = { version = "0.2.0" }
|
||||
insta = { version = "1.19.1", features = ["yaml"] }
|
||||
test-case = { version = "2.2.2" }
|
||||
|
||||
|
||||
494
README.md
494
README.md
@@ -22,7 +22,7 @@ An extremely fast Python linter, written in Rust.
|
||||
- 📦 [ESLint](https://eslint.org/docs/latest/user-guide/command-line-interface#caching)-inspired cache support
|
||||
- 🔧 [ESLint](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix)-inspired autofix support (e.g., automatically remove unused imports)
|
||||
- 👀 [TypeScript](https://www.typescriptlang.org/docs/handbook/configuring-watch.html)-inspired `--watch` support, for continuous file monitoring
|
||||
- ⚖️ [Near-parity](#Parity-with-Flake8) with the built-in Flake8 rule set
|
||||
- ⚖️ [Near-parity](#how-does-ruff-compare-to-flake8) with the built-in Flake8 rule set
|
||||
- 🔌 Native re-implementations of popular Flake8 plugins, like [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/) ([`pydocstyle`](https://pypi.org/project/pydocstyle/))
|
||||
|
||||
Ruff aims to be orders of magnitude faster than alternative tools while integrating more
|
||||
@@ -38,13 +38,25 @@ Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-mu
|
||||
1. [Installation and Usage](#installation-and-usage)
|
||||
2. [Configuration](#configuration)
|
||||
3. [Supported Rules](#supported-rules)
|
||||
4. [Editor Integrations](#editor-integrations)
|
||||
5. [FAQ](#faq)
|
||||
6. [Development](#development)
|
||||
7. [Releases](#releases)
|
||||
8. [Benchmarks](#benchmarks)
|
||||
9. [License](#license)
|
||||
10. [Contributing](#contributing)
|
||||
1. [Pyflakes](#pyflakes)
|
||||
2. [pycodestyle (error)](#pycodestyle-error)
|
||||
3. [pycodestyle (warning)](#pycodestyle-warning)
|
||||
4. [pydocstyle](#pydocstyle)
|
||||
5. [pyupgrade](#pyupgrade)
|
||||
6. [pep8-naming](#pep8-naming)
|
||||
7. [flake8-comprehensions](#flake8-comprehensions)
|
||||
8. [flake8-bugbear](#flake8-bugbear)
|
||||
9. [flake8-builtins](#flake8-builtins)
|
||||
10. [flake8-print](#flake8-print)
|
||||
11. [flake8-quotes](#flake8-quotes)
|
||||
12. [Meta rules](#meta-rules)
|
||||
5. [Editor Integrations](#editor-integrations)
|
||||
6. [FAQ](#faq)
|
||||
7. [Development](#development)
|
||||
8. [Releases](#releases)
|
||||
9. [Benchmarks](#benchmarks)
|
||||
10. [License](#license)
|
||||
11. [Contributing](#contributing)
|
||||
|
||||
## Installation and Usage
|
||||
|
||||
@@ -77,11 +89,14 @@ Ruff also works with [pre-commit](https://pre-commit.com):
|
||||
```yaml
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.76
|
||||
rev: v0.0.90
|
||||
hooks:
|
||||
- id: lint
|
||||
- id: ruff
|
||||
```
|
||||
|
||||
<!-- TODO(charlie): Remove this message a few versions after v0.0.86. -->
|
||||
_Note: prior to `v0.0.86`, `ruff-pre-commit` used `lint` (rather than `ruff`) as the hook ID._
|
||||
|
||||
## Configuration
|
||||
|
||||
Ruff is configurable both via `pyproject.toml` and the command line.
|
||||
@@ -91,13 +106,25 @@ For example, you could configure Ruff to only enforce a subset of rules with:
|
||||
```toml
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
select = [
|
||||
"F401",
|
||||
"F403",
|
||||
select = ["E", "F"]
|
||||
ignore = ["E501"]
|
||||
per-file-ignores = [
|
||||
"__init__.py:F401",
|
||||
"path/to/file.py:F401"
|
||||
]
|
||||
```
|
||||
|
||||
Alternatively, on the command-line:
|
||||
Plugin configurations should be expressed as subsections, e.g.:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
|
||||
[tool.ruff.flake8-quotes]
|
||||
docstring-quotes = "double"
|
||||
```
|
||||
|
||||
Alternatively, common configuration settings can be provided via the command-line:
|
||||
|
||||
```shell
|
||||
ruff path/to/code/ --select F401 --select F403
|
||||
@@ -219,173 +246,213 @@ add `noqa` directives to all failing lines, with the appropriate error codes.**
|
||||
|
||||
## Supported Rules
|
||||
|
||||
By default, Ruff enables all `E`, `W`, and `F` error codes, which correspond to those built-in to
|
||||
Flake8.
|
||||
By default, Ruff enables all `E` and `F` error codes, which correspond to those built-in to Flake8.
|
||||
|
||||
The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` command-line option.
|
||||
|
||||
### Pyflakes
|
||||
|
||||
| Code | Name | Message |
|
||||
| ---- | ---- | ------- |
|
||||
| F401 | UnusedImport | `...` imported but unused |
|
||||
| F402 | ImportShadowedByLoopVar | Import `...` from line 1 shadowed by loop variable |
|
||||
| F403 | ImportStarUsed | `from ... import *` used; unable to detect undefined names |
|
||||
| F404 | LateFutureImport | `from __future__` imports must occur at the beginning of the file |
|
||||
| F405 | ImportStarUsage | `...` may be undefined, or defined from star imports: `...` |
|
||||
| F406 | ImportStarNotPermitted | `from ... import *` only allowed at module level |
|
||||
| F407 | FutureFeatureNotDefined | Future feature `...` is not defined |
|
||||
| F541 | FStringMissingPlaceholders | f-string without any placeholders |
|
||||
| F601 | MultiValueRepeatedKeyLiteral | Dictionary key literal repeated |
|
||||
| F602 | MultiValueRepeatedKeyVariable | Dictionary key `...` repeated |
|
||||
| F621 | ExpressionsInStarAssignment | Too many expressions in star-unpacking assignment |
|
||||
| F622 | TwoStarredExpressions | Two starred expressions in assignment |
|
||||
| F631 | AssertTuple | Assert test is a non-empty tuple, which is always `True` |
|
||||
| F632 | IsLiteral | Use `==` and `!=` to compare constant literals |
|
||||
| F633 | InvalidPrintSyntax | Use of `>>` is invalid with `print` function |
|
||||
| F634 | IfTuple | If test is a tuple, which is always `True` |
|
||||
| F701 | BreakOutsideLoop | `break` outside loop |
|
||||
| F702 | ContinueOutsideLoop | `continue` not properly in loop |
|
||||
| F704 | YieldOutsideFunction | `yield` or `yield from` statement outside of a function/method |
|
||||
| F706 | ReturnOutsideFunction | `return` statement outside of a function/method |
|
||||
| F707 | DefaultExceptNotLast | An `except:` block as not the last exception handler |
|
||||
| F722 | ForwardAnnotationSyntaxError | Syntax error in forward annotation: `...` |
|
||||
| F821 | UndefinedName | Undefined name `...` |
|
||||
| F822 | UndefinedExport | Undefined name `...` in `__all__` |
|
||||
| F823 | UndefinedLocal | Local variable `...` referenced before assignment |
|
||||
| F831 | DuplicateArgumentName | Duplicate argument name in function definition |
|
||||
| F841 | UnusedVariable | Local variable `...` is assigned to but never used |
|
||||
| F901 | RaiseNotImplemented | `raise NotImplemented` should be `raise NotImplementedError` |
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| F401 | UnusedImport | `...` imported but unused | 🛠 |
|
||||
| F402 | ImportShadowedByLoopVar | Import `...` from line 1 shadowed by loop variable | |
|
||||
| F403 | ImportStarUsed | `from ... import *` used; unable to detect undefined names | |
|
||||
| F404 | LateFutureImport | `from __future__` imports must occur at the beginning of the file | |
|
||||
| F405 | ImportStarUsage | `...` may be undefined, or defined from star imports: `...` | |
|
||||
| F406 | ImportStarNotPermitted | `from ... import *` only allowed at module level | |
|
||||
| F407 | FutureFeatureNotDefined | Future feature `...` is not defined | |
|
||||
| F541 | FStringMissingPlaceholders | f-string without any placeholders | |
|
||||
| F601 | MultiValueRepeatedKeyLiteral | Dictionary key literal repeated | |
|
||||
| F602 | MultiValueRepeatedKeyVariable | Dictionary key `...` repeated | |
|
||||
| F621 | ExpressionsInStarAssignment | Too many expressions in star-unpacking assignment | |
|
||||
| F622 | TwoStarredExpressions | Two starred expressions in assignment | |
|
||||
| F631 | AssertTuple | Assert test is a non-empty tuple, which is always `True` | |
|
||||
| F632 | IsLiteral | Use `==` and `!=` to compare constant literals | |
|
||||
| F633 | InvalidPrintSyntax | Use of `>>` is invalid with `print` function | |
|
||||
| F634 | IfTuple | If test is a tuple, which is always `True` | |
|
||||
| F701 | BreakOutsideLoop | `break` outside loop | |
|
||||
| F702 | ContinueOutsideLoop | `continue` not properly in loop | |
|
||||
| F704 | YieldOutsideFunction | `yield` or `yield from` statement outside of a function/method | |
|
||||
| F706 | ReturnOutsideFunction | `return` statement outside of a function/method | |
|
||||
| F707 | DefaultExceptNotLast | An `except:` block as not the last exception handler | |
|
||||
| F722 | ForwardAnnotationSyntaxError | Syntax error in forward annotation: `...` | |
|
||||
| F821 | UndefinedName | Undefined name `...` | |
|
||||
| F822 | UndefinedExport | Undefined name `...` in `__all__` | |
|
||||
| F823 | UndefinedLocal | Local variable `...` referenced before assignment | |
|
||||
| F831 | DuplicateArgumentName | Duplicate argument name in function definition | |
|
||||
| F841 | UnusedVariable | Local variable `...` is assigned to but never used | |
|
||||
| F901 | RaiseNotImplemented | `raise NotImplemented` should be `raise NotImplementedError` | |
|
||||
|
||||
### pycodestyle
|
||||
### pycodestyle (error)
|
||||
|
||||
| Code | Name | Message |
|
||||
| ---- | ---- | ------- |
|
||||
| E402 | ModuleImportNotAtTopOfFile | Module level import not at top of file |
|
||||
| E501 | LineTooLong | Line too long (89 > 88 characters) |
|
||||
| E711 | NoneComparison | Comparison to `None` should be `cond is None` |
|
||||
| E712 | TrueFalseComparison | Comparison to `True` should be `cond is True` |
|
||||
| E713 | NotInTest | Test for membership should be `not in` |
|
||||
| E714 | NotIsTest | Test for object identity should be `is not` |
|
||||
| E721 | TypeComparison | Do not compare types, use `isinstance()` |
|
||||
| E722 | DoNotUseBareExcept | Do not use bare `except` |
|
||||
| E731 | DoNotAssignLambda | Do not assign a lambda expression, use a def |
|
||||
| E741 | AmbiguousVariableName | Ambiguous variable name: `...` |
|
||||
| E742 | AmbiguousClassName | Ambiguous class name: `...` |
|
||||
| E743 | AmbiguousFunctionName | Ambiguous function name: `...` |
|
||||
| E902 | IOError | IOError: `...` |
|
||||
| E999 | SyntaxError | SyntaxError: `...` |
|
||||
| W292 | NoNewLineAtEndOfFile | No newline at end of file |
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| E402 | ModuleImportNotAtTopOfFile | Module level import not at top of file | |
|
||||
| E501 | LineTooLong | Line too long (89 > 88 characters) | |
|
||||
| E711 | NoneComparison | Comparison to `None` should be `cond is None` | |
|
||||
| E712 | TrueFalseComparison | Comparison to `True` should be `cond is True` | |
|
||||
| E713 | NotInTest | Test for membership should be `not in` | |
|
||||
| E714 | NotIsTest | Test for object identity should be `is not` | |
|
||||
| E721 | TypeComparison | Do not compare types, use `isinstance()` | |
|
||||
| E722 | DoNotUseBareExcept | Do not use bare `except` | |
|
||||
| E731 | DoNotAssignLambda | Do not assign a lambda expression, use a def | |
|
||||
| E741 | AmbiguousVariableName | Ambiguous variable name: `...` | |
|
||||
| E742 | AmbiguousClassName | Ambiguous class name: `...` | |
|
||||
| E743 | AmbiguousFunctionName | Ambiguous function name: `...` | |
|
||||
| E902 | IOError | IOError: `...` | |
|
||||
| E999 | SyntaxError | SyntaxError: `...` | |
|
||||
|
||||
### pycodestyle (warning)
|
||||
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| W292 | NoNewLineAtEndOfFile | No newline at end of file | |
|
||||
| W605 | InvalidEscapeSequence | Invalid escape sequence: '\c' | |
|
||||
|
||||
### pydocstyle
|
||||
|
||||
| Code | Name | Message |
|
||||
| ---- | ---- | ------- |
|
||||
| D100 | PublicModule | Missing docstring in public module |
|
||||
| D101 | PublicClass | Missing docstring in public class |
|
||||
| D102 | PublicMethod | Missing docstring in public method |
|
||||
| D103 | PublicFunction | Missing docstring in public function |
|
||||
| D104 | PublicPackage | Missing docstring in public package |
|
||||
| D105 | MagicMethod | Missing docstring in magic method |
|
||||
| D106 | PublicNestedClass | Missing docstring in public nested class |
|
||||
| D107 | PublicInit | Missing docstring in `__init__` |
|
||||
| D200 | FitsOnOneLine | One-line docstring should fit on one line |
|
||||
| D201 | NoBlankLineBeforeFunction | No blank lines allowed before function docstring (found 1) |
|
||||
| D202 | NoBlankLineAfterFunction | No blank lines allowed after function docstring (found 1) |
|
||||
| D203 | OneBlankLineBeforeClass | 1 blank line required before class docstring |
|
||||
| D204 | OneBlankLineAfterClass | 1 blank line required after class docstring |
|
||||
| D205 | NoBlankLineAfterSummary | 1 blank line required between summary line and description |
|
||||
| D206 | IndentWithSpaces | Docstring should be indented with spaces, not tabs |
|
||||
| D207 | NoUnderIndentation | Docstring is under-indented |
|
||||
| D208 | NoOverIndentation | Docstring is over-indented |
|
||||
| D209 | NewLineAfterLastParagraph | Multi-line docstring closing quotes should be on a separate line |
|
||||
| D210 | NoSurroundingWhitespace | No whitespaces allowed surrounding docstring text |
|
||||
| D211 | NoBlankLineBeforeClass | No blank lines allowed before class docstring |
|
||||
| D212 | MultiLineSummaryFirstLine | Multi-line docstring summary should start at the first line |
|
||||
| D213 | MultiLineSummarySecondLine | Multi-line docstring summary should start at the second line |
|
||||
| D214 | SectionNotOverIndented | Section is over-indented ("Returns") |
|
||||
| D215 | SectionUnderlineNotOverIndented | Section underline is over-indented ("Returns") |
|
||||
| D300 | UsesTripleQuotes | Use """triple double quotes""" |
|
||||
| D400 | EndsInPeriod | First line should end with a period |
|
||||
| D402 | NoSignature | First line should not be the function's 'signature' |
|
||||
| D403 | FirstLineCapitalized | First word of the first line should be properly capitalized |
|
||||
| D404 | NoThisPrefix | First word of the docstring should not be `This` |
|
||||
| D405 | CapitalizeSectionName | Section name should be properly capitalized ("returns") |
|
||||
| D406 | NewLineAfterSectionName | Section name should end with a newline ("Returns") |
|
||||
| D407 | DashedUnderlineAfterSection | Missing dashed underline after section ("Returns") |
|
||||
| D408 | SectionUnderlineAfterName | Section underline should be in the line following the section's name ("Returns") |
|
||||
| D409 | SectionUnderlineMatchesSectionLength | Section underline should match the length of its name ("Returns") |
|
||||
| D410 | BlankLineAfterSection | Missing blank line after section ("Returns") |
|
||||
| D411 | BlankLineBeforeSection | Missing blank line before section ("Returns") |
|
||||
| D412 | NoBlankLinesBetweenHeaderAndContent | No blank lines allowed between a section header and its content ("Returns") |
|
||||
| D413 | BlankLineAfterLastSection | Missing blank line after last section ("Returns") |
|
||||
| D414 | NonEmptySection | Section has no content ("Returns") |
|
||||
| D415 | EndsInPunctuation | First line should end with a period, question mark, or exclamation point |
|
||||
| D416 | SectionNameEndsInColon | Section name should end with a colon ("Returns") |
|
||||
| D417 | DocumentAllArguments | Missing argument descriptions in the docstring: `x`, `y` |
|
||||
| D418 | SkipDocstring | Function decorated with @overload shouldn't contain a docstring |
|
||||
| D419 | NonEmpty | Docstring is empty |
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| D100 | PublicModule | Missing docstring in public module | |
|
||||
| D101 | PublicClass | Missing docstring in public class | |
|
||||
| D102 | PublicMethod | Missing docstring in public method | |
|
||||
| D103 | PublicFunction | Missing docstring in public function | |
|
||||
| D104 | PublicPackage | Missing docstring in public package | |
|
||||
| D105 | MagicMethod | Missing docstring in magic method | |
|
||||
| D106 | PublicNestedClass | Missing docstring in public nested class | |
|
||||
| D107 | PublicInit | Missing docstring in `__init__` | |
|
||||
| D200 | FitsOnOneLine | One-line docstring should fit on one line | |
|
||||
| D201 | NoBlankLineBeforeFunction | No blank lines allowed before function docstring (found 1) | 🛠 |
|
||||
| D202 | NoBlankLineAfterFunction | No blank lines allowed after function docstring (found 1) | 🛠 |
|
||||
| D203 | OneBlankLineBeforeClass | 1 blank line required before class docstring | 🛠 |
|
||||
| D204 | OneBlankLineAfterClass | 1 blank line required after class docstring | 🛠 |
|
||||
| D205 | BlankLineAfterSummary | 1 blank line required between summary line and description | 🛠 |
|
||||
| D206 | IndentWithSpaces | Docstring should be indented with spaces, not tabs | |
|
||||
| D207 | NoUnderIndentation | Docstring is under-indented | 🛠 |
|
||||
| D208 | NoOverIndentation | Docstring is over-indented | 🛠 |
|
||||
| D209 | NewLineAfterLastParagraph | Multi-line docstring closing quotes should be on a separate line | 🛠 |
|
||||
| D210 | NoSurroundingWhitespace | No whitespaces allowed surrounding docstring text | 🛠 |
|
||||
| D211 | NoBlankLineBeforeClass | No blank lines allowed before class docstring | 🛠 |
|
||||
| D212 | MultiLineSummaryFirstLine | Multi-line docstring summary should start at the first line | |
|
||||
| D213 | MultiLineSummarySecondLine | Multi-line docstring summary should start at the second line | |
|
||||
| D214 | SectionNotOverIndented | Section is over-indented ("Returns") | 🛠 |
|
||||
| D215 | SectionUnderlineNotOverIndented | Section underline is over-indented ("Returns") | 🛠 |
|
||||
| D300 | UsesTripleQuotes | Use """triple double quotes""" | |
|
||||
| D400 | EndsInPeriod | First line should end with a period | |
|
||||
| D402 | NoSignature | First line should not be the function's signature | |
|
||||
| D403 | FirstLineCapitalized | First word of the first line should be properly capitalized | |
|
||||
| D404 | NoThisPrefix | First word of the docstring should not be `This` | |
|
||||
| D405 | CapitalizeSectionName | Section name should be properly capitalized ("returns") | 🛠 |
|
||||
| D406 | NewLineAfterSectionName | Section name should end with a newline ("Returns") | 🛠 |
|
||||
| D407 | DashedUnderlineAfterSection | Missing dashed underline after section ("Returns") | 🛠 |
|
||||
| D408 | SectionUnderlineAfterName | Section underline should be in the line following the section's name ("Returns") | 🛠 |
|
||||
| D409 | SectionUnderlineMatchesSectionLength | Section underline should match the length of its name ("Returns") | 🛠 |
|
||||
| D410 | BlankLineAfterSection | Missing blank line after section ("Returns") | 🛠 |
|
||||
| D411 | BlankLineBeforeSection | Missing blank line before section ("Returns") | 🛠 |
|
||||
| D412 | NoBlankLinesBetweenHeaderAndContent | No blank lines allowed between a section header and its content ("Returns") | 🛠 |
|
||||
| D413 | BlankLineAfterLastSection | Missing blank line after last section ("Returns") | 🛠 |
|
||||
| D414 | NonEmptySection | Section has no content ("Returns") | |
|
||||
| D415 | EndsInPunctuation | First line should end with a period, question mark, or exclamation point | |
|
||||
| D416 | SectionNameEndsInColon | Section name should end with a colon ("Returns") | 🛠 |
|
||||
| D417 | DocumentAllArguments | Missing argument descriptions in the docstring: `x`, `y` | |
|
||||
| D418 | SkipDocstring | Function decorated with @overload shouldn't contain a docstring | |
|
||||
| D419 | NonEmpty | Docstring is empty | |
|
||||
|
||||
### pyupgrade
|
||||
|
||||
| Code | Name | Message |
|
||||
| ---- | ---- | ------- |
|
||||
| U001 | UselessMetaclassType | `__metaclass__ = type` is implied |
|
||||
| U002 | UnnecessaryAbspath | `abspath(__file__)` is unnecessary in Python 3.9 and later |
|
||||
| U003 | TypeOfPrimitive | Use `str` instead of `type(...)` |
|
||||
| U004 | UselessObjectInheritance | Class `...` inherits from object |
|
||||
| U005 | DeprecatedUnittestAlias | `assertEquals` is deprecated, use `assertEqual` instead |
|
||||
| U006 | UsePEP585Annotation | Use `list` instead of `List` for type annotations |
|
||||
| U007 | UsePEP604Annotation | Use `X \| Y` for type annotations |
|
||||
| U008 | SuperCallWithParameters | Use `super()` instead of `super(__class__, self)` |
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| U001 | UselessMetaclassType | `__metaclass__ = type` is implied | 🛠 |
|
||||
| U002 | UnnecessaryAbspath | `abspath(__file__)` is unnecessary in Python 3.9 and later | 🛠 |
|
||||
| U003 | TypeOfPrimitive | Use `str` instead of `type(...)` | 🛠 |
|
||||
| U004 | UselessObjectInheritance | Class `...` inherits from object | 🛠 |
|
||||
| U005 | DeprecatedUnittestAlias | `assertEquals` is deprecated, use `assertEqual` instead | 🛠 |
|
||||
| U006 | UsePEP585Annotation | Use `list` instead of `List` for type annotations | 🛠 |
|
||||
| U007 | UsePEP604Annotation | Use `X \| Y` for type annotations | 🛠 |
|
||||
| U008 | SuperCallWithParameters | Use `super()` instead of `super(__class__, self)` | 🛠 |
|
||||
|
||||
### pep8-naming
|
||||
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| N801 | InvalidClassName | Class name `...` should use CapWords convention | |
|
||||
| N802 | InvalidFunctionName | Function name `...` should be lowercase | |
|
||||
| N803 | InvalidArgumentName | Argument name `...` should be lowercase | |
|
||||
| N804 | InvalidFirstArgumentNameForClassMethod | First argument of a class method should be named `cls` | |
|
||||
| N805 | InvalidFirstArgumentNameForMethod | First argument of a method should be named `self` | |
|
||||
| N806 | NonLowercaseVariableInFunction | Variable `...` in function should be lowercase | |
|
||||
| N807 | DunderFunctionName | Function name should not start and end with `__` | |
|
||||
| N811 | ConstantImportedAsNonConstant | Constant `...` imported as non-constant `...` | |
|
||||
| N812 | LowercaseImportedAsNonLowercase | Lowercase `...` imported as non-lowercase `...` | |
|
||||
| N813 | CamelcaseImportedAsLowercase | Camelcase `...` imported as lowercase `...` | |
|
||||
| N814 | CamelcaseImportedAsConstant | Camelcase `...` imported as constant `...` | |
|
||||
| N815 | MixedCaseVariableInClassScope | Variable `mixedCase` in class scope should not be mixedCase | |
|
||||
| N816 | MixedCaseVariableInGlobalScope | Variable `mixedCase` in global scope should not be mixedCase | |
|
||||
| N817 | CamelcaseImportedAsAcronym | Camelcase `...` imported as acronym `...` | |
|
||||
| N818 | ErrorSuffixOnExceptionName | Exception name `...` should be named with an Error suffix | |
|
||||
|
||||
### flake8-comprehensions
|
||||
|
||||
| Code | Name | Message |
|
||||
| ---- | ---- | ------- |
|
||||
| C400 | UnnecessaryGeneratorList | Unnecessary generator - rewrite as a list comprehension |
|
||||
| C401 | UnnecessaryGeneratorSet | Unnecessary generator - rewrite as a set comprehension |
|
||||
| C402 | UnnecessaryGeneratorDict | Unnecessary generator - rewrite as a dict comprehension |
|
||||
| C403 | UnnecessaryListComprehensionSet | Unnecessary list comprehension - rewrite as a set comprehension |
|
||||
| C404 | UnnecessaryListComprehensionDict | Unnecessary list comprehension - rewrite as a dict comprehension |
|
||||
| C405 | UnnecessaryLiteralSet | Unnecessary <list/tuple> literal - rewrite as a set literal |
|
||||
| C406 | UnnecessaryLiteralDict | Unnecessary <list/tuple> literal - rewrite as a dict literal |
|
||||
| C408 | UnnecessaryCollectionCall | Unnecessary <dict/list/tuple> call - rewrite as a literal |
|
||||
| C409 | UnnecessaryLiteralWithinTupleCall | Unnecessary <list/tuple> literal passed to tuple() - remove the outer call to tuple() |
|
||||
| C410 | UnnecessaryLiteralWithinListCall | Unnecessary <list/tuple> literal passed to list() - rewrite as a list literal |
|
||||
| C411 | UnnecessaryListCall | Unnecessary list call - remove the outer call to list() |
|
||||
| C413 | UnnecessaryCallAroundSorted | Unnecessary <list/reversed> call around sorted() |
|
||||
| C414 | UnnecessaryDoubleCastOrProcess | Unnecessary <list/reversed/set/sorted/tuple> call within <list/set/sorted/tuple>(). |
|
||||
| C415 | UnnecessarySubscriptReversal | Unnecessary subscript reversal of iterable within <reversed/set/sorted>() |
|
||||
| C416 | UnnecessaryComprehension | Unnecessary <list/set> comprehension - rewrite using <list/set>() |
|
||||
| C417 | UnnecessaryMap | Unnecessary map usage - rewrite using a <list/set/dict> comprehension |
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| C400 | UnnecessaryGeneratorList | Unnecessary generator (rewrite as a `list` comprehension) | |
|
||||
| C401 | UnnecessaryGeneratorSet | Unnecessary generator (rewrite as a `set` comprehension) | |
|
||||
| C402 | UnnecessaryGeneratorDict | Unnecessary generator (rewrite as a `dict` comprehension) | |
|
||||
| C403 | UnnecessaryListComprehensionSet | Unnecessary `list` comprehension (rewrite as a `set` comprehension) | |
|
||||
| C404 | UnnecessaryListComprehensionDict | Unnecessary `list` comprehension (rewrite as a `dict` comprehension) | |
|
||||
| C405 | UnnecessaryLiteralSet | Unnecessary `(list\|tuple)` literal (rewrite as a `set` literal) | |
|
||||
| C406 | UnnecessaryLiteralDict | Unnecessary `(list\|tuple)` literal (rewrite as a `dict` literal) | |
|
||||
| C408 | UnnecessaryCollectionCall | Unnecessary `(dict\|list\|tuple)` call (rewrite as a literal) | |
|
||||
| C409 | UnnecessaryLiteralWithinTupleCall | Unnecessary `(list\|tuple)` literal passed to `tuple()` (remove the outer call to `tuple()`) | |
|
||||
| C410 | UnnecessaryLiteralWithinListCall | Unnecessary `(list\|tuple)` literal passed to `list()` (rewrite as a `list` literal) | |
|
||||
| C411 | UnnecessaryListCall | Unnecessary `list` call (remove the outer call to `list()`) | |
|
||||
| C413 | UnnecessaryCallAroundSorted | Unnecessary `(list\|reversed)` call around `sorted()` | |
|
||||
| C414 | UnnecessaryDoubleCastOrProcess | Unnecessary `(list\|reversed\|set\|sorted\|tuple)` call within `(list\|set\|sorted\|tuple)()` | |
|
||||
| C415 | UnnecessarySubscriptReversal | Unnecessary subscript reversal of iterable within `(reversed\|set\|sorted)()` | |
|
||||
| C416 | UnnecessaryComprehension | Unnecessary `(list\|set)` comprehension (rewrite using `(list\|set)()`) | |
|
||||
| C417 | UnnecessaryMap | Unnecessary `map` usage (rewrite using a `(list\|set\|dict)` comprehension) | |
|
||||
|
||||
### flake8-bugbear
|
||||
|
||||
| Code | Name | Message |
|
||||
| ---- | ---- | ------- |
|
||||
| B011 | DoNotAssertFalse | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` |
|
||||
| B014 | DuplicateHandlerException | Exception handler with duplicate exception: `ValueError` |
|
||||
| B025 | DuplicateTryBlockException | try-except block with duplicate exception `Exception` |
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| B002 | UnaryPrefixIncrement | Python does not support the unary prefix increment. | |
|
||||
| B007 | UnusedLoopControlVariable | Loop control variable `i` not used within the loop body. | 🛠 |
|
||||
| B011 | DoNotAssertFalse | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | 🛠 |
|
||||
| B013 | RedundantTupleInExceptionHandler | A length-one tuple literal is redundant. Write `except ValueError:` instead of `except (ValueError,):`. | |
|
||||
| B014 | DuplicateHandlerException | Exception handler with duplicate exception: `ValueError` | 🛠 |
|
||||
| B017 | NoAssertRaisesException | `assertRaises(Exception):` should be considered evil. | |
|
||||
| B025 | DuplicateTryBlockException | try-except block with duplicate exception `Exception` | |
|
||||
|
||||
### flake8-builtins
|
||||
|
||||
| Code | Name | Message |
|
||||
| ---- | ---- | ------- |
|
||||
| A001 | BuiltinVariableShadowing | Variable `...` is shadowing a python builtin |
|
||||
| A002 | BuiltinArgumentShadowing | Argument `...` is shadowing a python builtin |
|
||||
| A003 | BuiltinAttributeShadowing | Class attribute `...` is shadowing a python builtin |
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| A001 | BuiltinVariableShadowing | Variable `...` is shadowing a python builtin | |
|
||||
| A002 | BuiltinArgumentShadowing | Argument `...` is shadowing a python builtin | |
|
||||
| A003 | BuiltinAttributeShadowing | Class attribute `...` is shadowing a python builtin | |
|
||||
|
||||
### flake8-print
|
||||
|
||||
| Code | Name | Message |
|
||||
| ---- | ---- | ------- |
|
||||
| T201 | PrintFound | `print` found |
|
||||
| T203 | PPrintFound | `pprint` found |
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| T201 | PrintFound | `print` found | 🛠 |
|
||||
| T203 | PPrintFound | `pprint` found | 🛠 |
|
||||
|
||||
### flake8-quotes
|
||||
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| Q000 | BadQuotesInlineString | Single quotes found but double quotes preferred | |
|
||||
| Q001 | BadQuotesMultilineString | Single quote multiline found but double quotes preferred | |
|
||||
| Q002 | BadQuotesDocstring | Single quote docstring found but double quotes preferred | |
|
||||
| Q003 | AvoidQuoteEscape | Change outer quotes to avoid escaping inner quotes | |
|
||||
|
||||
### Meta rules
|
||||
|
||||
| Code | Name | Message |
|
||||
| ---- | ---- | ------- |
|
||||
| M001 | UnusedNOQA | Unused `noqa` directive |
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| M001 | UnusedNOQA | Unused `noqa` directive | 🛠 |
|
||||
|
||||
## Editor Integrations
|
||||
|
||||
@@ -447,13 +514,15 @@ Ruff re-implements some of the most popular Flake8 plugins and related code qual
|
||||
including:
|
||||
|
||||
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
|
||||
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
|
||||
- [`yesqa`](https://github.com/asottile/yesqa)
|
||||
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/)
|
||||
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
|
||||
- [`flake8-super`](https://pypi.org/project/flake8-super/)
|
||||
- [`flake8-print`](https://pypi.org/project/flake8-print/)
|
||||
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
|
||||
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (3/32)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (10/32)
|
||||
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (8/34)
|
||||
- [`autoflake`](https://pypi.org/project/autoflake/) (1/7)
|
||||
|
||||
@@ -468,12 +537,14 @@ Beyond rule-set parity, Ruff suffers from the following limitations vis-à-vis F
|
||||
|
||||
Today, Ruff can be used to replace Flake8 when used with any of the following plugins:
|
||||
|
||||
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
|
||||
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/)
|
||||
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
|
||||
- [`flake8-super`](https://pypi.org/project/flake8-super/)
|
||||
- [`flake8-print`](https://pypi.org/project/flake8-print/)
|
||||
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
|
||||
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (3/32)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (9/32)
|
||||
|
||||
Ruff also implements the functionality that you get from [`yesqa`](https://github.com/asottile/yesqa),
|
||||
and a subset of the rules implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (8/34).
|
||||
@@ -494,7 +565,7 @@ on Rust at all.
|
||||
### Can I write my own plugins for Ruff?
|
||||
|
||||
Ruff does not yet support third-party plugins, though a plugin system is within-scope for the
|
||||
project. See [#283](https://github.com/charliermarsh/ruff/issues/2830) for more.
|
||||
project. See [#283](https://github.com/charliermarsh/ruff/issues/283) for more.
|
||||
|
||||
### Does Ruff support NumPy- or Google-style docstrings?
|
||||
|
||||
@@ -502,35 +573,55 @@ Yes! To enable a specific docstring convention, start by enabling all `pydocstyl
|
||||
then selectively disabling based on your [preferred convention](https://www.pydocstyle.org/en/latest/error_codes.html#default-conventions).
|
||||
|
||||
For example, if you're coming from `flake8-docstrings`, the following configuration is equivalent to
|
||||
`--docstring-convention numpy`:
|
||||
`--docstring-convention=numpy`:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
extend-select = [
|
||||
"D100",
|
||||
"D101",
|
||||
"D102",
|
||||
"D103",
|
||||
"D104",
|
||||
"D105",
|
||||
"D106",
|
||||
"D200",
|
||||
"D201",
|
||||
"D202",
|
||||
extend-select = ["D"]
|
||||
extend-ignore = [
|
||||
"D107",
|
||||
"D203",
|
||||
"D212",
|
||||
"D213",
|
||||
"D402",
|
||||
"D413",
|
||||
"D415",
|
||||
"D416",
|
||||
"D417",
|
||||
]
|
||||
```
|
||||
|
||||
Similarly, the following is equivalent to `--docstring-convention=google`:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
extend-select = ["D"]
|
||||
extend-ignore = [
|
||||
"D203",
|
||||
"D204",
|
||||
"D205",
|
||||
"D206",
|
||||
"D207",
|
||||
"D208",
|
||||
"D209",
|
||||
"D210",
|
||||
"D211",
|
||||
"D213",
|
||||
"D215",
|
||||
"D400",
|
||||
"D404",
|
||||
"D406",
|
||||
"D407",
|
||||
"D408",
|
||||
"D409",
|
||||
"D413",
|
||||
]
|
||||
```
|
||||
|
||||
Similarly, the following is equivalent to `--docstring-convention=pep8`:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
extend-select = ["D"]
|
||||
extend-ignore = [
|
||||
"D203",
|
||||
"D212",
|
||||
"D213",
|
||||
"D214",
|
||||
"D215",
|
||||
"D300",
|
||||
"D400",
|
||||
"D402",
|
||||
"D403",
|
||||
"D404",
|
||||
"D405",
|
||||
"D406",
|
||||
@@ -539,51 +630,10 @@ extend-select = [
|
||||
"D409",
|
||||
"D410",
|
||||
"D411",
|
||||
"D412",
|
||||
"D413",
|
||||
"D418",
|
||||
"D419",
|
||||
]
|
||||
```
|
||||
|
||||
Similarly, the following is equivalent to `--docstring-convention google`:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
extend-select = [
|
||||
"D100",
|
||||
"D101",
|
||||
"D102",
|
||||
"D103",
|
||||
"D104",
|
||||
"D105",
|
||||
"D106",
|
||||
"D107",
|
||||
"D200",
|
||||
"D201",
|
||||
"D202",
|
||||
"D205",
|
||||
"D206",
|
||||
"D207",
|
||||
"D208",
|
||||
"D209",
|
||||
"D210",
|
||||
"D211",
|
||||
"D212",
|
||||
"D214",
|
||||
"D300",
|
||||
"D402",
|
||||
"D403",
|
||||
"D405",
|
||||
"D410",
|
||||
"D411",
|
||||
"D412",
|
||||
"D414",
|
||||
"D415",
|
||||
"D416",
|
||||
"D417",
|
||||
"D418",
|
||||
"D419",
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
105
examples/generate_check_code_prefix.rs
Normal file
105
examples/generate_check_code_prefix.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
//! Generate the CheckCodePrefix enum.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use codegen::{Scope, Type, Variant};
|
||||
use itertools::Itertools;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use ruff::checks::CheckCode;
|
||||
|
||||
fn main() {
|
||||
// Build up a map from prefix to matching CheckCodes.
|
||||
let mut prefix_to_codes: BTreeMap<String, BTreeSet<CheckCode>> = Default::default();
|
||||
for check_code in CheckCode::iter() {
|
||||
let as_ref = check_code.as_ref().to_string();
|
||||
for i in 1..=as_ref.len() {
|
||||
let prefix = as_ref[..i].to_string();
|
||||
let entry = prefix_to_codes
|
||||
.entry(prefix)
|
||||
.or_insert_with(|| Default::default());
|
||||
entry.insert(check_code.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut scope = Scope::new();
|
||||
|
||||
// Create the `CheckCodePrefix` definition.
|
||||
let mut gen = scope
|
||||
.new_enum("CheckCodePrefix")
|
||||
.vis("pub")
|
||||
.derive("EnumString")
|
||||
.derive("Debug")
|
||||
.derive("PartialEq")
|
||||
.derive("Eq")
|
||||
.derive("Clone")
|
||||
.derive("Serialize")
|
||||
.derive("Deserialize");
|
||||
for (prefix, _) in &prefix_to_codes {
|
||||
gen = gen.push_variant(Variant::new(prefix.to_string()));
|
||||
}
|
||||
|
||||
// Create the `PrefixSpecificity` definition.
|
||||
scope
|
||||
.new_enum("PrefixSpecificity")
|
||||
.vis("pub")
|
||||
.derive("PartialEq")
|
||||
.derive("Eq")
|
||||
.derive("PartialOrd")
|
||||
.derive("Ord")
|
||||
.push_variant(Variant::new("Category"))
|
||||
.push_variant(Variant::new("Hundreds"))
|
||||
.push_variant(Variant::new("Tens"))
|
||||
.push_variant(Variant::new("Explicit"));
|
||||
|
||||
// Create the `match` statement, to map from definition to relevant codes.
|
||||
let mut gen = scope
|
||||
.new_impl("CheckCodePrefix")
|
||||
.new_fn("codes")
|
||||
.arg_ref_self()
|
||||
.ret(Type::new("Vec<CheckCode>"))
|
||||
.vis("pub")
|
||||
.line("match self {");
|
||||
for (prefix, codes) in &prefix_to_codes {
|
||||
gen = gen.line(format!(
|
||||
"CheckCodePrefix::{prefix} => vec![{}],",
|
||||
codes
|
||||
.iter()
|
||||
.map(|code| format!("CheckCode::{}", code.as_ref()))
|
||||
.join(", ")
|
||||
));
|
||||
}
|
||||
gen.line("}");
|
||||
|
||||
// Create the `match` statement, to map from definition to specificity.
|
||||
let mut gen = scope
|
||||
.new_impl("CheckCodePrefix")
|
||||
.new_fn("specificity")
|
||||
.arg_ref_self()
|
||||
.ret(Type::new("PrefixSpecificity"))
|
||||
.vis("pub")
|
||||
.line("match self {");
|
||||
for (prefix, _) in &prefix_to_codes {
|
||||
let specificity = match prefix.len() {
|
||||
4 => "Explicit",
|
||||
3 => "Tens",
|
||||
2 => "Hundreds",
|
||||
1 => "Category",
|
||||
_ => panic!("Invalid prefix: {}", prefix),
|
||||
};
|
||||
gen = gen.line(format!(
|
||||
"CheckCodePrefix::{prefix} => PrefixSpecificity::{},",
|
||||
specificity
|
||||
));
|
||||
}
|
||||
gen.line("}");
|
||||
|
||||
println!("//! File automatically generated by examples/generate_check_code_prefix.rs.");
|
||||
println!();
|
||||
println!("use serde::{{Deserialize, Serialize}};");
|
||||
println!("use strum_macros::EnumString;");
|
||||
println!();
|
||||
println!("use crate::checks::CheckCode;");
|
||||
println!();
|
||||
println!("{}", scope.to_string());
|
||||
}
|
||||
@@ -9,16 +9,18 @@ fn main() {
|
||||
println!("### {}", check_category.title());
|
||||
println!();
|
||||
|
||||
println!("| Code | Name | Message |");
|
||||
println!("| ---- | ---- | ------- |");
|
||||
println!("| Code | Name | Message | Fix |");
|
||||
println!("| ---- | ---- | ------- | --- |");
|
||||
for check_code in CheckCode::iter() {
|
||||
if check_code.category() == check_category {
|
||||
let check_kind = check_code.kind();
|
||||
let fix_token = if check_kind.fixable() { "🛠" } else { "" };
|
||||
println!(
|
||||
"| {} | {} | {} |",
|
||||
"| {} | {} | {} | {} |",
|
||||
check_kind.code().as_ref(),
|
||||
check_kind.as_ref(),
|
||||
check_kind.body().replace("|", r"\|")
|
||||
check_kind.summary().replace("|", r"\|"),
|
||||
fix_token
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
20
resources/test/fixtures/B002.py
vendored
Normal file
20
resources/test/fixtures/B002.py
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
Should emit:
|
||||
B002 - on lines 15 and 20
|
||||
"""
|
||||
|
||||
|
||||
def this_is_all_fine(n):
|
||||
x = n + 1
|
||||
y = 1 + n
|
||||
z = +x + y
|
||||
return +z
|
||||
|
||||
|
||||
def this_is_buggy(n):
|
||||
x = ++n
|
||||
return x
|
||||
|
||||
|
||||
def this_is_buggy_too(n):
|
||||
return ++n
|
||||
31
resources/test/fixtures/B007.py
vendored
Normal file
31
resources/test/fixtures/B007.py
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
for i in range(10):
|
||||
print(i)
|
||||
|
||||
print(i) # name no longer defined on Python 3; no warning yet
|
||||
|
||||
for i in range(10): # name not used within the loop; B007
|
||||
print(10)
|
||||
|
||||
print(i) # name no longer defined on Python 3; no warning yet
|
||||
|
||||
|
||||
for _ in range(10): # _ is okay for a throw-away variable
|
||||
print(10)
|
||||
|
||||
|
||||
for i in range(10):
|
||||
for j in range(10):
|
||||
for k in range(10): # k not used, i and j used transitively
|
||||
print(i + j)
|
||||
|
||||
|
||||
def strange_generator():
|
||||
for i in range(10):
|
||||
for j in range(10):
|
||||
for k in range(10):
|
||||
for l in range(10):
|
||||
yield i, (j, (k, l))
|
||||
|
||||
|
||||
for i, (j, (k, l)) in strange_generator(): # i, k not used
|
||||
print(j, l)
|
||||
8
resources/test/fixtures/B013.py
vendored
Normal file
8
resources/test/fixtures/B013.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
try:
|
||||
pass
|
||||
except (ValueError,):
|
||||
pass
|
||||
except AttributeError:
|
||||
pass
|
||||
except (ImportError, TypeError):
|
||||
pass
|
||||
36
resources/test/fixtures/B017.py
vendored
Normal file
36
resources/test/fixtures/B017.py
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
Should emit:
|
||||
B017 - on lines 20
|
||||
"""
|
||||
import asyncio
|
||||
import unittest
|
||||
|
||||
CONSTANT = True
|
||||
|
||||
|
||||
def something_else() -> None:
|
||||
for i in (1, 2, 3):
|
||||
print(i)
|
||||
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
|
||||
class Foobar(unittest.TestCase):
|
||||
def evil_raises(self) -> None:
|
||||
with self.assertRaises(Exception):
|
||||
raise Exception("Evil I say!")
|
||||
|
||||
def context_manager_raises(self) -> None:
|
||||
with self.assertRaises(Exception) as ex:
|
||||
raise Exception("Context manager is good")
|
||||
self.assertEqual("Context manager is good", str(ex.exception))
|
||||
|
||||
def regex_raises(self) -> None:
|
||||
with self.assertRaisesRegex(Exception, "Regex is good"):
|
||||
raise Exception("Regex is good")
|
||||
|
||||
def raises_with_absolute_reference(self):
|
||||
with self.assertRaises(asyncio.CancelledError):
|
||||
Foo()
|
||||
2
resources/test/fixtures/F401_0.py
vendored
2
resources/test/fixtures/F401_0.py
vendored
@@ -61,7 +61,7 @@ Y = TypeVar("Y", bound="Dict")
|
||||
Z = TypeVar("Z", "List", "Set")
|
||||
|
||||
a = list["Fruit"]
|
||||
b = Union["Nut", None]
|
||||
b = Union["""Nut""", None]
|
||||
c = cast("Vegetable", b)
|
||||
|
||||
Field = lambda default=MISSING: field(default=default)
|
||||
|
||||
5
resources/test/fixtures/F401_5.py
vendored
Normal file
5
resources/test/fixtures/F401_5.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Test: removal of multi-segment and aliases imports."""
|
||||
from a.b import c
|
||||
from d.e import f as g
|
||||
import h.i
|
||||
import j.k as l
|
||||
34
resources/test/fixtures/N801.py
vendored
Normal file
34
resources/test/fixtures/N801.py
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
class bad:
|
||||
pass
|
||||
|
||||
|
||||
class _bad:
|
||||
pass
|
||||
|
||||
|
||||
class bad_class:
|
||||
pass
|
||||
|
||||
|
||||
class Bad_Class:
|
||||
pass
|
||||
|
||||
|
||||
class BAD_CLASS:
|
||||
pass
|
||||
|
||||
|
||||
class Good:
|
||||
pass
|
||||
|
||||
|
||||
class _Good:
|
||||
pass
|
||||
|
||||
|
||||
class GoodClass:
|
||||
pass
|
||||
|
||||
|
||||
class GOOD:
|
||||
pass
|
||||
41
resources/test/fixtures/N802.py
vendored
Normal file
41
resources/test/fixtures/N802.py
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
import unittest
|
||||
|
||||
|
||||
def Bad():
|
||||
pass
|
||||
|
||||
|
||||
def _Bad():
|
||||
pass
|
||||
|
||||
|
||||
def BAD():
|
||||
pass
|
||||
|
||||
|
||||
def BAD_FUNC():
|
||||
pass
|
||||
|
||||
|
||||
def good():
|
||||
pass
|
||||
|
||||
|
||||
def _good():
|
||||
pass
|
||||
|
||||
|
||||
def good_func():
|
||||
pass
|
||||
|
||||
|
||||
def tearDownModule():
|
||||
pass
|
||||
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
return super().tearDown()
|
||||
|
||||
def testTest(self):
|
||||
assert True
|
||||
7
resources/test/fixtures/N803.py
vendored
Normal file
7
resources/test/fixtures/N803.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
def func(a, A):
|
||||
return a, A
|
||||
|
||||
|
||||
class Class:
|
||||
def method(self, a, A):
|
||||
return a, A
|
||||
19
resources/test/fixtures/N804.py
vendored
Normal file
19
resources/test/fixtures/N804.py
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
class Class:
|
||||
@classmethod
|
||||
def bad_class_method(this):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def good_class_method(cls):
|
||||
pass
|
||||
|
||||
def method(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def static_method(x):
|
||||
return x
|
||||
|
||||
|
||||
def func(x):
|
||||
return x
|
||||
26
resources/test/fixtures/N805.py
vendored
Normal file
26
resources/test/fixtures/N805.py
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
import random
|
||||
|
||||
|
||||
class Class:
|
||||
def bad_method(this):
|
||||
pass
|
||||
|
||||
if random.random(0, 2) == 0:
|
||||
|
||||
def extra_bad_method(this):
|
||||
pass
|
||||
|
||||
def good_method(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def class_method(cls):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def static_method(x):
|
||||
return x
|
||||
|
||||
|
||||
def func(x):
|
||||
return x
|
||||
4
resources/test/fixtures/N806.py
vendored
Normal file
4
resources/test/fixtures/N806.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
def f():
|
||||
lower = 0
|
||||
Camel = 0
|
||||
CONSTANT = 0
|
||||
15
resources/test/fixtures/N807.py
vendored
Normal file
15
resources/test/fixtures/N807.py
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
def __bad__():
|
||||
pass
|
||||
|
||||
|
||||
def __good():
|
||||
pass
|
||||
|
||||
|
||||
def good__():
|
||||
pass
|
||||
|
||||
|
||||
class Class:
|
||||
def __good__(self):
|
||||
pass
|
||||
3
resources/test/fixtures/N811.py
vendored
Normal file
3
resources/test/fixtures/N811.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import mod.CONST as const
|
||||
from mod import CONSTANT as constant
|
||||
from mod import ANOTHER_CONSTANT as another_constant
|
||||
3
resources/test/fixtures/N812.py
vendored
Normal file
3
resources/test/fixtures/N812.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import modl.lowercase as Lower
|
||||
from mod import lowercase as Lowercase
|
||||
from mod import another_lowercase as AnotherLowercase
|
||||
3
resources/test/fixtures/N813.py
vendored
Normal file
3
resources/test/fixtures/N813.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import mod.Camel as camel
|
||||
from mod import CamelCase as camelcase
|
||||
from mod import AnotherCamelCase as another_camelcase
|
||||
3
resources/test/fixtures/N814.py
vendored
Normal file
3
resources/test/fixtures/N814.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import mod.Camel as CAMEL
|
||||
from mod import CamelCase as CAMELCASE
|
||||
from mod import AnotherCamelCase as ANOTHER_CAMELCASE
|
||||
6
resources/test/fixtures/N815.py
vendored
Normal file
6
resources/test/fixtures/N815.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
class C:
|
||||
lower = 0
|
||||
CONSTANT = 0
|
||||
mixedCase = 0
|
||||
_mixedCase = 0
|
||||
mixed_Case = 0
|
||||
5
resources/test/fixtures/N816.py
vendored
Normal file
5
resources/test/fixtures/N816.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
lower = 0
|
||||
CONSTANT = 0
|
||||
mixedCase = 0
|
||||
_mixedCase = 0
|
||||
mixed_Case = 0
|
||||
2
resources/test/fixtures/N817.py
vendored
Normal file
2
resources/test/fixtures/N817.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import mod.CaMel as CM
|
||||
from mod import CamelCase as CC
|
||||
10
resources/test/fixtures/N818.py
vendored
Normal file
10
resources/test/fixtures/N818.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AnotherError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class C(Exception):
|
||||
pass
|
||||
35
resources/test/fixtures/W605.py
vendored
Normal file
35
resources/test/fixtures/W605.py
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
#: W605:1:10
|
||||
regex = '\.png$'
|
||||
|
||||
#: W605:2:1
|
||||
regex = '''
|
||||
\.png$
|
||||
'''
|
||||
|
||||
#: W605:2:6
|
||||
f(
|
||||
'\_'
|
||||
)
|
||||
|
||||
#: W605:4:6
|
||||
"""
|
||||
multi-line
|
||||
literal
|
||||
with \_ somewhere
|
||||
in the middle
|
||||
"""
|
||||
|
||||
#: Okay
|
||||
regex = r'\.png$'
|
||||
regex = '\\.png$'
|
||||
regex = r'''
|
||||
\.png$
|
||||
'''
|
||||
regex = r'''
|
||||
\\.png$
|
||||
'''
|
||||
s = '\\'
|
||||
regex = '\w' # noqa
|
||||
regex = '''
|
||||
\w
|
||||
''' # noqa
|
||||
2
resources/test/fixtures/__init__.py
vendored
2
resources/test/fixtures/__init__.py
vendored
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
|
||||
print(__path__)
|
||||
|
||||
__all__ = ["a", "b", "c"]
|
||||
|
||||
38
resources/test/fixtures/flake8_quotes/docstring_doubles.py
vendored
Normal file
38
resources/test/fixtures/flake8_quotes/docstring_doubles.py
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Double quotes multiline module docstring
|
||||
"""
|
||||
|
||||
"""
|
||||
this is not a docstring
|
||||
"""
|
||||
|
||||
l = []
|
||||
|
||||
class Cls:
|
||||
"""
|
||||
Double quotes multiline class docstring
|
||||
"""
|
||||
|
||||
"""
|
||||
this is not a docstring
|
||||
"""
|
||||
|
||||
# The colon in the list indexing below is an edge case for the docstring scanner
|
||||
def f(self, bar="""
|
||||
definitely not a docstring""",
|
||||
val=l[Cls():3]):
|
||||
"""
|
||||
Double quotes multiline function docstring
|
||||
"""
|
||||
|
||||
some_expression = 'hello world'
|
||||
|
||||
"""
|
||||
this is not a docstring
|
||||
"""
|
||||
|
||||
if l:
|
||||
"""
|
||||
Looks like a docstring, but in reality it isn't - only modules, classes and functions
|
||||
"""
|
||||
pass
|
||||
9
resources/test/fixtures/flake8_quotes/docstring_doubles_class.py
vendored
Normal file
9
resources/test/fixtures/flake8_quotes/docstring_doubles_class.py
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
class SingleLineDocstrings():
|
||||
""" Double quotes single line class docstring """
|
||||
""" Not a docstring """
|
||||
|
||||
def foo(self, bar="""not a docstring"""):
|
||||
""" Double quotes single line method docstring"""
|
||||
pass
|
||||
|
||||
class Nested(foo()[:]): """ inline docstring """; pass
|
||||
22
resources/test/fixtures/flake8_quotes/docstring_doubles_function.py
vendored
Normal file
22
resources/test/fixtures/flake8_quotes/docstring_doubles_function.py
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
def foo():
|
||||
"""function without params, single line docstring"""
|
||||
""" not a docstring"""
|
||||
return
|
||||
|
||||
|
||||
def foo2():
|
||||
"""
|
||||
function without params, multiline docstring
|
||||
"""
|
||||
""" not a docstring"""
|
||||
return
|
||||
|
||||
|
||||
def fun_with_params_no_docstring(a, b="""
|
||||
not a
|
||||
""" """docstring"""):
|
||||
pass
|
||||
|
||||
def fun_with_params_no_docstring2(a, b=c[foo():], c=\
|
||||
""" not a docstring """):
|
||||
pass
|
||||
11
resources/test/fixtures/flake8_quotes/docstring_doubles_module_multiline.py
vendored
Normal file
11
resources/test/fixtures/flake8_quotes/docstring_doubles_module_multiline.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
Double quotes multiline module docstring
|
||||
"""
|
||||
"""
|
||||
this is not a docstring
|
||||
"""
|
||||
def foo():
|
||||
pass
|
||||
"""
|
||||
this is not a docstring
|
||||
"""
|
||||
6
resources/test/fixtures/flake8_quotes/docstring_doubles_module_singleline.py
vendored
Normal file
6
resources/test/fixtures/flake8_quotes/docstring_doubles_module_singleline.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
""" Double quotes singleline module docstring """
|
||||
""" this is not a docstring """
|
||||
|
||||
def foo():
|
||||
pass
|
||||
""" this is not a docstring """
|
||||
40
resources/test/fixtures/flake8_quotes/docstring_singles.py
vendored
Normal file
40
resources/test/fixtures/flake8_quotes/docstring_singles.py
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
'''
|
||||
Single quotes multiline module docstring
|
||||
'''
|
||||
|
||||
'''
|
||||
this is not a docstring
|
||||
'''
|
||||
|
||||
l = []
|
||||
|
||||
class Cls(MakeKlass('''
|
||||
class params \t not a docstring
|
||||
''')):
|
||||
'''
|
||||
Single quotes multiline class docstring
|
||||
'''
|
||||
|
||||
'''
|
||||
this is not a docstring
|
||||
'''
|
||||
|
||||
# The colon in the list indexing below is an edge case for the docstring scanner
|
||||
def f(self, bar='''
|
||||
definitely not a docstring''',
|
||||
val=l[Cls():3]):
|
||||
'''
|
||||
Single quotes multiline function docstring
|
||||
'''
|
||||
|
||||
some_expression = 'hello world'
|
||||
|
||||
'''
|
||||
this is not a docstring
|
||||
'''
|
||||
|
||||
if l:
|
||||
'''
|
||||
Looks like a docstring, but in reality it isn't - only modules, classes and functions
|
||||
'''
|
||||
pass
|
||||
9
resources/test/fixtures/flake8_quotes/docstring_singles_class.py
vendored
Normal file
9
resources/test/fixtures/flake8_quotes/docstring_singles_class.py
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
class SingleLineDocstrings():
|
||||
''' Double quotes single line class docstring '''
|
||||
''' Not a docstring '''
|
||||
|
||||
def foo(self, bar='''not a docstring'''):
|
||||
''' Double quotes single line method docstring'''
|
||||
pass
|
||||
|
||||
class Nested(foo()[:]): ''' inline docstring '''; pass
|
||||
23
resources/test/fixtures/flake8_quotes/docstring_singles_function.py
vendored
Normal file
23
resources/test/fixtures/flake8_quotes/docstring_singles_function.py
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
def foo():
|
||||
'''function without params, single line docstring'''
|
||||
''' not a docstring'''
|
||||
return
|
||||
|
||||
|
||||
def foo2():
|
||||
'''
|
||||
function without params, multiline docstring
|
||||
'''
|
||||
''' not a docstring'''
|
||||
return
|
||||
|
||||
|
||||
def fun_with_params_no_docstring(a, b='''
|
||||
not a
|
||||
''' '''docstring'''):
|
||||
pass
|
||||
|
||||
def fun_with_params_no_docstring2(a, b=c[foo():], c=\
|
||||
''' not a docstring '''):
|
||||
pass
|
||||
|
||||
11
resources/test/fixtures/flake8_quotes/docstring_singles_module_multiline.py
vendored
Normal file
11
resources/test/fixtures/flake8_quotes/docstring_singles_module_multiline.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
'''
|
||||
Double quotes multiline module docstring
|
||||
'''
|
||||
'''
|
||||
this is not a docstring
|
||||
'''
|
||||
def foo():
|
||||
pass
|
||||
'''
|
||||
this is not a docstring
|
||||
'''
|
||||
6
resources/test/fixtures/flake8_quotes/docstring_singles_module_singleline.py
vendored
Normal file
6
resources/test/fixtures/flake8_quotes/docstring_singles_module_singleline.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
''' Double quotes singleline module docstring '''
|
||||
''' this is not a docstring '''
|
||||
|
||||
def foo():
|
||||
pass
|
||||
''' this is not a docstring '''
|
||||
2
resources/test/fixtures/flake8_quotes/doubles.py
vendored
Normal file
2
resources/test/fixtures/flake8_quotes/doubles.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
this_should_be_linted = "double quote string"
|
||||
this_should_be_linted = u"double quote string"
|
||||
5
resources/test/fixtures/flake8_quotes/doubles_escaped.py
vendored
Normal file
5
resources/test/fixtures/flake8_quotes/doubles_escaped.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
this_should_raise_Q003 = 'This is a \'string\''
|
||||
this_is_fine = '"This" is a \'string\''
|
||||
this_is_fine = "This is a 'string'"
|
||||
this_is_fine = "\"This\" is a 'string'"
|
||||
this_is_fine = r'This is a \'string\''
|
||||
9
resources/test/fixtures/flake8_quotes/doubles_multiline_string.py
vendored
Normal file
9
resources/test/fixtures/flake8_quotes/doubles_multiline_string.py
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
s = """ This "should"
|
||||
be
|
||||
"linted" """
|
||||
|
||||
s = ''' This "should"
|
||||
"not" be
|
||||
"linted" '''
|
||||
|
||||
s = """'This should not be linted due to having would-be quadruple end quote'"""
|
||||
1
resources/test/fixtures/flake8_quotes/doubles_noqa.py
vendored
Normal file
1
resources/test/fixtures/flake8_quotes/doubles_noqa.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
this_should_not_be_linted = "double quote string" # noqa
|
||||
2
resources/test/fixtures/flake8_quotes/doubles_wrapped.py
vendored
Normal file
2
resources/test/fixtures/flake8_quotes/doubles_wrapped.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
s = 'double "quotes" wrapped in singles are ignored'
|
||||
s = "single 'quotes' wrapped in doubles are ignored"
|
||||
2
resources/test/fixtures/flake8_quotes/singles.py
vendored
Normal file
2
resources/test/fixtures/flake8_quotes/singles.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
this_should_be_linted = 'single quote string'
|
||||
this_should_be_linted = u'double quote string'
|
||||
5
resources/test/fixtures/flake8_quotes/singles_escaped.py
vendored
Normal file
5
resources/test/fixtures/flake8_quotes/singles_escaped.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
this_should_raise_Q003 = "This is a \"string\""
|
||||
this_is_fine = "'This' is a \"string\""
|
||||
this_is_fine = 'This is a "string"'
|
||||
this_is_fine = '\'This\' is a "string"'
|
||||
this_is_fine = r"This is a \"string\""
|
||||
9
resources/test/fixtures/flake8_quotes/singles_multiline_string.py
vendored
Normal file
9
resources/test/fixtures/flake8_quotes/singles_multiline_string.py
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
s = ''' This 'should'
|
||||
be
|
||||
'linted' '''
|
||||
|
||||
s = """ This 'should'
|
||||
'not' be
|
||||
'linted' """
|
||||
|
||||
s = '''"This should not be linted due to having would-be quadruple end quote"'''
|
||||
1
resources/test/fixtures/flake8_quotes/singles_noqa.py
vendored
Normal file
1
resources/test/fixtures/flake8_quotes/singles_noqa.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
this_should_not_be_linted = 'single quote string' # noqa
|
||||
2
resources/test/fixtures/flake8_quotes/singles_wrapped.py
vendored
Normal file
2
resources/test/fixtures/flake8_quotes/singles_wrapped.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
s = "single 'quotes' wrapped in doubles are ignored"
|
||||
s = 'double "quotes" wrapped in singles are ignored'
|
||||
31
resources/test/fixtures/pyproject.toml
vendored
31
resources/test/fixtures/pyproject.toml
vendored
@@ -5,3 +5,34 @@ extend-exclude = [
|
||||
"migrations",
|
||||
"directory/also_excluded.py",
|
||||
]
|
||||
per-file-ignores = [
|
||||
"__init__.py:F401",
|
||||
]
|
||||
|
||||
[tool.ruff.flake8-quotes]
|
||||
inline-quotes = "single"
|
||||
multiline-quotes = "double"
|
||||
docstring-quotes = "double"
|
||||
avoid-escape = true
|
||||
|
||||
[tool.ruff.pep8-naming]
|
||||
ignore-names = [
|
||||
"setUp",
|
||||
"tearDown",
|
||||
"setUpClass",
|
||||
"tearDownClass",
|
||||
"setUpModule",
|
||||
"tearDownModule",
|
||||
"asyncSetUp",
|
||||
"asyncTearDown",
|
||||
"setUpTestData",
|
||||
"failureException",
|
||||
"longMessage",
|
||||
"maxDiff",
|
||||
]
|
||||
classmethod-decorators = [
|
||||
"classmethod",
|
||||
]
|
||||
staticmethod-decorators = [
|
||||
"staticmethod",
|
||||
]
|
||||
|
||||
1333
src/ast/checkers.rs
1333
src/ast/checkers.rs
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, StmtKind};
|
||||
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, Location, StmtKind};
|
||||
|
||||
use crate::python::typing;
|
||||
|
||||
@@ -123,7 +123,7 @@ pub fn extract_handler_names(handlers: &[Excepthandler]) -> Vec<String> {
|
||||
}
|
||||
|
||||
/// Returns `true` if a call is an argumented `super` invocation.
|
||||
pub fn is_super_call_with_arguments(func: &Expr, args: &Vec<Expr>) -> bool {
|
||||
pub fn is_super_call_with_arguments(func: &Expr, args: &[Expr]) -> bool {
|
||||
// Check: is this a `super` call?
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
id == "super" && !args.is_empty()
|
||||
@@ -131,3 +131,15 @@ pub fn is_super_call_with_arguments(func: &Expr, args: &Vec<Expr>) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a location within a file (relative to `base`) to an absolute position.
|
||||
pub fn to_absolute(relative: &Location, base: &Location) -> Location {
|
||||
if relative.row() == 1 {
|
||||
Location::new(
|
||||
relative.row() + base.row() - 1,
|
||||
relative.column() + base.column() - 1,
|
||||
)
|
||||
} else {
|
||||
Location::new(relative.row() + base.row() - 1, relative.column())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod checkers;
|
||||
pub mod helpers;
|
||||
pub mod operations;
|
||||
pub mod relocate;
|
||||
@@ -1,3 +1,4 @@
|
||||
use once_cell::unsync::OnceCell;
|
||||
use rustpython_parser::ast::{Constant, Expr, ExprKind, Location, Stmt, StmtKind};
|
||||
|
||||
use crate::ast::types::{BindingKind, Range, Scope};
|
||||
@@ -120,66 +121,88 @@ pub fn is_unpacking_assignment(stmt: &Stmt) -> bool {
|
||||
|
||||
/// Struct used to efficiently slice source code at (row, column) Locations.
|
||||
pub struct SourceCodeLocator<'a> {
|
||||
content: &'a str,
|
||||
offsets: Vec<Vec<usize>>,
|
||||
initialized: bool,
|
||||
contents: &'a str,
|
||||
offsets: OnceCell<Vec<Vec<usize>>>,
|
||||
}
|
||||
|
||||
impl<'a> SourceCodeLocator<'a> {
|
||||
pub fn new(content: &'a str) -> Self {
|
||||
pub fn new(contents: &'a str) -> Self {
|
||||
SourceCodeLocator {
|
||||
content,
|
||||
offsets: vec![],
|
||||
initialized: false,
|
||||
contents,
|
||||
offsets: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn init(&mut self) {
|
||||
if !self.initialized {
|
||||
let mut offset = 0;
|
||||
for line in self.content.lines() {
|
||||
let mut newline = 0;
|
||||
let mut line_offsets: Vec<usize> = vec![];
|
||||
for (i, _char) in line.char_indices() {
|
||||
line_offsets.push(offset + i);
|
||||
newline = i + 1;
|
||||
}
|
||||
line_offsets.push(offset + newline);
|
||||
self.offsets.push(line_offsets);
|
||||
offset += newline + 1;
|
||||
fn compute_offsets(content: &str) -> Vec<Vec<usize>> {
|
||||
let mut offsets = vec![vec![]];
|
||||
let mut line_index = 0;
|
||||
for (i, char) in content.char_indices() {
|
||||
offsets[line_index].push(i);
|
||||
if char == '\n' {
|
||||
line_index += 1;
|
||||
offsets.push(vec![]);
|
||||
}
|
||||
self.offsets.push(vec![offset]);
|
||||
self.initialized = true;
|
||||
}
|
||||
offsets
|
||||
}
|
||||
|
||||
pub fn slice_source_code_at(&mut self, location: &Location) -> &'a str {
|
||||
self.init();
|
||||
let offset = self.offsets[location.row() - 1][location.column() - 1];
|
||||
&self.content[offset..]
|
||||
fn get_or_init_offsets(&self) -> &Vec<Vec<usize>> {
|
||||
self.offsets
|
||||
.get_or_init(|| Self::compute_offsets(self.contents))
|
||||
}
|
||||
|
||||
pub fn slice_source_code_range(&mut self, range: &Range) -> &'a str {
|
||||
self.init();
|
||||
let start = self.offsets[range.location.row() - 1][range.location.column() - 1];
|
||||
let end = self.offsets[range.end_location.row() - 1][range.end_location.column() - 1];
|
||||
&self.content[start..end]
|
||||
pub fn slice_source_code_at(&self, location: &Location) -> &'a str {
|
||||
let offsets = self.get_or_init_offsets();
|
||||
let offset = offsets[location.row() - 1][location.column() - 1];
|
||||
&self.contents[offset..]
|
||||
}
|
||||
|
||||
pub fn slice_source_code_range(&self, range: &Range) -> &'a str {
|
||||
let offsets = self.get_or_init_offsets();
|
||||
let start = offsets[range.location.row() - 1][range.location.column() - 1];
|
||||
let end = offsets[range.end_location.row() - 1][range.end_location.column() - 1];
|
||||
&self.contents[start..end]
|
||||
}
|
||||
|
||||
pub fn partition_source_code_at(
|
||||
&mut self,
|
||||
&self,
|
||||
outer: &Range,
|
||||
inner: &Range,
|
||||
) -> (&'a str, &'a str, &'a str) {
|
||||
self.init();
|
||||
let outer_start = self.offsets[outer.location.row() - 1][outer.location.column() - 1];
|
||||
let outer_end = self.offsets[outer.end_location.row() - 1][outer.end_location.column() - 1];
|
||||
let inner_start = self.offsets[inner.location.row() - 1][inner.location.column() - 1];
|
||||
let inner_end = self.offsets[inner.end_location.row() - 1][inner.end_location.column() - 1];
|
||||
let offsets = self.get_or_init_offsets();
|
||||
let outer_start = offsets[outer.location.row() - 1][outer.location.column() - 1];
|
||||
let outer_end = offsets[outer.end_location.row() - 1][outer.end_location.column() - 1];
|
||||
let inner_start = offsets[inner.location.row() - 1][inner.location.column() - 1];
|
||||
let inner_end = offsets[inner.end_location.row() - 1][inner.end_location.column() - 1];
|
||||
(
|
||||
&self.content[outer_start..inner_start],
|
||||
&self.content[inner_start..inner_end],
|
||||
&self.content[inner_end..outer_end],
|
||||
&self.contents[outer_start..inner_start],
|
||||
&self.contents[inner_start..inner_end],
|
||||
&self.contents[inner_end..outer_end],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::SourceCodeLocator;
|
||||
|
||||
#[test]
|
||||
fn source_code_locator_init() {
|
||||
let content = "x = 1\ny = 2\nz = x + y\n";
|
||||
let locator = SourceCodeLocator::new(content);
|
||||
let offsets = locator.get_or_init_offsets();
|
||||
assert_eq!(offsets.len(), 4);
|
||||
assert_eq!(offsets[0], [0, 1, 2, 3, 4, 5]);
|
||||
assert_eq!(offsets[1], [6, 7, 8, 9, 10, 11]);
|
||||
assert_eq!(offsets[2], [12, 13, 14, 15, 16, 17, 18, 19, 20, 21]);
|
||||
assert!(offsets[3].is_empty());
|
||||
|
||||
let content = "# \u{4e9c}\nclass Foo:\n \"\"\".\"\"\"";
|
||||
let locator = SourceCodeLocator::new(content);
|
||||
let offsets = locator.get_or_init_offsets();
|
||||
assert_eq!(offsets.len(), 3);
|
||||
assert_eq!(offsets[0], [0, 1, 2, 5]);
|
||||
assert_eq!(offsets[1], [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
|
||||
assert_eq!(offsets[2], [17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ use crate::ast::types::Range;
|
||||
|
||||
fn relocate_keyword(keyword: &mut Keyword, location: Range) {
|
||||
keyword.location = location.location;
|
||||
keyword.end_location = location.end_location;
|
||||
keyword.end_location = Some(location.end_location);
|
||||
relocate_expr(&mut keyword.node.value, location);
|
||||
}
|
||||
|
||||
/// Change an expression's location (recursively) to match a desired, fixed location.
|
||||
pub fn relocate_expr(expr: &mut Expr, location: Range) {
|
||||
expr.location = location.location;
|
||||
expr.end_location = location.end_location;
|
||||
expr.end_location = Some(location.end_location);
|
||||
match &mut expr.node {
|
||||
ExprKind::BoolOp { values, .. } => {
|
||||
for expr in values {
|
||||
|
||||
@@ -18,7 +18,9 @@ impl Range {
|
||||
pub fn from_located<T>(located: &Located<T>) -> Self {
|
||||
Range {
|
||||
location: located.location,
|
||||
end_location: located.end_location,
|
||||
end_location: located
|
||||
.end_location
|
||||
.expect("AST nodes should have end_location."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod fixer;
|
||||
pub mod fixes;
|
||||
@@ -1,7 +1,11 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use itertools::Itertools;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::checks::{Check, Fix};
|
||||
use crate::autofix::Fix;
|
||||
use crate::autofix::Patch;
|
||||
use crate::checks::Check;
|
||||
|
||||
#[derive(Hash)]
|
||||
pub enum Mode {
|
||||
@@ -10,6 +14,17 @@ pub enum Mode {
|
||||
None,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
/// Return `true` if a patch should be generated under the given `Mode`.
|
||||
pub fn patch(&self) -> bool {
|
||||
match &self {
|
||||
Mode::Generate => true,
|
||||
Mode::Apply => true,
|
||||
Mode::None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Mode {
|
||||
fn from(value: bool) -> Self {
|
||||
match value {
|
||||
@@ -35,34 +50,44 @@ pub fn fix_file(checks: &mut [Check], contents: &str) -> Option<String> {
|
||||
fn apply_fixes<'a>(fixes: impl Iterator<Item = &'a mut Fix>, contents: &str) -> String {
|
||||
let lines: Vec<&str> = contents.lines().collect();
|
||||
|
||||
let mut output = "".to_string();
|
||||
let mut last_pos: Location = Location::new(0, 0);
|
||||
let mut output: String = Default::default();
|
||||
let mut last_pos: Location = Default::default();
|
||||
let mut applied: BTreeSet<&Patch> = Default::default();
|
||||
|
||||
for fix in fixes.sorted_by_key(|fix| fix.location) {
|
||||
// Best-effort approach: if this fix overlaps with a fix we've already applied, skip it.
|
||||
if last_pos > fix.location {
|
||||
for fix in fixes.sorted_by_key(|fix| fix.patch.location) {
|
||||
// If we already applied an identical fix as part of another correction, skip any
|
||||
// re-application.
|
||||
if applied.contains(&fix.patch) {
|
||||
fix.applied = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if fix.location.row() > last_pos.row() {
|
||||
// Best-effort approach: if this fix overlaps with a fix we've already applied, skip it.
|
||||
if last_pos > fix.patch.location {
|
||||
continue;
|
||||
}
|
||||
|
||||
if fix.patch.location.row() > last_pos.row() {
|
||||
if last_pos.row() > 0 || last_pos.column() > 0 {
|
||||
output.push_str(&lines[last_pos.row() - 1][last_pos.column() - 1..]);
|
||||
output.push('\n');
|
||||
}
|
||||
for line in &lines[last_pos.row()..fix.location.row() - 1] {
|
||||
for line in &lines[last_pos.row()..fix.patch.location.row() - 1] {
|
||||
output.push_str(line);
|
||||
output.push('\n');
|
||||
}
|
||||
output.push_str(&lines[fix.location.row() - 1][..fix.location.column() - 1]);
|
||||
output.push_str(&fix.content);
|
||||
output
|
||||
.push_str(&lines[fix.patch.location.row() - 1][..fix.patch.location.column() - 1]);
|
||||
output.push_str(&fix.patch.content);
|
||||
} else {
|
||||
output.push_str(
|
||||
&lines[last_pos.row() - 1][last_pos.column() - 1..fix.location.column() - 1],
|
||||
&lines[last_pos.row() - 1][last_pos.column() - 1..fix.patch.location.column() - 1],
|
||||
);
|
||||
output.push_str(&fix.content);
|
||||
output.push_str(&fix.patch.content);
|
||||
}
|
||||
last_pos = fix.patch.end_location;
|
||||
|
||||
last_pos = fix.end_location;
|
||||
applied.insert(&fix.patch);
|
||||
fix.applied = true;
|
||||
}
|
||||
|
||||
@@ -89,7 +114,8 @@ mod tests {
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::autofix::fixer::apply_fixes;
|
||||
use crate::checks::Fix;
|
||||
use crate::autofix::Fix;
|
||||
use crate::autofix::Patch;
|
||||
|
||||
#[test]
|
||||
fn empty_file() -> Result<()> {
|
||||
@@ -105,9 +131,11 @@ mod tests {
|
||||
#[test]
|
||||
fn apply_single_replacement() -> Result<()> {
|
||||
let mut fixes = vec![Fix {
|
||||
content: "Bar".to_string(),
|
||||
location: Location::new(1, 9),
|
||||
end_location: Location::new(1, 15),
|
||||
patch: Patch {
|
||||
content: "Bar".to_string(),
|
||||
location: Location::new(1, 9),
|
||||
end_location: Location::new(1, 15),
|
||||
},
|
||||
applied: false,
|
||||
}];
|
||||
let actual = apply_fixes(
|
||||
@@ -129,9 +157,11 @@ mod tests {
|
||||
#[test]
|
||||
fn apply_single_removal() -> Result<()> {
|
||||
let mut fixes = vec![Fix {
|
||||
content: "".to_string(),
|
||||
location: Location::new(1, 8),
|
||||
end_location: Location::new(1, 16),
|
||||
patch: Patch {
|
||||
content: "".to_string(),
|
||||
location: Location::new(1, 8),
|
||||
end_location: Location::new(1, 16),
|
||||
},
|
||||
applied: false,
|
||||
}];
|
||||
let actual = apply_fixes(
|
||||
@@ -154,15 +184,19 @@ mod tests {
|
||||
fn apply_double_removal() -> Result<()> {
|
||||
let mut fixes = vec![
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
location: Location::new(1, 8),
|
||||
end_location: Location::new(1, 17),
|
||||
patch: Patch {
|
||||
content: "".to_string(),
|
||||
location: Location::new(1, 8),
|
||||
end_location: Location::new(1, 17),
|
||||
},
|
||||
applied: false,
|
||||
},
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
location: Location::new(1, 17),
|
||||
end_location: Location::new(1, 24),
|
||||
patch: Patch {
|
||||
content: "".to_string(),
|
||||
location: Location::new(1, 17),
|
||||
end_location: Location::new(1, 24),
|
||||
},
|
||||
applied: false,
|
||||
},
|
||||
];
|
||||
@@ -186,15 +220,19 @@ mod tests {
|
||||
fn ignore_overlapping_fixes() -> Result<()> {
|
||||
let mut fixes = vec![
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
location: Location::new(1, 8),
|
||||
end_location: Location::new(1, 16),
|
||||
patch: Patch {
|
||||
content: "".to_string(),
|
||||
location: Location::new(1, 8),
|
||||
end_location: Location::new(1, 16),
|
||||
},
|
||||
applied: false,
|
||||
},
|
||||
Fix {
|
||||
content: "ignored".to_string(),
|
||||
location: Location::new(1, 10),
|
||||
end_location: Location::new(1, 12),
|
||||
patch: Patch {
|
||||
content: "ignored".to_string(),
|
||||
location: Location::new(1, 10),
|
||||
end_location: Location::new(1, 12),
|
||||
},
|
||||
applied: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,392 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use libcst_native::ImportNames::Aliases;
|
||||
use libcst_native::NameOrAttribute::N;
|
||||
use libcst_native::{Codegen, Expression, SmallStatement, Statement};
|
||||
use rustpython_parser::ast::{ExcepthandlerKind, Expr, Keyword, Location, Stmt, StmtKind};
|
||||
use rustpython_parser::lexer;
|
||||
use rustpython_parser::token::Tok;
|
||||
|
||||
use crate::ast::operations::SourceCodeLocator;
|
||||
use crate::ast::types::Range;
|
||||
use crate::checks::Fix;
|
||||
|
||||
/// Convert a location within a file (relative to `base`) to an absolute position.
|
||||
fn to_absolute(relative: &Location, base: &Location) -> Location {
|
||||
if relative.row() == 1 {
|
||||
Location::new(
|
||||
relative.row() + base.row() - 1,
|
||||
relative.column() + base.column() - 1,
|
||||
)
|
||||
} else {
|
||||
Location::new(relative.row() + base.row() - 1, relative.column())
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a fix to remove a base from a ClassDef statement.
|
||||
pub fn remove_class_def_base(
|
||||
locator: &mut SourceCodeLocator,
|
||||
stmt_at: &Location,
|
||||
expr_at: Location,
|
||||
bases: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
) -> Option<Fix> {
|
||||
let content = locator.slice_source_code_at(stmt_at);
|
||||
|
||||
// Case 1: `object` is the only base.
|
||||
if bases.len() == 1 && keywords.is_empty() {
|
||||
let mut fix_start = None;
|
||||
let mut fix_end = None;
|
||||
let mut count: usize = 0;
|
||||
for (start, tok, end) in lexer::make_tokenizer(content).flatten() {
|
||||
if matches!(tok, Tok::Lpar) {
|
||||
if count == 0 {
|
||||
fix_start = Some(to_absolute(&start, stmt_at));
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
|
||||
if matches!(tok, Tok::Rpar) {
|
||||
count -= 1;
|
||||
if count == 0 {
|
||||
fix_end = Some(to_absolute(&end, stmt_at));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return match (fix_start, fix_end) {
|
||||
(Some(start), Some(end)) => Some(Fix {
|
||||
content: "".to_string(),
|
||||
location: start,
|
||||
end_location: end,
|
||||
applied: false,
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
|
||||
if bases
|
||||
.iter()
|
||||
.map(|node| node.location)
|
||||
.chain(keywords.iter().map(|node| node.location))
|
||||
.any(|location| location > expr_at)
|
||||
{
|
||||
// Case 2: `object` is _not_ the last node.
|
||||
let mut fix_start: Option<Location> = None;
|
||||
let mut fix_end: Option<Location> = None;
|
||||
let mut seen_comma = false;
|
||||
for (start, tok, end) in lexer::make_tokenizer(content).flatten() {
|
||||
let start = to_absolute(&start, stmt_at);
|
||||
if seen_comma {
|
||||
if matches!(tok, Tok::Newline) {
|
||||
fix_end = Some(end);
|
||||
} else {
|
||||
fix_end = Some(start);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if start == expr_at {
|
||||
fix_start = Some(start);
|
||||
}
|
||||
if fix_start.is_some() && matches!(tok, Tok::Comma) {
|
||||
seen_comma = true;
|
||||
}
|
||||
}
|
||||
|
||||
match (fix_start, fix_end) {
|
||||
(Some(start), Some(end)) => Some(Fix {
|
||||
content: "".to_string(),
|
||||
location: start,
|
||||
end_location: end,
|
||||
applied: false,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
// Case 3: `object` is the last node, so we have to find the last token that isn't a comma.
|
||||
let mut fix_start: Option<Location> = None;
|
||||
let mut fix_end: Option<Location> = None;
|
||||
for (start, tok, end) in lexer::make_tokenizer(content).flatten() {
|
||||
let start = to_absolute(&start, stmt_at);
|
||||
let end = to_absolute(&end, stmt_at);
|
||||
if start == expr_at {
|
||||
fix_end = Some(end);
|
||||
break;
|
||||
}
|
||||
if matches!(tok, Tok::Comma) {
|
||||
fix_start = Some(start);
|
||||
}
|
||||
}
|
||||
|
||||
match (fix_start, fix_end) {
|
||||
(Some(start), Some(end)) => Some(Fix {
|
||||
content: "".to_string(),
|
||||
location: start,
|
||||
end_location: end,
|
||||
applied: false,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_super_arguments(locator: &mut SourceCodeLocator, expr: &Expr) -> Option<Fix> {
|
||||
let range = Range::from_located(expr);
|
||||
let contents = locator.slice_source_code_range(&range);
|
||||
|
||||
let mut tree = match libcst_native::parse_module(contents, None) {
|
||||
Ok(m) => m,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
if let Some(Statement::Simple(body)) = tree.body.first_mut() {
|
||||
if let Some(SmallStatement::Expr(body)) = body.body.first_mut() {
|
||||
if let Expression::Call(body) = &mut body.value {
|
||||
body.args = vec![];
|
||||
body.whitespace_before_args = Default::default();
|
||||
body.whitespace_after_func = Default::default();
|
||||
|
||||
let mut state = Default::default();
|
||||
tree.codegen(&mut state);
|
||||
|
||||
return Some(Fix {
|
||||
content: state.to_string(),
|
||||
location: range.location,
|
||||
end_location: range.end_location,
|
||||
applied: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Determine if a body contains only a single statement, taking into account deleted.
|
||||
fn has_single_child(body: &[Stmt], deleted: &[&Stmt]) -> bool {
|
||||
body.iter().filter(|child| !deleted.contains(child)).count() == 1
|
||||
}
|
||||
|
||||
/// Determine if a child is the only statement in its body.
|
||||
fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool> {
|
||||
match &parent.node {
|
||||
StmtKind::FunctionDef { body, .. }
|
||||
| StmtKind::AsyncFunctionDef { body, .. }
|
||||
| StmtKind::ClassDef { body, .. }
|
||||
| StmtKind::With { body, .. }
|
||||
| StmtKind::AsyncWith { body, .. } => {
|
||||
if body.iter().contains(child) {
|
||||
Ok(has_single_child(body, deleted))
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
||||
}
|
||||
}
|
||||
StmtKind::For { body, orelse, .. }
|
||||
| StmtKind::AsyncFor { body, orelse, .. }
|
||||
| StmtKind::While { body, orelse, .. }
|
||||
| StmtKind::If { body, orelse, .. } => {
|
||||
if body.iter().contains(child) {
|
||||
Ok(has_single_child(body, deleted))
|
||||
} else if orelse.iter().contains(child) {
|
||||
Ok(has_single_child(orelse, deleted))
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
||||
}
|
||||
}
|
||||
StmtKind::Try {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
} => {
|
||||
if body.iter().contains(child) {
|
||||
Ok(has_single_child(body, deleted))
|
||||
} else if orelse.iter().contains(child) {
|
||||
Ok(has_single_child(orelse, deleted))
|
||||
} else if finalbody.iter().contains(child) {
|
||||
Ok(has_single_child(finalbody, deleted))
|
||||
} else if let Some(body) = handlers.iter().find_map(|handler| match &handler.node {
|
||||
ExcepthandlerKind::ExceptHandler { body, .. } => {
|
||||
if body.iter().contains(child) {
|
||||
Some(body)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Ok(has_single_child(body, deleted))
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
||||
}
|
||||
}
|
||||
_ => Err(anyhow::anyhow!("Unable to find child in parent body.")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_stmt(stmt: &Stmt, parent: Option<&Stmt>, deleted: &[&Stmt]) -> Result<Fix> {
|
||||
if parent
|
||||
.map(|parent| is_lone_child(stmt, parent, deleted))
|
||||
.map_or(Ok(None), |v| v.map(Some))?
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// If removing this node would lead to an invalid syntax tree, replace
|
||||
// it with a `pass`.
|
||||
Ok(Fix {
|
||||
location: stmt.location,
|
||||
end_location: stmt.end_location,
|
||||
content: "pass".to_string(),
|
||||
applied: false,
|
||||
})
|
||||
} else {
|
||||
// Otherwise, nuke the entire line.
|
||||
// TODO(charlie): This logic assumes that there are no multi-statement physical lines.
|
||||
Ok(Fix {
|
||||
location: Location::new(stmt.location.row(), 1),
|
||||
end_location: Location::new(stmt.end_location.row() + 1, 1),
|
||||
content: "".to_string(),
|
||||
applied: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a Fix to remove any unused imports from an `import` statement.
|
||||
pub fn remove_unused_imports(
|
||||
locator: &mut SourceCodeLocator,
|
||||
full_names: &[&str],
|
||||
stmt: &Stmt,
|
||||
parent: Option<&Stmt>,
|
||||
deleted: &[&Stmt],
|
||||
) -> Result<Fix> {
|
||||
let mut tree = match libcst_native::parse_module(
|
||||
locator.slice_source_code_range(&Range::from_located(stmt)),
|
||||
None,
|
||||
) {
|
||||
Ok(m) => m,
|
||||
Err(_) => return Err(anyhow::anyhow!("Failed to extract CST from source.")),
|
||||
};
|
||||
|
||||
let body = if let Some(Statement::Simple(body)) = tree.body.first_mut() {
|
||||
body
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Expected node to be: Statement::Simple."));
|
||||
};
|
||||
let body = if let Some(SmallStatement::Import(body)) = body.body.first_mut() {
|
||||
body
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Expected node to be: SmallStatement::ImportFrom."
|
||||
));
|
||||
};
|
||||
let aliases = &mut body.names;
|
||||
|
||||
// Preserve the trailing comma (or not) from the last entry.
|
||||
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
|
||||
|
||||
// Identify unused imports from within the `import from`.
|
||||
let mut removable = vec![];
|
||||
for (index, alias) in aliases.iter().enumerate() {
|
||||
if let N(import_name) = &alias.name {
|
||||
if full_names.contains(&import_name.value) {
|
||||
removable.push(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO(charlie): This is quadratic.
|
||||
for index in removable.iter().rev() {
|
||||
aliases.remove(*index);
|
||||
}
|
||||
|
||||
if let Some(alias) = aliases.last_mut() {
|
||||
alias.comma = trailing_comma;
|
||||
}
|
||||
|
||||
if aliases.is_empty() {
|
||||
remove_stmt(stmt, parent, deleted)
|
||||
} else {
|
||||
let mut state = Default::default();
|
||||
tree.codegen(&mut state);
|
||||
|
||||
Ok(Fix {
|
||||
content: state.to_string(),
|
||||
location: stmt.location,
|
||||
end_location: stmt.end_location,
|
||||
applied: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a Fix to remove any unused imports from an `import from` statement.
|
||||
pub fn remove_unused_import_froms(
|
||||
locator: &mut SourceCodeLocator,
|
||||
full_names: &[&str],
|
||||
stmt: &Stmt,
|
||||
parent: Option<&Stmt>,
|
||||
deleted: &[&Stmt],
|
||||
) -> Result<Fix> {
|
||||
let mut tree = match libcst_native::parse_module(
|
||||
locator.slice_source_code_range(&Range::from_located(stmt)),
|
||||
None,
|
||||
) {
|
||||
Ok(m) => m,
|
||||
Err(_) => return Err(anyhow::anyhow!("Failed to extract CST from source.")),
|
||||
};
|
||||
|
||||
let body = if let Some(Statement::Simple(body)) = tree.body.first_mut() {
|
||||
body
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Expected node to be: Statement::Simple."));
|
||||
};
|
||||
let body = if let Some(SmallStatement::ImportFrom(body)) = body.body.first_mut() {
|
||||
body
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Expected node to be: SmallStatement::ImportFrom."
|
||||
));
|
||||
};
|
||||
let aliases = if let Aliases(aliases) = &mut body.names {
|
||||
aliases
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Expected node to be: Aliases."));
|
||||
};
|
||||
|
||||
// Preserve the trailing comma (or not) from the last entry.
|
||||
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
|
||||
|
||||
// Identify unused imports from within the `import from`.
|
||||
let mut removable = vec![];
|
||||
for (index, alias) in aliases.iter().enumerate() {
|
||||
if let N(name) = &alias.name {
|
||||
let import_name = if let Some(N(module_name)) = &body.module {
|
||||
format!("{}.{}", module_name.value, name.value)
|
||||
} else {
|
||||
name.value.to_string()
|
||||
};
|
||||
if full_names.contains(&import_name.as_str()) {
|
||||
removable.push(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO(charlie): This is quadratic.
|
||||
for index in removable.iter().rev() {
|
||||
aliases.remove(*index);
|
||||
}
|
||||
|
||||
if let Some(alias) = aliases.last_mut() {
|
||||
alias.comma = trailing_comma;
|
||||
}
|
||||
|
||||
if aliases.is_empty() {
|
||||
remove_stmt(stmt, parent, deleted)
|
||||
} else {
|
||||
let mut state = Default::default();
|
||||
tree.codegen(&mut state);
|
||||
|
||||
Ok(Fix {
|
||||
content: state.to_string(),
|
||||
location: stmt.location,
|
||||
end_location: stmt.end_location,
|
||||
applied: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
89
src/autofix/helpers.rs
Normal file
89
src/autofix/helpers.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use rustpython_parser::ast::{ExcepthandlerKind, Location, Stmt, StmtKind};
|
||||
|
||||
use crate::autofix::Fix;
|
||||
|
||||
/// Determine if a body contains only a single statement, taking into account deleted.
|
||||
fn has_single_child(body: &[Stmt], deleted: &[&Stmt]) -> bool {
|
||||
body.iter().filter(|child| !deleted.contains(child)).count() == 1
|
||||
}
|
||||
|
||||
/// Determine if a child is the only statement in its body.
|
||||
fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool> {
|
||||
match &parent.node {
|
||||
StmtKind::FunctionDef { body, .. }
|
||||
| StmtKind::AsyncFunctionDef { body, .. }
|
||||
| StmtKind::ClassDef { body, .. }
|
||||
| StmtKind::With { body, .. }
|
||||
| StmtKind::AsyncWith { body, .. } => {
|
||||
if body.iter().contains(child) {
|
||||
Ok(has_single_child(body, deleted))
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
||||
}
|
||||
}
|
||||
StmtKind::For { body, orelse, .. }
|
||||
| StmtKind::AsyncFor { body, orelse, .. }
|
||||
| StmtKind::While { body, orelse, .. }
|
||||
| StmtKind::If { body, orelse, .. } => {
|
||||
if body.iter().contains(child) {
|
||||
Ok(has_single_child(body, deleted))
|
||||
} else if orelse.iter().contains(child) {
|
||||
Ok(has_single_child(orelse, deleted))
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
||||
}
|
||||
}
|
||||
StmtKind::Try {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
} => {
|
||||
if body.iter().contains(child) {
|
||||
Ok(has_single_child(body, deleted))
|
||||
} else if orelse.iter().contains(child) {
|
||||
Ok(has_single_child(orelse, deleted))
|
||||
} else if finalbody.iter().contains(child) {
|
||||
Ok(has_single_child(finalbody, deleted))
|
||||
} else if let Some(body) = handlers.iter().find_map(|handler| match &handler.node {
|
||||
ExcepthandlerKind::ExceptHandler { body, .. } => {
|
||||
if body.iter().contains(child) {
|
||||
Some(body)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Ok(has_single_child(body, deleted))
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
||||
}
|
||||
}
|
||||
_ => Err(anyhow::anyhow!("Unable to find child in parent body.")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_stmt(stmt: &Stmt, parent: Option<&Stmt>, deleted: &[&Stmt]) -> Result<Fix> {
|
||||
if parent
|
||||
.map(|parent| is_lone_child(stmt, parent, deleted))
|
||||
.map_or(Ok(None), |v| v.map(Some))?
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// If removing this node would lead to an invalid syntax tree, replace
|
||||
// it with a `pass`.
|
||||
Ok(Fix::replacement(
|
||||
"pass".to_string(),
|
||||
stmt.location,
|
||||
stmt.end_location.unwrap(),
|
||||
))
|
||||
} else {
|
||||
// Otherwise, nuke the entire line.
|
||||
// TODO(charlie): This logic assumes that there are no multi-statement physical lines.
|
||||
Ok(Fix::deletion(
|
||||
Location::new(stmt.location.row(), 1),
|
||||
Location::new(stmt.end_location.unwrap().row() + 1, 1),
|
||||
))
|
||||
}
|
||||
}
|
||||
53
src/autofix/mod.rs
Normal file
53
src/autofix/mod.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use rustpython_ast::Location;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod fixer;
|
||||
pub mod helpers;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct Patch {
|
||||
pub content: String,
|
||||
pub location: Location,
|
||||
pub end_location: Location,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Fix {
|
||||
pub patch: Patch,
|
||||
pub applied: bool,
|
||||
}
|
||||
|
||||
impl Fix {
|
||||
pub fn deletion(start: Location, end: Location) -> Self {
|
||||
Self {
|
||||
patch: Patch {
|
||||
content: "".to_string(),
|
||||
location: start,
|
||||
end_location: end,
|
||||
},
|
||||
applied: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replacement(content: String, start: Location, end: Location) -> Self {
|
||||
Self {
|
||||
patch: Patch {
|
||||
content,
|
||||
location: start,
|
||||
end_location: end,
|
||||
},
|
||||
applied: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insertion(content: String, at: Location) -> Self {
|
||||
Self {
|
||||
patch: Patch {
|
||||
content,
|
||||
location: at,
|
||||
end_location: at,
|
||||
},
|
||||
applied: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
748
src/check_ast.rs
748
src/check_ast.rs
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,13 @@
|
||||
//! Lint rules based on checking raw physical lines.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::fixer;
|
||||
use crate::checks::{Check, CheckCode, CheckKind, Fix};
|
||||
use crate::autofix::Fix;
|
||||
use crate::checks::{Check, CheckCode, CheckKind};
|
||||
use crate::noqa;
|
||||
use crate::noqa::Directive;
|
||||
use crate::settings::Settings;
|
||||
@@ -165,16 +168,11 @@ pub fn check_lines(
|
||||
end_location: Location::new(row + 1, end + 1),
|
||||
},
|
||||
);
|
||||
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
check.amend(Fix {
|
||||
content: "".to_string(),
|
||||
location: Location::new(row + 1, start + 1),
|
||||
end_location: Location::new(
|
||||
row + 1,
|
||||
lines[row].chars().count() + 1,
|
||||
),
|
||||
applied: false,
|
||||
});
|
||||
if autofix.patch() {
|
||||
check.amend(Fix::deletion(
|
||||
Location::new(row + 1, start + 1),
|
||||
Location::new(row + 1, lines[row].chars().count() + 1),
|
||||
));
|
||||
}
|
||||
line_checks.push(check);
|
||||
}
|
||||
@@ -198,27 +196,18 @@ pub fn check_lines(
|
||||
end_location: Location::new(row + 1, end + 1),
|
||||
},
|
||||
);
|
||||
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if autofix.patch() {
|
||||
if valid_codes.is_empty() {
|
||||
check.amend(Fix {
|
||||
content: "".to_string(),
|
||||
location: Location::new(row + 1, start + 1),
|
||||
end_location: Location::new(
|
||||
row + 1,
|
||||
lines[row].chars().count() + 1,
|
||||
),
|
||||
applied: false,
|
||||
});
|
||||
check.amend(Fix::deletion(
|
||||
Location::new(row + 1, start + 1),
|
||||
Location::new(row + 1, lines[row].chars().count() + 1),
|
||||
));
|
||||
} else {
|
||||
check.amend(Fix {
|
||||
content: format!(" # noqa: {}", valid_codes.join(", ")),
|
||||
location: Location::new(row + 1, start + 1),
|
||||
end_location: Location::new(
|
||||
row + 1,
|
||||
lines[row].chars().count() + 1,
|
||||
),
|
||||
applied: false,
|
||||
});
|
||||
check.amend(Fix::replacement(
|
||||
format!(" # noqa: {}", valid_codes.join(", ")),
|
||||
Location::new(row + 1, start + 1),
|
||||
Location::new(row + 1, lines[row].chars().count() + 1),
|
||||
));
|
||||
}
|
||||
}
|
||||
line_checks.push(check);
|
||||
@@ -240,7 +229,7 @@ pub fn check_lines(
|
||||
mod tests {
|
||||
use crate::autofix::fixer;
|
||||
use crate::checks::{Check, CheckCode};
|
||||
use crate::settings;
|
||||
use crate::settings::Settings;
|
||||
|
||||
use super::check_lines;
|
||||
|
||||
@@ -254,9 +243,9 @@ mod tests {
|
||||
&mut checks,
|
||||
line,
|
||||
&noqa_line_for,
|
||||
&settings::Settings {
|
||||
&Settings {
|
||||
line_length,
|
||||
..settings::Settings::for_rule(CheckCode::E501)
|
||||
..Settings::for_rule(CheckCode::E501)
|
||||
},
|
||||
&fixer::Mode::Generate,
|
||||
);
|
||||
|
||||
51
src/check_tokens.rs
Normal file
51
src/check_tokens.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
//! Lint rules based on token traversal.
|
||||
|
||||
use rustpython_parser::lexer::{LexResult, Tok};
|
||||
|
||||
use crate::ast::operations::SourceCodeLocator;
|
||||
use crate::checks::{Check, CheckCode};
|
||||
use crate::flake8_quotes::docstring_detection::StateMachine;
|
||||
use crate::{flake8_quotes, pycodestyle, Settings};
|
||||
|
||||
pub fn check_tokens(
|
||||
checks: &mut Vec<Check>,
|
||||
locator: &SourceCodeLocator,
|
||||
tokens: &[LexResult],
|
||||
settings: &Settings,
|
||||
) {
|
||||
let enforce_invalid_escape_sequence = settings.enabled.contains(&CheckCode::W605);
|
||||
let enforce_quotes = settings.enabled.contains(&CheckCode::Q000)
|
||||
| settings.enabled.contains(&CheckCode::Q001)
|
||||
| settings.enabled.contains(&CheckCode::Q002)
|
||||
| settings.enabled.contains(&CheckCode::Q003);
|
||||
|
||||
let mut state_machine = StateMachine::new();
|
||||
for (start, tok, end) in tokens.iter().flatten() {
|
||||
// W605
|
||||
if enforce_invalid_escape_sequence {
|
||||
if matches!(tok, Tok::String { .. }) {
|
||||
checks.extend(pycodestyle::checks::invalid_escape_sequence(
|
||||
locator, start, end,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// flake8-quotes
|
||||
if enforce_quotes {
|
||||
let is_docstring = state_machine.consume(tok);
|
||||
if matches!(tok, Tok::String { .. }) {
|
||||
if let Some(check) = flake8_quotes::checks::quotes(
|
||||
locator,
|
||||
start,
|
||||
end,
|
||||
is_docstring,
|
||||
&settings.flake8_quotes,
|
||||
) {
|
||||
if settings.enabled.contains(check.kind.code()) {
|
||||
checks.push(check);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
490
src/checks.rs
490
src/checks.rs
@@ -5,57 +5,10 @@ use rustpython_parser::ast::Location;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::{AsRefStr, EnumIter, EnumString};
|
||||
|
||||
use crate::ast::checkers::Primitive;
|
||||
use crate::ast::types::Range;
|
||||
|
||||
pub const DEFAULT_CHECK_CODES: [CheckCode; 43] = [
|
||||
// pycodestyle errors
|
||||
CheckCode::E402,
|
||||
CheckCode::E501,
|
||||
CheckCode::E711,
|
||||
CheckCode::E712,
|
||||
CheckCode::E713,
|
||||
CheckCode::E714,
|
||||
CheckCode::E721,
|
||||
CheckCode::E722,
|
||||
CheckCode::E731,
|
||||
CheckCode::E741,
|
||||
CheckCode::E742,
|
||||
CheckCode::E743,
|
||||
CheckCode::E902,
|
||||
CheckCode::E999,
|
||||
// pycodestyle warnings
|
||||
CheckCode::W292,
|
||||
// pyflakes
|
||||
CheckCode::F401,
|
||||
CheckCode::F402,
|
||||
CheckCode::F403,
|
||||
CheckCode::F404,
|
||||
CheckCode::F405,
|
||||
CheckCode::F406,
|
||||
CheckCode::F407,
|
||||
CheckCode::F541,
|
||||
CheckCode::F601,
|
||||
CheckCode::F602,
|
||||
CheckCode::F621,
|
||||
CheckCode::F622,
|
||||
CheckCode::F631,
|
||||
CheckCode::F632,
|
||||
CheckCode::F633,
|
||||
CheckCode::F634,
|
||||
CheckCode::F701,
|
||||
CheckCode::F702,
|
||||
CheckCode::F704,
|
||||
CheckCode::F706,
|
||||
CheckCode::F707,
|
||||
CheckCode::F722,
|
||||
CheckCode::F821,
|
||||
CheckCode::F822,
|
||||
CheckCode::F823,
|
||||
CheckCode::F831,
|
||||
CheckCode::F841,
|
||||
CheckCode::F901,
|
||||
];
|
||||
use crate::autofix::Fix;
|
||||
use crate::flake8_quotes::settings::Quote;
|
||||
use crate::pyupgrade::types::Primitive;
|
||||
|
||||
#[derive(
|
||||
AsRefStr,
|
||||
@@ -89,6 +42,7 @@ pub enum CheckCode {
|
||||
E999,
|
||||
// pycodestyle warnings
|
||||
W292,
|
||||
W605,
|
||||
// pyflakes
|
||||
F401,
|
||||
F402,
|
||||
@@ -123,8 +77,12 @@ pub enum CheckCode {
|
||||
A002,
|
||||
A003,
|
||||
// flake8-bugbear
|
||||
B002,
|
||||
B007,
|
||||
B011,
|
||||
B013,
|
||||
B014,
|
||||
B017,
|
||||
B025,
|
||||
// flake8-comprehensions
|
||||
C400,
|
||||
@@ -146,6 +104,11 @@ pub enum CheckCode {
|
||||
// flake8-print
|
||||
T201,
|
||||
T203,
|
||||
// flake8-quotes
|
||||
Q000,
|
||||
Q001,
|
||||
Q002,
|
||||
Q003,
|
||||
// pyupgrade
|
||||
U001,
|
||||
U002,
|
||||
@@ -200,6 +163,22 @@ pub enum CheckCode {
|
||||
D417,
|
||||
D418,
|
||||
D419,
|
||||
// pep8-naming
|
||||
N801,
|
||||
N802,
|
||||
N803,
|
||||
N804,
|
||||
N805,
|
||||
N806,
|
||||
N807,
|
||||
N811,
|
||||
N812,
|
||||
N813,
|
||||
N814,
|
||||
N815,
|
||||
N816,
|
||||
N817,
|
||||
N818,
|
||||
// Meta
|
||||
M001,
|
||||
}
|
||||
@@ -207,27 +186,33 @@ pub enum CheckCode {
|
||||
#[derive(EnumIter, Debug, PartialEq, Eq)]
|
||||
pub enum CheckCategory {
|
||||
Pyflakes,
|
||||
Pycodestyle,
|
||||
PycodestyleError,
|
||||
PycodestyleWarning,
|
||||
Pydocstyle,
|
||||
Pyupgrade,
|
||||
PEP8Naming,
|
||||
Flake8Comprehensions,
|
||||
Flake8Bugbear,
|
||||
Flake8Builtins,
|
||||
Flake8Print,
|
||||
Flake8Quotes,
|
||||
Meta,
|
||||
}
|
||||
|
||||
impl CheckCategory {
|
||||
pub fn title(&self) -> &'static str {
|
||||
match self {
|
||||
CheckCategory::Pycodestyle => "pycodestyle",
|
||||
CheckCategory::PycodestyleError => "pycodestyle (error)",
|
||||
CheckCategory::PycodestyleWarning => "pycodestyle (warning)",
|
||||
CheckCategory::Pyflakes => "Pyflakes",
|
||||
CheckCategory::Flake8Builtins => "flake8-builtins",
|
||||
CheckCategory::Flake8Bugbear => "flake8-bugbear",
|
||||
CheckCategory::Flake8Comprehensions => "flake8-comprehensions",
|
||||
CheckCategory::Flake8Print => "flake8-print",
|
||||
CheckCategory::Flake8Quotes => "flake8-quotes",
|
||||
CheckCategory::Pyupgrade => "pyupgrade",
|
||||
CheckCategory::Pydocstyle => "pydocstyle",
|
||||
CheckCategory::PEP8Naming => "pep8-naming",
|
||||
CheckCategory::Meta => "Meta rules",
|
||||
}
|
||||
}
|
||||
@@ -236,8 +221,9 @@ impl CheckCategory {
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub enum LintSource {
|
||||
AST,
|
||||
Lines,
|
||||
FileSystem,
|
||||
Lines,
|
||||
Tokens,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -252,18 +238,30 @@ pub enum CheckKind {
|
||||
AmbiguousClassName(String),
|
||||
AmbiguousFunctionName(String),
|
||||
AmbiguousVariableName(String),
|
||||
DoNotAssignLambda,
|
||||
DoNotUseBareExcept,
|
||||
IOError(String),
|
||||
LineTooLong(usize, usize),
|
||||
ModuleImportNotAtTopOfFile,
|
||||
NoneComparison(RejectedCmpop),
|
||||
NotInTest,
|
||||
NotIsTest,
|
||||
SyntaxError(String),
|
||||
TrueFalseComparison(bool, RejectedCmpop),
|
||||
TypeComparison,
|
||||
// pycodestyle warnings
|
||||
NoNewLineAtEndOfFile,
|
||||
InvalidEscapeSequence(char),
|
||||
// pyflakes
|
||||
AssertTuple,
|
||||
BreakOutsideLoop,
|
||||
ContinueOutsideLoop,
|
||||
DefaultExceptNotLast,
|
||||
DoNotAssignLambda,
|
||||
DoNotUseBareExcept,
|
||||
DuplicateArgumentName,
|
||||
ExpressionsInStarAssignment,
|
||||
FStringMissingPlaceholders,
|
||||
ForwardAnnotationSyntaxError(String),
|
||||
FutureFeatureNotDefined(String),
|
||||
IOError(String),
|
||||
IfTuple,
|
||||
ImportShadowedByLoopVar(String, usize),
|
||||
ImportStarNotPermitted(String),
|
||||
@@ -272,34 +270,28 @@ pub enum CheckKind {
|
||||
InvalidPrintSyntax,
|
||||
IsLiteral,
|
||||
LateFutureImport,
|
||||
LineTooLong(usize, usize),
|
||||
ModuleImportNotAtTopOfFile,
|
||||
MultiValueRepeatedKeyLiteral,
|
||||
MultiValueRepeatedKeyVariable(String),
|
||||
NoneComparison(RejectedCmpop),
|
||||
NotInTest,
|
||||
NotIsTest,
|
||||
RaiseNotImplemented,
|
||||
ReturnOutsideFunction,
|
||||
SyntaxError(String),
|
||||
TrueFalseComparison(bool, RejectedCmpop),
|
||||
TwoStarredExpressions,
|
||||
TypeComparison,
|
||||
UndefinedExport(String),
|
||||
UndefinedLocal(String),
|
||||
UndefinedName(String),
|
||||
UnusedImport(Vec<String>),
|
||||
UnusedImport(Vec<String>, bool),
|
||||
UnusedVariable(String),
|
||||
YieldOutsideFunction,
|
||||
// pycodestyle warnings
|
||||
NoNewLineAtEndOfFile,
|
||||
// flake8-builtin
|
||||
// flake8-builtins
|
||||
BuiltinVariableShadowing(String),
|
||||
BuiltinArgumentShadowing(String),
|
||||
BuiltinAttributeShadowing(String),
|
||||
// flake8-bugbear
|
||||
UnaryPrefixIncrement,
|
||||
UnusedLoopControlVariable(String),
|
||||
DoNotAssertFalse,
|
||||
RedundantTupleInExceptionHandler(String),
|
||||
DuplicateHandlerException(Vec<String>),
|
||||
NoAssertRaisesException,
|
||||
DuplicateTryBlockException(String),
|
||||
// flake8-comprehensions
|
||||
UnnecessaryGeneratorList,
|
||||
@@ -321,6 +313,11 @@ pub enum CheckKind {
|
||||
// flake8-print
|
||||
PrintFound,
|
||||
PPrintFound,
|
||||
// flake8-quotes
|
||||
BadQuotesInlineString(Quote),
|
||||
BadQuotesMultilineString(Quote),
|
||||
BadQuotesDocstring(Quote),
|
||||
AvoidQuoteEscape,
|
||||
// pyupgrade
|
||||
TypeOfPrimitive(Primitive),
|
||||
UnnecessaryAbspath,
|
||||
@@ -333,6 +330,7 @@ pub enum CheckKind {
|
||||
// pydocstyle
|
||||
BlankLineAfterLastSection(String),
|
||||
BlankLineAfterSection(String),
|
||||
BlankLineAfterSummary,
|
||||
BlankLineBeforeSection(String),
|
||||
CapitalizeSectionName(String),
|
||||
DashedUnderlineAfterSection(String),
|
||||
@@ -348,7 +346,6 @@ pub enum CheckKind {
|
||||
NewLineAfterLastParagraph,
|
||||
NewLineAfterSectionName(String),
|
||||
NoBlankLineAfterFunction(usize),
|
||||
NoBlankLineAfterSummary,
|
||||
NoBlankLineBeforeClass(usize),
|
||||
NoBlankLineBeforeFunction(usize),
|
||||
NoBlankLinesBetweenHeaderAndContent(String),
|
||||
@@ -375,6 +372,22 @@ pub enum CheckKind {
|
||||
SectionUnderlineNotOverIndented(String),
|
||||
SkipDocstring,
|
||||
UsesTripleQuotes,
|
||||
// pep8-naming
|
||||
InvalidClassName(String),
|
||||
InvalidFunctionName(String),
|
||||
InvalidArgumentName(String),
|
||||
InvalidFirstArgumentNameForClassMethod,
|
||||
InvalidFirstArgumentNameForMethod,
|
||||
NonLowercaseVariableInFunction(String),
|
||||
DunderFunctionName,
|
||||
ConstantImportedAsNonConstant(String, String),
|
||||
LowercaseImportedAsNonLowercase(String, String),
|
||||
CamelcaseImportedAsLowercase(String, String),
|
||||
CamelcaseImportedAsConstant(String, String),
|
||||
MixedCaseVariableInClassScope(String),
|
||||
MixedCaseVariableInGlobalScope(String),
|
||||
CamelcaseImportedAsAcronym(String, String),
|
||||
ErrorSuffixOnExceptionName(String),
|
||||
// Meta
|
||||
UnusedNOQA(Option<Vec<String>>),
|
||||
}
|
||||
@@ -384,6 +397,11 @@ impl CheckCode {
|
||||
pub fn lint_source(&self) -> &'static LintSource {
|
||||
match self {
|
||||
CheckCode::E501 | CheckCode::W292 | CheckCode::M001 => &LintSource::Lines,
|
||||
CheckCode::W605
|
||||
| CheckCode::Q000
|
||||
| CheckCode::Q001
|
||||
| CheckCode::Q002
|
||||
| CheckCode::Q003 => &LintSource::Tokens,
|
||||
CheckCode::E902 => &LintSource::FileSystem,
|
||||
_ => &LintSource::AST,
|
||||
}
|
||||
@@ -409,8 +427,9 @@ impl CheckCode {
|
||||
CheckCode::E999 => CheckKind::SyntaxError("`...`".to_string()),
|
||||
// pycodestyle warnings
|
||||
CheckCode::W292 => CheckKind::NoNewLineAtEndOfFile,
|
||||
CheckCode::W605 => CheckKind::InvalidEscapeSequence('c'),
|
||||
// pyflakes
|
||||
CheckCode::F401 => CheckKind::UnusedImport(vec!["...".to_string()]),
|
||||
CheckCode::F401 => CheckKind::UnusedImport(vec!["...".to_string()], false),
|
||||
CheckCode::F402 => CheckKind::ImportShadowedByLoopVar("...".to_string(), 1),
|
||||
CheckCode::F403 => CheckKind::ImportStarUsed("...".to_string()),
|
||||
CheckCode::F404 => CheckKind::LateFutureImport,
|
||||
@@ -445,8 +464,14 @@ impl CheckCode {
|
||||
CheckCode::A002 => CheckKind::BuiltinArgumentShadowing("...".to_string()),
|
||||
CheckCode::A003 => CheckKind::BuiltinAttributeShadowing("...".to_string()),
|
||||
// flake8-bugbear
|
||||
CheckCode::B002 => CheckKind::UnaryPrefixIncrement,
|
||||
CheckCode::B007 => CheckKind::UnusedLoopControlVariable("i".to_string()),
|
||||
CheckCode::B011 => CheckKind::DoNotAssertFalse,
|
||||
CheckCode::B013 => {
|
||||
CheckKind::RedundantTupleInExceptionHandler("ValueError".to_string())
|
||||
}
|
||||
CheckCode::B014 => CheckKind::DuplicateHandlerException(vec!["ValueError".to_string()]),
|
||||
CheckCode::B017 => CheckKind::NoAssertRaisesException,
|
||||
CheckCode::B025 => CheckKind::DuplicateTryBlockException("Exception".to_string()),
|
||||
// flake8-comprehensions
|
||||
CheckCode::C400 => CheckKind::UnnecessaryGeneratorList,
|
||||
@@ -454,33 +479,38 @@ impl CheckCode {
|
||||
CheckCode::C402 => CheckKind::UnnecessaryGeneratorDict,
|
||||
CheckCode::C403 => CheckKind::UnnecessaryListComprehensionSet,
|
||||
CheckCode::C404 => CheckKind::UnnecessaryListComprehensionDict,
|
||||
CheckCode::C405 => CheckKind::UnnecessaryLiteralSet("<list/tuple>".to_string()),
|
||||
CheckCode::C406 => CheckKind::UnnecessaryLiteralDict("<list/tuple>".to_string()),
|
||||
CheckCode::C405 => CheckKind::UnnecessaryLiteralSet("(list|tuple)".to_string()),
|
||||
CheckCode::C406 => CheckKind::UnnecessaryLiteralDict("(list|tuple)".to_string()),
|
||||
CheckCode::C408 => {
|
||||
CheckKind::UnnecessaryCollectionCall("<dict/list/tuple>".to_string())
|
||||
CheckKind::UnnecessaryCollectionCall("(dict|list|tuple)".to_string())
|
||||
}
|
||||
CheckCode::C409 => {
|
||||
CheckKind::UnnecessaryLiteralWithinTupleCall("<list/tuple>".to_string())
|
||||
CheckKind::UnnecessaryLiteralWithinTupleCall("(list|tuple)".to_string())
|
||||
}
|
||||
CheckCode::C410 => {
|
||||
CheckKind::UnnecessaryLiteralWithinListCall("<list/tuple>".to_string())
|
||||
CheckKind::UnnecessaryLiteralWithinListCall("(list|tuple)".to_string())
|
||||
}
|
||||
CheckCode::C411 => CheckKind::UnnecessaryListCall,
|
||||
CheckCode::C413 => {
|
||||
CheckKind::UnnecessaryCallAroundSorted("<list/reversed>".to_string())
|
||||
CheckKind::UnnecessaryCallAroundSorted("(list|reversed)".to_string())
|
||||
}
|
||||
CheckCode::C414 => CheckKind::UnnecessaryDoubleCastOrProcess(
|
||||
"<list/reversed/set/sorted/tuple>".to_string(),
|
||||
"<list/set/sorted/tuple>".to_string(),
|
||||
"(list|reversed|set|sorted|tuple)".to_string(),
|
||||
"(list|set|sorted|tuple)".to_string(),
|
||||
),
|
||||
CheckCode::C415 => {
|
||||
CheckKind::UnnecessarySubscriptReversal("<reversed/set/sorted>".to_string())
|
||||
CheckKind::UnnecessarySubscriptReversal("(reversed|set|sorted)".to_string())
|
||||
}
|
||||
CheckCode::C416 => CheckKind::UnnecessaryComprehension("<list/set>".to_string()),
|
||||
CheckCode::C417 => CheckKind::UnnecessaryMap("<list/set/dict>".to_string()),
|
||||
CheckCode::C416 => CheckKind::UnnecessaryComprehension("(list|set)".to_string()),
|
||||
CheckCode::C417 => CheckKind::UnnecessaryMap("(list|set|dict)".to_string()),
|
||||
// flake8-print
|
||||
CheckCode::T201 => CheckKind::PrintFound,
|
||||
CheckCode::T203 => CheckKind::PPrintFound,
|
||||
// flake8-quotes
|
||||
CheckCode::Q000 => CheckKind::BadQuotesInlineString(Quote::Double),
|
||||
CheckCode::Q001 => CheckKind::BadQuotesMultilineString(Quote::Double),
|
||||
CheckCode::Q002 => CheckKind::BadQuotesDocstring(Quote::Double),
|
||||
CheckCode::Q003 => CheckKind::AvoidQuoteEscape,
|
||||
// pyupgrade
|
||||
CheckCode::U001 => CheckKind::UselessMetaclassType,
|
||||
CheckCode::U002 => CheckKind::UnnecessaryAbspath,
|
||||
@@ -507,7 +537,7 @@ impl CheckCode {
|
||||
CheckCode::D202 => CheckKind::NoBlankLineAfterFunction(1),
|
||||
CheckCode::D203 => CheckKind::OneBlankLineBeforeClass(0),
|
||||
CheckCode::D204 => CheckKind::OneBlankLineAfterClass(0),
|
||||
CheckCode::D205 => CheckKind::NoBlankLineAfterSummary,
|
||||
CheckCode::D205 => CheckKind::BlankLineAfterSummary,
|
||||
CheckCode::D206 => CheckKind::IndentWithSpaces,
|
||||
CheckCode::D207 => CheckKind::NoUnderIndentation,
|
||||
CheckCode::D208 => CheckKind::NoOverIndentation,
|
||||
@@ -544,6 +574,32 @@ impl CheckCode {
|
||||
}
|
||||
CheckCode::D418 => CheckKind::SkipDocstring,
|
||||
CheckCode::D419 => CheckKind::NonEmpty,
|
||||
// pep8-naming
|
||||
CheckCode::N801 => CheckKind::InvalidClassName("...".to_string()),
|
||||
CheckCode::N802 => CheckKind::InvalidFunctionName("...".to_string()),
|
||||
CheckCode::N803 => CheckKind::InvalidArgumentName("...".to_string()),
|
||||
CheckCode::N804 => CheckKind::InvalidFirstArgumentNameForClassMethod,
|
||||
CheckCode::N805 => CheckKind::InvalidFirstArgumentNameForMethod,
|
||||
CheckCode::N806 => CheckKind::NonLowercaseVariableInFunction("...".to_string()),
|
||||
CheckCode::N807 => CheckKind::DunderFunctionName,
|
||||
CheckCode::N811 => {
|
||||
CheckKind::ConstantImportedAsNonConstant("...".to_string(), "...".to_string())
|
||||
}
|
||||
CheckCode::N812 => {
|
||||
CheckKind::LowercaseImportedAsNonLowercase("...".to_string(), "...".to_string())
|
||||
}
|
||||
CheckCode::N813 => {
|
||||
CheckKind::CamelcaseImportedAsLowercase("...".to_string(), "...".to_string())
|
||||
}
|
||||
CheckCode::N814 => {
|
||||
CheckKind::CamelcaseImportedAsConstant("...".to_string(), "...".to_string())
|
||||
}
|
||||
CheckCode::N815 => CheckKind::MixedCaseVariableInClassScope("mixedCase".to_string()),
|
||||
CheckCode::N816 => CheckKind::MixedCaseVariableInGlobalScope("mixedCase".to_string()),
|
||||
CheckCode::N817 => {
|
||||
CheckKind::CamelcaseImportedAsAcronym("...".to_string(), "...".to_string())
|
||||
}
|
||||
CheckCode::N818 => CheckKind::ErrorSuffixOnExceptionName("...".to_string()),
|
||||
// Meta
|
||||
CheckCode::M001 => CheckKind::UnusedNOQA(None),
|
||||
}
|
||||
@@ -551,21 +607,22 @@ impl CheckCode {
|
||||
|
||||
pub fn category(&self) -> CheckCategory {
|
||||
match self {
|
||||
CheckCode::E402 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E501 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E711 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E712 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E713 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E714 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E721 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E722 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E731 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E741 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E742 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E743 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E902 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E999 => CheckCategory::Pycodestyle,
|
||||
CheckCode::W292 => CheckCategory::Pycodestyle,
|
||||
CheckCode::E402 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E501 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E711 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E712 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E713 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E714 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E721 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E722 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E731 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E741 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E742 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E743 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E902 => CheckCategory::PycodestyleError,
|
||||
CheckCode::E999 => CheckCategory::PycodestyleError,
|
||||
CheckCode::W292 => CheckCategory::PycodestyleWarning,
|
||||
CheckCode::W605 => CheckCategory::PycodestyleWarning,
|
||||
CheckCode::F401 => CheckCategory::Pyflakes,
|
||||
CheckCode::F402 => CheckCategory::Pyflakes,
|
||||
CheckCode::F403 => CheckCategory::Pyflakes,
|
||||
@@ -597,8 +654,12 @@ impl CheckCode {
|
||||
CheckCode::A001 => CheckCategory::Flake8Builtins,
|
||||
CheckCode::A002 => CheckCategory::Flake8Builtins,
|
||||
CheckCode::A003 => CheckCategory::Flake8Builtins,
|
||||
CheckCode::B002 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B007 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B011 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B013 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B014 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B017 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B025 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::C400 => CheckCategory::Flake8Comprehensions,
|
||||
CheckCode::C401 => CheckCategory::Flake8Comprehensions,
|
||||
@@ -618,6 +679,10 @@ impl CheckCode {
|
||||
CheckCode::C417 => CheckCategory::Flake8Comprehensions,
|
||||
CheckCode::T201 => CheckCategory::Flake8Print,
|
||||
CheckCode::T203 => CheckCategory::Flake8Print,
|
||||
CheckCode::Q000 => CheckCategory::Flake8Quotes,
|
||||
CheckCode::Q001 => CheckCategory::Flake8Quotes,
|
||||
CheckCode::Q002 => CheckCategory::Flake8Quotes,
|
||||
CheckCode::Q003 => CheckCategory::Flake8Quotes,
|
||||
CheckCode::U001 => CheckCategory::Pyupgrade,
|
||||
CheckCode::U002 => CheckCategory::Pyupgrade,
|
||||
CheckCode::U003 => CheckCategory::Pyupgrade,
|
||||
@@ -670,6 +735,21 @@ impl CheckCode {
|
||||
CheckCode::D417 => CheckCategory::Pydocstyle,
|
||||
CheckCode::D418 => CheckCategory::Pydocstyle,
|
||||
CheckCode::D419 => CheckCategory::Pydocstyle,
|
||||
CheckCode::N801 => CheckCategory::PEP8Naming,
|
||||
CheckCode::N802 => CheckCategory::PEP8Naming,
|
||||
CheckCode::N803 => CheckCategory::PEP8Naming,
|
||||
CheckCode::N804 => CheckCategory::PEP8Naming,
|
||||
CheckCode::N805 => CheckCategory::PEP8Naming,
|
||||
CheckCode::N806 => CheckCategory::PEP8Naming,
|
||||
CheckCode::N807 => CheckCategory::PEP8Naming,
|
||||
CheckCode::N811 => CheckCategory::PEP8Naming,
|
||||
CheckCode::N812 => CheckCategory::PEP8Naming,
|
||||
CheckCode::N813 => CheckCategory::PEP8Naming,
|
||||
CheckCode::N814 => CheckCategory::PEP8Naming,
|
||||
CheckCode::N815 => CheckCategory::PEP8Naming,
|
||||
CheckCode::N816 => CheckCategory::PEP8Naming,
|
||||
CheckCode::N817 => CheckCategory::PEP8Naming,
|
||||
CheckCode::N818 => CheckCategory::PEP8Naming,
|
||||
CheckCode::M001 => CheckCategory::Meta,
|
||||
}
|
||||
}
|
||||
@@ -719,18 +799,23 @@ impl CheckKind {
|
||||
CheckKind::UndefinedExport(_) => &CheckCode::F822,
|
||||
CheckKind::UndefinedLocal(_) => &CheckCode::F823,
|
||||
CheckKind::UndefinedName(_) => &CheckCode::F821,
|
||||
CheckKind::UnusedImport(_) => &CheckCode::F401,
|
||||
CheckKind::UnusedImport(_, _) => &CheckCode::F401,
|
||||
CheckKind::UnusedVariable(_) => &CheckCode::F841,
|
||||
CheckKind::YieldOutsideFunction => &CheckCode::F704,
|
||||
// pycodestyle warnings
|
||||
CheckKind::NoNewLineAtEndOfFile => &CheckCode::W292,
|
||||
CheckKind::InvalidEscapeSequence(_) => &CheckCode::W605,
|
||||
// flake8-builtins
|
||||
CheckKind::BuiltinVariableShadowing(_) => &CheckCode::A001,
|
||||
CheckKind::BuiltinArgumentShadowing(_) => &CheckCode::A002,
|
||||
CheckKind::BuiltinAttributeShadowing(_) => &CheckCode::A003,
|
||||
// flake8-bugbear
|
||||
CheckKind::UnaryPrefixIncrement => &CheckCode::B002,
|
||||
CheckKind::UnusedLoopControlVariable(_) => &CheckCode::B007,
|
||||
CheckKind::DoNotAssertFalse => &CheckCode::B011,
|
||||
CheckKind::RedundantTupleInExceptionHandler(_) => &CheckCode::B013,
|
||||
CheckKind::DuplicateHandlerException(_) => &CheckCode::B014,
|
||||
CheckKind::NoAssertRaisesException => &CheckCode::B017,
|
||||
CheckKind::DuplicateTryBlockException(_) => &CheckCode::B025,
|
||||
// flake8-comprehensions
|
||||
CheckKind::UnnecessaryGeneratorList => &CheckCode::C400,
|
||||
@@ -752,6 +837,11 @@ impl CheckKind {
|
||||
// flake8-print
|
||||
CheckKind::PrintFound => &CheckCode::T201,
|
||||
CheckKind::PPrintFound => &CheckCode::T203,
|
||||
// flake8-quotes
|
||||
CheckKind::BadQuotesInlineString(_) => &CheckCode::Q000,
|
||||
CheckKind::BadQuotesMultilineString(_) => &CheckCode::Q001,
|
||||
CheckKind::BadQuotesDocstring(_) => &CheckCode::Q002,
|
||||
CheckKind::AvoidQuoteEscape => &CheckCode::Q003,
|
||||
// pyupgrade
|
||||
CheckKind::TypeOfPrimitive(_) => &CheckCode::U003,
|
||||
CheckKind::UnnecessaryAbspath => &CheckCode::U002,
|
||||
@@ -779,7 +869,7 @@ impl CheckKind {
|
||||
CheckKind::NewLineAfterLastParagraph => &CheckCode::D209,
|
||||
CheckKind::NewLineAfterSectionName(_) => &CheckCode::D406,
|
||||
CheckKind::NoBlankLineAfterFunction(_) => &CheckCode::D202,
|
||||
CheckKind::NoBlankLineAfterSummary => &CheckCode::D205,
|
||||
CheckKind::BlankLineAfterSummary => &CheckCode::D205,
|
||||
CheckKind::NoBlankLineBeforeClass(_) => &CheckCode::D211,
|
||||
CheckKind::NoBlankLineBeforeFunction(_) => &CheckCode::D201,
|
||||
CheckKind::NoBlankLinesBetweenHeaderAndContent(_) => &CheckCode::D412,
|
||||
@@ -806,6 +896,22 @@ impl CheckKind {
|
||||
CheckKind::SectionUnderlineNotOverIndented(_) => &CheckCode::D215,
|
||||
CheckKind::SkipDocstring => &CheckCode::D418,
|
||||
CheckKind::UsesTripleQuotes => &CheckCode::D300,
|
||||
// pep8-naming
|
||||
CheckKind::InvalidClassName(_) => &CheckCode::N801,
|
||||
CheckKind::InvalidFunctionName(_) => &CheckCode::N802,
|
||||
CheckKind::InvalidArgumentName(_) => &CheckCode::N803,
|
||||
CheckKind::InvalidFirstArgumentNameForClassMethod => &CheckCode::N804,
|
||||
CheckKind::InvalidFirstArgumentNameForMethod => &CheckCode::N805,
|
||||
CheckKind::NonLowercaseVariableInFunction(..) => &CheckCode::N806,
|
||||
CheckKind::DunderFunctionName => &CheckCode::N807,
|
||||
CheckKind::ConstantImportedAsNonConstant(..) => &CheckCode::N811,
|
||||
CheckKind::LowercaseImportedAsNonLowercase(..) => &CheckCode::N812,
|
||||
CheckKind::CamelcaseImportedAsLowercase(..) => &CheckCode::N813,
|
||||
CheckKind::CamelcaseImportedAsConstant(..) => &CheckCode::N814,
|
||||
CheckKind::MixedCaseVariableInClassScope(..) => &CheckCode::N815,
|
||||
CheckKind::MixedCaseVariableInGlobalScope(..) => &CheckCode::N816,
|
||||
CheckKind::CamelcaseImportedAsAcronym(..) => &CheckCode::N817,
|
||||
CheckKind::ErrorSuffixOnExceptionName(..) => &CheckCode::N818,
|
||||
// Meta
|
||||
CheckKind::UnusedNOQA(_) => &CheckCode::M001,
|
||||
}
|
||||
@@ -932,9 +1038,13 @@ impl CheckKind {
|
||||
CheckKind::UndefinedName(name) => {
|
||||
format!("Undefined name `{name}`")
|
||||
}
|
||||
CheckKind::UnusedImport(names) => {
|
||||
CheckKind::UnusedImport(names, in_init_py) => {
|
||||
let names = names.iter().map(|name| format!("`{name}`")).join(", ");
|
||||
format!("{names} imported but unused")
|
||||
if *in_init_py {
|
||||
format!("{names} imported but unused and missing from `__all__`")
|
||||
} else {
|
||||
format!("{names} imported but unused")
|
||||
}
|
||||
}
|
||||
CheckKind::UnusedVariable(name) => {
|
||||
format!("Local variable `{name}` is assigned to but never used")
|
||||
@@ -944,6 +1054,7 @@ impl CheckKind {
|
||||
}
|
||||
// pycodestyle warnings
|
||||
CheckKind::NoNewLineAtEndOfFile => "No newline at end of file".to_string(),
|
||||
CheckKind::InvalidEscapeSequence(char) => format!("Invalid escape sequence: '\\{char}'"),
|
||||
// flake8-builtins
|
||||
CheckKind::BuiltinVariableShadowing(name) => {
|
||||
format!("Variable `{name}` is shadowing a python builtin")
|
||||
@@ -955,10 +1066,15 @@ impl CheckKind {
|
||||
format!("Class attribute `{name}` is shadowing a python builtin")
|
||||
}
|
||||
// flake8-bugbear
|
||||
CheckKind::UnaryPrefixIncrement => "Python does not support the unary prefix increment. Writing `++n` is equivalent to `+(+(n))`, which equals `n`. You meant `n += 1`.".to_string(),
|
||||
CheckKind::UnusedLoopControlVariable(name) => format!("Loop control variable `{name}` not used within the loop body. If this is intended, start the name with an underscore."),
|
||||
CheckKind::DoNotAssertFalse => {
|
||||
"Do not `assert False` (`python -O` removes these calls), raise `AssertionError()`"
|
||||
.to_string()
|
||||
}
|
||||
CheckKind::RedundantTupleInExceptionHandler(name) => {
|
||||
format!("A length-one tuple literal is redundant. Write `except {name}:` instead of `except ({name},):`.")
|
||||
}
|
||||
CheckKind::DuplicateHandlerException(names) => {
|
||||
if names.len() == 1 {
|
||||
let name = &names[0];
|
||||
@@ -968,81 +1084,104 @@ impl CheckKind {
|
||||
format!("Exception handler with duplicate exceptions: {names}")
|
||||
}
|
||||
}
|
||||
CheckKind::NoAssertRaisesException => {
|
||||
"`assertRaises(Exception):` should be considered evil. It can lead to your test passing even if the code being tested is never executed due to a typo. Either assert for a more specific exception (builtin or custom), use `assertRaisesRegex`, or use the context manager form of `assertRaises`.".to_string()
|
||||
}
|
||||
CheckKind::DuplicateTryBlockException(name) => {
|
||||
format!("try-except block with duplicate exception `{name}`")
|
||||
}
|
||||
// flake8-comprehensions
|
||||
CheckKind::UnnecessaryGeneratorList => {
|
||||
"Unnecessary generator - rewrite as a list comprehension".to_string()
|
||||
"Unnecessary generator (rewrite as a `list` comprehension)".to_string()
|
||||
}
|
||||
CheckKind::UnnecessaryGeneratorSet => {
|
||||
"Unnecessary generator - rewrite as a set comprehension".to_string()
|
||||
"Unnecessary generator (rewrite as a `set` comprehension)".to_string()
|
||||
}
|
||||
CheckKind::UnnecessaryGeneratorDict => {
|
||||
"Unnecessary generator - rewrite as a dict comprehension".to_string()
|
||||
"Unnecessary generator (rewrite as a `dict` comprehension)".to_string()
|
||||
}
|
||||
CheckKind::UnnecessaryListComprehensionSet => {
|
||||
"Unnecessary list comprehension - rewrite as a set comprehension".to_string()
|
||||
"Unnecessary `list` comprehension (rewrite as a `set` comprehension)".to_string()
|
||||
}
|
||||
CheckKind::UnnecessaryListComprehensionDict => {
|
||||
"Unnecessary list comprehension - rewrite as a dict comprehension".to_string()
|
||||
"Unnecessary `list` comprehension (rewrite as a `dict` comprehension)".to_string()
|
||||
}
|
||||
CheckKind::UnnecessaryLiteralSet(obj_type) => {
|
||||
format!("Unnecessary {obj_type} literal - rewrite as a set literal")
|
||||
format!("Unnecessary `{obj_type}` literal (rewrite as a `set` literal)")
|
||||
}
|
||||
CheckKind::UnnecessaryLiteralDict(obj_type) => {
|
||||
format!("Unnecessary {obj_type} literal - rewrite as a dict literal")
|
||||
format!("Unnecessary `{obj_type}` literal (rewrite as a `dict` literal)")
|
||||
}
|
||||
CheckKind::UnnecessaryCollectionCall(obj_type) => {
|
||||
format!("Unnecessary {obj_type} call - rewrite as a literal")
|
||||
format!("Unnecessary `{obj_type}` call (rewrite as a literal)")
|
||||
}
|
||||
CheckKind::UnnecessaryLiteralWithinTupleCall(literal) => {
|
||||
if literal == "list" {
|
||||
format!(
|
||||
"Unnecessary {literal} literal passed to tuple() - rewrite as a tuple literal"
|
||||
"Unnecessary `{literal}` literal passed to `tuple()` (rewrite as a `tuple` literal)"
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Unnecessary {literal} literal passed to tuple() - remove the outer call to tuple()"
|
||||
"Unnecessary `{literal}` literal passed to `tuple()` (remove the outer call to `tuple()`)"
|
||||
)
|
||||
}
|
||||
}
|
||||
CheckKind::UnnecessaryLiteralWithinListCall(literal) => {
|
||||
if literal == "list" {
|
||||
format!(
|
||||
"Unnecessary {literal} literal passed to list() - remove the outer call to list()"
|
||||
"Unnecessary `{literal}` literal passed to `list()` (remove the outer call to `list()`)"
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Unnecessary {literal} literal passed to list() - rewrite as a list literal"
|
||||
"Unnecessary `{literal}` literal passed to `list()` (rewrite as a `list` literal)"
|
||||
)
|
||||
}
|
||||
}
|
||||
CheckKind::UnnecessaryListCall => {
|
||||
"Unnecessary list call - remove the outer call to list()".to_string()
|
||||
"Unnecessary `list` call (remove the outer call to `list()`)".to_string()
|
||||
}
|
||||
CheckKind::UnnecessaryCallAroundSorted(func) => {
|
||||
format!("Unnecessary {func} call around sorted()")
|
||||
format!("Unnecessary `{func}` call around `sorted()`")
|
||||
}
|
||||
CheckKind::UnnecessaryDoubleCastOrProcess(inner, outer) => {
|
||||
format!("Unnecessary {inner} call within {outer}().")
|
||||
format!("Unnecessary `{inner}` call within `{outer}()`")
|
||||
}
|
||||
CheckKind::UnnecessarySubscriptReversal(func) => {
|
||||
format!("Unnecessary subscript reversal of iterable within {func}()")
|
||||
format!("Unnecessary subscript reversal of iterable within `{func}()`")
|
||||
}
|
||||
CheckKind::UnnecessaryComprehension(obj_type) => {
|
||||
format!(" Unnecessary {obj_type} comprehension - rewrite using {obj_type}()")
|
||||
format!("Unnecessary `{obj_type}` comprehension (rewrite using `{obj_type}()`)")
|
||||
}
|
||||
CheckKind::UnnecessaryMap(obj_type) => {
|
||||
if obj_type == "generator" {
|
||||
"Unnecessary map usage - rewrite using a generator expression".to_string()
|
||||
"Unnecessary `map` usage (rewrite using a generator expression)".to_string()
|
||||
} else {
|
||||
format!("Unnecessary map usage - rewrite using a {obj_type} comprehension")
|
||||
format!("Unnecessary `map` usage (rewrite using a `{obj_type}` comprehension)")
|
||||
}
|
||||
}
|
||||
// flake8-print
|
||||
CheckKind::PrintFound => "`print` found".to_string(),
|
||||
CheckKind::PPrintFound => "`pprint` found".to_string(),
|
||||
// flake8-quotes
|
||||
CheckKind::BadQuotesInlineString(quote) => {
|
||||
match quote {
|
||||
Quote::Single => "Double quotes found but single quotes preferred".to_string(),
|
||||
Quote::Double => "Single quotes found but double quotes preferred".to_string(),
|
||||
}
|
||||
},
|
||||
CheckKind::BadQuotesMultilineString(quote) => {
|
||||
match quote {
|
||||
Quote::Single => "Double quote multiline found but single quotes preferred".to_string(),
|
||||
Quote::Double => "Single quote multiline found but double quotes preferred".to_string(),
|
||||
}
|
||||
},
|
||||
CheckKind::BadQuotesDocstring(quote) => {
|
||||
match quote {
|
||||
Quote::Single => "Double quote docstring found but single quotes preferred".to_string(),
|
||||
Quote::Double => "Single quote docstring found but double quotes preferred".to_string(),
|
||||
}
|
||||
},
|
||||
CheckKind::AvoidQuoteEscape => "Change outer quotes to avoid escaping inner quotes".to_string(),
|
||||
// pyupgrade
|
||||
CheckKind::TypeOfPrimitive(primitive) => {
|
||||
format!("Use `{}` instead of `type(...)`", primitive.builtin())
|
||||
@@ -1070,7 +1209,7 @@ impl CheckKind {
|
||||
}
|
||||
// pydocstyle
|
||||
CheckKind::FitsOnOneLine => "One-line docstring should fit on one line".to_string(),
|
||||
CheckKind::NoBlankLineAfterSummary => {
|
||||
CheckKind::BlankLineAfterSummary => {
|
||||
"1 blank line required between summary line and description".to_string()
|
||||
}
|
||||
CheckKind::NewLineAfterLastParagraph => {
|
||||
@@ -1096,7 +1235,7 @@ impl CheckKind {
|
||||
"Multi-line docstring summary should start at the second line".to_string()
|
||||
}
|
||||
CheckKind::NoSignature => {
|
||||
"First line should not be the function's 'signature'".to_string()
|
||||
"First line should not be the function's signature".to_string()
|
||||
}
|
||||
CheckKind::NoBlankLineBeforeFunction(num_lines) => {
|
||||
format!("No blank lines allowed before function docstring (found {num_lines})")
|
||||
@@ -1180,6 +1319,52 @@ impl CheckKind {
|
||||
}
|
||||
CheckKind::NoUnderIndentation => "Docstring is under-indented".to_string(),
|
||||
CheckKind::NoOverIndentation => "Docstring is over-indented".to_string(),
|
||||
// pep8-naming
|
||||
CheckKind::InvalidClassName(name) => {
|
||||
format!("Class name `{name}` should use CapWords convention ")
|
||||
}
|
||||
CheckKind::InvalidFunctionName(name) => {
|
||||
format!("Function name `{name}` should be lowercase")
|
||||
}
|
||||
CheckKind::InvalidArgumentName(name) => {
|
||||
format!("Argument name `{name}` should be lowercase")
|
||||
}
|
||||
CheckKind::InvalidFirstArgumentNameForClassMethod => {
|
||||
"First argument of a class method should be named `cls`".to_string()
|
||||
}
|
||||
CheckKind::InvalidFirstArgumentNameForMethod => {
|
||||
"First argument of a method should be named `self`".to_string()
|
||||
}
|
||||
CheckKind::NonLowercaseVariableInFunction(name) => {
|
||||
format!("Variable `{name}` in function should be lowercase")
|
||||
}
|
||||
CheckKind::DunderFunctionName => {
|
||||
"Function name should not start and end with `__`".to_string()
|
||||
}
|
||||
CheckKind::ConstantImportedAsNonConstant(name, asname) => {
|
||||
format!("Constant `{name}` imported as non-constant `{asname}`")
|
||||
}
|
||||
CheckKind::LowercaseImportedAsNonLowercase(name, asname) => {
|
||||
format!("Lowercase `{name}` imported as non-lowercase `{asname}`")
|
||||
}
|
||||
CheckKind::CamelcaseImportedAsLowercase(name, asname) => {
|
||||
format!("Camelcase `{name}` imported as lowercase `{asname}`")
|
||||
}
|
||||
CheckKind::CamelcaseImportedAsConstant(name, asname) => {
|
||||
format!("Camelcase `{name}` imported as constant `{asname}`")
|
||||
}
|
||||
CheckKind::MixedCaseVariableInClassScope(name) => {
|
||||
format!("Variable `{name}` in class scope should not be mixedCase")
|
||||
}
|
||||
CheckKind::MixedCaseVariableInGlobalScope(name) => {
|
||||
format!("Variable `{name}` in global scope should not be mixedCase")
|
||||
}
|
||||
CheckKind::CamelcaseImportedAsAcronym(name, asname) => {
|
||||
format!("Camelcase `{name}` imported as acronym `{asname}`")
|
||||
}
|
||||
CheckKind::ErrorSuffixOnExceptionName(name) => {
|
||||
format!("Exception name `{name}` should be named with an Error suffix")
|
||||
}
|
||||
// Meta
|
||||
CheckKind::UnusedNOQA(codes) => match codes {
|
||||
None => "Unused `noqa` directive".to_string(),
|
||||
@@ -1200,36 +1385,67 @@ impl CheckKind {
|
||||
}
|
||||
}
|
||||
|
||||
/// The summary text for the check. Typically a truncated form of the body text.
|
||||
pub fn summary(&self) -> String {
|
||||
match self {
|
||||
CheckKind::UnaryPrefixIncrement => {
|
||||
"Python does not support the unary prefix increment.".to_string()
|
||||
}
|
||||
CheckKind::UnusedLoopControlVariable(name) => {
|
||||
format!("Loop control variable `{name}` not used within the loop body.")
|
||||
}
|
||||
CheckKind::NoAssertRaisesException => {
|
||||
"`assertRaises(Exception):` should be considered evil.".to_string()
|
||||
}
|
||||
_ => self.body(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the check kind is (potentially) fixable.
|
||||
pub fn fixable(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
CheckKind::DeprecatedUnittestAlias(_, _)
|
||||
CheckKind::BlankLineAfterLastSection(_)
|
||||
| CheckKind::BlankLineAfterSection(_)
|
||||
| CheckKind::BlankLineAfterSummary
|
||||
| CheckKind::BlankLineBeforeSection(_)
|
||||
| CheckKind::CapitalizeSectionName(_)
|
||||
| CheckKind::DashedUnderlineAfterSection(_)
|
||||
| CheckKind::DeprecatedUnittestAlias(_, _)
|
||||
| CheckKind::DoNotAssertFalse
|
||||
| CheckKind::DuplicateHandlerException(_)
|
||||
| CheckKind::NewLineAfterLastParagraph
|
||||
| CheckKind::NewLineAfterSectionName(_)
|
||||
| CheckKind::NoBlankLineAfterFunction(_)
|
||||
| CheckKind::NoBlankLineBeforeClass(_)
|
||||
| CheckKind::NoBlankLineBeforeFunction(_)
|
||||
| CheckKind::NoBlankLinesBetweenHeaderAndContent(_)
|
||||
| CheckKind::NoOverIndentation
|
||||
| CheckKind::NoSurroundingWhitespace
|
||||
| CheckKind::NoUnderIndentation
|
||||
| CheckKind::OneBlankLineAfterClass(_)
|
||||
| CheckKind::OneBlankLineBeforeClass(_)
|
||||
| CheckKind::PPrintFound
|
||||
| CheckKind::PrintFound
|
||||
| CheckKind::SectionNameEndsInColon(_)
|
||||
| CheckKind::SectionNotOverIndented(_)
|
||||
| CheckKind::SectionUnderlineAfterName(_)
|
||||
| CheckKind::SectionUnderlineMatchesSectionLength(_)
|
||||
| CheckKind::SectionUnderlineNotOverIndented(_)
|
||||
| CheckKind::SuperCallWithParameters
|
||||
| CheckKind::TypeOfPrimitive(_)
|
||||
| CheckKind::UnnecessaryAbspath
|
||||
| CheckKind::UnusedImport(_)
|
||||
| CheckKind::UnusedImport(_, false)
|
||||
| CheckKind::UnusedLoopControlVariable(_)
|
||||
| CheckKind::UnusedNOQA(_)
|
||||
| CheckKind::UselessMetaclassType
|
||||
| CheckKind::UselessObjectInheritance(_)
|
||||
| CheckKind::UsePEP585Annotation(_)
|
||||
| CheckKind::UsePEP604Annotation
|
||||
| CheckKind::UselessMetaclassType
|
||||
| CheckKind::UselessObjectInheritance(_)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Fix {
|
||||
pub content: String,
|
||||
pub location: Location,
|
||||
pub end_location: Location,
|
||||
pub applied: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Check {
|
||||
pub kind: CheckKind,
|
||||
@@ -1239,11 +1455,11 @@ pub struct Check {
|
||||
}
|
||||
|
||||
impl Check {
|
||||
pub fn new(kind: CheckKind, span: Range) -> Self {
|
||||
pub fn new(kind: CheckKind, range: Range) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
location: span.location,
|
||||
end_location: span.end_location,
|
||||
location: range.location,
|
||||
end_location: range.end_location,
|
||||
fix: None,
|
||||
}
|
||||
}
|
||||
|
||||
1101
src/checks_gen.rs
Normal file
1101
src/checks_gen.rs
Normal file
File diff suppressed because it is too large
Load Diff
37
src/cli.rs
37
src/cli.rs
@@ -5,11 +5,11 @@ use clap::{command, Parser};
|
||||
use log::warn;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::checks::CheckCode;
|
||||
use crate::checks_gen::CheckCodePrefix;
|
||||
use crate::printer::SerializationFormat;
|
||||
use crate::pyproject::StrCheckCodePair;
|
||||
use crate::settings::PythonVersion;
|
||||
use crate::RawSettings;
|
||||
use crate::settings::configuration::Configuration;
|
||||
use crate::settings::types::PythonVersion;
|
||||
use crate::settings::types::StrCheckCodePair;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(author, about = "ruff: An extremely fast Python linter.")]
|
||||
@@ -21,11 +21,14 @@ pub struct Cli {
|
||||
#[arg(long)]
|
||||
pub config: Option<PathBuf>,
|
||||
/// Enable verbose logging.
|
||||
#[arg(short, long)]
|
||||
#[arg(short, long, group = "verbosity")]
|
||||
pub verbose: bool,
|
||||
/// Disable all logging (but still exit with status code "1" upon detecting errors).
|
||||
#[arg(short, long)]
|
||||
/// Only log errors.
|
||||
#[arg(short, long, group = "verbosity")]
|
||||
pub quiet: bool,
|
||||
/// Disable all logging (but still exit with status code "1" upon detecting errors).
|
||||
#[arg(short, long, group = "verbosity")]
|
||||
pub silent: bool,
|
||||
/// Exit with status code "0", even upon detecting errors.
|
||||
#[arg(short, long)]
|
||||
pub exit_zero: bool,
|
||||
@@ -40,16 +43,16 @@ pub struct Cli {
|
||||
pub no_cache: bool,
|
||||
/// List of error codes to enable.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub select: Vec<CheckCode>,
|
||||
pub select: Vec<CheckCodePrefix>,
|
||||
/// Like --select, but adds additional error codes on top of the selected ones.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub extend_select: Vec<CheckCode>,
|
||||
pub extend_select: Vec<CheckCodePrefix>,
|
||||
/// List of error codes to ignore.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub ignore: Vec<CheckCode>,
|
||||
pub ignore: Vec<CheckCodePrefix>,
|
||||
/// Like --ignore, but adds additional error codes on top of the ignored ones.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub extend_ignore: Vec<CheckCode>,
|
||||
pub extend_ignore: Vec<CheckCodePrefix>,
|
||||
/// List of paths, used to exclude files and/or directories from checks.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub exclude: Vec<String>,
|
||||
@@ -103,10 +106,10 @@ impl fmt::Display for Warnable {
|
||||
/// Warn the user if they attempt to enable a code that won't be respected.
|
||||
pub fn warn_on(
|
||||
flag: Warnable,
|
||||
codes: &Vec<CheckCode>,
|
||||
cli_ignore: &Vec<CheckCode>,
|
||||
cli_extend_ignore: &Vec<CheckCode>,
|
||||
pyproject_settings: &RawSettings,
|
||||
codes: &[CheckCodePrefix],
|
||||
cli_ignore: &[CheckCodePrefix],
|
||||
cli_extend_ignore: &[CheckCodePrefix],
|
||||
pyproject_configuration: &Configuration,
|
||||
pyproject_path: &Option<PathBuf>,
|
||||
) {
|
||||
for code in codes {
|
||||
@@ -114,7 +117,7 @@ pub fn warn_on(
|
||||
if cli_ignore.contains(code) {
|
||||
warn!("{code:?} was passed to {flag}, but ignored via --ignore")
|
||||
}
|
||||
} else if pyproject_settings.ignore.contains(code) {
|
||||
} else if pyproject_configuration.ignore.contains(code) {
|
||||
if let Some(path) = pyproject_path {
|
||||
warn!(
|
||||
"{code:?} was passed to {flag}, but ignored by the `ignore` field in {}",
|
||||
@@ -128,7 +131,7 @@ pub fn warn_on(
|
||||
if cli_extend_ignore.contains(code) {
|
||||
warn!("{code:?} was passed to {flag}, but ignored via --extend-ignore")
|
||||
}
|
||||
} else if pyproject_settings.extend_ignore.contains(code) {
|
||||
} else if pyproject_configuration.extend_ignore.contains(code) {
|
||||
if let Some(path) = pyproject_path {
|
||||
warn!(
|
||||
"{code:?} was passed to {flag}, but ignored by the `extend_ignore` field in {}",
|
||||
|
||||
42
src/cst/helpers.rs
Normal file
42
src/cst/helpers.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use libcst_native::{Expression, NameOrAttribute};
|
||||
|
||||
fn compose_call_path_inner<'a>(expr: &'a Expression, parts: &mut Vec<&'a str>) {
|
||||
match &expr {
|
||||
Expression::Call(expr) => {
|
||||
compose_call_path_inner(&expr.func, parts);
|
||||
}
|
||||
Expression::Attribute(expr) => {
|
||||
compose_call_path_inner(&expr.value, parts);
|
||||
parts.push(expr.attr.value);
|
||||
}
|
||||
Expression::Name(expr) => {
|
||||
parts.push(expr.value);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compose_call_path(expr: &Expression) -> Option<String> {
|
||||
let mut segments = vec![];
|
||||
compose_call_path_inner(expr, &mut segments);
|
||||
if segments.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(segments.join("."))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compose_module_path(module: &NameOrAttribute) -> String {
|
||||
match module {
|
||||
NameOrAttribute::N(name) => name.value.to_string(),
|
||||
NameOrAttribute::A(attr) => {
|
||||
let name = attr.attr.value;
|
||||
let prefix = compose_call_path(&attr.value);
|
||||
if let Some(prefix) = prefix {
|
||||
format!("{prefix}.{name}")
|
||||
} else {
|
||||
name.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/cst/mod.rs
Normal file
1
src/cst/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod helpers;
|
||||
@@ -1,8 +0,0 @@
|
||||
pub mod docstring_plugins;
|
||||
pub mod extraction;
|
||||
mod google;
|
||||
mod helpers;
|
||||
mod numpy;
|
||||
pub mod sections;
|
||||
mod styles;
|
||||
pub mod types;
|
||||
@@ -1,707 +0,0 @@
|
||||
//! Abstractions for tracking and validating docstrings in Python code.
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use rustpython_ast::{Constant, ExprKind, Location, StmtKind};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckCode, CheckKind};
|
||||
use crate::docstrings::google::check_google_section;
|
||||
use crate::docstrings::helpers;
|
||||
use crate::docstrings::helpers::{indentation, leading_space};
|
||||
use crate::docstrings::numpy::check_numpy_section;
|
||||
use crate::docstrings::sections::section_contexts;
|
||||
use crate::docstrings::styles::SectionStyle;
|
||||
use crate::docstrings::types::{Definition, DefinitionKind};
|
||||
use crate::visibility::{is_init, is_magic, is_overload, Visibility};
|
||||
|
||||
/// D100, D101, D102, D103, D104, D105, D106, D107
|
||||
pub fn not_missing(
|
||||
checker: &mut Checker,
|
||||
definition: &Definition,
|
||||
visibility: &Visibility,
|
||||
) -> bool {
|
||||
if matches!(visibility, Visibility::Private) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if definition.docstring.is_some() {
|
||||
return true;
|
||||
}
|
||||
|
||||
match definition.kind {
|
||||
DefinitionKind::Module => {
|
||||
if checker.settings.enabled.contains(&CheckCode::D100) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::PublicModule,
|
||||
Range {
|
||||
location: Location::new(1, 1),
|
||||
end_location: Location::new(1, 1),
|
||||
},
|
||||
));
|
||||
}
|
||||
false
|
||||
}
|
||||
DefinitionKind::Package => {
|
||||
if checker.settings.enabled.contains(&CheckCode::D104) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::PublicPackage,
|
||||
Range {
|
||||
location: Location::new(1, 1),
|
||||
end_location: Location::new(1, 1),
|
||||
},
|
||||
));
|
||||
}
|
||||
false
|
||||
}
|
||||
DefinitionKind::Class(stmt) => {
|
||||
if checker.settings.enabled.contains(&CheckCode::D101) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::PublicClass,
|
||||
Range::from_located(stmt),
|
||||
));
|
||||
}
|
||||
false
|
||||
}
|
||||
DefinitionKind::NestedClass(stmt) => {
|
||||
if checker.settings.enabled.contains(&CheckCode::D106) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::PublicNestedClass,
|
||||
Range::from_located(stmt),
|
||||
));
|
||||
}
|
||||
false
|
||||
}
|
||||
DefinitionKind::Function(stmt) | DefinitionKind::NestedFunction(stmt) => {
|
||||
if is_overload(stmt) {
|
||||
true
|
||||
} else {
|
||||
if checker.settings.enabled.contains(&CheckCode::D103) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::PublicFunction,
|
||||
Range::from_located(stmt),
|
||||
));
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
DefinitionKind::Method(stmt) => {
|
||||
if is_overload(stmt) {
|
||||
true
|
||||
} else if is_magic(stmt) {
|
||||
if checker.settings.enabled.contains(&CheckCode::D105) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::MagicMethod,
|
||||
Range::from_located(stmt),
|
||||
));
|
||||
}
|
||||
true
|
||||
} else if is_init(stmt) {
|
||||
if checker.settings.enabled.contains(&CheckCode::D107) {
|
||||
checker.add_check(Check::new(CheckKind::PublicInit, Range::from_located(stmt)));
|
||||
}
|
||||
true
|
||||
} else {
|
||||
if checker.settings.enabled.contains(&CheckCode::D102) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::PublicMethod,
|
||||
Range::from_located(stmt),
|
||||
));
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// D200
|
||||
pub fn one_liner(checker: &mut Checker, definition: &Definition) {
|
||||
if let Some(docstring) = &definition.docstring {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(string),
|
||||
..
|
||||
} = &docstring.node
|
||||
{
|
||||
let mut line_count = 0;
|
||||
let mut non_empty_line_count = 0;
|
||||
for line in string.lines() {
|
||||
line_count += 1;
|
||||
if !line.trim().is_empty() {
|
||||
non_empty_line_count += 1;
|
||||
}
|
||||
if non_empty_line_count > 1 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if non_empty_line_count == 1 && line_count > 1 {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::FitsOnOneLine,
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static COMMENT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\s*#").unwrap());
|
||||
|
||||
static INNER_FUNCTION_OR_CLASS_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^\s+(?:(?:class|def|async def)\s|@)").unwrap());
|
||||
|
||||
/// D201, D202
|
||||
pub fn blank_before_after_function(checker: &mut Checker, definition: &Definition) {
|
||||
if let Some(docstring) = definition.docstring {
|
||||
if let DefinitionKind::Function(parent)
|
||||
| DefinitionKind::NestedFunction(parent)
|
||||
| DefinitionKind::Method(parent) = &definition.kind
|
||||
{
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(_),
|
||||
..
|
||||
} = &docstring.node
|
||||
{
|
||||
let (before, _, after) = checker.locator.partition_source_code_at(
|
||||
&Range::from_located(parent),
|
||||
&helpers::range_for(docstring),
|
||||
);
|
||||
|
||||
if checker.settings.enabled.contains(&CheckCode::D201) {
|
||||
let blank_lines_before = before
|
||||
.lines()
|
||||
.rev()
|
||||
.skip(1)
|
||||
.take_while(|line| line.trim().is_empty())
|
||||
.count();
|
||||
if blank_lines_before != 0 {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoBlankLineBeforeFunction(blank_lines_before),
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if checker.settings.enabled.contains(&CheckCode::D202) {
|
||||
let blank_lines_after = after
|
||||
.lines()
|
||||
.skip(1)
|
||||
.take_while(|line| line.trim().is_empty())
|
||||
.count();
|
||||
let all_blank_after = after
|
||||
.lines()
|
||||
.skip(1)
|
||||
.all(|line| line.trim().is_empty() || COMMENT_REGEX.is_match(line));
|
||||
// Report a D202 violation if the docstring is followed by a blank line
|
||||
// and the blank line is not itself followed by an inner function or
|
||||
// class.
|
||||
if !all_blank_after
|
||||
&& blank_lines_after != 0
|
||||
&& !(blank_lines_after == 1
|
||||
&& INNER_FUNCTION_OR_CLASS_REGEX.is_match(after))
|
||||
{
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoBlankLineAfterFunction(blank_lines_after),
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// D203, D204, D211
|
||||
pub fn blank_before_after_class(checker: &mut Checker, definition: &Definition) {
|
||||
if let Some(docstring) = &definition.docstring {
|
||||
if let DefinitionKind::Class(parent) | DefinitionKind::NestedClass(parent) =
|
||||
&definition.kind
|
||||
{
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(_),
|
||||
..
|
||||
} = &docstring.node
|
||||
{
|
||||
let (before, _, after) = checker.locator.partition_source_code_at(
|
||||
&Range::from_located(parent),
|
||||
&helpers::range_for(docstring),
|
||||
);
|
||||
|
||||
if checker.settings.enabled.contains(&CheckCode::D203)
|
||||
|| checker.settings.enabled.contains(&CheckCode::D211)
|
||||
{
|
||||
let blank_lines_before = before
|
||||
.lines()
|
||||
.rev()
|
||||
.skip(1)
|
||||
.take_while(|line| line.trim().is_empty())
|
||||
.count();
|
||||
if blank_lines_before != 0
|
||||
&& checker.settings.enabled.contains(&CheckCode::D211)
|
||||
{
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoBlankLineBeforeClass(blank_lines_before),
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
if blank_lines_before != 1
|
||||
&& checker.settings.enabled.contains(&CheckCode::D203)
|
||||
{
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::OneBlankLineBeforeClass(blank_lines_before),
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if checker.settings.enabled.contains(&CheckCode::D204) {
|
||||
let blank_lines_after = after
|
||||
.lines()
|
||||
.skip(1)
|
||||
.take_while(|line| line.trim().is_empty())
|
||||
.count();
|
||||
let all_blank_after = after
|
||||
.lines()
|
||||
.skip(1)
|
||||
.all(|line| line.trim().is_empty() || COMMENT_REGEX.is_match(line));
|
||||
if !all_blank_after && blank_lines_after != 1 {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::OneBlankLineAfterClass(blank_lines_after),
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// D205
|
||||
pub fn blank_after_summary(checker: &mut Checker, definition: &Definition) {
|
||||
if let Some(docstring) = definition.docstring {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(string),
|
||||
..
|
||||
} = &docstring.node
|
||||
{
|
||||
let mut lines_count = 1;
|
||||
let mut blanks_count = 0;
|
||||
for line in string.trim().lines().skip(1) {
|
||||
lines_count += 1;
|
||||
if line.trim().is_empty() {
|
||||
blanks_count += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if lines_count > 1 && blanks_count != 1 {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoBlankLineAfterSummary,
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// D206, D207, D208
|
||||
pub fn indent(checker: &mut Checker, definition: &Definition) {
|
||||
if let Some(docstring) = definition.docstring {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(string),
|
||||
..
|
||||
} = &docstring.node
|
||||
{
|
||||
let lines: Vec<&str> = string.lines().collect();
|
||||
if lines.len() <= 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut has_seen_tab = false;
|
||||
let mut has_seen_over_indent = false;
|
||||
let mut has_seen_under_indent = false;
|
||||
|
||||
let docstring_indent = indentation(checker, docstring).to_string();
|
||||
if !has_seen_tab {
|
||||
if docstring_indent.contains('\t') {
|
||||
if checker.settings.enabled.contains(&CheckCode::D206) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::IndentWithSpaces,
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
has_seen_tab = true;
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..lines.len() {
|
||||
// First lines and continuations doesn't need any indentation.
|
||||
if i == 0 || lines[i - 1].ends_with('\\') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Omit empty lines, except for the last line, which is non-empty by way of
|
||||
// containing the closing quotation marks.
|
||||
if i < lines.len() - 1 && lines[i].trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let line_indent = leading_space(lines[i]);
|
||||
if !has_seen_tab {
|
||||
if line_indent.contains('\t') {
|
||||
if checker.settings.enabled.contains(&CheckCode::D206) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::IndentWithSpaces,
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
has_seen_tab = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !has_seen_over_indent {
|
||||
if line_indent.len() > docstring_indent.len() {
|
||||
if checker.settings.enabled.contains(&CheckCode::D208) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoOverIndentation,
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
has_seen_over_indent = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !has_seen_under_indent {
|
||||
if line_indent.len() < docstring_indent.len() {
|
||||
if checker.settings.enabled.contains(&CheckCode::D207) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoUnderIndentation,
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
has_seen_under_indent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// D209
|
||||
pub fn newline_after_last_paragraph(checker: &mut Checker, definition: &Definition) {
|
||||
if let Some(docstring) = definition.docstring {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(string),
|
||||
..
|
||||
} = &docstring.node
|
||||
{
|
||||
let mut line_count = 0;
|
||||
for line in string.lines() {
|
||||
if !line.trim().is_empty() {
|
||||
line_count += 1;
|
||||
}
|
||||
if line_count > 1 {
|
||||
let content = checker
|
||||
.locator
|
||||
.slice_source_code_range(&helpers::range_for(docstring));
|
||||
if let Some(line) = content.lines().last() {
|
||||
let line = line.trim();
|
||||
if line != "\"\"\"" && line != "'''" {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NewLineAfterLastParagraph,
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// D210
|
||||
pub fn no_surrounding_whitespace(checker: &mut Checker, definition: &Definition) {
|
||||
if let Some(docstring) = definition.docstring {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(string),
|
||||
..
|
||||
} = &docstring.node
|
||||
{
|
||||
let mut lines = string.lines();
|
||||
if let Some(line) = lines.next() {
|
||||
if line.trim().is_empty() {
|
||||
return;
|
||||
}
|
||||
if line.starts_with(' ') || (matches!(lines.next(), None) && line.ends_with(' ')) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoSurroundingWhitespace,
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// D212, D213
|
||||
pub fn multi_line_summary_start(checker: &mut Checker, definition: &Definition) {
|
||||
if let Some(docstring) = definition.docstring {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(string),
|
||||
..
|
||||
} = &docstring.node
|
||||
{
|
||||
if string.lines().nth(1).is_some() {
|
||||
let content = checker
|
||||
.locator
|
||||
.slice_source_code_range(&helpers::range_for(docstring));
|
||||
if let Some(first_line) = content.lines().next() {
|
||||
let first_line = first_line.trim();
|
||||
if first_line == "\"\"\"" || first_line == "'''" {
|
||||
if checker.settings.enabled.contains(&CheckCode::D212) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::MultiLineSummaryFirstLine,
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
if checker.settings.enabled.contains(&CheckCode::D213) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::MultiLineSummarySecondLine,
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// D300
|
||||
pub fn triple_quotes(checker: &mut Checker, definition: &Definition) {
|
||||
if let Some(docstring) = definition.docstring {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(string),
|
||||
..
|
||||
} = &docstring.node
|
||||
{
|
||||
let content = checker
|
||||
.locator
|
||||
.slice_source_code_range(&helpers::range_for(docstring));
|
||||
if string.contains("\"\"\"") {
|
||||
if !content.starts_with("'''") {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::UsesTripleQuotes,
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
} else if !content.starts_with("\"\"\"") {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::UsesTripleQuotes,
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// D400
|
||||
pub fn ends_with_period(checker: &mut Checker, definition: &Definition) {
|
||||
if let Some(docstring) = definition.docstring {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(string),
|
||||
..
|
||||
} = &docstring.node
|
||||
{
|
||||
if let Some(string) = string.lines().next() {
|
||||
if !string.ends_with('.') {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::EndsInPeriod,
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// D402
|
||||
pub fn no_signature(checker: &mut Checker, definition: &Definition) {
|
||||
if let Some(docstring) = definition.docstring {
|
||||
if let DefinitionKind::Function(parent)
|
||||
| DefinitionKind::NestedFunction(parent)
|
||||
| DefinitionKind::Method(parent) = definition.kind
|
||||
{
|
||||
if let StmtKind::FunctionDef { name, .. } = &parent.node {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(string),
|
||||
..
|
||||
} = &docstring.node
|
||||
{
|
||||
if let Some(first_line) = string.lines().next() {
|
||||
if first_line.contains(&format!("{name}(")) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoSignature,
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// D403
|
||||
pub fn capitalized(checker: &mut Checker, definition: &Definition) {
|
||||
if !matches!(definition.kind, DefinitionKind::Function(_)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(docstring) = definition.docstring {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(string),
|
||||
..
|
||||
} = &docstring.node
|
||||
{
|
||||
if let Some(first_word) = string.split(' ').next() {
|
||||
if first_word == first_word.to_uppercase() {
|
||||
return;
|
||||
}
|
||||
for char in first_word.chars() {
|
||||
if !char.is_ascii_alphabetic() && char != '\'' {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Some(first_char) = first_word.chars().next() {
|
||||
if !first_char.is_uppercase() {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::FirstLineCapitalized,
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// D404
|
||||
pub fn starts_with_this(checker: &mut Checker, definition: &Definition) {
|
||||
if let Some(docstring) = definition.docstring {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(string),
|
||||
..
|
||||
} = &docstring.node
|
||||
{
|
||||
let trimmed = string.trim();
|
||||
if trimmed.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(first_word) = string.split(' ').next() {
|
||||
if first_word
|
||||
.replace(|c: char| !c.is_alphanumeric(), "")
|
||||
.to_lowercase()
|
||||
== "this"
|
||||
{
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoThisPrefix,
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// D415
|
||||
pub fn ends_with_punctuation(checker: &mut Checker, definition: &Definition) {
|
||||
if let Some(docstring) = definition.docstring {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(string),
|
||||
..
|
||||
} = &docstring.node
|
||||
{
|
||||
if let Some(string) = string.lines().next() {
|
||||
if !(string.ends_with('.') || string.ends_with('!') || string.ends_with('?')) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::EndsInPunctuation,
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// D418
|
||||
pub fn if_needed(checker: &mut Checker, definition: &Definition) {
|
||||
if definition.docstring.is_some() {
|
||||
if let DefinitionKind::Function(stmt)
|
||||
| DefinitionKind::NestedFunction(stmt)
|
||||
| DefinitionKind::Method(stmt) = definition.kind
|
||||
{
|
||||
if is_overload(stmt) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::SkipDocstring,
|
||||
Range::from_located(stmt),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// D419
|
||||
pub fn not_empty(checker: &mut Checker, definition: &Definition) -> bool {
|
||||
if let Some(docstring) = definition.docstring {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(string),
|
||||
..
|
||||
} = &docstring.node
|
||||
{
|
||||
if string.trim().is_empty() {
|
||||
if checker.settings.enabled.contains(&CheckCode::D419) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NonEmpty,
|
||||
helpers::range_for(docstring),
|
||||
));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// D212, D214, D215, D405, D406, D407, D408, D409, D410, D411, D412, D413, D414, D416, D417
|
||||
pub fn sections(checker: &mut Checker, definition: &Definition) {
|
||||
if let Some(docstring) = definition.docstring {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(string),
|
||||
..
|
||||
} = &docstring.node
|
||||
{
|
||||
let lines: Vec<&str> = string.lines().collect();
|
||||
if lines.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
// First, interpret as NumPy-style sections.
|
||||
let mut found_numpy_section = false;
|
||||
for context in §ion_contexts(&lines, &SectionStyle::NumPy) {
|
||||
found_numpy_section = true;
|
||||
check_numpy_section(checker, definition, context);
|
||||
}
|
||||
|
||||
// If no such sections were identified, interpret as Google-style sections.
|
||||
if !found_numpy_section {
|
||||
for context in §ion_contexts(&lines, &SectionStyle::Google) {
|
||||
check_google_section(checker, definition, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use rustpython_ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
|
||||
|
||||
use crate::docstrings::types::{Definition, DefinitionKind, Documentable};
|
||||
use crate::docstrings::definition::{Definition, DefinitionKind, Documentable};
|
||||
use crate::visibility::{Modifier, VisibleScope};
|
||||
|
||||
/// Extract a docstring from a function or class body.
|
||||
|
||||
@@ -3,15 +3,6 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckCode, CheckKind};
|
||||
use crate::docstrings::helpers::range_for;
|
||||
use crate::docstrings::sections;
|
||||
use crate::docstrings::sections::SectionContext;
|
||||
use crate::docstrings::styles::SectionStyle;
|
||||
use crate::docstrings::types::Definition;
|
||||
|
||||
pub(crate) static GOOGLE_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
|
||||
BTreeSet::from([
|
||||
@@ -78,73 +69,3 @@ pub(crate) static LOWERCASE_GOOGLE_SECTION_NAMES: Lazy<BTreeSet<&'static str>> =
|
||||
"yields",
|
||||
])
|
||||
});
|
||||
|
||||
// See: `GOOGLE_ARGS_REGEX` in `pydocstyle/checker.py`.
|
||||
static GOOGLE_ARGS_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^\s*(\w+)\s*(\(.*?\))?\s*:\n?\s*.+").expect("Invalid regex"));
|
||||
|
||||
fn check_args_section(checker: &mut Checker, definition: &Definition, context: &SectionContext) {
|
||||
let mut args_sections: Vec<String> = vec![];
|
||||
for line in textwrap::dedent(&context.following_lines.join("\n")).lines() {
|
||||
if line
|
||||
.chars()
|
||||
.next()
|
||||
.map(|char| char.is_whitespace())
|
||||
.unwrap_or(true)
|
||||
{
|
||||
// This is a continuation of documentation for the last
|
||||
// parameter because it does start with whitespace.
|
||||
if let Some(current) = args_sections.last_mut() {
|
||||
current.push_str(line);
|
||||
}
|
||||
} else {
|
||||
// This line is the start of documentation for the next
|
||||
// parameter because it doesn't start with any whitespace.
|
||||
args_sections.push(line.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
sections::check_missing_args(
|
||||
checker,
|
||||
definition,
|
||||
// Collect the list of arguments documented in the docstring.
|
||||
&BTreeSet::from_iter(args_sections.iter().filter_map(|section| {
|
||||
match GOOGLE_ARGS_REGEX.captures(section.as_str()) {
|
||||
Some(caps) => caps.get(1).map(|arg_name| arg_name.as_str()),
|
||||
None => None,
|
||||
}
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn check_google_section(
|
||||
checker: &mut Checker,
|
||||
definition: &Definition,
|
||||
context: &SectionContext,
|
||||
) {
|
||||
sections::check_common_section(checker, definition, context, &SectionStyle::Google);
|
||||
|
||||
if checker.settings.enabled.contains(&CheckCode::D416) {
|
||||
let suffix = context
|
||||
.line
|
||||
.trim()
|
||||
.strip_prefix(&context.section_name)
|
||||
.unwrap();
|
||||
if suffix != ":" {
|
||||
let docstring = definition
|
||||
.docstring
|
||||
.expect("Sections are only available for docstrings.");
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::SectionNameEndsInColon(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
if checker.settings.enabled.contains(&CheckCode::D417) {
|
||||
let capitalized_section_name = titlecase::titlecase(&context.section_name);
|
||||
if capitalized_section_name == "Args" || capitalized_section_name == "Arguments" {
|
||||
check_args_section(checker, definition, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@ use rustpython_ast::{Expr, Location};
|
||||
use crate::ast::types::Range;
|
||||
use crate::check_ast::Checker;
|
||||
|
||||
pub const TRIPLE_QUOTE_PREFIXES: &[&str] = &[
|
||||
"ur\"\"\"", "ur'''", "u\"\"\"", "u'''", "r\"\"\"", "r'''", "\"\"\"", "'''",
|
||||
];
|
||||
|
||||
pub const SINGLE_QUOTE_PREFIXES: &[&str] = &["ur\"", "ur'", "u\"", "u'", "r\"", "r'", "\"", "'"];
|
||||
|
||||
/// Extract the leading words from a line of text.
|
||||
pub fn leading_words(line: &str) -> String {
|
||||
line.trim()
|
||||
@@ -19,19 +25,18 @@ pub fn leading_space(line: &str) -> String {
|
||||
}
|
||||
|
||||
/// Extract the leading indentation from a docstring.
|
||||
pub fn indentation<'a>(checker: &'a mut Checker, docstring: &Expr) -> &'a str {
|
||||
let range = range_for(docstring);
|
||||
pub fn indentation<'a>(checker: &'a Checker, docstring: &Expr) -> &'a str {
|
||||
let range = Range::from_located(docstring);
|
||||
checker.locator.slice_source_code_range(&Range {
|
||||
location: Location::new(range.location.row(), 1),
|
||||
end_location: Location::new(range.location.row(), range.location.column()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Extract the source code range for a docstring.
|
||||
pub fn range_for(docstring: &Expr) -> Range {
|
||||
// RustPython currently omits the first quotation mark in a string, so offset the location.
|
||||
Range {
|
||||
location: Location::new(docstring.location.row(), docstring.location.column() - 1),
|
||||
end_location: docstring.end_location,
|
||||
}
|
||||
/// Replace any non-whitespace characters from an indentation string.
|
||||
pub fn clean(indentation: &str) -> String {
|
||||
indentation
|
||||
.chars()
|
||||
.map(|char| if char.is_whitespace() { char } else { ' ' })
|
||||
.collect()
|
||||
}
|
||||
|
||||
7
src/docstrings/mod.rs
Normal file
7
src/docstrings/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod definition;
|
||||
pub mod extraction;
|
||||
pub mod google;
|
||||
pub mod helpers;
|
||||
pub mod numpy;
|
||||
pub mod sections;
|
||||
pub mod styles;
|
||||
@@ -4,14 +4,6 @@ use std::collections::BTreeSet;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckCode, CheckKind};
|
||||
use crate::docstrings::helpers::range_for;
|
||||
use crate::docstrings::sections::SectionContext;
|
||||
use crate::docstrings::styles::SectionStyle;
|
||||
use crate::docstrings::types::Definition;
|
||||
use crate::docstrings::{helpers, sections};
|
||||
|
||||
pub(crate) static LOWERCASE_NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
|
||||
BTreeSet::from([
|
||||
"short summary",
|
||||
@@ -47,68 +39,3 @@ pub(crate) static NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(
|
||||
"Methods",
|
||||
])
|
||||
});
|
||||
|
||||
fn check_parameters_section(
|
||||
checker: &mut Checker,
|
||||
definition: &Definition,
|
||||
context: &SectionContext,
|
||||
) {
|
||||
// Collect the list of arguments documented in the docstring.
|
||||
let mut docstring_args: BTreeSet<&str> = Default::default();
|
||||
let section_level_indent = helpers::leading_space(context.line);
|
||||
for i in 1..context.following_lines.len() {
|
||||
let current_line = context.following_lines[i - 1];
|
||||
let current_leading_space = helpers::leading_space(current_line);
|
||||
let next_line = context.following_lines[i];
|
||||
if current_leading_space == section_level_indent
|
||||
&& (helpers::leading_space(next_line).len() > current_leading_space.len())
|
||||
&& !next_line.trim().is_empty()
|
||||
{
|
||||
let parameters = if let Some(semi_index) = current_line.find(':') {
|
||||
// If the parameter has a type annotation, exclude it.
|
||||
¤t_line[..semi_index]
|
||||
} else {
|
||||
// Otherwise, it's just a list of parameters on the current line.
|
||||
current_line.trim()
|
||||
};
|
||||
// Notably, NumPy lets you put multiple parameters of the same type on the same line.
|
||||
for parameter in parameters.split(',') {
|
||||
docstring_args.insert(parameter.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Validate that all arguments were documented.
|
||||
sections::check_missing_args(checker, definition, &docstring_args);
|
||||
}
|
||||
|
||||
pub(crate) fn check_numpy_section(
|
||||
checker: &mut Checker,
|
||||
definition: &Definition,
|
||||
context: &SectionContext,
|
||||
) {
|
||||
sections::check_common_section(checker, definition, context, &SectionStyle::NumPy);
|
||||
|
||||
if checker.settings.enabled.contains(&CheckCode::D406) {
|
||||
let suffix = context
|
||||
.line
|
||||
.trim()
|
||||
.strip_prefix(&context.section_name)
|
||||
.unwrap();
|
||||
if !suffix.is_empty() {
|
||||
let docstring = definition
|
||||
.docstring
|
||||
.expect("Sections are only available for docstrings.");
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NewLineAfterSectionName(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
if checker.settings.enabled.contains(&CheckCode::D417) {
|
||||
let capitalized_section_name = titlecase::titlecase(&context.section_name);
|
||||
if capitalized_section_name == "Parameters" {
|
||||
check_parameters_section(checker, definition, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use itertools::Itertools;
|
||||
use rustpython_ast::{Arg, StmtKind};
|
||||
use titlecase::titlecase;
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckCode, CheckKind};
|
||||
use crate::docstrings::helpers;
|
||||
use crate::docstrings::helpers::range_for;
|
||||
use crate::docstrings::styles::SectionStyle;
|
||||
use crate::docstrings::types::{Definition, DefinitionKind};
|
||||
use crate::visibility::is_static;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SectionContext<'a> {
|
||||
@@ -20,7 +8,7 @@ pub(crate) struct SectionContext<'a> {
|
||||
pub(crate) line: &'a str,
|
||||
pub(crate) following_lines: &'a [&'a str],
|
||||
pub(crate) is_last_section: bool,
|
||||
original_index: usize,
|
||||
pub(crate) original_index: usize,
|
||||
}
|
||||
|
||||
fn suspected_as_section(line: &str, style: &SectionStyle) -> bool {
|
||||
@@ -109,269 +97,3 @@ pub(crate) fn section_contexts<'a>(
|
||||
truncated_contexts.reverse();
|
||||
truncated_contexts
|
||||
}
|
||||
|
||||
fn check_blanks_and_section_underline(
|
||||
checker: &mut Checker,
|
||||
definition: &Definition,
|
||||
context: &SectionContext,
|
||||
) {
|
||||
let docstring = definition
|
||||
.docstring
|
||||
.expect("Sections are only available for docstrings.");
|
||||
|
||||
let mut blank_lines_after_header = 0;
|
||||
for line in context.following_lines {
|
||||
if !line.trim().is_empty() {
|
||||
break;
|
||||
}
|
||||
blank_lines_after_header += 1;
|
||||
}
|
||||
|
||||
// Nothing but blank lines after the section header.
|
||||
if blank_lines_after_header == context.following_lines.len() {
|
||||
if checker.settings.enabled.contains(&CheckCode::D407) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::DashedUnderlineAfterSection(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
if checker.settings.enabled.contains(&CheckCode::D414) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NonEmptySection(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let non_empty_line = context.following_lines[blank_lines_after_header];
|
||||
let dash_line_found = non_empty_line
|
||||
.chars()
|
||||
.all(|char| char.is_whitespace() || char == '-');
|
||||
|
||||
if !dash_line_found {
|
||||
if checker.settings.enabled.contains(&CheckCode::D407) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::DashedUnderlineAfterSection(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
if blank_lines_after_header > 0 {
|
||||
if checker.settings.enabled.contains(&CheckCode::D212) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoBlankLinesBetweenHeaderAndContent(
|
||||
context.section_name.to_string(),
|
||||
),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if blank_lines_after_header > 0 {
|
||||
if checker.settings.enabled.contains(&CheckCode::D408) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::SectionUnderlineAfterName(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if non_empty_line
|
||||
.trim()
|
||||
.chars()
|
||||
.filter(|char| *char == '-')
|
||||
.count()
|
||||
!= context.section_name.len()
|
||||
{
|
||||
if checker.settings.enabled.contains(&CheckCode::D409) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::SectionUnderlineMatchesSectionLength(
|
||||
context.section_name.to_string(),
|
||||
),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if checker.settings.enabled.contains(&CheckCode::D215) {
|
||||
if helpers::leading_space(non_empty_line).len()
|
||||
> helpers::indentation(checker, docstring).len()
|
||||
{
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::SectionUnderlineNotOverIndented(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let line_after_dashes_index = blank_lines_after_header + 1;
|
||||
|
||||
if line_after_dashes_index < context.following_lines.len() {
|
||||
let line_after_dashes = context.following_lines[line_after_dashes_index];
|
||||
if line_after_dashes.trim().is_empty() {
|
||||
let rest_of_lines = &context.following_lines[line_after_dashes_index..];
|
||||
if rest_of_lines.iter().all(|line| line.trim().is_empty()) {
|
||||
if checker.settings.enabled.contains(&CheckCode::D414) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NonEmptySection(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
if checker.settings.enabled.contains(&CheckCode::D412) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoBlankLinesBetweenHeaderAndContent(
|
||||
context.section_name.to_string(),
|
||||
),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if checker.settings.enabled.contains(&CheckCode::D414) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NonEmptySection(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check_common_section(
|
||||
checker: &mut Checker,
|
||||
definition: &Definition,
|
||||
context: &SectionContext,
|
||||
style: &SectionStyle,
|
||||
) {
|
||||
let docstring = definition
|
||||
.docstring
|
||||
.expect("Sections are only available for docstrings.");
|
||||
|
||||
if checker.settings.enabled.contains(&CheckCode::D405) {
|
||||
if !style
|
||||
.section_names()
|
||||
.contains(&context.section_name.as_str())
|
||||
&& style
|
||||
.section_names()
|
||||
.contains(titlecase(&context.section_name).as_str())
|
||||
{
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::CapitalizeSectionName(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
if checker.settings.enabled.contains(&CheckCode::D214) {
|
||||
if helpers::leading_space(context.line).len()
|
||||
> helpers::indentation(checker, docstring).len()
|
||||
{
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::SectionNotOverIndented(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
if context
|
||||
.following_lines
|
||||
.last()
|
||||
.map(|line| !line.trim().is_empty())
|
||||
.unwrap_or(true)
|
||||
{
|
||||
if context.is_last_section {
|
||||
if checker.settings.enabled.contains(&CheckCode::D413) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::BlankLineAfterLastSection(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
if checker.settings.enabled.contains(&CheckCode::D410) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::BlankLineAfterSection(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if checker.settings.enabled.contains(&CheckCode::D411) {
|
||||
if !context.previous_line.is_empty() {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::BlankLineBeforeSection(context.section_name.to_string()),
|
||||
range_for(docstring),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
check_blanks_and_section_underline(checker, definition, context);
|
||||
}
|
||||
|
||||
pub(crate) fn check_missing_args(
|
||||
checker: &mut Checker,
|
||||
definition: &Definition,
|
||||
docstrings_args: &BTreeSet<&str>,
|
||||
) {
|
||||
if let DefinitionKind::Function(parent)
|
||||
| DefinitionKind::NestedFunction(parent)
|
||||
| DefinitionKind::Method(parent) = definition.kind
|
||||
{
|
||||
if let StmtKind::FunctionDef {
|
||||
args: arguments, ..
|
||||
}
|
||||
| StmtKind::AsyncFunctionDef {
|
||||
args: arguments, ..
|
||||
} = &parent.node
|
||||
{
|
||||
// Collect all the arguments into a single vector.
|
||||
let mut all_arguments: Vec<&Arg> = arguments
|
||||
.args
|
||||
.iter()
|
||||
.chain(arguments.posonlyargs.iter())
|
||||
.chain(arguments.kwonlyargs.iter())
|
||||
.skip(
|
||||
// If this is a non-static method, skip `cls` or `self`.
|
||||
if matches!(definition.kind, DefinitionKind::Method(_)) && !is_static(parent) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
if let Some(arg) = &arguments.vararg {
|
||||
all_arguments.push(arg);
|
||||
}
|
||||
if let Some(arg) = &arguments.kwarg {
|
||||
all_arguments.push(arg);
|
||||
}
|
||||
|
||||
// Look for arguments that weren't included in the docstring.
|
||||
let mut missing_args: BTreeSet<&str> = Default::default();
|
||||
for arg in all_arguments {
|
||||
let arg_name = arg.node.arg.as_str();
|
||||
if arg_name.starts_with('_') {
|
||||
continue;
|
||||
}
|
||||
if docstrings_args.contains(&arg_name) {
|
||||
continue;
|
||||
}
|
||||
missing_args.insert(arg_name);
|
||||
}
|
||||
|
||||
if !missing_args.is_empty() {
|
||||
let names = missing_args
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.sorted()
|
||||
.collect();
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::DocumentAllArguments(names),
|
||||
Range::from_located(parent),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
src/flake8_bugbear/mod.rs
Normal file
1
src/flake8_bugbear/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod plugins;
|
||||
@@ -1,9 +1,9 @@
|
||||
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Stmt, StmtKind};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::fixer;
|
||||
use crate::autofix::Fix;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckKind, Fix};
|
||||
use crate::checks::{Check, CheckKind};
|
||||
use crate::code_gen::SourceGenerator;
|
||||
|
||||
fn assertion_error(msg: &Option<Box<Expr>>) -> Stmt {
|
||||
@@ -43,16 +43,15 @@ pub fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: &Optio
|
||||
} = &test.node
|
||||
{
|
||||
let mut check = Check::new(CheckKind::DoNotAssertFalse, Range::from_located(test));
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
let mut generator = SourceGenerator::new();
|
||||
if let Ok(()) = generator.unparse_stmt(&assertion_error(msg)) {
|
||||
if let Ok(content) = generator.generate() {
|
||||
check.amend(Fix {
|
||||
check.amend(Fix::replacement(
|
||||
content,
|
||||
location: stmt.location,
|
||||
end_location: stmt.end_location,
|
||||
applied: false,
|
||||
})
|
||||
stmt.location,
|
||||
stmt.end_location.unwrap(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/flake8_bugbear/plugins/assert_raises_exception.rs
Normal file
25
src/flake8_bugbear/plugins/assert_raises_exception.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use rustpython_ast::{ExprKind, Stmt, Withitem};
|
||||
|
||||
use crate::ast::helpers::match_name_or_attr;
|
||||
use crate::ast::types::Range;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
|
||||
/// B017
|
||||
pub fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: &[Withitem]) {
|
||||
if let Some(item) = items.first() {
|
||||
let item_context = &item.context_expr;
|
||||
if let ExprKind::Call { func, args, .. } = &item_context.node {
|
||||
if match_name_or_attr(func, "assertRaises")
|
||||
&& args.len() == 1
|
||||
&& match_name_or_attr(args.first().unwrap(), "Exception")
|
||||
&& item.optional_vars.is_none()
|
||||
{
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoAssertRaisesException,
|
||||
Range::from_located(stmt),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,9 @@ use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKi
|
||||
|
||||
use crate::ast::helpers;
|
||||
use crate::ast::types::{CheckLocator, Range};
|
||||
use crate::autofix::fixer;
|
||||
use crate::autofix::Fix;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckCode, CheckKind, Fix};
|
||||
use crate::checks::{Check, CheckCode, CheckKind};
|
||||
use crate::code_gen::SourceGenerator;
|
||||
|
||||
fn type_pattern(elts: Vec<&Expr>) -> Expr {
|
||||
@@ -24,7 +24,7 @@ fn type_pattern(elts: Vec<&Expr>) -> Expr {
|
||||
pub fn duplicate_handler_exceptions(
|
||||
checker: &mut Checker,
|
||||
expr: &Expr,
|
||||
elts: &Vec<Expr>,
|
||||
elts: &[Expr],
|
||||
) -> BTreeSet<String> {
|
||||
let mut seen: BTreeSet<String> = Default::default();
|
||||
let mut duplicates: BTreeSet<String> = Default::default();
|
||||
@@ -49,17 +49,16 @@ pub fn duplicate_handler_exceptions(
|
||||
),
|
||||
checker.locate_check(Range::from_located(expr)),
|
||||
);
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
// TODO(charlie): If we have a single element, remove the tuple.
|
||||
let mut generator = SourceGenerator::new();
|
||||
if let Ok(()) = generator.unparse_expr(&type_pattern(unique_elts), 0) {
|
||||
if let Ok(content) = generator.generate() {
|
||||
check.amend(Fix {
|
||||
check.amend(Fix::replacement(
|
||||
content,
|
||||
location: expr.location,
|
||||
end_location: expr.end_location,
|
||||
applied: false,
|
||||
})
|
||||
expr.location,
|
||||
expr.end_location.unwrap(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/flake8_bugbear/plugins/mod.rs
Normal file
14
src/flake8_bugbear/plugins/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
pub use assert_false::assert_false;
|
||||
pub use assert_raises_exception::assert_raises_exception;
|
||||
pub use duplicate_exceptions::duplicate_exceptions;
|
||||
pub use duplicate_exceptions::duplicate_handler_exceptions;
|
||||
pub use redundant_tuple_in_exception_handler::redundant_tuple_in_exception_handler;
|
||||
pub use unary_prefix_increment::unary_prefix_increment;
|
||||
pub use unused_loop_control_variable::unused_loop_control_variable;
|
||||
|
||||
mod assert_false;
|
||||
mod assert_raises_exception;
|
||||
mod duplicate_exceptions;
|
||||
mod redundant_tuple_in_exception_handler;
|
||||
mod unary_prefix_increment;
|
||||
mod unused_loop_control_variable;
|
||||
@@ -0,0 +1,22 @@
|
||||
use rustpython_ast::{Excepthandler, ExcepthandlerKind, ExprKind};
|
||||
|
||||
use crate::ast::types::{CheckLocator, Range};
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
|
||||
/// B013
|
||||
pub fn redundant_tuple_in_exception_handler(checker: &mut Checker, handlers: &[Excepthandler]) {
|
||||
for handler in handlers {
|
||||
let ExcepthandlerKind::ExceptHandler { type_, .. } = &handler.node;
|
||||
if let Some(type_) = type_ {
|
||||
if let ExprKind::Tuple { elts, .. } = &type_.node {
|
||||
if elts.len() == 1 {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::RedundantTupleInExceptionHandler(elts[0].to_string()),
|
||||
checker.locate_check(Range::from_located(type_)),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/flake8_bugbear/plugins/unary_prefix_increment.rs
Normal file
19
src/flake8_bugbear/plugins/unary_prefix_increment.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use rustpython_ast::{Expr, ExprKind, Unaryop};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
|
||||
/// B002
|
||||
pub fn unary_prefix_increment(checker: &mut Checker, expr: &Expr, op: &Unaryop, operand: &Expr) {
|
||||
if matches!(op, Unaryop::UAdd) {
|
||||
if let ExprKind::UnaryOp { op, .. } = &operand.node {
|
||||
if matches!(op, Unaryop::UAdd) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::UnaryPrefixIncrement,
|
||||
Range::from_located(expr),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/flake8_bugbear/plugins/unused_loop_control_variable.rs
Normal file
79
src/flake8_bugbear/plugins/unused_loop_control_variable.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use rustpython_ast::{Expr, ExprKind, Stmt};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::ast::visitor;
|
||||
use crate::ast::visitor::Visitor;
|
||||
use crate::autofix::Fix;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
|
||||
/// Identify all `ExprKind::Name` nodes in an AST.
|
||||
struct NameFinder<'a> {
|
||||
/// A map from identifier to defining expression.
|
||||
names: BTreeMap<&'a str, &'a Expr>,
|
||||
}
|
||||
|
||||
impl NameFinder<'_> {
|
||||
fn new() -> Self {
|
||||
NameFinder {
|
||||
names: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Visitor<'b> for NameFinder<'a>
|
||||
where
|
||||
'b: 'a,
|
||||
{
|
||||
fn visit_expr(&mut self, expr: &'a Expr) {
|
||||
if let ExprKind::Name { id, .. } = &expr.node {
|
||||
self.names.insert(id, expr);
|
||||
}
|
||||
visitor::walk_expr(self, expr);
|
||||
}
|
||||
}
|
||||
|
||||
/// B007
|
||||
pub fn unused_loop_control_variable(checker: &mut Checker, target: &Expr, body: &[Stmt]) {
|
||||
let control_names = {
|
||||
let mut finder = NameFinder::new();
|
||||
finder.visit_expr(target);
|
||||
finder.names
|
||||
};
|
||||
|
||||
let used_names = {
|
||||
let mut finder = NameFinder::new();
|
||||
for stmt in body {
|
||||
finder.visit_stmt(stmt);
|
||||
}
|
||||
finder.names
|
||||
};
|
||||
|
||||
for (name, expr) in control_names {
|
||||
// Ignore names that are already underscore-prefixed.
|
||||
if name.starts_with('_') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore any names that are actually used in the loop body.
|
||||
if used_names.contains_key(name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut check = Check::new(
|
||||
CheckKind::UnusedLoopControlVariable(name.to_string()),
|
||||
Range::from_located(expr),
|
||||
);
|
||||
if checker.patch() {
|
||||
// Prefix the variable name with an underscore.
|
||||
check.amend(Fix::replacement(
|
||||
format!("_{name}"),
|
||||
expr.location,
|
||||
expr.end_location.unwrap(),
|
||||
))
|
||||
}
|
||||
checker.add_check(check);
|
||||
}
|
||||
}
|
||||
20
src/flake8_builtins/checks.rs
Normal file
20
src/flake8_builtins/checks.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use crate::ast::types::Range;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
use crate::flake8_builtins::types::ShadowingType;
|
||||
use crate::python::builtins::BUILTINS;
|
||||
|
||||
/// Check builtin name shadowing.
|
||||
pub fn builtin_shadowing(name: &str, location: Range, node_type: ShadowingType) -> Option<Check> {
|
||||
if BUILTINS.contains(&name) {
|
||||
Some(Check::new(
|
||||
match node_type {
|
||||
ShadowingType::Variable => CheckKind::BuiltinVariableShadowing(name.to_string()),
|
||||
ShadowingType::Argument => CheckKind::BuiltinArgumentShadowing(name.to_string()),
|
||||
ShadowingType::Attribute => CheckKind::BuiltinAttributeShadowing(name.to_string()),
|
||||
},
|
||||
location,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
2
src/flake8_builtins/mod.rs
Normal file
2
src/flake8_builtins/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod checks;
|
||||
pub mod types;
|
||||
5
src/flake8_builtins/types.rs
Normal file
5
src/flake8_builtins/types.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub enum ShadowingType {
|
||||
Variable,
|
||||
Argument,
|
||||
Attribute,
|
||||
}
|
||||
387
src/flake8_comprehensions/checks.rs
Normal file
387
src/flake8_comprehensions/checks.rs
Normal file
@@ -0,0 +1,387 @@
|
||||
use num_bigint::BigInt;
|
||||
use rustpython_ast::{Comprehension, Constant, Expr, ExprKind, KeywordData, Located, Unaryop};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
|
||||
fn function_name(func: &Expr) -> Option<&str> {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn exactly_one_argument_with_matching_function<'a>(
|
||||
name: &str,
|
||||
func: &Expr,
|
||||
args: &'a [Expr],
|
||||
) -> Option<&'a ExprKind> {
|
||||
if args.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
if function_name(func)? != name {
|
||||
return None;
|
||||
}
|
||||
Some(&args[0].node)
|
||||
}
|
||||
|
||||
fn first_argument_with_matching_function<'a>(
|
||||
name: &str,
|
||||
func: &Expr,
|
||||
args: &'a [Expr],
|
||||
) -> Option<&'a ExprKind> {
|
||||
if function_name(func)? != name {
|
||||
return None;
|
||||
}
|
||||
Some(&args.first()?.node)
|
||||
}
|
||||
|
||||
/// C400 (`list(generator)`)
|
||||
pub fn unnecessary_generator_list(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
|
||||
let argument = exactly_one_argument_with_matching_function("list", func, args)?;
|
||||
if let ExprKind::GeneratorExp { .. } = argument {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryGeneratorList,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// C401 (`set(generator)`)
|
||||
pub fn unnecessary_generator_set(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
|
||||
let argument = exactly_one_argument_with_matching_function("set", func, args)?;
|
||||
if let ExprKind::GeneratorExp { .. } = argument {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryGeneratorSet,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// C402 (`dict((x, y) for x, y in iterable)`)
|
||||
pub fn unnecessary_generator_dict(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
|
||||
let argument = exactly_one_argument_with_matching_function("dict", func, args)?;
|
||||
if let ExprKind::GeneratorExp { elt, .. } = argument {
|
||||
match &elt.node {
|
||||
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryGeneratorDict,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// C403 (`set([...])`)
|
||||
pub fn unnecessary_list_comprehension_set(
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
) -> Option<Check> {
|
||||
let argument = exactly_one_argument_with_matching_function("set", func, args)?;
|
||||
if let ExprKind::ListComp { .. } = &argument {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryListComprehensionSet,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// C404 (`dict([...])`)
|
||||
pub fn unnecessary_list_comprehension_dict(
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
) -> Option<Check> {
|
||||
let argument = exactly_one_argument_with_matching_function("dict", func, args)?;
|
||||
if let ExprKind::ListComp { elt, .. } = &argument {
|
||||
match &elt.node {
|
||||
ExprKind::Tuple { elts, .. } if elts.len() == 2 => {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryListComprehensionDict,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// C405 (`set([1, 2])`)
|
||||
pub fn unnecessary_literal_set(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
|
||||
let argument = exactly_one_argument_with_matching_function("set", func, args)?;
|
||||
let kind = match argument {
|
||||
ExprKind::List { .. } => "list",
|
||||
ExprKind::Tuple { .. } => "tuple",
|
||||
_ => return None,
|
||||
};
|
||||
Some(Check::new(
|
||||
CheckKind::UnnecessaryLiteralSet(kind.to_string()),
|
||||
Range::from_located(expr),
|
||||
))
|
||||
}
|
||||
|
||||
/// C406 (`dict([(1, 2)])`)
|
||||
pub fn unnecessary_literal_dict(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
|
||||
let argument = exactly_one_argument_with_matching_function("dict", func, args)?;
|
||||
let (kind, elts) = match argument {
|
||||
ExprKind::Tuple { elts, .. } => ("tuple", elts),
|
||||
ExprKind::List { elts, .. } => ("list", elts),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
if let Some(elt) = elts.first() {
|
||||
// dict((1, 2), ...)) or dict([(1, 2), ...])
|
||||
if !matches!(&elt.node, ExprKind::Tuple { elts, .. } if elts.len() == 2) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(Check::new(
|
||||
CheckKind::UnnecessaryLiteralDict(kind.to_string()),
|
||||
Range::from_located(expr),
|
||||
))
|
||||
}
|
||||
|
||||
/// C408
|
||||
pub fn unnecessary_collection_call(
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Located<KeywordData>],
|
||||
) -> Option<Check> {
|
||||
if !args.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let id = function_name(func)?;
|
||||
match id {
|
||||
"dict" if keywords.is_empty() || keywords.iter().all(|kw| kw.node.arg.is_some()) => (),
|
||||
"list" | "tuple" => {
|
||||
// list() or tuple()
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
Some(Check::new(
|
||||
CheckKind::UnnecessaryCollectionCall(id.to_string()),
|
||||
Range::from_located(expr),
|
||||
))
|
||||
}
|
||||
|
||||
/// C409
|
||||
pub fn unnecessary_literal_within_tuple_call(
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
) -> Option<Check> {
|
||||
let argument = first_argument_with_matching_function("tuple", func, args)?;
|
||||
let argument_kind = match argument {
|
||||
ExprKind::Tuple { .. } => "tuple",
|
||||
ExprKind::List { .. } => "list",
|
||||
_ => return None,
|
||||
};
|
||||
Some(Check::new(
|
||||
CheckKind::UnnecessaryLiteralWithinTupleCall(argument_kind.to_string()),
|
||||
Range::from_located(expr),
|
||||
))
|
||||
}
|
||||
|
||||
/// C410
|
||||
pub fn unnecessary_literal_within_list_call(
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
) -> Option<Check> {
|
||||
let argument = first_argument_with_matching_function("list", func, args)?;
|
||||
let argument_kind = match argument {
|
||||
ExprKind::Tuple { .. } => "tuple",
|
||||
ExprKind::List { .. } => "list",
|
||||
_ => return None,
|
||||
};
|
||||
Some(Check::new(
|
||||
CheckKind::UnnecessaryLiteralWithinListCall(argument_kind.to_string()),
|
||||
Range::from_located(expr),
|
||||
))
|
||||
}
|
||||
|
||||
/// C411
|
||||
pub fn unnecessary_list_call(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
|
||||
let argument = first_argument_with_matching_function("list", func, args)?;
|
||||
if let ExprKind::ListComp { .. } = argument {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryListCall,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// C413
|
||||
pub fn unnecessary_call_around_sorted(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
|
||||
let outer = function_name(func)?;
|
||||
if !(outer == "list" || outer == "reversed") {
|
||||
return None;
|
||||
}
|
||||
if let ExprKind::Call { func, .. } = &args.first()?.node {
|
||||
if function_name(func)? == "sorted" {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessaryCallAroundSorted(outer.to_string()),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// C414
|
||||
pub fn unnecessary_double_cast_or_process(
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
) -> Option<Check> {
|
||||
let outer = function_name(func)?;
|
||||
if !["list", "tuple", "set", "reversed", "sorted"].contains(&outer) {
|
||||
return None;
|
||||
}
|
||||
|
||||
fn new_check(inner: &str, outer: &str, expr: &Expr) -> Check {
|
||||
Check::new(
|
||||
CheckKind::UnnecessaryDoubleCastOrProcess(inner.to_string(), outer.to_string()),
|
||||
Range::from_located(expr),
|
||||
)
|
||||
}
|
||||
|
||||
if let ExprKind::Call { func, .. } = &args.first()?.node {
|
||||
let inner = function_name(func)?;
|
||||
// Ex) set(tuple(...))
|
||||
if (outer == "set" || outer == "sorted")
|
||||
&& (inner == "list" || inner == "tuple" || inner == "reversed" || inner == "sorted")
|
||||
{
|
||||
return Some(new_check(inner, outer, expr));
|
||||
}
|
||||
|
||||
// Ex) list(tuple(...))
|
||||
if (outer == "list" || outer == "tuple") && (inner == "list" || inner == "tuple") {
|
||||
return Some(new_check(inner, outer, expr));
|
||||
}
|
||||
|
||||
// Ex) set(set(...))
|
||||
if outer == "set" && inner == "set" {
|
||||
return Some(new_check(inner, outer, expr));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// C415
|
||||
pub fn unnecessary_subscript_reversal(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
|
||||
let first_arg = args.first()?;
|
||||
let id = function_name(func)?;
|
||||
if !["set", "sorted", "reversed"].contains(&id) {
|
||||
return None;
|
||||
}
|
||||
if let ExprKind::Subscript { slice, .. } = &first_arg.node {
|
||||
if let ExprKind::Slice { lower, upper, step } = &slice.node {
|
||||
if lower.is_none() && upper.is_none() {
|
||||
if let Some(step) = step {
|
||||
if let ExprKind::UnaryOp {
|
||||
op: Unaryop::USub,
|
||||
operand,
|
||||
} = &step.node
|
||||
{
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Int(val),
|
||||
..
|
||||
} = &operand.node
|
||||
{
|
||||
if *val == BigInt::from(1) {
|
||||
return Some(Check::new(
|
||||
CheckKind::UnnecessarySubscriptReversal(id.to_string()),
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// C416
|
||||
pub fn unnecessary_comprehension(
|
||||
expr: &Expr,
|
||||
elt: &Expr,
|
||||
generators: &[Comprehension],
|
||||
) -> Option<Check> {
|
||||
if generators.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
let generator = &generators[0];
|
||||
if !(generator.ifs.is_empty() && generator.is_async == 0) {
|
||||
return None;
|
||||
}
|
||||
let elt_id = function_name(elt)?;
|
||||
let target_id = function_name(&generator.target)?;
|
||||
if elt_id != target_id {
|
||||
return None;
|
||||
}
|
||||
let expr_kind = match &expr.node {
|
||||
ExprKind::ListComp { .. } => "list",
|
||||
ExprKind::SetComp { .. } => "set",
|
||||
_ => return None,
|
||||
};
|
||||
Some(Check::new(
|
||||
CheckKind::UnnecessaryComprehension(expr_kind.to_string()),
|
||||
Range::from_located(expr),
|
||||
))
|
||||
}
|
||||
|
||||
/// C417
|
||||
pub fn unnecessary_map(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
|
||||
fn new_check(kind: &str, expr: &Expr) -> Check {
|
||||
Check::new(
|
||||
CheckKind::UnnecessaryMap(kind.to_string()),
|
||||
Range::from_located(expr),
|
||||
)
|
||||
}
|
||||
let id = function_name(func)?;
|
||||
match id {
|
||||
"map" => {
|
||||
if args.len() == 2 && matches!(&args[0].node, ExprKind::Lambda { .. }) {
|
||||
return Some(new_check("generator", expr));
|
||||
}
|
||||
}
|
||||
"list" | "set" => {
|
||||
if let ExprKind::Call { func, args, .. } = &args.first()?.node {
|
||||
let argument = first_argument_with_matching_function("map", func, args)?;
|
||||
if let ExprKind::Lambda { .. } = argument {
|
||||
return Some(new_check(id, expr));
|
||||
}
|
||||
}
|
||||
}
|
||||
"dict" => {
|
||||
if args.len() == 1 {
|
||||
if let ExprKind::Call { func, args, .. } = &args[0].node {
|
||||
let argument = first_argument_with_matching_function("map", func, args)?;
|
||||
if let ExprKind::Lambda { body, .. } = &argument {
|
||||
if matches!(&body.node, ExprKind::Tuple { elts, .. } | ExprKind::List { elts, .. } if elts.len() == 2)
|
||||
{
|
||||
return Some(new_check(id, expr));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
None
|
||||
}
|
||||
1
src/flake8_comprehensions/mod.rs
Normal file
1
src/flake8_comprehensions/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod checks;
|
||||
36
src/flake8_print/checks.rs
Normal file
36
src/flake8_print/checks.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use rustpython_ast::{Expr, ExprKind};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
|
||||
/// Check whether a function call is a `print` or `pprint` invocation
|
||||
pub fn print_call(
|
||||
expr: &Expr,
|
||||
func: &Expr,
|
||||
check_print: bool,
|
||||
check_pprint: bool,
|
||||
) -> Option<Check> {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
if check_print && id == "print" {
|
||||
return Some(Check::new(CheckKind::PrintFound, Range::from_located(expr)));
|
||||
} else if check_pprint && id == "pprint" {
|
||||
return Some(Check::new(
|
||||
CheckKind::PPrintFound,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let ExprKind::Attribute { value, attr, .. } = &func.node {
|
||||
if let ExprKind::Name { id, .. } = &value.node {
|
||||
if check_pprint && id == "pprint" && attr == "pprint" {
|
||||
return Some(Check::new(
|
||||
CheckKind::PPrintFound,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
2
src/flake8_print/mod.rs
Normal file
2
src/flake8_print/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod checks;
|
||||
pub mod plugins;
|
||||
3
src/flake8_print/plugins/mod.rs
Normal file
3
src/flake8_print/plugins/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub use print_call::print_call;
|
||||
|
||||
mod print_call;
|
||||
@@ -1,19 +1,19 @@
|
||||
use log::error;
|
||||
use rustpython_ast::{Expr, Stmt, StmtKind};
|
||||
|
||||
use crate::ast::checkers;
|
||||
use crate::autofix::{fixer, fixes};
|
||||
use crate::autofix::helpers;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::CheckCode;
|
||||
use crate::flake8_print::checks;
|
||||
|
||||
pub fn print_call(checker: &mut Checker, expr: &Expr, func: &Expr) {
|
||||
if let Some(mut check) = checkers::print_call(
|
||||
if let Some(mut check) = checks::print_call(
|
||||
expr,
|
||||
func,
|
||||
checker.settings.enabled.contains(&CheckCode::T201),
|
||||
checker.settings.enabled.contains(&CheckCode::T203),
|
||||
) {
|
||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if checker.patch() {
|
||||
let context = checker.binding_context();
|
||||
if matches!(
|
||||
checker.parents[context.defined_by].node,
|
||||
@@ -25,13 +25,13 @@ pub fn print_call(checker: &mut Checker, expr: &Expr, func: &Expr) {
|
||||
.map(|index| checker.parents[*index])
|
||||
.collect();
|
||||
|
||||
match fixes::remove_stmt(
|
||||
match helpers::remove_stmt(
|
||||
checker.parents[context.defined_by],
|
||||
context.defined_in.map(|index| checker.parents[index]),
|
||||
&deleted,
|
||||
) {
|
||||
Ok(fix) => {
|
||||
if fix.content.is_empty() || fix.content == "pass" {
|
||||
if fix.patch.content.is_empty() || fix.patch.content == "pass" {
|
||||
checker.deletions.insert(context.defined_by);
|
||||
}
|
||||
check.amend(fix)
|
||||
298
src/flake8_quotes/checks.rs
Normal file
298
src/flake8_quotes/checks.rs
Normal file
@@ -0,0 +1,298 @@
|
||||
use rustpython_ast::Location;
|
||||
|
||||
use crate::ast::operations::SourceCodeLocator;
|
||||
use crate::ast::types::Range;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
use crate::flake8_quotes::settings::{Quote, Settings};
|
||||
|
||||
fn good_single(quote: &Quote) -> char {
|
||||
match quote {
|
||||
Quote::Single => '\'',
|
||||
Quote::Double => '"',
|
||||
}
|
||||
}
|
||||
|
||||
fn bad_single(quote: &Quote) -> char {
|
||||
match quote {
|
||||
Quote::Double => '\'',
|
||||
Quote::Single => '"',
|
||||
}
|
||||
}
|
||||
|
||||
fn good_multiline(quote: &Quote) -> &str {
|
||||
match quote {
|
||||
Quote::Single => "'''",
|
||||
Quote::Double => "\"\"\"",
|
||||
}
|
||||
}
|
||||
|
||||
fn good_multiline_ending(quote: &Quote) -> &str {
|
||||
match quote {
|
||||
Quote::Single => "'\"\"\"",
|
||||
Quote::Double => "\"'''",
|
||||
}
|
||||
}
|
||||
|
||||
fn good_docstring(quote: &Quote) -> &str {
|
||||
match quote {
|
||||
Quote::Single => "'''",
|
||||
Quote::Double => "\"\"\"",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn quotes(
|
||||
locator: &SourceCodeLocator,
|
||||
start: &Location,
|
||||
end: &Location,
|
||||
is_docstring: bool,
|
||||
settings: &Settings,
|
||||
) -> Option<Check> {
|
||||
let text = locator.slice_source_code_range(&Range {
|
||||
location: *start,
|
||||
end_location: *end,
|
||||
});
|
||||
|
||||
// Remove any prefixes (e.g., remove `u` from `u"foo"`).
|
||||
let last_quote_char = text.chars().last().unwrap();
|
||||
let first_quote_char = text.find(last_quote_char).unwrap();
|
||||
let prefix = &text[..first_quote_char].to_lowercase();
|
||||
let raw_text = &text[first_quote_char..];
|
||||
|
||||
// Determine if the string is multiline-based.
|
||||
let is_multiline = if raw_text.len() >= 3 {
|
||||
let mut chars = raw_text.chars();
|
||||
let first = chars.next().unwrap();
|
||||
let second = chars.next().unwrap();
|
||||
let third = chars.next().unwrap();
|
||||
first == second && second == third
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if is_docstring {
|
||||
if raw_text.contains(good_docstring(&settings.docstring_quotes)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
return Some(Check::new(
|
||||
CheckKind::BadQuotesDocstring(settings.docstring_quotes.clone()),
|
||||
Range {
|
||||
location: *start,
|
||||
end_location: *end,
|
||||
},
|
||||
));
|
||||
} else if is_multiline {
|
||||
// If our string is or contains a known good string, ignore it.
|
||||
if raw_text.contains(good_multiline(&settings.multiline_quotes)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// If our string ends with a known good ending, then ignore it.
|
||||
if raw_text.ends_with(good_multiline_ending(&settings.multiline_quotes)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
return Some(Check::new(
|
||||
CheckKind::BadQuotesMultilineString(settings.multiline_quotes.clone()),
|
||||
Range {
|
||||
location: *start,
|
||||
end_location: *end,
|
||||
},
|
||||
));
|
||||
} else {
|
||||
let string_contents = &raw_text[1..raw_text.len() - 1];
|
||||
|
||||
// If we're using the preferred quotation type, check for escapes.
|
||||
if last_quote_char == good_single(&settings.inline_quotes) {
|
||||
if !settings.avoid_escape || prefix.contains('r') {
|
||||
return None;
|
||||
}
|
||||
if string_contents.contains(good_single(&settings.inline_quotes))
|
||||
&& !string_contents.contains(bad_single(&settings.inline_quotes))
|
||||
{
|
||||
return Some(Check::new(
|
||||
CheckKind::AvoidQuoteEscape,
|
||||
Range {
|
||||
location: *start,
|
||||
end_location: *end,
|
||||
},
|
||||
));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// If we're not using the preferred type, only allow use to avoid escapes.
|
||||
if !string_contents.contains(good_single(&settings.inline_quotes)) {
|
||||
return Some(Check::new(
|
||||
CheckKind::BadQuotesInlineString(settings.inline_quotes.clone()),
|
||||
Range {
|
||||
location: *start,
|
||||
end_location: *end,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::autofix::fixer;
|
||||
use crate::checks::{Check, CheckCode};
|
||||
use crate::flake8_quotes::settings::Quote;
|
||||
use crate::linter::tokenize;
|
||||
use crate::{flake8_quotes, linter, Settings};
|
||||
use crate::{fs, noqa};
|
||||
|
||||
fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result<Vec<Check>> {
|
||||
let contents = fs::read_file(path)?;
|
||||
let tokens: Vec<LexResult> = tokenize(&contents);
|
||||
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
|
||||
linter::check_path(path, &contents, tokens, &noqa_line_for, settings, autofix)
|
||||
}
|
||||
|
||||
#[test_case(Path::new("doubles.py"))]
|
||||
#[test_case(Path::new("doubles_escaped.py"))]
|
||||
#[test_case(Path::new("doubles_multiline_string.py"))]
|
||||
#[test_case(Path::new("doubles_noqa.py"))]
|
||||
#[test_case(Path::new("doubles_wrapped.py"))]
|
||||
fn doubles(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("doubles_{}", path.to_string_lossy());
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/flake8_quotes")
|
||||
.join(path)
|
||||
.as_path(),
|
||||
&Settings {
|
||||
flake8_quotes: flake8_quotes::settings::Settings {
|
||||
inline_quotes: Quote::Single,
|
||||
multiline_quotes: Quote::Single,
|
||||
docstring_quotes: Quote::Single,
|
||||
avoid_escape: true,
|
||||
},
|
||||
..Settings::for_rules(vec![
|
||||
CheckCode::Q000,
|
||||
CheckCode::Q001,
|
||||
CheckCode::Q002,
|
||||
CheckCode::Q003,
|
||||
])
|
||||
},
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(snapshot, checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Path::new("singles.py"))]
|
||||
#[test_case(Path::new("singles_escaped.py"))]
|
||||
#[test_case(Path::new("singles_multiline_string.py"))]
|
||||
#[test_case(Path::new("singles_noqa.py"))]
|
||||
#[test_case(Path::new("singles_wrapped.py"))]
|
||||
fn singles(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("singles_{}", path.to_string_lossy());
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/flake8_quotes")
|
||||
.join(path)
|
||||
.as_path(),
|
||||
&Settings {
|
||||
flake8_quotes: flake8_quotes::settings::Settings {
|
||||
inline_quotes: Quote::Double,
|
||||
multiline_quotes: Quote::Double,
|
||||
docstring_quotes: Quote::Double,
|
||||
avoid_escape: true,
|
||||
},
|
||||
..Settings::for_rules(vec![
|
||||
CheckCode::Q000,
|
||||
CheckCode::Q001,
|
||||
CheckCode::Q002,
|
||||
CheckCode::Q003,
|
||||
])
|
||||
},
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(snapshot, checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Path::new("docstring_doubles.py"))]
|
||||
#[test_case(Path::new("docstring_doubles_module_multiline.py"))]
|
||||
#[test_case(Path::new("docstring_doubles_module_singleline.py"))]
|
||||
#[test_case(Path::new("docstring_doubles_class.py"))]
|
||||
#[test_case(Path::new("docstring_doubles_function.py"))]
|
||||
#[test_case(Path::new("docstring_singles.py"))]
|
||||
#[test_case(Path::new("docstring_singles_module_multiline.py"))]
|
||||
#[test_case(Path::new("docstring_singles_module_singleline.py"))]
|
||||
#[test_case(Path::new("docstring_singles_class.py"))]
|
||||
#[test_case(Path::new("docstring_singles_function.py"))]
|
||||
fn double_docstring(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("double_docstring_{}", path.to_string_lossy());
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/flake8_quotes")
|
||||
.join(path)
|
||||
.as_path(),
|
||||
&Settings {
|
||||
flake8_quotes: flake8_quotes::settings::Settings {
|
||||
inline_quotes: Quote::Single,
|
||||
multiline_quotes: Quote::Single,
|
||||
docstring_quotes: Quote::Double,
|
||||
avoid_escape: true,
|
||||
},
|
||||
..Settings::for_rules(vec![
|
||||
CheckCode::Q000,
|
||||
CheckCode::Q001,
|
||||
CheckCode::Q002,
|
||||
CheckCode::Q003,
|
||||
])
|
||||
},
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(snapshot, checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Path::new("docstring_doubles.py"))]
|
||||
#[test_case(Path::new("docstring_doubles_module_multiline.py"))]
|
||||
#[test_case(Path::new("docstring_doubles_module_singleline.py"))]
|
||||
#[test_case(Path::new("docstring_doubles_class.py"))]
|
||||
#[test_case(Path::new("docstring_doubles_function.py"))]
|
||||
#[test_case(Path::new("docstring_singles.py"))]
|
||||
#[test_case(Path::new("docstring_singles_module_multiline.py"))]
|
||||
#[test_case(Path::new("docstring_singles_module_singleline.py"))]
|
||||
#[test_case(Path::new("docstring_singles_class.py"))]
|
||||
#[test_case(Path::new("docstring_singles_function.py"))]
|
||||
fn single_docstring(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("single_docstring_{}", path.to_string_lossy());
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/flake8_quotes")
|
||||
.join(path)
|
||||
.as_path(),
|
||||
&Settings {
|
||||
flake8_quotes: flake8_quotes::settings::Settings {
|
||||
inline_quotes: Quote::Single,
|
||||
multiline_quotes: Quote::Double,
|
||||
docstring_quotes: Quote::Single,
|
||||
avoid_escape: true,
|
||||
},
|
||||
..Settings::for_rules(vec![
|
||||
CheckCode::Q000,
|
||||
CheckCode::Q001,
|
||||
CheckCode::Q002,
|
||||
CheckCode::Q003,
|
||||
])
|
||||
},
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(snapshot, checks);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
118
src/flake8_quotes/docstring_detection.rs
Normal file
118
src/flake8_quotes/docstring_detection.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
//! Extract docstrings via tokenization.
|
||||
//!
|
||||
//! See: https://github.com/zheller/flake8-quotes/blob/ef0d9a90249a080e460b70ab62bf4b65e5aa5816/flake8_quotes/docstring_detection.py#L29
|
||||
//!
|
||||
//! TODO(charlie): Consolidate with the existing AST-based docstring extraction.
|
||||
|
||||
use rustpython_parser::lexer::Tok;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum State {
|
||||
// Start of the module: first string gets marked as a docstring.
|
||||
ExpectModuleDocstring,
|
||||
// After seeing a class definition, we're waiting for the block colon (and do bracket counting).
|
||||
ExpectClassColon,
|
||||
// After seeing the block colon in a class definition, we expect a docstring.
|
||||
ExpectClassDocstring,
|
||||
// Same as ExpectClassColon, but for function definitions.
|
||||
ExpectFunctionColon,
|
||||
// Same as ExpectClassDocstring, but for function definitions.
|
||||
ExpectFunctionDocstring,
|
||||
// Skip tokens until we observe a `class` or `def`.
|
||||
Other,
|
||||
}
|
||||
|
||||
pub struct StateMachine {
|
||||
state: State,
|
||||
bracket_count: usize,
|
||||
}
|
||||
|
||||
impl StateMachine {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: State::ExpectModuleDocstring,
|
||||
bracket_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn consume(&mut self, tok: &Tok) -> bool {
|
||||
if matches!(tok, Tok::Newline | Tok::Indent | Tok::Dedent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if matches!(tok, Tok::String { .. }) {
|
||||
return if matches!(
|
||||
self.state,
|
||||
State::ExpectModuleDocstring
|
||||
| State::ExpectClassDocstring
|
||||
| State::ExpectFunctionDocstring
|
||||
) {
|
||||
self.state = State::Other;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
}
|
||||
|
||||
if matches!(tok, Tok::Class) {
|
||||
self.state = State::ExpectClassColon;
|
||||
self.bracket_count = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if matches!(tok, Tok::Def) {
|
||||
self.state = State::ExpectFunctionColon;
|
||||
self.bracket_count = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if matches!(tok, Tok::Colon) {
|
||||
if self.bracket_count == 0 {
|
||||
if matches!(self.state, State::ExpectClassColon) {
|
||||
self.state = State::ExpectClassDocstring;
|
||||
} else if matches!(self.state, State::ExpectFunctionColon) {
|
||||
self.state = State::ExpectFunctionDocstring;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if matches!(tok, Tok::Lpar | Tok::Lbrace | Tok::Lsqb) {
|
||||
self.bracket_count += 1;
|
||||
if matches!(
|
||||
self.state,
|
||||
State::ExpectModuleDocstring
|
||||
| State::ExpectClassDocstring
|
||||
| State::ExpectFunctionDocstring
|
||||
) {
|
||||
self.state = State::Other;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if matches!(tok, Tok::Rpar | Tok::Rbrace | Tok::Rsqb) {
|
||||
self.bracket_count -= 1;
|
||||
if matches!(
|
||||
self.state,
|
||||
State::ExpectModuleDocstring
|
||||
| State::ExpectClassDocstring
|
||||
| State::ExpectFunctionDocstring
|
||||
) {
|
||||
self.state = State::Other;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if matches!(
|
||||
self.state,
|
||||
State::ExpectModuleDocstring
|
||||
| State::ExpectClassDocstring
|
||||
| State::ExpectFunctionDocstring
|
||||
) {
|
||||
self.state = State::Other;
|
||||
return false;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
3
src/flake8_quotes/mod.rs
Normal file
3
src/flake8_quotes/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod checks;
|
||||
pub mod docstring_detection;
|
||||
pub mod settings;
|
||||
49
src/flake8_quotes/settings.rs
Normal file
49
src/flake8_quotes/settings.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
//! Settings for the `flake-quotes` plugin.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||
pub enum Quote {
|
||||
Single,
|
||||
Double,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||
pub struct Options {
|
||||
pub inline_quotes: Option<Quote>,
|
||||
pub multiline_quotes: Option<Quote>,
|
||||
pub docstring_quotes: Option<Quote>,
|
||||
pub avoid_escape: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Settings {
|
||||
pub inline_quotes: Quote,
|
||||
pub multiline_quotes: Quote,
|
||||
pub docstring_quotes: Quote,
|
||||
pub avoid_escape: bool,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn from_options(options: Options) -> Self {
|
||||
Self {
|
||||
inline_quotes: options.inline_quotes.unwrap_or(Quote::Single),
|
||||
multiline_quotes: options.multiline_quotes.unwrap_or(Quote::Double),
|
||||
docstring_quotes: options.docstring_quotes.unwrap_or(Quote::Double),
|
||||
avoid_escape: options.avoid_escape.unwrap_or(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inline_quotes: Quote::Single,
|
||||
multiline_quotes: Quote::Double,
|
||||
docstring_quotes: Quote::Double,
|
||||
avoid_escape: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
source: src/flake8_quotes/checks.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind:
|
||||
BadQuotesMultilineString: single
|
||||
location:
|
||||
row: 5
|
||||
column: 1
|
||||
end_location:
|
||||
row: 7
|
||||
column: 4
|
||||
fix: ~
|
||||
- kind:
|
||||
BadQuotesMultilineString: single
|
||||
location:
|
||||
row: 16
|
||||
column: 5
|
||||
end_location:
|
||||
row: 18
|
||||
column: 8
|
||||
fix: ~
|
||||
- kind:
|
||||
BadQuotesMultilineString: single
|
||||
location:
|
||||
row: 21
|
||||
column: 21
|
||||
end_location:
|
||||
row: 22
|
||||
column: 38
|
||||
fix: ~
|
||||
- kind:
|
||||
BadQuotesMultilineString: single
|
||||
location:
|
||||
row: 30
|
||||
column: 9
|
||||
end_location:
|
||||
row: 32
|
||||
column: 12
|
||||
fix: ~
|
||||
- kind:
|
||||
BadQuotesMultilineString: single
|
||||
location:
|
||||
row: 35
|
||||
column: 13
|
||||
end_location:
|
||||
row: 37
|
||||
column: 16
|
||||
fix: ~
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user