Compare commits

..

40 Commits

Author SHA1 Message Date
Charlie Marsh
2f69be0d41 Bump version to 0.0.71 2022-10-12 17:14:28 -04:00
Charlie Marsh
54cb2eb15b Only run section checks when CheckCodes are enabled 2022-10-12 17:14:16 -04:00
Charlie Marsh
167992ad48 Implement D407, D408, D409, D412, and D414 (#413) 2022-10-12 17:12:54 -04:00
Charlie Marsh
f0dab24079 Implement D405, D406, D410, D411, and D413 (#411) 2022-10-12 16:31:14 -04:00
Charlie Marsh
77055faab6 Implement D404 and D418 for pydocstyle (#409) 2022-10-12 13:20:55 -04:00
Charlie Marsh
e08e1caf71 Bump version to 0.0.70 2022-10-12 12:59:14 -04:00
Harutaka Kawamura
0072dfd81e Implement C414 (#406) 2022-10-12 12:58:46 -04:00
Charlie Marsh
6ffe02ee05 Pass around VisibleScope 2022-10-12 12:52:48 -04:00
Charlie Marsh
688fc0cd02 Implement docstring visibility checks (#408) 2022-10-12 12:46:40 -04:00
Charlie Marsh
f30e5e45ab Remove initial field 2022-10-12 11:07:31 -04:00
Charlie Marsh
1a68a38306 Enable definition tracking for docstrings (#407) 2022-10-12 11:06:28 -04:00
Charlie Marsh
590aa92ead Implement D201, D202, D203, D204, and D211 (#404) 2022-10-11 21:08:30 -04:00
Charlie Marsh
8868f57a74 Implement D402 for pydocstyle (#403) 2022-10-11 13:19:56 -04:00
Charlie Marsh
71802f8861 Bump version to 0.0.69 2022-10-11 12:54:56 -04:00
Charlie Marsh
5b6fb8cefa Skip docstring checks for empty docstrings (#402) 2022-10-11 12:54:30 -04:00
Charlie Marsh
2ff964107c Add D212, D213, D300, D403, and D415 (#400) 2022-10-11 12:48:23 -04:00
Charlie Marsh
141132d5be Add fake setup.py (#399) 2022-10-11 12:38:48 -04:00
Harutaka Kawamura
8ba872ece4 Support linting input from stdin (#387) 2022-10-11 09:56:20 -04:00
Charlie Marsh
209dce2033 Fix missing backtick 2022-10-10 17:23:58 -04:00
Charlie Marsh
90d88dfb10 Make pyupgrade tally more precise 2022-10-10 17:18:20 -04:00
Charlie Marsh
4730911b25 Bump version to 0.0.68 2022-10-10 16:50:09 -04:00
Charlie Marsh
4e9fb9907a Implement D205, D209, and D210 (#398) 2022-10-10 16:49:51 -04:00
Charlie Marsh
b8dce8922d Implement D200 (OneLinerDocstring) (#397) 2022-10-10 16:07:51 -04:00
Charlie Marsh
30877127bc Implement D400 (DocstringEndsInNonPeriod) (#396) 2022-10-10 15:35:23 -04:00
Charlie Marsh
8b66bbdc9b Regenerate rules table 2022-10-10 15:18:18 -04:00
Charlie Marsh
71d3a84b14 Implement D410 (EmptyDocstring) (#395) 2022-10-10 15:15:38 -04:00
Charlie Marsh
323a5c857c Implement docstring tracking (#394) 2022-10-10 15:15:09 -04:00
Charlie Marsh
42cec3f5a0 Regenerate rules table 2022-10-10 14:02:31 -04:00
Charlie Marsh
ee42413e10 Enable autofix for B014 2022-10-10 13:18:43 -04:00
Charlie Marsh
765db12b84 Remove check_ prefix from check utilities (#393) 2022-10-10 12:58:40 -04:00
Charlie Marsh
e1b711d9c6 Bump version to 0.0.67 2022-10-10 12:55:04 -04:00
Charlie Marsh
35f593846e Implement B014 from flake8-bugbear (#392) 2022-10-10 12:53:42 -04:00
Charlie Marsh
c384fa513b Implement B025 from flake8-bugbear (#391) 2022-10-10 12:18:31 -04:00
Charlie Marsh
022ff64d29 Implement B011 from flake8-bugbear (#390) 2022-10-10 10:55:55 -04:00
Charlie Marsh
5a06fb28fd Bump version to 0.0.66 2022-10-10 10:03:59 -04:00
Charlie Marsh
46750a3e17 Flag unimplemented error codes in M001 (#388) 2022-10-10 10:03:40 -04:00
Charlie Marsh
9cc902b802 Avoid F821 false-positives with NameError (#386) 2022-10-10 09:39:59 -04:00
Harutaka Kawamura
c2a36ebd1e Implement C410 (#382) 2022-10-09 23:33:55 -04:00
Charlie Marsh
34ca225393 Rename flakes8 to flake8 2022-10-09 23:02:44 -04:00
Harutaka Kawamura
38c30905e6 Implement C409 (#381) 2022-10-09 22:34:52 -04:00
90 changed files with 5832 additions and 281 deletions

89
Cargo.lock generated
View File

@@ -64,6 +64,20 @@ dependencies = [
"term",
]
[[package]]
name = "assert_cmd"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ae1ddd39efd67689deb1979d80bad3bf7f2b09c6e6117c8d1f2443b5e2f83e"
dependencies = [
"bstr",
"doc-comment",
"predicates",
"predicates-core",
"predicates-tree",
"wait-timeout",
]
[[package]]
name = "async-channel"
version = "1.7.1"
@@ -580,6 +594,12 @@ version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "digest"
version = "0.8.1"
@@ -658,6 +678,12 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "either"
version = "1.8.0"
@@ -1089,6 +1115,12 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
[[package]]
name = "joinery"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72167d68f5fce3b8655487b8038691a3c9984ee769590f93f2a631f4ad64e4f5"
[[package]]
name = "js-sys"
version = "0.3.60"
@@ -1685,6 +1717,33 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "predicates"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c"
dependencies = [
"difflib",
"itertools",
"predicates-core",
]
[[package]]
name = "predicates-core"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb"
[[package]]
name = "predicates-tree"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@@ -1907,9 +1966,10 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.65"
version = "0.0.71"
dependencies = [
"anyhow",
"assert_cmd",
"bincode",
"cacache",
"chrono",
@@ -1938,6 +1998,7 @@ dependencies = [
"serde_json",
"strum",
"strum_macros",
"titlecase",
"toml",
"update-informer",
"walkdir",
@@ -2345,6 +2406,12 @@ dependencies = [
"phf_codegen 0.8.0",
]
[[package]]
name = "termtree"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b"
[[package]]
name = "thiserror"
version = "1.0.37"
@@ -2400,6 +2467,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "titlecase"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38397a8cdb017cfeb48bf6c154d6de975ac69ffeed35980fde199d2ee0842042"
dependencies = [
"joinery",
"lazy_static",
"regex",
]
[[package]]
name = "toml"
version = "0.5.9"
@@ -2593,6 +2671,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8e76fae08f03f96e166d2dfda232190638c10e0383841252416f9cfe2ae60e6"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "waker-fn"
version = "1.1.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.65"
version = "0.0.71"
edition = "2021"
[lib]
@@ -38,8 +38,10 @@ walkdir = { version = "2.3.2" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = "0.24.3"
num-bigint = "0.4.3"
titlecase = "2.2.1"
[dev-dependencies]
assert_cmd = "2.0.4"
insta = { version = "1.19.1", features = ["yaml"] }
[features]

View File

@@ -57,7 +57,7 @@ ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.48
rev: v0.0.71
hooks:
- id: lint
```
@@ -215,8 +215,10 @@ ruff also implements some of the most popular Flake8 plugins natively, including
- [`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-comprehensions`](https://pypi.org/project/flake8-comprehensions/) (partial)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (partial)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/) (12/16)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (3/32)
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/) (37/48)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (8/34)
Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis Flake8:
@@ -278,6 +280,9 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| 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 | | |
| 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` | | |
| 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 | | |
@@ -286,6 +291,9 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| 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 | | |
| 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>() | | |
| T201 | PrintFound | `print` found | | 🛠 |
| T203 | PPrintFound | `pprint` found | | 🛠 |
@@ -293,10 +301,47 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| 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 | NoAssertEquals | `assertEquals` is deprecated, use `assertEqual` instead | | 🛠 |
| 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)` | | 🛠 |
| 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 | | |
| D205 | NoBlankLineAfterSummary | 1 blank line required between summary line and description | | |
| D209 | NewLineAfterLastParagraph | Multi-line docstring closing quotes should be on a separate line | | |
| D210 | NoSurroundingWhitespace | No whitespaces allowed surrounding docstring text | | |
| D212 | MultiLineSummaryFirstLine | Multi-line docstring summary should start at the first line | | |
| D213 | MultiLineSummarySecondLine | Multi-line docstring summary should start at the second line | | |
| 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` | | |
| D415 | EndsInPunctuation | First line should end with a period, question mark, or exclamation point | | |
| D418 | SkipDocstring | Function decorated with @overload shouldn't contain a docstring | | |
| D419 | NonEmpty | Docstring is empty | | |
| D201 | NoBlankLineBeforeFunction | No blank lines allowed before function docstring (found 1) | | |
| D202 | NoBlankLineAfterFunction | No blank lines allowed after function docstring (found 1) | | |
| D211 | NoBlankLineBeforeClass | No blank lines allowed before class docstring | | |
| D203 | OneBlankLineBeforeClass | 1 blank line required before class docstring | | |
| D204 | OneBlankLineAfterClass | 1 blank line required after class docstring | | |
| D405 | CapitalizeSectionName | Section name should be properly capitalized ("returns") | | |
| D413 | BlankLineAfterLastSection | Missing blank line after last section ("Returns") | | |
| D410 | BlankLineAfterSection | Missing blank line after section ("Returns") | | |
| D411 | BlankLineBeforeSection | Missing blank line before section ("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") | | |
| D412 | NoBlankLinesBetweenHeaderAndContent | No blank lines allowed between a section header and its content ("Returns") | | |
| D414 | NonEmptySection | Section has no content ("Returns") | | |
| M001 | UnusedNOQA | Unused `noqa` directive | | 🛠 |
## Integrations

10
resources/test/fixtures/B011.py vendored Normal file
View File

@@ -0,0 +1,10 @@
"""
Should emit:
B011 - on line 8
B011 - on line 10
"""
assert 1 != 2
assert False
assert 1 != 2, "message"
assert False, "message"

76
resources/test/fixtures/B014.py vendored Normal file
View File

@@ -0,0 +1,76 @@
"""
Should emit:
B014 - on lines 11, 17, 28, 42, 49, 56, and 74.
"""
import binascii
import re
try:
pass
except (Exception, TypeError):
# TypeError is a subclass of Exception, so it doesn't add anything
pass
try:
pass
except (OSError, OSError) as err:
# Duplicate exception types are useless
pass
class MyError(Exception):
pass
try:
pass
except (MyError, MyError):
# Detect duplicate non-builtin errors
pass
try:
pass
except (MyError, Exception) as e:
# Don't assume that we're all subclasses of Exception
pass
try:
pass
except (MyError, BaseException) as e:
# But we *can* assume that everything is a subclass of BaseException
pass
try:
pass
except (re.error, re.error):
# Duplicate exception types as attributes
pass
try:
pass
except (IOError, EnvironmentError, OSError):
# Detect if a primary exception and any its aliases are present.
#
# Since Python 3.3, IOError, EnvironmentError, WindowsError, mmap.error,
# socket.error and select.error are aliases of OSError. See PEP 3151 for
# more info.
pass
try:
pass
except (MyException, NotImplemented):
# NotImplemented is not an exception, let's not crash on it.
pass
try:
pass
except (ValueError, binascii.Error):
# binascii.Error is a subclass of ValueError.
pass

38
resources/test/fixtures/B025.py vendored Normal file
View File

@@ -0,0 +1,38 @@
"""
Should emit:
B025 - on lines 15, 22, 31
"""
import pickle
try:
a = 1
except ValueError:
a = 2
finally:
a = 3
try:
a = 1
except ValueError:
a = 2
except ValueError:
a = 2
try:
a = 1
except pickle.PickleError:
a = 2
except ValueError:
a = 2
except pickle.PickleError:
a = 2
try:
a = 1
except (ValueError, TypeError):
a = 2
except ValueError:
a = 2
except (OSError, TypeError):
a = 2

3
resources/test/fixtures/C409.py vendored Normal file
View File

@@ -0,0 +1,3 @@
t1 = tuple([1, 2])
t2 = tuple((1, 2))
t3 = tuple([])

4
resources/test/fixtures/C410.py vendored Normal file
View File

@@ -0,0 +1,4 @@
l1 = list([1, 2])
l2 = list((1, 2))
l3 = list([])
l4 = list(())

14
resources/test/fixtures/C414.py vendored Normal file
View File

@@ -0,0 +1,14 @@
x = [1, 2, 3]
list(list(x))
list(tuple(x))
tuple(list(x))
tuple(tuple(x))
set(set(x))
set(list(x))
set(tuple(x))
set(sorted(x))
set(reversed(x))
sorted(list(x))
sorted(tuple(x))
sorted(sorted(x))
sorted(reversed(x))

534
resources/test/fixtures/D.py vendored Normal file
View File

@@ -0,0 +1,534 @@
# No docstring, so we can test D100
from functools import wraps
import os
from .expected import Expectation
from typing import overload
expectation = Expectation()
expect = expectation.expect
expect('class_', 'D101: Missing docstring in public class')
class class_:
expect('meta', 'D419: Docstring is empty')
class meta:
""""""
@expect('D102: Missing docstring in public method')
def method(self=None):
pass
def _ok_since_private(self=None):
pass
@overload
def overloaded_method(self, a: int) -> str:
...
@overload
def overloaded_method(self, a: str) -> str:
"""Foo bar documentation."""
...
def overloaded_method(a):
"""Foo bar documentation."""
return str(a)
expect('overloaded_method',
"D418: Function/ Method decorated with @overload"
" shouldn't contain a docstring")
@property
def foo(self):
"""The foo of the thing, which isn't in imperitive mood."""
return "hello"
@expect('D102: Missing docstring in public method')
def __new__(self=None):
pass
@expect('D107: Missing docstring in __init__')
def __init__(self=None):
pass
@expect('D105: Missing docstring in magic method')
def __str__(self=None):
pass
@expect('D102: Missing docstring in public method')
def __call__(self=None, x=None, y=None, z=None):
pass
@expect('D419: Docstring is empty')
def function():
""" """
def ok_since_nested():
pass
@expect('D419: Docstring is empty')
def nested():
''
def function_with_nesting():
"""Foo bar documentation."""
@overload
def nested_overloaded_func(a: int) -> str:
...
@overload
def nested_overloaded_func(a: str) -> str:
"""Foo bar documentation."""
...
def nested_overloaded_func(a):
"""Foo bar documentation."""
return str(a)
expect('nested_overloaded_func',
"D418: Function/ Method decorated with @overload"
" shouldn't contain a docstring")
@overload
def overloaded_func(a: int) -> str:
...
@overload
def overloaded_func(a: str) -> str:
"""Foo bar documentation."""
...
def overloaded_func(a):
"""Foo bar documentation."""
return str(a)
expect('overloaded_func',
"D418: Function/ Method decorated with @overload"
" shouldn't contain a docstring")
@expect('D200: One-line docstring should fit on one line with quotes '
'(found 3)')
@expect('D212: Multi-line docstring summary should start at the first line')
def asdlkfasd():
"""
Wrong.
"""
@expect('D201: No blank lines allowed before function docstring (found 1)')
def leading_space():
"""Leading space."""
@expect('D202: No blank lines allowed after function docstring (found 1)')
def trailing_space():
"""Leading space."""
pass
@expect('D201: No blank lines allowed before function docstring (found 1)')
@expect('D202: No blank lines allowed after function docstring (found 1)')
def trailing_and_leading_space():
"""Trailing and leading space."""
pass
expect('LeadingSpaceMissing',
'D203: 1 blank line required before class docstring (found 0)')
class LeadingSpaceMissing:
"""Leading space missing."""
expect('WithLeadingSpace',
'D211: No blank lines allowed before class docstring (found 1)')
class WithLeadingSpace:
"""With leading space."""
expect('TrailingSpace',
'D204: 1 blank line required after class docstring (found 0)')
expect('TrailingSpace',
'D211: No blank lines allowed before class docstring (found 1)')
class TrailingSpace:
"""TrailingSpace."""
pass
expect('LeadingAndTrailingSpaceMissing',
'D203: 1 blank line required before class docstring (found 0)')
expect('LeadingAndTrailingSpaceMissing',
'D204: 1 blank line required after class docstring (found 0)')
class LeadingAndTrailingSpaceMissing:
"""Leading and trailing space missing."""
pass
@expect('D205: 1 blank line required between summary line and description '
'(found 0)')
@expect('D213: Multi-line docstring summary should start at the second line')
def multi_line_zero_separating_blanks():
"""Summary.
Description.
"""
@expect('D205: 1 blank line required between summary line and description '
'(found 2)')
@expect('D213: Multi-line docstring summary should start at the second line')
def multi_line_two_separating_blanks():
"""Summary.
Description.
"""
@expect('D213: Multi-line docstring summary should start at the second line')
def multi_line_one_separating_blanks():
"""Summary.
Description.
"""
@expect('D207: Docstring is under-indented')
@expect('D213: Multi-line docstring summary should start at the second line')
def asdfsdf():
"""Summary.
Description.
"""
@expect('D207: Docstring is under-indented')
@expect('D213: Multi-line docstring summary should start at the second line')
def asdsdfsdffsdf():
"""Summary.
Description.
"""
@expect('D208: Docstring is over-indented')
@expect('D213: Multi-line docstring summary should start at the second line')
def asdfsdsdf24():
"""Summary.
Description.
"""
@expect('D208: Docstring is over-indented')
@expect('D213: Multi-line docstring summary should start at the second line')
def asdfsdsdfsdf24():
"""Summary.
Description.
"""
@expect('D208: Docstring is over-indented')
@expect('D213: Multi-line docstring summary should start at the second line')
def asdfsdfsdsdsdfsdf24():
"""Summary.
Description.
"""
@expect('D209: Multi-line docstring closing quotes should be on a separate '
'line')
@expect('D213: Multi-line docstring summary should start at the second line')
def asdfljdf24():
"""Summary.
Description."""
@expect('D210: No whitespaces allowed surrounding docstring text')
def endswith():
"""Whitespace at the end. """
@expect('D210: No whitespaces allowed surrounding docstring text')
def around():
""" Whitespace at everywhere. """
@expect('D210: No whitespaces allowed surrounding docstring text')
@expect('D213: Multi-line docstring summary should start at the second line')
def multiline():
""" Whitespace at the beginning.
This is the end.
"""
@expect('D300: Use """triple double quotes""" (found \'\'\'-quotes)')
def triple_single_quotes_raw():
r'''Summary.'''
@expect('D300: Use """triple double quotes""" (found \'\'\'-quotes)')
def triple_single_quotes_raw_uppercase():
R'''Summary.'''
@expect('D300: Use """triple double quotes""" (found \'-quotes)')
def single_quotes_raw():
r'Summary.'
@expect('D300: Use """triple double quotes""" (found \'-quotes)')
def single_quotes_raw_uppercase():
R'Summary.'
@expect('D300: Use """triple double quotes""" (found \'-quotes)')
@expect('D301: Use r""" if any backslashes in a docstring')
def single_quotes_raw_uppercase_backslash():
R'Sum\mary.'
@expect('D301: Use r""" if any backslashes in a docstring')
def double_quotes_backslash():
"""Sum\\mary."""
@expect('D301: Use r""" if any backslashes in a docstring')
def double_quotes_backslash_uppercase():
R"""Sum\\mary."""
@expect('D213: Multi-line docstring summary should start at the second line')
def exceptions_of_D301():
"""Exclude some backslashes from D301.
In particular, line continuations \
and unicode literals \u0394 and \N{GREEK CAPITAL LETTER DELTA}.
They are considered to be intentionally unescaped.
"""
@expect("D400: First line should end with a period (not 'y')")
@expect("D415: First line should end with a period, question mark, "
"or exclamation point (not 'y')")
def lwnlkjl():
"""Summary"""
@expect("D401: First line should be in imperative mood "
"(perhaps 'Return', not 'Returns')")
def liouiwnlkjl():
"""Returns foo."""
@expect("D401: First line should be in imperative mood; try rephrasing "
"(found 'Constructor')")
def sdgfsdg23245():
"""Constructor for a foo."""
@expect("D401: First line should be in imperative mood; try rephrasing "
"(found 'Constructor')")
def sdgfsdg23245777():
"""Constructor."""
@expect('D402: First line should not be the function\'s "signature"')
def foobar():
"""Signature: foobar()."""
@expect('D213: Multi-line docstring summary should start at the second line')
def new_209():
"""First line.
More lines.
"""
pass
@expect('D213: Multi-line docstring summary should start at the second line')
def old_209():
"""One liner.
Multi-line comments. OK to have extra blank line
"""
@expect("D103: Missing docstring in public function")
def oneliner_d102(): return
@expect("D400: First line should end with a period (not 'r')")
@expect("D415: First line should end with a period, question mark,"
" or exclamation point (not 'r')")
def oneliner_withdoc(): """One liner"""
def ignored_decorator(func): # noqa: D400,D401,D415
"""Runs something"""
func()
pass
def decorator_for_test(func): # noqa: D400,D401,D415
"""Runs something"""
func()
pass
@ignored_decorator
def oneliner_ignored_decorator(): """One liner"""
@decorator_for_test
@expect("D400: First line should end with a period (not 'r')")
@expect("D415: First line should end with a period, question mark,"
" or exclamation point (not 'r')")
def oneliner_with_decorator_expecting_errors(): """One liner"""
@decorator_for_test
def valid_oneliner_with_decorator(): """One liner."""
@expect("D207: Docstring is under-indented")
@expect('D213: Multi-line docstring summary should start at the second line')
def docstring_start_in_same_line(): """First Line.
Second Line
"""
def function_with_lambda_arg(x=lambda y: y):
"""Wrap the given lambda."""
@expect('D213: Multi-line docstring summary should start at the second line')
def a_following_valid_function(x=None):
"""Check for a bug where the previous function caused an assertion.
The assertion was caused in the next function, so this one is necessary.
"""
def outer_function():
"""Do something."""
def inner_function():
"""Do inner something."""
return 0
@expect("D400: First line should end with a period (not 'g')")
@expect("D401: First line should be in imperative mood "
"(perhaps 'Run', not 'Runs')")
@expect("D415: First line should end with a period, question mark, "
"or exclamation point (not 'g')")
def docstring_bad():
"""Runs something"""
pass
def docstring_bad_ignore_all(): # noqa
"""Runs something"""
pass
def docstring_bad_ignore_one(): # noqa: D400,D401,D415
"""Runs something"""
pass
@expect("D401: First line should be in imperative mood "
"(perhaps 'Run', not 'Runs')")
def docstring_ignore_some_violations_but_catch_D401(): # noqa: E501,D400,D415
"""Runs something"""
pass
@expect(
"D401: First line should be in imperative mood "
"(perhaps 'Initiate', not 'Initiates')"
)
def docstring_initiates():
"""Initiates the process."""
@expect(
"D401: First line should be in imperative mood "
"(perhaps 'Initialize', not 'Initializes')"
)
def docstring_initializes():
"""Initializes the process."""
@wraps(docstring_bad_ignore_one)
def bad_decorated_function():
"""Bad (E501) but decorated"""
pass
def valid_google_string(): # noqa: D400
"""Test a valid something!"""
@expect("D415: First line should end with a period, question mark, "
"or exclamation point (not 'g')")
def bad_google_string(): # noqa: D400
"""Test a valid something"""
# This is reproducing a bug where AttributeError is raised when parsing class
# parameters as functions for Google / Numpy conventions.
class Blah: # noqa: D203,D213
"""A Blah.
Parameters
----------
x : int
"""
def __init__(self, x):
pass
expect(os.path.normcase(__file__ if __file__[-1] != 'c' else __file__[:-1]),
'D100: Missing docstring in public module')

View File

@@ -1,4 +1,3 @@
from __future__ import print_function
"""Docstring"""
from __future__ import absolute_import

View File

@@ -122,3 +122,12 @@ class PEP593Test:
dict["foo", "bar"], # Expected to fail as undefined.
123,
]
def in_ipython_notebook() -> bool:
try:
# autoimported by notebooks
get_ipython() # type: ignore[name-defined]
except NameError:
return False # not in notebook
return True

View File

@@ -15,6 +15,9 @@ def f() -> None:
# Invalid
d = 1 # noqa: F841, E501
# Invalid (and unimplemented)
d = 1 # noqa: F841, W191
# Valid
_ = """Lorem ipsum dolor sit amet.

497
resources/test/fixtures/sections.py vendored Normal file
View File

@@ -0,0 +1,497 @@
"""A valid module docstring."""
from .expected import Expectation
expectation = Expectation()
expect = expectation.expect
_D213 = 'D213: Multi-line docstring summary should start at the second line'
_D400 = "D400: First line should end with a period (not '!')"
@expect(_D213)
@expect("D405: Section name should be properly capitalized "
"('Returns', not 'returns')")
def not_capitalized(): # noqa: D416
"""Toggle the gizmo.
returns
-------
A value of some sort.
"""
@expect(_D213)
@expect("D406: Section name should end with a newline "
"('Returns', not 'Returns:')")
def superfluous_suffix(): # noqa: D416
"""Toggle the gizmo.
Returns:
-------
A value of some sort.
"""
@expect(_D213)
@expect("D407: Missing dashed underline after section ('Returns')")
def no_underline(): # noqa: D416
"""Toggle the gizmo.
Returns
A value of some sort.
"""
@expect(_D213)
@expect("D407: Missing dashed underline after section ('Returns')")
@expect("D414: Section has no content ('Returns')")
def no_underline_and_no_description(): # noqa: D416
"""Toggle the gizmo.
Returns
"""
@expect(_D213)
@expect("D410: Missing blank line after section ('Returns')")
@expect("D414: Section has no content ('Returns')")
@expect("D411: Missing blank line before section ('Yields')")
@expect("D414: Section has no content ('Yields')")
def consecutive_sections(): # noqa: D416
"""Toggle the gizmo.
Returns
-------
Yields
------
Raises
------
Questions.
"""
@expect(_D213)
@expect("D408: Section underline should be in the line following the "
"section's name ('Returns')")
def blank_line_before_underline(): # noqa: D416
"""Toggle the gizmo.
Returns
-------
A value of some sort.
"""
@expect(_D213)
@expect("D409: Section underline should match the length of its name "
"(Expected 7 dashes in section 'Returns', got 2)")
def bad_underline_length(): # noqa: D416
"""Toggle the gizmo.
Returns
--
A value of some sort.
"""
@expect(_D213)
@expect("D413: Missing blank line after last section ('Returns')")
def no_blank_line_after_last_section(): # noqa: D416
"""Toggle the gizmo.
Returns
-------
A value of some sort.
"""
@expect(_D213)
@expect("D411: Missing blank line before section ('Returns')")
def no_blank_line_before_section(): # noqa: D416
"""Toggle the gizmo.
The function's description.
Returns
-------
A value of some sort.
"""
@expect(_D213)
@expect("D214: Section is over-indented ('Returns')")
def section_overindented(): # noqa: D416
"""Toggle the gizmo.
Returns
-------
A value of some sort.
"""
@expect(_D213)
@expect("D215: Section underline is over-indented (in section 'Returns')")
def section_underline_overindented(): # noqa: D416
"""Toggle the gizmo.
Returns
-------
A value of some sort.
"""
@expect(_D213)
@expect("D215: Section underline is over-indented (in section 'Returns')")
@expect("D413: Missing blank line after last section ('Returns')")
@expect("D414: Section has no content ('Returns')")
def section_underline_overindented_and_contentless(): # noqa: D416
"""Toggle the gizmo.
Returns
-------
"""
@expect(_D213)
def ignore_non_actual_section(): # noqa: D416
"""Toggle the gizmo.
This is the function's description, which will also specify what it
returns
"""
@expect(_D213)
@expect("D401: First line should be in imperative mood "
"(perhaps 'Return', not 'Returns')")
@expect("D400: First line should end with a period (not 's')")
@expect("D415: First line should end with a period, question "
"mark, or exclamation point (not 's')")
@expect("D205: 1 blank line required between summary line and description "
"(found 0)")
def section_name_in_first_line(): # noqa: D416
"""Returns
-------
A value of some sort.
"""
@expect(_D213)
@expect("D405: Section name should be properly capitalized "
"('Short Summary', not 'Short summary')")
@expect("D412: No blank lines allowed between a section header and its "
"content ('Short Summary')")
@expect("D409: Section underline should match the length of its name "
"(Expected 7 dashes in section 'Returns', got 6)")
@expect("D410: Missing blank line after section ('Returns')")
@expect("D411: Missing blank line before section ('Raises')")
@expect("D406: Section name should end with a newline "
"('Raises', not 'Raises:')")
@expect("D407: Missing dashed underline after section ('Raises')")
def multiple_sections(): # noqa: D416
"""Toggle the gizmo.
Short summary
-------------
This is the function's description, which will also specify what it
returns.
Returns
------
Many many wonderful things.
Raises:
My attention.
"""
@expect(_D213)
def false_positive_section_prefix(): # noqa: D416
"""Toggle the gizmo.
Parameters
----------
attributes_are_fun: attributes for the function.
"""
@expect(_D213)
def section_names_as_parameter_names(): # noqa: D416
"""Toggle the gizmo.
Parameters
----------
notes : list
A list of wonderful notes.
examples: list
A list of horrible examples.
"""
@expect(_D213)
@expect("D414: Section has no content ('Returns')")
def valid_google_style_section(): # noqa: D406, D407
"""Toggle the gizmo.
Args:
note: A random string.
Returns:
Raises:
RandomError: A random error that occurs randomly.
"""
@expect(_D213)
@expect("D416: Section name should end with a colon "
"('Args:', not 'Args')")
def missing_colon_google_style_section(): # noqa: D406, D407
"""Toggle the gizmo.
Args
note: A random string.
"""
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) y are missing descriptions in "
"'bar' docstring)", func_name="bar")
def _test_nested_functions():
x = 1
def bar(y=2): # noqa: D207, D213, D406, D407
"""Nested function test for docstrings.
Will this work when referencing x?
Args:
x: Test something
that is broken.
"""
print(x)
@expect(_D213)
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) y are missing descriptions in "
"'test_missing_google_args' docstring)")
def test_missing_google_args(x=1, y=2, _private=3): # noqa: D406, D407
"""Toggle the gizmo.
Args:
x (int): The greatest integer.
"""
class TestGoogle: # noqa: D203
"""Test class."""
def test_method(self, test, another_test, _): # noqa: D213, D407
"""Test a valid args section.
Args:
test: A parameter.
another_test: Another parameter.
"""
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) test, y, z are missing descriptions in "
"'test_missing_args' docstring)", arg_count=5)
def test_missing_args(self, test, x, y, z=3, _private_arg=3): # noqa: D213, D407
"""Test a valid args section.
Args:
x: Another parameter.
"""
@classmethod
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) test, y, z are missing descriptions in "
"'test_missing_args_class_method' docstring)", arg_count=5)
def test_missing_args_class_method(cls, test, x, y, _, z=3): # noqa: D213, D407
"""Test a valid args section.
Args:
x: Another parameter. The parameter below is missing description.
y:
"""
@staticmethod
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) a, y, z are missing descriptions in "
"'test_missing_args_static_method' docstring)", arg_count=4)
def test_missing_args_static_method(a, x, y, _test, z=3): # noqa: D213, D407
"""Test a valid args section.
Args:
x: Another parameter.
"""
@staticmethod
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) a, b are missing descriptions in "
"'test_missing_docstring' docstring)", arg_count=2)
def test_missing_docstring(a, b): # noqa: D213, D407
"""Test a valid args section.
Args:
a:
"""
@staticmethod
def test_hanging_indent(skip, verbose): # noqa: D213, D407
"""Do stuff.
Args:
skip (:attr:`.Skip`):
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Etiam at tellus a tellus faucibus maximus. Curabitur tellus
mauris, semper id vehicula ac, feugiat ut tortor.
verbose (bool):
If True, print out as much infromation as possible.
If False, print out concise "one-liner" information.
"""
@expect(_D213)
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) y are missing descriptions in "
"'test_missing_numpy_args' docstring)")
def test_missing_numpy_args(_private_arg=0, x=1, y=2): # noqa: D406, D407
"""Toggle the gizmo.
Parameters
----------
x : int
The greatest integer in the history \
of the entire world.
"""
class TestNumpy: # noqa: D203
"""Test class."""
def test_method(self, test, another_test, z, _, x=1, y=2, _private_arg=1): # noqa: D213, D407
"""Test a valid args section.
Some long string with a \
line continuation.
Parameters
----------
test, another_test
Some parameters without type.
z : some parameter with a very long type description that requires a \
line continuation.
But no further description.
x, y : int
Some integer parameters.
"""
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) test, y, z are missing descriptions in "
"'test_missing_args' docstring)", arg_count=5)
def test_missing_args(self, test, x, y, z=3, t=1, _private=0): # noqa: D213, D407
"""Test a valid args section.
Parameters
----------
x, t : int
Some parameters.
"""
@classmethod
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) test, y, z are missing descriptions in "
"'test_missing_args_class_method' docstring)", arg_count=4)
def test_missing_args_class_method(cls, test, x, y, z=3): # noqa: D213, D407
"""Test a valid args section.
Parameters
----------
z
x
Another parameter. The parameters y, test below are
missing descriptions. The parameter z above is also missing
a description.
y
test
"""
@staticmethod
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) a, z are missing descriptions in "
"'test_missing_args_static_method' docstring)", arg_count=3)
def test_missing_args_static_method(a, x, y, z=3, t=1): # noqa: D213, D407
"""Test a valid args section.
Parameters
----------
x, y
Another parameter.
t : int
Yet another parameter.
"""
@staticmethod
def test_mixing_numpy_and_google(danger): # noqa: D213
"""Repro for #388.
Parameters
----------
danger
Zoneeeeee!
"""
class TestIncorrectIndent: # noqa: D203
"""Test class."""
@expect("D417: Missing argument descriptions in the docstring "
"(argument(s) y are missing descriptions in "
"'test_incorrect_indent' docstring)", arg_count=3)
def test_incorrect_indent(self, x=1, y=2): # noqa: D207, D213, D407
"""Reproducing issue #437.
Testing this incorrectly indented docstring.
Args:
x: Test argument.
"""

23
setup.py Normal file
View File

@@ -0,0 +1,23 @@
import sys
from setuptools import setup
sys.stderr.write(
"""
===============================
Unsupported installation method
===============================
ruff no longer supports installation with `python setup.py install`.
Please use `python -m pip install .` instead.
"""
)
sys.exit(1)
# The below code will never execute, however GitHub is particularly
# picky about where it finds Python packaging metadata.
# See: https://github.com/github/feedback/discussions/6456
#
# To be removed once GitHub catches up.
setup(name="ruff", install_requires=[])

View File

@@ -1,4 +1,4 @@
pub mod checks;
pub mod checkers;
pub mod helpers;
pub mod operations;
pub mod relocate;

View File

@@ -9,6 +9,7 @@ use rustpython_parser::ast::{
};
use serde::{Deserialize, Serialize};
use crate::ast::helpers;
use crate::ast::types::{
Binding, BindingKind, CheckLocator, FunctionScope, Range, Scope, ScopeKind,
};
@@ -16,7 +17,7 @@ use crate::checks::{Check, CheckKind, RejectedCmpop};
use crate::python::builtins::BUILTINS;
/// Check IfTuple compliance.
pub fn check_if_tuple(test: &Expr, location: Range) -> Option<Check> {
pub fn if_tuple(test: &Expr, location: Range) -> Option<Check> {
if let ExprKind::Tuple { elts, .. } = &test.node {
if !elts.is_empty() {
return Some(Check::new(CheckKind::IfTuple, location));
@@ -26,7 +27,7 @@ pub fn check_if_tuple(test: &Expr, location: Range) -> Option<Check> {
}
/// Check AssertTuple compliance.
pub fn check_assert_tuple(test: &Expr, location: Range) -> Option<Check> {
pub fn assert_tuple(test: &Expr, location: Range) -> Option<Check> {
if let ExprKind::Tuple { elts, .. } = &test.node {
if !elts.is_empty() {
return Some(Check::new(CheckKind::AssertTuple, location));
@@ -36,7 +37,7 @@ pub fn check_assert_tuple(test: &Expr, location: Range) -> Option<Check> {
}
/// Check NotInTest and NotIsTest compliance.
pub fn check_not_tests(
pub fn not_tests(
op: &Unaryop,
operand: &Expr,
check_not_in: bool,
@@ -75,7 +76,7 @@ pub fn check_not_tests(
}
/// Check UnusedVariable compliance.
pub fn check_unused_variables(
pub fn unused_variables(
scope: &Scope,
locator: &dyn CheckLocator,
dummy_variable_rgx: &Regex,
@@ -108,7 +109,7 @@ pub fn check_unused_variables(
}
/// Check DoNotAssignLambda compliance.
pub fn check_do_not_assign_lambda(value: &Expr, location: Range) -> Option<Check> {
pub fn do_not_assign_lambda(value: &Expr, location: Range) -> Option<Check> {
if let ExprKind::Lambda { .. } = &value.node {
Some(Check::new(CheckKind::DoNotAssignLambda, location))
} else {
@@ -117,11 +118,7 @@ pub fn check_do_not_assign_lambda(value: &Expr, location: Range) -> Option<Check
}
/// Check UselessMetaclassType compliance.
pub fn check_useless_metaclass_type(
targets: &Vec<Expr>,
value: &Expr,
location: Range,
) -> Option<Check> {
pub fn useless_metaclass_type(targets: &Vec<Expr>, value: &Expr, location: Range) -> Option<Check> {
if targets.len() == 1 {
if let ExprKind::Name { id, .. } = targets.first().map(|expr| &expr.node).unwrap() {
if id == "__metaclass__" {
@@ -137,7 +134,7 @@ pub fn check_useless_metaclass_type(
}
/// Check UnnecessaryAbspath compliance.
pub fn check_unnecessary_abspath(func: &Expr, args: &Vec<Expr>, location: Range) -> Option<Check> {
pub fn unnecessary_abspath(func: &Expr, args: &Vec<Expr>, location: Range) -> Option<Check> {
// Validate the arguments.
if args.len() == 1 {
if let ExprKind::Name { id, .. } = &args[0].node {
@@ -193,7 +190,7 @@ impl Primitive {
}
/// Check TypeOfPrimitive compliance.
pub fn check_type_of_primitive(func: &Expr, args: &Vec<Expr>, location: Range) -> Option<Check> {
pub fn type_of_primitive(func: &Expr, args: &Vec<Expr>, location: Range) -> Option<Check> {
// Validate the arguments.
if args.len() == 1 {
match &func.node {
@@ -221,7 +218,7 @@ fn is_ambiguous_name(name: &str) -> bool {
}
/// Check AmbiguousVariableName compliance.
pub fn check_ambiguous_variable_name(name: &str, location: Range) -> Option<Check> {
pub fn ambiguous_variable_name(name: &str, location: Range) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
CheckKind::AmbiguousVariableName(name.to_string()),
@@ -233,7 +230,7 @@ pub fn check_ambiguous_variable_name(name: &str, location: Range) -> Option<Chec
}
/// Check AmbiguousClassName compliance.
pub fn check_ambiguous_class_name(name: &str, location: Range) -> Option<Check> {
pub fn ambiguous_class_name(name: &str, location: Range) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
CheckKind::AmbiguousClassName(name.to_string()),
@@ -245,7 +242,7 @@ pub fn check_ambiguous_class_name(name: &str, location: Range) -> Option<Check>
}
/// Check AmbiguousFunctionName compliance.
pub fn check_ambiguous_function_name(name: &str, location: Range) -> Option<Check> {
pub fn ambiguous_function_name(name: &str, location: Range) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
CheckKind::AmbiguousFunctionName(name.to_string()),
@@ -257,11 +254,7 @@ pub fn check_ambiguous_function_name(name: &str, location: Range) -> Option<Chec
}
/// Check UselessObjectInheritance compliance.
pub fn check_useless_object_inheritance(
name: &str,
bases: &[Expr],
scope: &Scope,
) -> Option<Check> {
pub fn useless_object_inheritance(name: &str, bases: &[Expr], scope: &Scope) -> Option<Check> {
for expr in bases {
if let ExprKind::Name { id, .. } = &expr.node {
if id == "object" {
@@ -286,7 +279,7 @@ pub fn check_useless_object_inheritance(
}
/// Check DefaultExceptNotLast compliance.
pub fn check_default_except_not_last(handlers: &Vec<Excepthandler>) -> Option<Check> {
pub fn default_except_not_last(handlers: &Vec<Excepthandler>) -> Option<Check> {
for (idx, handler) in handlers.iter().enumerate() {
let ExcepthandlerKind::ExceptHandler { type_, .. } = &handler.node;
if type_.is_none() && idx < handlers.len() - 1 {
@@ -301,7 +294,7 @@ pub fn check_default_except_not_last(handlers: &Vec<Excepthandler>) -> Option<Ch
}
/// Check RaiseNotImplemented compliance.
pub fn check_raise_not_implemented(expr: &Expr) -> Option<Check> {
pub fn raise_not_implemented(expr: &Expr) -> Option<Check> {
match &expr.node {
ExprKind::Call { func, .. } => {
if let ExprKind::Name { id, .. } = &func.node {
@@ -328,7 +321,7 @@ pub fn check_raise_not_implemented(expr: &Expr) -> Option<Check> {
}
/// Check DuplicateArgumentName compliance.
pub fn check_duplicate_arguments(arguments: &Arguments) -> Vec<Check> {
pub fn duplicate_arguments(arguments: &Arguments) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
// Collect all the arguments into a single vector.
@@ -376,7 +369,7 @@ fn convert_to_value(expr: &Expr) -> Option<DictionaryKey> {
}
/// Check MultiValueRepeatedKeyLiteral and MultiValueRepeatedKeyVariable compliance.
pub fn check_repeated_keys(
pub fn repeated_keys(
keys: &Vec<Expr>,
check_repeated_literals: bool,
check_repeated_variables: bool,
@@ -416,7 +409,7 @@ pub fn check_repeated_keys(
}
/// Check TrueFalseComparison and NoneComparison compliance.
pub fn check_literal_comparisons(
pub fn literal_comparisons(
left: &Expr,
ops: &Vec<Cmpop>,
comparators: &Vec<Expr>,
@@ -547,7 +540,7 @@ fn is_constant_non_singleton(expr: &Expr) -> bool {
}
/// Check IsLiteral compliance.
pub fn check_is_literal(
pub fn is_literal(
left: &Expr,
ops: &Vec<Cmpop>,
comparators: &Vec<Expr>,
@@ -569,11 +562,7 @@ pub fn check_is_literal(
}
/// Check TypeComparison compliance.
pub fn check_type_comparison(
ops: &Vec<Cmpop>,
comparators: &Vec<Expr>,
location: Range,
) -> Vec<Check> {
pub fn type_comparison(ops: &Vec<Cmpop>, comparators: &Vec<Expr>, location: Range) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
for (op, right) in izip!(ops, comparators) {
@@ -609,7 +598,7 @@ pub fn check_type_comparison(
}
/// Check TwoStarredExpressions and TooManyExpressionsInStarredAssignment compliance.
pub fn check_starred_expressions(
pub fn starred_expressions(
elts: &[Expr],
check_too_many_expressions: bool,
check_two_starred_expressions: bool,
@@ -639,7 +628,7 @@ pub fn check_starred_expressions(
}
/// Check BreakOutsideLoop compliance.
pub fn check_break_outside_loop(
pub fn break_outside_loop(
stmt: &Stmt,
parents: &[&Stmt],
parent_stack: &[usize],
@@ -680,7 +669,7 @@ pub fn check_break_outside_loop(
}
/// Check ContinueOutsideLoop compliance.
pub fn check_continue_outside_loop(
pub fn continue_outside_loop(
stmt: &Stmt,
parents: &[&Stmt],
parent_stack: &[usize],
@@ -728,11 +717,7 @@ pub enum ShadowingType {
}
/// Check builtin name shadowing
pub fn check_builtin_shadowing(
name: &str,
location: Range,
node_type: ShadowingType,
) -> Option<Check> {
pub fn builtin_shadowing(name: &str, location: Range, node_type: ShadowingType) -> Option<Check> {
if BUILTINS.contains(&name) {
Some(Check::new(
match node_type {
@@ -747,17 +732,7 @@ pub fn check_builtin_shadowing(
}
}
/// Returns `true` if a call is an argumented `super` invocation.
pub fn is_super_call_with_arguments(func: &Expr, args: &Vec<Expr>) -> bool {
// Check: is this a `super` call?
if let ExprKind::Name { id, .. } = &func.node {
id == "super" && !args.is_empty()
} else {
false
}
}
// flakes8-comprehensions
// flake8-comprehensions
/// Check `list(generator)` compliance.
pub fn unnecessary_generator_list(expr: &Expr, func: &Expr, args: &Vec<Expr>) -> Option<Check> {
if args.len() == 1 {
@@ -969,6 +944,128 @@ pub fn unnecessary_collection_call(
None
}
pub fn unnecessary_literal_within_tuple_call(
expr: &Expr,
func: &Expr,
args: &[Expr],
) -> Option<Check> {
if let ExprKind::Name { id, .. } = &func.node {
if id == "tuple" {
if let Some(arg) = args.first() {
match &arg.node {
ExprKind::Tuple { .. } => {
return Some(Check::new(
CheckKind::UnnecessaryLiteralWithinTupleCall("tuple".to_string()),
Range::from_located(expr),
));
}
ExprKind::List { .. } => {
return Some(Check::new(
CheckKind::UnnecessaryLiteralWithinTupleCall("list".to_string()),
Range::from_located(expr),
));
}
_ => {}
}
}
}
}
None
}
pub fn unnecessary_literal_within_list_call(
expr: &Expr,
func: &Expr,
args: &[Expr],
) -> Option<Check> {
if let ExprKind::Name { id, .. } = &func.node {
if id == "list" {
if let Some(arg) = args.first() {
match &arg.node {
ExprKind::Tuple { .. } => {
return Some(Check::new(
CheckKind::UnnecessaryLiteralWithinListCall("tuple".to_string()),
Range::from_located(expr),
));
}
ExprKind::List { .. } => {
return Some(Check::new(
CheckKind::UnnecessaryLiteralWithinListCall("list".to_string()),
Range::from_located(expr),
));
}
_ => {}
}
}
}
}
None
}
pub fn unnecessary_double_cast_or_process(
expr: &Expr,
func: &Expr,
args: &[Expr],
) -> Option<Check> {
if let ExprKind::Name { id: outer, .. } = &func.node {
if outer == "list"
|| outer == "tuple"
|| outer == "set"
|| outer == "reversed"
|| outer == "sorted"
{
if let Some(arg) = args.first() {
if let ExprKind::Call { func, .. } = &arg.node {
if let ExprKind::Name { id: inner, .. } = &func.node {
// Ex) set(tuple(...))
if (outer == "set" || outer == "sorted")
&& (inner == "list"
|| inner == "tuple"
|| inner == "reversed"
|| inner == "sorted")
{
return Some(Check::new(
CheckKind::UnnecessaryDoubleCastOrProcess(
inner.to_string(),
outer.to_string(),
),
Range::from_located(expr),
));
}
// Ex) list(tuple(...))
if (outer == "list" || outer == "tuple")
&& (inner == "list" || inner == "tuple")
{
return Some(Check::new(
CheckKind::UnnecessaryDoubleCastOrProcess(
inner.to_string(),
outer.to_string(),
),
Range::from_located(expr),
));
}
// Ex) set(set(...))
if outer == "set" && inner == "set" {
return Some(Check::new(
CheckKind::UnnecessaryDoubleCastOrProcess(
inner.to_string(),
outer.to_string(),
),
Range::from_located(expr),
));
}
}
}
}
}
}
None
}
pub fn unnecessary_subscript_reversal(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
if let Some(first_arg) = args.first() {
if let ExprKind::Name { id, .. } = &func.node {
@@ -1009,14 +1106,14 @@ pub fn unnecessary_subscript_reversal(expr: &Expr, func: &Expr, args: &[Expr]) -
// flake8-super
/// Check that `super()` has no args
pub fn check_super_args(
pub fn super_args(
scope: &Scope,
parents: &[&Stmt],
expr: &Expr,
func: &Expr,
args: &Vec<Expr>,
) -> Option<Check> {
if !is_super_call_with_arguments(func, args) {
if !helpers::is_super_call_with_arguments(func, args) {
return None;
}
@@ -1075,7 +1172,7 @@ pub fn check_super_args(
// flake8-print
/// Check whether a function call is a `print` or `pprint` invocation
pub fn check_print_call(
pub fn print_call(
expr: &Expr,
func: &Expr,
check_print: bool,

View File

@@ -1,10 +1,42 @@
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_ast::{Expr, ExprKind, StmtKind};
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, StmtKind};
use crate::python::typing;
static DUNDER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"__[^\s]+__").unwrap());
fn compose_call_path_inner<'a>(expr: &'a Expr, parts: &mut Vec<&'a str>) {
match &expr.node {
ExprKind::Call { func, .. } => {
compose_call_path_inner(func, parts);
}
ExprKind::Attribute { value, attr, .. } => {
compose_call_path_inner(value, parts);
parts.push(attr);
}
ExprKind::Name { id, .. } => {
parts.push(id);
}
_ => {}
}
}
pub fn compose_call_path(expr: &Expr) -> Option<String> {
let mut segments = vec![];
compose_call_path_inner(expr, &mut segments);
if segments.is_empty() {
None
} else {
Some(segments.join("."))
}
}
pub fn match_name_or_attr(expr: &Expr, target: &str) -> bool {
match &expr.node {
ExprKind::Attribute { attr, .. } => target == attr,
ExprKind::Name { id, .. } => target == id,
_ => false,
}
}
pub enum SubscriptKind {
AnnotatedSubscript,
@@ -35,6 +67,8 @@ pub fn match_annotated_subscript(expr: &Expr) -> Option<SubscriptKind> {
}
}
static DUNDER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"__[^\s]+__").unwrap());
pub fn is_assignment_to_a_dunder(node: &StmtKind) -> bool {
// Check whether it's an assignment to a dunder, with or without a type annotation.
// This is what pycodestyle (as of 2.9.1) does.
@@ -65,10 +99,35 @@ pub fn is_assignment_to_a_dunder(node: &StmtKind) -> bool {
}
}
pub fn match_name_or_attr(expr: &Expr, target: &str) -> bool {
match &expr.node {
ExprKind::Attribute { attr, .. } => target == attr,
ExprKind::Name { id, .. } => target == id,
_ => false,
/// Extract the names of all handled exceptions.
pub fn extract_handler_names(handlers: &[Excepthandler]) -> Vec<String> {
let mut handler_names = vec![];
for handler in handlers {
match &handler.node {
ExcepthandlerKind::ExceptHandler { type_, .. } => {
if let Some(type_) = type_ {
if let ExprKind::Tuple { elts, .. } = &type_.node {
for type_ in elts {
if let Some(name) = compose_call_path(type_) {
handler_names.push(name);
}
}
} else if let Some(name) = compose_call_path(type_) {
handler_names.push(name);
}
}
}
}
}
handler_names
}
/// Returns `true` if a call is an argumented `super` invocation.
pub fn is_super_call_with_arguments(func: &Expr, args: &Vec<Expr>) -> bool {
// Check: is this a `super` call?
if let ExprKind::Name { id, .. } = &func.node {
id == "super" && !args.is_empty()
} else {
false
}
}

View File

@@ -134,7 +134,7 @@ impl<'a> SourceCodeLocator<'a> {
}
}
pub fn slice_source_code_at(&mut self, location: &Location) -> &'a str {
fn init(&mut self) {
if !self.initialized {
let mut offset = 0;
for i in self.content.lines() {
@@ -142,24 +142,40 @@ impl<'a> SourceCodeLocator<'a> {
offset += i.len();
offset += 1;
}
self.offsets.push(offset);
self.initialized = true;
}
}
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..]
}
pub fn slice_source_code_range(&mut self, range: &Range) -> &'a str {
if !self.initialized {
let mut offset = 0;
for i in self.content.lines() {
self.offsets.push(offset);
offset += i.len();
offset += 1;
}
self.initialized = true;
}
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 partition_source_code_at(
&mut 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;
(
&self.content[outer_start..inner_start],
&self.content[inner_start..inner_end],
&self.content[inner_end..outer_end],
)
}
}

View File

@@ -1,10 +1,11 @@
use crate::ast::helpers::match_name_or_attr;
use rustpython_parser::ast::{
Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, Excepthandler,
ExcepthandlerKind, Expr, ExprContext, ExprKind, Keyword, MatchCase, Operator, Pattern,
PatternKind, Stmt, StmtKind, Unaryop, Withitem,
};
use crate::ast::helpers::match_name_or_attr;
pub trait Visitor<'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
walk_stmt(self, stmt);

View File

@@ -10,7 +10,7 @@ use rustpython_parser::ast::{
};
use rustpython_parser::parser;
use crate::ast::helpers::{match_name_or_attr, SubscriptKind};
use crate::ast::helpers::{extract_handler_names, match_name_or_attr, SubscriptKind};
use crate::ast::operations::{extract_all_names, SourceCodeLocator};
use crate::ast::relocate::relocate_expr;
use crate::ast::types::{
@@ -18,25 +18,30 @@ use crate::ast::types::{
ScopeKind,
};
use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::{checks, helpers, operations, visitor};
use crate::ast::{checkers, helpers, operations, visitor};
use crate::autofix::{fixer, fixes};
use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::docstring_checks;
use crate::docstrings::types::{Definition, DefinitionKind, Documentable};
use crate::plugins;
use crate::python::builtins::{BUILTINS, MAGIC_GLOBALS};
use crate::python::future::ALL_FEATURE_NAMES;
use crate::settings::{PythonVersion, Settings};
use crate::visibility::{module_visibility, transition_scope, Modifier, Visibility, VisibleScope};
pub const GLOBAL_SCOPE_INDEX: usize = 0;
pub struct Checker<'a> {
// Input data.
path: &'a Path,
pub(crate) path: &'a Path,
// TODO(charlie): Separate immutable from mutable state. (None of these should ever change.)
pub(crate) locator: SourceCodeLocator<'a>,
pub(crate) settings: &'a Settings,
pub(crate) autofix: &'a fixer::Mode,
// Computed checks.
checks: Vec<Check>,
// Docstring tracking.
docstrings: Vec<(Definition<'a>, Visibility)>,
// Edit tracking.
// TODO(charlie): Instead of exposing deletions, wrap in a public API.
pub(crate) deletions: BTreeSet<usize>,
@@ -49,17 +54,18 @@ pub struct Checker<'a> {
dead_scopes: Vec<usize>,
deferred_string_annotations: Vec<(Range, &'a str)>,
deferred_annotations: Vec<(&'a Expr, Vec<usize>, Vec<usize>)>,
deferred_functions: Vec<(&'a Stmt, Vec<usize>, Vec<usize>)>,
deferred_functions: Vec<(&'a Stmt, Vec<usize>, Vec<usize>, VisibleScope)>,
deferred_lambdas: Vec<(&'a Expr, Vec<usize>, Vec<usize>)>,
deferred_assignments: Vec<usize>,
// Internal, derivative state.
visible_scope: VisibleScope,
in_f_string: Option<Range>,
in_annotation: bool,
in_literal: bool,
seen_non_import: bool,
seen_docstring: bool,
seen_import_boundary: bool,
futures_allowed: bool,
annotations_future_enabled: bool,
except_handlers: Vec<Vec<String>>,
}
impl<'a> Checker<'a> {
@@ -74,25 +80,30 @@ impl<'a> Checker<'a> {
autofix,
path,
locator: SourceCodeLocator::new(content),
checks: vec![],
parents: vec![],
parent_stack: vec![],
scopes: vec![],
scope_stack: vec![],
dead_scopes: vec![],
deferred_string_annotations: vec![],
deferred_annotations: vec![],
deferred_functions: vec![],
deferred_lambdas: vec![],
deferred_assignments: vec![],
in_f_string: None,
in_annotation: false,
in_literal: false,
seen_non_import: false,
seen_docstring: false,
futures_allowed: true,
annotations_future_enabled: false,
checks: Default::default(),
docstrings: Default::default(),
deletions: Default::default(),
parents: Default::default(),
parent_stack: Default::default(),
scopes: Default::default(),
scope_stack: Default::default(),
dead_scopes: Default::default(),
deferred_string_annotations: Default::default(),
deferred_annotations: Default::default(),
deferred_functions: Default::default(),
deferred_lambdas: Default::default(),
deferred_assignments: Default::default(),
visible_scope: VisibleScope {
modifier: Modifier::Module,
visibility: module_visibility(path),
},
in_f_string: None,
in_annotation: Default::default(),
in_literal: Default::default(),
seen_import_boundary: Default::default(),
futures_allowed: true,
annotations_future_enabled: Default::default(),
except_handlers: Default::default(),
}
}
}
@@ -119,48 +130,13 @@ where
StmtKind::Import { .. } => {
self.futures_allowed = false;
}
StmtKind::Expr { value } => {
if self.seen_docstring
&& !self.seen_non_import
&& !operations::in_nested_block(&self.parent_stack, &self.parents)
{
self.seen_non_import = true;
}
if !self.seen_docstring
&& !operations::in_nested_block(&self.parent_stack, &self.parents)
&& matches!(
&value.node,
ExprKind::Constant {
value: Constant::Str(_),
..
},
)
{
self.seen_docstring = true;
}
// Allow docstrings to interrupt __future__ imports.
if self.futures_allowed
&& !matches!(
&value.node,
ExprKind::Constant {
value: Constant::Str(_),
..
},
)
{
self.futures_allowed = false;
}
}
node => {
self.futures_allowed = false;
if !self.seen_non_import
if !self.seen_import_boundary
&& !helpers::is_assignment_to_a_dunder(node)
&& !operations::in_nested_block(&self.parent_stack, &self.parents)
{
self.seen_non_import = true;
self.seen_import_boundary = true;
}
}
}
@@ -190,27 +166,24 @@ where
if self.settings.enabled.contains(&CheckCode::E741) {
let location = self.locate_check(Range::from_located(stmt));
self.checks.extend(
names.iter().filter_map(|name| {
checks::check_ambiguous_variable_name(name, location)
}),
names
.iter()
.filter_map(|name| checkers::ambiguous_variable_name(name, location)),
);
}
}
StmtKind::Break => {
if self.settings.enabled.contains(&CheckCode::F701) {
if let Some(check) = checks::check_break_outside_loop(
stmt,
&self.parents,
&self.parent_stack,
self,
) {
if let Some(check) =
checkers::break_outside_loop(stmt, &self.parents, &self.parent_stack, self)
{
self.checks.push(check);
}
}
}
StmtKind::Continue => {
if self.settings.enabled.contains(&CheckCode::F702) {
if let Some(check) = checks::check_continue_outside_loop(
if let Some(check) = checkers::continue_outside_loop(
stmt,
&self.parents,
&self.parent_stack,
@@ -235,7 +208,7 @@ where
..
} => {
if self.settings.enabled.contains(&CheckCode::E743) {
if let Some(check) = checks::check_ambiguous_function_name(
if let Some(check) = checkers::ambiguous_function_name(
name,
self.locate_check(Range::from_located(stmt)),
) {
@@ -323,7 +296,7 @@ where
}
if self.settings.enabled.contains(&CheckCode::E742) {
if let Some(check) = checks::check_ambiguous_class_name(
if let Some(check) = checkers::ambiguous_class_name(
name,
self.locate_check(Range::from_located(stmt)),
) {
@@ -353,7 +326,7 @@ where
.settings
.enabled
.contains(CheckKind::ModuleImportNotAtTopOfFile.code())
&& self.seen_non_import
&& self.seen_import_boundary
&& stmt.location.column() == 1
{
self.checks.push(Check::new(
@@ -414,7 +387,7 @@ where
.settings
.enabled
.contains(CheckKind::ModuleImportNotAtTopOfFile.code())
&& self.seen_non_import
&& self.seen_import_boundary
&& stmt.location.column() == 1
{
self.checks.push(Check::new(
@@ -532,7 +505,7 @@ where
StmtKind::Raise { exc, .. } => {
if self.settings.enabled.contains(&CheckCode::F901) {
if let Some(expr) = exc {
if let Some(check) = checks::check_raise_not_implemented(expr) {
if let Some(check) = checkers::raise_not_implemented(expr) {
self.checks.push(check);
}
}
@@ -546,21 +519,29 @@ where
plugins::if_tuple(self, stmt, test);
}
}
StmtKind::Assert { test, .. } => {
StmtKind::Assert { test, msg } => {
if self.settings.enabled.contains(&CheckCode::F631) {
plugins::assert_tuple(self, stmt, test);
}
if self.settings.enabled.contains(&CheckCode::B011) {
plugins::assert_false(self, stmt, test, msg);
}
}
StmtKind::Try { handlers, .. } => {
if self.settings.enabled.contains(&CheckCode::F707) {
if let Some(check) = checks::check_default_except_not_last(handlers) {
if let Some(check) = checkers::default_except_not_last(handlers) {
self.checks.push(check);
}
}
if self.settings.enabled.contains(&CheckCode::B014)
|| self.settings.enabled.contains(&CheckCode::B025)
{
plugins::duplicate_exceptions(self, stmt, handlers);
}
}
StmtKind::Assign { targets, value, .. } => {
if self.settings.enabled.contains(&CheckCode::E731) {
if let Some(check) = checks::check_do_not_assign_lambda(
if let Some(check) = checkers::do_not_assign_lambda(
value,
self.locate_check(Range::from_located(stmt)),
) {
@@ -574,7 +555,7 @@ where
StmtKind::AnnAssign { value, .. } => {
if self.settings.enabled.contains(&CheckCode::E731) {
if let Some(value) = value {
if let Some(check) = checks::check_do_not_assign_lambda(
if let Some(check) = checkers::do_not_assign_lambda(
value,
self.locate_check(Range::from_located(stmt)),
) {
@@ -588,21 +569,65 @@ where
}
// Recurse.
let prev_visibile_scope = self.visible_scope.clone();
match &stmt.node {
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
StmtKind::FunctionDef { body, .. } | StmtKind::AsyncFunctionDef { body, .. } => {
let definition = docstring_checks::extract(
&self.visible_scope,
stmt,
body,
&Documentable::Function,
);
let scope = transition_scope(&self.visible_scope, stmt, &Documentable::Function);
self.docstrings.push((definition, scope.visibility.clone()));
self.visible_scope = scope;
self.deferred_functions.push((
stmt,
self.scope_stack.clone(),
self.parent_stack.clone(),
self.visible_scope.clone(),
));
}
StmtKind::ClassDef { body, .. } => {
let definition = docstring_checks::extract(
&self.visible_scope,
stmt,
body,
&Documentable::Class,
);
let scope = transition_scope(&self.visible_scope, stmt, &Documentable::Class);
self.docstrings.push((definition, scope.visibility.clone()));
self.visible_scope = scope;
for stmt in body {
self.visit_stmt(stmt);
}
}
StmtKind::Try {
body,
handlers,
orelse,
finalbody,
} => {
self.except_handlers.push(extract_handler_names(handlers));
for stmt in body {
self.visit_stmt(stmt);
}
self.except_handlers.pop();
for excepthandler in handlers {
self.visit_excepthandler(excepthandler)
}
for stmt in orelse {
self.visit_stmt(stmt);
}
for stmt in finalbody {
self.visit_stmt(stmt);
}
}
_ => visitor::walk_stmt(self, stmt),
};
self.visible_scope = prev_visibile_scope;
// Post-visit.
if let StmtKind::ClassDef { name, .. } = &stmt.node {
@@ -670,7 +695,7 @@ where
self.settings.enabled.contains(&CheckCode::F621);
let check_two_starred_expressions =
self.settings.enabled.contains(&CheckCode::F622);
if let Some(check) = checks::check_starred_expressions(
if let Some(check) = checkers::starred_expressions(
elts,
check_too_many_expressions,
check_two_starred_expressions,
@@ -693,7 +718,7 @@ where
}
ExprContext::Store => {
if self.settings.enabled.contains(&CheckCode::E741) {
if let Some(check) = checks::check_ambiguous_variable_name(
if let Some(check) = checkers::ambiguous_variable_name(
id,
self.locate_check(Range::from_located(expr)),
) {
@@ -743,26 +768,26 @@ where
// flake8-comprehensions
if self.settings.enabled.contains(&CheckCode::C400) {
if let Some(check) = checks::unnecessary_generator_list(expr, func, args) {
if let Some(check) = checkers::unnecessary_generator_list(expr, func, args) {
self.checks.push(check);
};
}
if self.settings.enabled.contains(&CheckCode::C401) {
if let Some(check) = checks::unnecessary_generator_set(expr, func, args) {
if let Some(check) = checkers::unnecessary_generator_set(expr, func, args) {
self.checks.push(check);
};
}
if self.settings.enabled.contains(&CheckCode::C402) {
if let Some(check) = checks::unnecessary_generator_dict(expr, func, args) {
if let Some(check) = checkers::unnecessary_generator_dict(expr, func, args) {
self.checks.push(check);
};
}
if self.settings.enabled.contains(&CheckCode::C403) {
if let Some(check) =
checks::unnecessary_list_comprehension_set(expr, func, args)
checkers::unnecessary_list_comprehension_set(expr, func, args)
{
self.checks.push(check);
};
@@ -770,34 +795,59 @@ where
if self.settings.enabled.contains(&CheckCode::C404) {
if let Some(check) =
checks::unnecessary_list_comprehension_dict(expr, func, args)
checkers::unnecessary_list_comprehension_dict(expr, func, args)
{
self.checks.push(check);
};
}
if self.settings.enabled.contains(&CheckCode::C405) {
if let Some(check) = checks::unnecessary_literal_set(expr, func, args) {
if let Some(check) = checkers::unnecessary_literal_set(expr, func, args) {
self.checks.push(check);
};
}
if self.settings.enabled.contains(&CheckCode::C406) {
if let Some(check) = checks::unnecessary_literal_dict(expr, func, args) {
if let Some(check) = checkers::unnecessary_literal_dict(expr, func, args) {
self.checks.push(check);
};
}
if self.settings.enabled.contains(&CheckCode::C408) {
if let Some(check) =
checks::unnecessary_collection_call(expr, func, args, keywords)
checkers::unnecessary_collection_call(expr, func, args, keywords)
{
self.checks.push(check);
};
}
if self.settings.enabled.contains(&CheckCode::C409) {
if let Some(check) =
checkers::unnecessary_literal_within_tuple_call(expr, func, args)
{
self.checks.push(check);
};
}
if self.settings.enabled.contains(&CheckCode::C410) {
if let Some(check) =
checkers::unnecessary_literal_within_list_call(expr, func, args)
{
self.checks.push(check);
};
}
if self.settings.enabled.contains(&CheckCode::C414) {
if let Some(check) =
checkers::unnecessary_double_cast_or_process(expr, func, args)
{
self.checks.push(check);
};
}
if self.settings.enabled.contains(&CheckCode::C415) {
if let Some(check) = checks::unnecessary_subscript_reversal(expr, func, args) {
if let Some(check) = checkers::unnecessary_subscript_reversal(expr, func, args)
{
self.checks.push(check);
};
}
@@ -832,7 +882,7 @@ where
let check_repeated_literals = self.settings.enabled.contains(&CheckCode::F601);
let check_repeated_variables = self.settings.enabled.contains(&CheckCode::F602);
if check_repeated_literals || check_repeated_variables {
self.checks.extend(checks::check_repeated_keys(
self.checks.extend(checkers::repeated_keys(
keys,
check_repeated_literals,
check_repeated_variables,
@@ -884,7 +934,7 @@ where
let check_not_in = self.settings.enabled.contains(&CheckCode::E713);
let check_not_is = self.settings.enabled.contains(&CheckCode::E714);
if check_not_in || check_not_is {
self.checks.extend(checks::check_not_tests(
self.checks.extend(checkers::not_tests(
op,
operand,
check_not_in,
@@ -901,7 +951,7 @@ where
let check_none_comparisons = self.settings.enabled.contains(&CheckCode::E711);
let check_true_false_comparisons = self.settings.enabled.contains(&CheckCode::E712);
if check_none_comparisons || check_true_false_comparisons {
self.checks.extend(checks::check_literal_comparisons(
self.checks.extend(checkers::literal_comparisons(
left,
ops,
comparators,
@@ -912,7 +962,7 @@ where
}
if self.settings.enabled.contains(&CheckCode::F632) {
self.checks.extend(checks::check_is_literal(
self.checks.extend(checkers::is_literal(
left,
ops,
comparators,
@@ -921,7 +971,7 @@ where
}
if self.settings.enabled.contains(&CheckCode::E721) {
self.checks.extend(checks::check_type_comparison(
self.checks.extend(checkers::type_comparison(
ops,
comparators,
self.locate_check(Range::from_located(expr)),
@@ -1148,7 +1198,7 @@ where
match name {
Some(name) => {
if self.settings.enabled.contains(&CheckCode::E741) {
if let Some(check) = checks::check_ambiguous_variable_name(
if let Some(check) = checkers::ambiguous_variable_name(
name,
self.locate_check(Range::from_located(excepthandler)),
) {
@@ -1216,8 +1266,7 @@ where
fn visit_arguments(&mut self, arguments: &'b Arguments) {
if self.settings.enabled.contains(&CheckCode::F831) {
self.checks
.extend(checks::check_duplicate_arguments(arguments));
self.checks.extend(checkers::duplicate_arguments(arguments));
}
// Bind, but intentionally avoid walking default expressions, as we handle them upstream.
@@ -1250,7 +1299,7 @@ where
);
if self.settings.enabled.contains(&CheckCode::E741) {
if let Some(check) = checks::check_ambiguous_variable_name(
if let Some(check) = checkers::ambiguous_variable_name(
&arg.node.arg,
self.locate_check(Range::from_located(arg)),
) {
@@ -1471,6 +1520,14 @@ impl<'a> Checker<'a> {
if self.path.ends_with("__init__.py") && id == "__path__" {
return;
}
// Avoid flagging if NameError is handled.
if let Some(handler_names) = self.except_handlers.last() {
if handler_names.contains(&"NameError".to_string()) {
return;
}
}
self.checks.push(Check::new(
CheckKind::UndefinedName(id.clone()),
self.locate_check(Range::from_located(expr)),
@@ -1594,6 +1651,25 @@ impl<'a> Checker<'a> {
}
}
fn visit_docstring<'b>(&mut self, python_ast: &'b Suite) -> bool
where
'b: 'a,
{
let docstring = docstring_checks::docstring_from(python_ast);
self.docstrings.push((
Definition {
kind: if self.path.ends_with("__init__.py") {
DefinitionKind::Package
} else {
DefinitionKind::Module
},
docstring,
},
self.visible_scope.visibility.clone(),
));
docstring.is_some()
}
fn check_deferred_annotations(&mut self) {
while let Some((expr, scopes, parents)) = self.deferred_annotations.pop() {
self.parent_stack = parents;
@@ -1646,9 +1722,10 @@ impl<'a> Checker<'a> {
}
fn check_deferred_functions(&mut self) {
while let Some((stmt, scopes, parents)) = self.deferred_functions.pop() {
while let Some((stmt, scopes, parents, visibility)) = self.deferred_functions.pop() {
self.parent_stack = parents;
self.scope_stack = scopes;
self.visible_scope = visibility;
self.push_scope(Scope::new(ScopeKind::Function(Default::default())));
match &stmt.node {
@@ -1690,7 +1767,7 @@ impl<'a> Checker<'a> {
fn check_deferred_assignments(&mut self) {
if self.settings.enabled.contains(&CheckCode::F841) {
while let Some(index) = self.deferred_assignments.pop() {
self.checks.extend(checks::check_unused_variables(
self.checks.extend(checkers::unused_variables(
&self.scopes[index],
self,
&self.settings.dummy_variable_rgx,
@@ -1839,25 +1916,102 @@ impl<'a> Checker<'a> {
}
}
fn check_docstrings(&mut self) {
while let Some((docstring, visibility)) = self.docstrings.pop() {
if !docstring_checks::not_empty(self, &docstring) {
continue;
}
if !docstring_checks::not_missing(self, &docstring, &visibility) {
continue;
}
if self.settings.enabled.contains(&CheckCode::D200) {
docstring_checks::one_liner(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D201)
|| self.settings.enabled.contains(&CheckCode::D202)
{
docstring_checks::blank_before_after_function(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D203)
|| self.settings.enabled.contains(&CheckCode::D204)
|| self.settings.enabled.contains(&CheckCode::D211)
{
docstring_checks::blank_before_after_class(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D205) {
docstring_checks::blank_after_summary(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D209) {
docstring_checks::newline_after_last_paragraph(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D210) {
docstring_checks::no_surrounding_whitespace(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D212)
|| self.settings.enabled.contains(&CheckCode::D213)
{
docstring_checks::multi_line_summary_start(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D300) {
docstring_checks::triple_quotes(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D400) {
docstring_checks::ends_with_period(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D402) {
docstring_checks::no_signature(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D403) {
docstring_checks::capitalized(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D404) {
docstring_checks::starts_with_this(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D415) {
docstring_checks::ends_with_punctuation(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D418) {
docstring_checks::if_needed(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D407)
|| self.settings.enabled.contains(&CheckCode::D414)
|| self.settings.enabled.contains(&CheckCode::D407)
|| self.settings.enabled.contains(&CheckCode::D212)
|| self.settings.enabled.contains(&CheckCode::D408)
|| self.settings.enabled.contains(&CheckCode::D409)
|| self.settings.enabled.contains(&CheckCode::D414)
|| self.settings.enabled.contains(&CheckCode::D412)
|| self.settings.enabled.contains(&CheckCode::D414)
|| self.settings.enabled.contains(&CheckCode::D405)
|| self.settings.enabled.contains(&CheckCode::D413)
|| self.settings.enabled.contains(&CheckCode::D410)
|| self.settings.enabled.contains(&CheckCode::D411)
|| self.settings.enabled.contains(&CheckCode::D406)
{
docstring_checks::check_sections(self, &docstring);
}
}
}
fn check_builtin_shadowing(&mut self, name: &str, location: Range, is_attribute: bool) {
let scope = self.current_scope();
// flake8-builtins
if is_attribute && matches!(scope.kind, ScopeKind::Class) {
if self.settings.enabled.contains(&CheckCode::A003) {
if let Some(check) = checks::check_builtin_shadowing(
if let Some(check) = checkers::builtin_shadowing(
name,
self.locate_check(location),
checks::ShadowingType::Attribute,
checkers::ShadowingType::Attribute,
) {
self.checks.push(check);
}
}
} else if self.settings.enabled.contains(&CheckCode::A001) {
if let Some(check) = checks::check_builtin_shadowing(
if let Some(check) = checkers::builtin_shadowing(
name,
self.locate_check(location),
checks::ShadowingType::Variable,
checkers::ShadowingType::Variable,
) {
self.checks.push(check);
}
@@ -1866,10 +2020,10 @@ impl<'a> Checker<'a> {
fn check_builtin_arg_shadowing(&mut self, name: &str, location: Range) {
if self.settings.enabled.contains(&CheckCode::A002) {
if let Some(check) = checks::check_builtin_shadowing(
if let Some(check) = checkers::builtin_shadowing(
name,
self.locate_check(location),
checks::ShadowingType::Argument,
checkers::ShadowingType::Argument,
) {
self.checks.push(check);
}
@@ -1888,6 +2042,13 @@ pub fn check_ast(
checker.push_scope(Scope::new(ScopeKind::Module));
checker.bind_builtins();
// Check for module docstring.
let python_ast = if checker.visit_docstring(python_ast) {
&python_ast[1..]
} else {
python_ast
};
// Iterate over the AST.
for stmt in python_ast {
checker.visit_stmt(stmt);
@@ -1906,5 +2067,8 @@ pub fn check_ast(
checker.pop_scope();
checker.check_dead_scopes();
// Check docstrings.
checker.check_docstrings();
checker.checks
}

View File

@@ -184,15 +184,15 @@ pub fn check_lines(
let mut valid_codes = vec![];
for code in codes {
if !matches.contains(&code) {
invalid_codes.push(code);
invalid_codes.push(code.to_string());
} else {
valid_codes.push(code);
valid_codes.push(code.to_string());
}
}
if !invalid_codes.is_empty() {
let mut check = Check::new(
CheckKind::UnusedNOQA(Some(invalid_codes.join(", "))),
CheckKind::UnusedNOQA(Some(invalid_codes)),
Range {
location: Location::new(row + 1, start + 1),
end_location: Location::new(row + 1, end + 1),

View File

@@ -1,9 +1,11 @@
use std::str::FromStr;
use itertools::Itertools;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
use strum_macros::{AsRefStr, EnumIter, EnumString};
use crate::ast::checks::Primitive;
use crate::ast::checkers::Primitive;
use crate::ast::types::Range;
pub const DEFAULT_CHECK_CODES: [CheckCode; 43] = [
@@ -120,6 +122,10 @@ pub enum CheckCode {
A001,
A002,
A003,
// flake8-bugbear
B011,
B014,
B025,
// flake8-comprehensions
C400,
C401,
@@ -129,6 +135,9 @@ pub enum CheckCode {
C405,
C406,
C408,
C409,
C410,
C414,
C415,
// flake8-print
T201,
@@ -142,6 +151,44 @@ pub enum CheckCode {
U006,
U007,
U008,
// pydocstyle
D100,
D101,
D102,
D103,
D104,
D105,
D106,
D107,
D200,
D201,
D202,
D203,
D204,
D205,
D209,
D210,
D211,
D212,
D213,
D300,
D400,
D402,
D403,
D404,
D405,
D406,
D407,
D408,
D409,
D410,
D411,
D412,
D413,
D414,
D415,
D418,
D419,
// Meta
M001,
}
@@ -210,7 +257,11 @@ pub enum CheckKind {
BuiltinVariableShadowing(String),
BuiltinArgumentShadowing(String),
BuiltinAttributeShadowing(String),
// flakes8-comprehensions
// flake8-bugbear
DoNotAssertFalse,
DuplicateHandlerException(Vec<String>),
DuplicateTryBlockException(String),
// flake8-comprehensions
UnnecessaryGeneratorList,
UnnecessaryGeneratorSet,
UnnecessaryGeneratorDict,
@@ -219,6 +270,9 @@ pub enum CheckKind {
UnnecessaryLiteralSet(String),
UnnecessaryLiteralDict(String),
UnnecessaryCollectionCall(String),
UnnecessaryLiteralWithinTupleCall(String),
UnnecessaryLiteralWithinListCall(String),
UnnecessaryDoubleCastOrProcess(String, String),
UnnecessarySubscriptReversal(String),
// flake8-print
PrintFound,
@@ -232,8 +286,46 @@ pub enum CheckKind {
UsePEP585Annotation(String),
UsePEP604Annotation,
SuperCallWithParameters,
// pydocstyle
BlankLineAfterLastSection(String),
BlankLineAfterSection(String),
BlankLineBeforeSection(String),
CapitalizeSectionName(String),
DashedUnderlineAfterSection(String),
EndsInPeriod,
EndsInPunctuation,
FirstLineCapitalized,
FitsOnOneLine,
MagicMethod,
MultiLineSummaryFirstLine,
MultiLineSummarySecondLine,
NewLineAfterLastParagraph,
NewLineAfterSectionName(String),
NoBlankLineAfterFunction(usize),
NoBlankLineAfterSummary,
NoBlankLineBeforeClass(usize),
NoBlankLineBeforeFunction(usize),
NoBlankLinesBetweenHeaderAndContent(String),
NoSignature,
NoSurroundingWhitespace,
NoThisPrefix,
NonEmpty,
NonEmptySection(String),
OneBlankLineAfterClass(usize),
OneBlankLineBeforeClass(usize),
PublicClass,
PublicFunction,
PublicInit,
PublicMethod,
PublicModule,
PublicNestedClass,
PublicPackage,
SectionUnderlineAfterName(String),
SectionUnderlineMatchesSectionLength(String),
SkipDocstring,
UsesTripleQuotes,
// Meta
UnusedNOQA(Option<String>),
UnusedNOQA(Option<Vec<String>>),
}
impl CheckCode {
@@ -301,6 +393,10 @@ impl CheckCode {
CheckCode::A001 => CheckKind::BuiltinVariableShadowing("...".to_string()),
CheckCode::A002 => CheckKind::BuiltinArgumentShadowing("...".to_string()),
CheckCode::A003 => CheckKind::BuiltinAttributeShadowing("...".to_string()),
// flake8-bugbear
CheckCode::B011 => CheckKind::DoNotAssertFalse,
CheckCode::B014 => CheckKind::DuplicateHandlerException(vec!["ValueError".to_string()]),
CheckCode::B025 => CheckKind::DuplicateTryBlockException("Exception".to_string()),
// flake8-comprehensions
CheckCode::C400 => CheckKind::UnnecessaryGeneratorList,
CheckCode::C401 => CheckKind::UnnecessaryGeneratorSet,
@@ -312,6 +408,16 @@ impl CheckCode {
CheckCode::C408 => {
CheckKind::UnnecessaryCollectionCall("<dict/list/tuple>".to_string())
}
CheckCode::C409 => {
CheckKind::UnnecessaryLiteralWithinTupleCall("<list/tuple>".to_string())
}
CheckCode::C410 => {
CheckKind::UnnecessaryLiteralWithinListCall("<list/tuple>".to_string())
}
CheckCode::C414 => CheckKind::UnnecessaryDoubleCastOrProcess(
"<list/reversed/set/sorted/tuple>".to_string(),
"<list/set/sorted/tuple>".to_string(),
),
CheckCode::C415 => {
CheckKind::UnnecessarySubscriptReversal("<reversed/set/sorted>".to_string())
}
@@ -330,6 +436,48 @@ impl CheckCode {
CheckCode::U006 => CheckKind::UsePEP585Annotation("List".to_string()),
CheckCode::U007 => CheckKind::UsePEP604Annotation,
CheckCode::U008 => CheckKind::SuperCallWithParameters,
// pydocstyle
CheckCode::D100 => CheckKind::PublicModule,
CheckCode::D101 => CheckKind::PublicClass,
CheckCode::D102 => CheckKind::PublicMethod,
CheckCode::D103 => CheckKind::PublicFunction,
CheckCode::D104 => CheckKind::PublicPackage,
CheckCode::D105 => CheckKind::MagicMethod,
CheckCode::D106 => CheckKind::PublicNestedClass,
CheckCode::D107 => CheckKind::PublicInit,
CheckCode::D200 => CheckKind::FitsOnOneLine,
CheckCode::D201 => CheckKind::NoBlankLineBeforeFunction(1),
CheckCode::D202 => CheckKind::NoBlankLineAfterFunction(1),
CheckCode::D203 => CheckKind::OneBlankLineBeforeClass(0),
CheckCode::D204 => CheckKind::OneBlankLineAfterClass(0),
CheckCode::D205 => CheckKind::NoBlankLineAfterSummary,
CheckCode::D209 => CheckKind::NewLineAfterLastParagraph,
CheckCode::D210 => CheckKind::NoSurroundingWhitespace,
CheckCode::D211 => CheckKind::NoBlankLineBeforeClass(1),
CheckCode::D212 => CheckKind::MultiLineSummaryFirstLine,
CheckCode::D213 => CheckKind::MultiLineSummarySecondLine,
CheckCode::D300 => CheckKind::UsesTripleQuotes,
CheckCode::D400 => CheckKind::EndsInPeriod,
CheckCode::D402 => CheckKind::NoSignature,
CheckCode::D403 => CheckKind::FirstLineCapitalized,
CheckCode::D404 => CheckKind::NoThisPrefix,
CheckCode::D405 => CheckKind::CapitalizeSectionName("returns".to_string()),
CheckCode::D406 => CheckKind::NewLineAfterSectionName("Returns".to_string()),
CheckCode::D407 => CheckKind::DashedUnderlineAfterSection("Returns".to_string()),
CheckCode::D408 => CheckKind::SectionUnderlineAfterName("Returns".to_string()),
CheckCode::D409 => {
CheckKind::SectionUnderlineMatchesSectionLength("Returns".to_string())
}
CheckCode::D410 => CheckKind::BlankLineAfterSection("Returns".to_string()),
CheckCode::D411 => CheckKind::BlankLineBeforeSection("Returns".to_string()),
CheckCode::D412 => {
CheckKind::NoBlankLinesBetweenHeaderAndContent("Returns".to_string())
}
CheckCode::D413 => CheckKind::BlankLineAfterLastSection("Returns".to_string()),
CheckCode::D414 => CheckKind::NonEmptySection("Returns".to_string()),
CheckCode::D415 => CheckKind::EndsInPunctuation,
CheckCode::D418 => CheckKind::SkipDocstring,
CheckCode::D419 => CheckKind::NonEmpty,
// Meta
CheckCode::M001 => CheckKind::UnusedNOQA(None),
}
@@ -389,6 +537,10 @@ impl CheckKind {
CheckKind::BuiltinVariableShadowing(_) => &CheckCode::A001,
CheckKind::BuiltinArgumentShadowing(_) => &CheckCode::A002,
CheckKind::BuiltinAttributeShadowing(_) => &CheckCode::A003,
// flake8-bugbear
CheckKind::DoNotAssertFalse => &CheckCode::B011,
CheckKind::DuplicateHandlerException(_) => &CheckCode::B014,
CheckKind::DuplicateTryBlockException(_) => &CheckCode::B025,
// flake8-comprehensions
CheckKind::UnnecessaryGeneratorList => &CheckCode::C400,
CheckKind::UnnecessaryGeneratorSet => &CheckCode::C401,
@@ -398,6 +550,9 @@ impl CheckKind {
CheckKind::UnnecessaryLiteralSet(_) => &CheckCode::C405,
CheckKind::UnnecessaryLiteralDict(_) => &CheckCode::C406,
CheckKind::UnnecessaryCollectionCall(_) => &CheckCode::C408,
CheckKind::UnnecessaryLiteralWithinTupleCall(..) => &CheckCode::C409,
CheckKind::UnnecessaryLiteralWithinListCall(..) => &CheckCode::C410,
CheckKind::UnnecessaryDoubleCastOrProcess(..) => &CheckCode::C414,
CheckKind::UnnecessarySubscriptReversal(_) => &CheckCode::C415,
// flake8-print
CheckKind::PrintFound => &CheckCode::T201,
@@ -411,6 +566,44 @@ impl CheckKind {
CheckKind::UsePEP604Annotation => &CheckCode::U007,
CheckKind::UselessObjectInheritance(_) => &CheckCode::U004,
CheckKind::SuperCallWithParameters => &CheckCode::U008,
// pydocstyle
CheckKind::BlankLineAfterLastSection(_) => &CheckCode::D413,
CheckKind::BlankLineAfterSection(_) => &CheckCode::D410,
CheckKind::BlankLineBeforeSection(_) => &CheckCode::D411,
CheckKind::CapitalizeSectionName(_) => &CheckCode::D405,
CheckKind::DashedUnderlineAfterSection(_) => &CheckCode::D407,
CheckKind::EndsInPeriod => &CheckCode::D400,
CheckKind::EndsInPunctuation => &CheckCode::D415,
CheckKind::FirstLineCapitalized => &CheckCode::D403,
CheckKind::FitsOnOneLine => &CheckCode::D200,
CheckKind::MagicMethod => &CheckCode::D105,
CheckKind::MultiLineSummaryFirstLine => &CheckCode::D212,
CheckKind::MultiLineSummarySecondLine => &CheckCode::D213,
CheckKind::NewLineAfterLastParagraph => &CheckCode::D209,
CheckKind::NewLineAfterSectionName(_) => &CheckCode::D406,
CheckKind::NoBlankLineAfterFunction(_) => &CheckCode::D202,
CheckKind::NoBlankLineAfterSummary => &CheckCode::D205,
CheckKind::NoBlankLineBeforeClass(_) => &CheckCode::D211,
CheckKind::NoBlankLineBeforeFunction(_) => &CheckCode::D201,
CheckKind::NoBlankLinesBetweenHeaderAndContent(_) => &CheckCode::D412,
CheckKind::NoSignature => &CheckCode::D402,
CheckKind::NoSurroundingWhitespace => &CheckCode::D210,
CheckKind::NoThisPrefix => &CheckCode::D404,
CheckKind::NonEmpty => &CheckCode::D419,
CheckKind::NonEmptySection(_) => &CheckCode::D414,
CheckKind::OneBlankLineAfterClass(_) => &CheckCode::D204,
CheckKind::OneBlankLineBeforeClass(_) => &CheckCode::D203,
CheckKind::PublicClass => &CheckCode::D101,
CheckKind::PublicFunction => &CheckCode::D103,
CheckKind::PublicInit => &CheckCode::D107,
CheckKind::PublicMethod => &CheckCode::D102,
CheckKind::PublicModule => &CheckCode::D100,
CheckKind::PublicNestedClass => &CheckCode::D106,
CheckKind::PublicPackage => &CheckCode::D104,
CheckKind::SectionUnderlineAfterName(_) => &CheckCode::D408,
CheckKind::SectionUnderlineMatchesSectionLength(_) => &CheckCode::D409,
CheckKind::SkipDocstring => &CheckCode::D418,
CheckKind::UsesTripleQuotes => &CheckCode::D300,
// Meta
CheckKind::UnusedNOQA(_) => &CheckCode::M001,
}
@@ -559,6 +752,23 @@ impl CheckKind {
CheckKind::BuiltinAttributeShadowing(name) => {
format!("Class attribute `{name}` is shadowing a python builtin")
}
// flake8-bugbear
CheckKind::DoNotAssertFalse => {
"Do not `assert False` (`python -O` removes these calls), raise `AssertionError()`"
.to_string()
}
CheckKind::DuplicateHandlerException(names) => {
if names.len() == 1 {
let name = &names[0];
format!("Exception handler with duplicate exception: `{name}`")
} else {
let names = names.iter().map(|name| format!("`{name}`")).join(", ");
format!("Exception handler with duplicate exceptions: {names}")
}
}
CheckKind::DuplicateTryBlockException(name) => {
format!("try-except block with duplicate exception `{name}`")
}
// flake8-comprehensions
CheckKind::UnnecessaryGeneratorList => {
"Unnecessary generator - rewrite as a list comprehension".to_string()
@@ -584,6 +794,31 @@ impl CheckKind {
CheckKind::UnnecessaryCollectionCall(obj_type) => {
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"
)
} else {
format!(
"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()"
)
} else {
format!(
"Unnecessary {literal} literal passed to list() - rewrite as a list literal"
)
}
}
CheckKind::UnnecessaryDoubleCastOrProcess(inner, outer) => {
format!("Unnecessary {inner} call within {outer}().")
}
CheckKind::UnnecessarySubscriptReversal(func) => {
format!("Unnecessary subscript reversal of iterable within {func}()")
}
@@ -615,10 +850,111 @@ impl CheckKind {
CheckKind::SuperCallWithParameters => {
"Use `super()` instead of `super(__class__, self)`".to_string()
}
// pydocstyle
CheckKind::FitsOnOneLine => "One-line docstring should fit on one line".to_string(),
CheckKind::NoBlankLineAfterSummary => {
"1 blank line required between summary line and description".to_string()
}
CheckKind::NewLineAfterLastParagraph => {
"Multi-line docstring closing quotes should be on a separate line".to_string()
}
CheckKind::NoSurroundingWhitespace => {
"No whitespaces allowed surrounding docstring text".to_string()
}
CheckKind::EndsInPeriod => "First line should end with a period".to_string(),
CheckKind::NonEmpty => "Docstring is empty".to_string(),
CheckKind::EndsInPunctuation => {
"First line should end with a period, question mark, or exclamation point"
.to_string()
}
CheckKind::FirstLineCapitalized => {
"First word of the first line should be properly capitalized".to_string()
}
CheckKind::UsesTripleQuotes => r#"Use """triple double quotes""""#.to_string(),
CheckKind::MultiLineSummaryFirstLine => {
"Multi-line docstring summary should start at the first line".to_string()
}
CheckKind::MultiLineSummarySecondLine => {
"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()
}
CheckKind::NoBlankLineBeforeFunction(num_lines) => {
format!("No blank lines allowed before function docstring (found {num_lines})")
}
CheckKind::NoBlankLineAfterFunction(num_lines) => {
format!("No blank lines allowed after function docstring (found {num_lines})")
}
CheckKind::NoBlankLineBeforeClass(_) => {
"No blank lines allowed before class docstring".to_string()
}
CheckKind::OneBlankLineBeforeClass(_) => {
"1 blank line required before class docstring".to_string()
}
CheckKind::OneBlankLineAfterClass(_) => {
"1 blank line required after class docstring".to_string()
}
CheckKind::PublicModule => "Missing docstring in public module".to_string(),
CheckKind::PublicClass => "Missing docstring in public class".to_string(),
CheckKind::PublicMethod => "Missing docstring in public method".to_string(),
CheckKind::PublicFunction => "Missing docstring in public function".to_string(),
CheckKind::PublicPackage => "Missing docstring in public package".to_string(),
CheckKind::MagicMethod => "Missing docstring in magic method".to_string(),
CheckKind::PublicNestedClass => "Missing docstring in public nested class".to_string(),
CheckKind::PublicInit => "Missing docstring in __init__".to_string(),
CheckKind::NoThisPrefix => {
"First word of the docstring should not be `This`".to_string()
}
CheckKind::SkipDocstring => {
"Function decorated with @overload shouldn't contain a docstring".to_string()
}
CheckKind::CapitalizeSectionName(name) => {
format!("Section name should be properly capitalized (\"{name}\")")
}
CheckKind::BlankLineAfterLastSection(name) => {
format!("Missing blank line after last section (\"{name}\")")
}
CheckKind::BlankLineAfterSection(name) => {
format!("Missing blank line after section (\"{name}\")")
}
CheckKind::BlankLineBeforeSection(name) => {
format!("Missing blank line before section (\"{name}\")")
}
CheckKind::NewLineAfterSectionName(name) => {
format!("Section name should end with a newline (\"{name}\")")
}
CheckKind::DashedUnderlineAfterSection(name) => {
format!("Missing dashed underline after section (\"{name}\")")
}
CheckKind::SectionUnderlineAfterName(name) => {
format!("Section underline should be in the line following the section's name (\"{name}\")")
}
CheckKind::SectionUnderlineMatchesSectionLength(name) => {
format!("Section underline should match the length of its name (\"{name}\")")
}
CheckKind::NoBlankLinesBetweenHeaderAndContent(name) => {
format!(
"No blank lines allowed between a section header and its content (\"{name}\")"
)
}
CheckKind::NonEmptySection(name) => format!("Section has no content (\"{name}\")"),
// Meta
CheckKind::UnusedNOQA(code) => match code {
CheckKind::UnusedNOQA(codes) => match codes {
None => "Unused `noqa` directive".to_string(),
Some(code) => format!("Unused `noqa` directive for: {code}"),
Some(codes) => {
let codes = codes
.iter()
.map(|code| {
if CheckCode::from_str(code).is_ok() {
code.to_string()
} else {
format!("{code} (not implemented)")
}
})
.join(", ");
format!("Unused `noqa` directive for: {codes}")
}
},
}
}
@@ -628,6 +964,8 @@ impl CheckKind {
matches!(
self,
CheckKind::DeprecatedUnittestAlias(_, _)
| CheckKind::DoNotAssertFalse
| CheckKind::DuplicateHandlerException(_)
| CheckKind::PPrintFound
| CheckKind::PrintFound
| CheckKind::SuperCallWithParameters

View File

@@ -78,6 +78,9 @@ pub struct Cli {
// TODO(charlie): This should be a sub-command.
#[arg(long, hide = true)]
pub autoformat: bool,
/// The name of the file when passing it through stdin.
#[arg(long)]
pub stdin_filename: Option<String>,
}
pub enum Warnable {

View File

@@ -115,7 +115,7 @@ impl SourceGenerator {
Ok(())
}
fn unparse_stmt<U>(&mut self, ast: &Stmt<U>) -> fmt::Result {
pub fn unparse_stmt<U>(&mut self, ast: &Stmt<U>) -> fmt::Result {
macro_rules! statement {
($body:block) => {{
self.newline()?;

3
src/docstrings.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod docstring_checks;
pub mod sections;
pub mod types;

View File

@@ -0,0 +1,676 @@
//! Abstractions for tracking and validating docstrings in Python code.
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_ast::{Constant, Expr, ExprKind, Location, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::sections::{check_numpy_section, section_contexts};
use crate::docstrings::types::{Definition, DefinitionKind, Documentable};
use crate::visibility::{is_init, is_magic, is_overload, Modifier, Visibility, VisibleScope};
/// Extract a docstring from a function or class body.
pub fn docstring_from(suite: &[Stmt]) -> Option<&Expr> {
if let Some(stmt) = suite.first() {
if let StmtKind::Expr { value } = &stmt.node {
if matches!(
&value.node,
ExprKind::Constant {
value: Constant::Str(_),
..
}
) {
return Some(value);
}
}
}
None
}
/// Extract a `Definition` from the AST node defined by a `Stmt`.
pub fn extract<'a>(
scope: &VisibleScope,
stmt: &'a Stmt,
body: &'a [Stmt],
kind: &Documentable,
) -> Definition<'a> {
let expr = docstring_from(body);
match kind {
Documentable::Function => match scope {
VisibleScope {
modifier: Modifier::Module,
..
} => Definition {
kind: DefinitionKind::Function(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Class,
..
} => Definition {
kind: DefinitionKind::Method(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Function,
..
} => Definition {
kind: DefinitionKind::NestedFunction(stmt),
docstring: expr,
},
},
Documentable::Class => match scope {
VisibleScope {
modifier: Modifier::Module,
..
} => Definition {
kind: DefinitionKind::Class(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Class,
..
} => Definition {
kind: DefinitionKind::NestedClass(stmt),
docstring: expr,
},
VisibleScope {
modifier: Modifier::Function,
..
} => Definition {
kind: DefinitionKind::NestedClass(stmt),
docstring: expr,
},
},
}
}
/// 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,
}
}
/// 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, 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), &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),
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),
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), &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),
range_for(docstring),
));
}
if blank_lines_before != 1
&& checker.settings.enabled.contains(&CheckCode::D203)
{
checker.add_check(Check::new(
CheckKind::OneBlankLineBeforeClass(blank_lines_before),
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),
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,
range_for(docstring),
));
}
}
}
}
/// 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(&range_for(docstring));
if let Some(line) = content.lines().last() {
let line = line.trim();
if line != "\"\"\"" && line != "'''" {
checker.add_check(Check::new(
CheckKind::NewLineAfterLastParagraph,
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,
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(&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,
range_for(docstring),
));
}
} else if checker.settings.enabled.contains(&CheckCode::D213) {
checker.add_check(Check::new(
CheckKind::MultiLineSummarySecondLine,
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(&range_for(docstring));
if string.contains("\"\"\"") {
if !content.starts_with("'''") {
checker.add_check(Check::new(
CheckKind::UsesTripleQuotes,
range_for(docstring),
));
}
} else if !content.starts_with("\"\"\"") {
checker.add_check(Check::new(
CheckKind::UsesTripleQuotes,
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, 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,
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,
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, 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,
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, range_for(docstring)));
}
return false;
}
}
}
true
}
pub fn check_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;
}
for context in &section_contexts(&lines) {
check_numpy_section(checker, definition, context);
}
}
}
}

384
src/docstrings/sections.rs Normal file
View File

@@ -0,0 +1,384 @@
use std::collections::BTreeSet;
use once_cell::sync::Lazy;
use titlecase::titlecase;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::docstring_checks::range_for;
use crate::docstrings::types::Definition;
static NUMPY_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"Short Summary",
"Extended Summary",
"Parameters",
"Returns",
"Yields",
"Other Parameters",
"Raises",
"See Also",
"Notes",
"References",
"Examples",
"Attributes",
"Methods",
])
});
static NUMPY_SECTION_NAMES_LOWERCASE: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"short summary",
"extended summary",
"parameters",
"returns",
"yields",
"other parameters",
"raises",
"see also",
"notes",
"references",
"examples",
"attributes",
"methods",
])
});
// TODO(charlie): Include Google section names.
// static GOOGLE_SECTION_NAMES: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
// BTreeSet::from([
// "Args",
// "Arguments",
// "Attention",
// "Attributes",
// "Caution",
// "Danger",
// "Error",
// "Example",
// "Examples",
// "Hint",
// "Important",
// "Keyword Args",
// "Keyword Arguments",
// "Methods",
// "Note",
// "Notes",
// "Return",
// "Returns",
// "Raises",
// "References",
// "See Also",
// "Tip",
// "Todo",
// "Warning",
// "Warnings",
// "Warns",
// "Yield",
// "Yields",
// ])
// });
fn get_leading_words(line: &str) -> String {
line.trim()
.chars()
.take_while(|char| char.is_alphanumeric() || char.is_whitespace())
.collect()
}
fn suspected_as_section(line: &str) -> bool {
NUMPY_SECTION_NAMES_LOWERCASE.contains(&get_leading_words(line).to_lowercase().as_str())
}
#[derive(Debug)]
pub struct SectionContext<'a> {
section_name: String,
previous_line: &'a str,
line: &'a str,
following_lines: &'a [&'a str],
original_index: usize,
is_last_section: bool,
}
/// Check if the suspected context is really a section header.
fn is_docstring_section(context: &SectionContext) -> bool {
let section_name_suffix = context
.line
.trim()
.strip_prefix(&context.section_name)
.unwrap()
.trim();
let this_looks_like_a_section_name =
section_name_suffix == ":" || section_name_suffix.is_empty();
if !this_looks_like_a_section_name {
return false;
}
let prev_line = context.previous_line.trim();
let prev_line_ends_with_punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')']
.into_iter()
.any(|char| prev_line.ends_with(char));
let prev_line_looks_like_end_of_paragraph =
prev_line_ends_with_punctuation || prev_line.is_empty();
if !prev_line_looks_like_end_of_paragraph {
return false;
}
true
}
/// Extract all `SectionContext` values from a docstring.
pub fn section_contexts<'a>(lines: &'a [&'a str]) -> Vec<SectionContext<'a>> {
let suspected_section_indices: Vec<usize> = lines
.iter()
.enumerate()
.filter_map(|(lineno, line)| {
if lineno > 0 && suspected_as_section(line) {
Some(lineno)
} else {
None
}
})
.collect();
let mut contexts = vec![];
for lineno in suspected_section_indices {
let context = SectionContext {
section_name: get_leading_words(lines[lineno]),
previous_line: lines[lineno - 1],
line: lines[lineno],
following_lines: &lines[lineno + 1..],
original_index: lineno,
is_last_section: false,
};
if is_docstring_section(&context) {
contexts.push(context);
}
}
let mut truncated_contexts = vec![];
let mut end: Option<usize> = None;
for context in contexts.into_iter().rev() {
let next_end = context.original_index;
truncated_contexts.push(SectionContext {
section_name: context.section_name,
previous_line: context.previous_line,
line: context.line,
following_lines: if let Some(end) = end {
&lines[context.original_index + 1..end]
} else {
context.following_lines
},
original_index: context.original_index,
is_last_section: end.is_none(),
});
end = Some(next_end);
}
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() {
// D407
if checker.settings.enabled.contains(&CheckCode::D407) {
checker.add_check(Check::new(
CheckKind::DashedUnderlineAfterSection(context.section_name.to_string()),
range_for(docstring),
));
}
// D414
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 {
// D407
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 {
// D212
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 {
// D408
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()
{
// D409
if checker.settings.enabled.contains(&CheckCode::D409) {
checker.add_check(Check::new(
CheckKind::SectionUnderlineMatchesSectionLength(
context.section_name.to_string(),
),
range_for(docstring),
));
}
}
// TODO(charlie): Implement D215, which requires indentation and leading space tracking.
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()) {
// D414
if checker.settings.enabled.contains(&CheckCode::D414) {
checker.add_check(Check::new(
CheckKind::NonEmptySection(context.section_name.to_string()),
range_for(docstring),
));
}
} else {
// 412
if checker.settings.enabled.contains(&CheckCode::D412) {
checker.add_check(Check::new(
CheckKind::NoBlankLinesBetweenHeaderAndContent(
context.section_name.to_string(),
),
range_for(docstring),
));
}
}
}
} else {
// D414
if checker.settings.enabled.contains(&CheckCode::D414) {
checker.add_check(Check::new(
CheckKind::NonEmptySection(context.section_name.to_string()),
range_for(docstring),
));
}
}
}
}
fn check_common_section(checker: &mut Checker, definition: &Definition, context: &SectionContext) {
// TODO(charlie): Implement D214, which requires indentation and leading space tracking.
let docstring = definition
.docstring
.expect("Sections are only available for docstrings.");
if checker.settings.enabled.contains(&CheckCode::D405) {
if !NUMPY_SECTION_NAMES.contains(&context.section_name.as_str())
&& NUMPY_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 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),
))
}
}
}
pub fn check_numpy_section(
checker: &mut Checker,
definition: &Definition,
context: &SectionContext,
) {
// TODO(charlie): Implement `_check_parameters_section`.
check_common_section(checker, definition, context);
check_blanks_and_section_underline(checker, definition, context);
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),
))
}
}
}

23
src/docstrings/types.rs Normal file
View File

@@ -0,0 +1,23 @@
use rustpython_ast::{Expr, Stmt};
#[derive(Debug)]
pub enum DefinitionKind<'a> {
Module,
Package,
Class(&'a Stmt),
NestedClass(&'a Stmt),
Function(&'a Stmt),
NestedFunction(&'a Stmt),
Method(&'a Stmt),
}
#[derive(Debug)]
pub struct Definition<'a> {
pub kind: DefinitionKind<'a>,
pub docstring: Option<&'a Expr>,
}
pub enum Documentable {
Class,
Function,
}

View File

@@ -1,3 +1,4 @@
#![allow(clippy::collapsible_if, clippy::collapsible_else_if)]
use std::path::Path;
use anyhow::Result;
@@ -17,6 +18,7 @@ mod check_lines;
pub mod checks;
pub mod cli;
pub mod code_gen;
mod docstrings;
pub mod fs;
pub mod linter;
pub mod logging;
@@ -27,6 +29,7 @@ pub mod printer;
pub mod pyproject;
mod python;
pub mod settings;
pub mod visibility;
/// Run ruff over Python source code directly.
pub fn check(path: &Path, contents: &str) -> Result<Vec<Message>> {

View File

@@ -83,6 +83,36 @@ pub(crate) fn check_path(
Ok(checks)
}
pub fn lint_stdin(path: &Path, stdin: &str, settings: &Settings) -> Result<Vec<Message>> {
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(stdin);
// Determine the noqa line for every line in the source.
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
// Generate checks.
let checks = check_path(
path,
stdin,
tokens,
&noqa_line_for,
settings,
&fixer::Mode::None,
)?;
// Convert to messages.
Ok(checks
.into_iter()
.map(|check| Message {
kind: check.kind,
fixed: check.fix.map(|fix| fix.applied).unwrap_or_default(),
location: check.location,
end_location: check.end_location,
filename: path.to_string_lossy().to_string(),
})
.collect())
}
pub fn lint_path(
path: &Path,
settings: &Settings,
@@ -762,42 +792,6 @@ mod tests {
Ok(())
}
#[test]
fn m001() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/M001.py"),
&settings::Settings::for_rules(vec![CheckCode::M001, CheckCode::E501, CheckCode::F841]),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn init() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/__init__.py"),
&settings::Settings::for_rules(vec![CheckCode::F821, CheckCode::F822]),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn future_annotations() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/future_annotations.py"),
&settings::Settings::for_rules(vec![CheckCode::F401, CheckCode::F821]),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn e999() -> Result<()> {
let mut checks = check_path(
@@ -846,6 +840,42 @@ mod tests {
Ok(())
}
#[test]
fn b011() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/B011.py"),
&settings::Settings::for_rule(CheckCode::B011),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn b014() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/B014.py"),
&settings::Settings::for_rule(CheckCode::B014),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn b025() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/B025.py"),
&settings::Settings::for_rule(CheckCode::B025),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c400() -> Result<()> {
let mut checks = check_path(
@@ -942,6 +972,42 @@ mod tests {
Ok(())
}
#[test]
fn c409() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C409.py"),
&settings::Settings::for_rule(CheckCode::C409),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c410() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C410.py"),
&settings::Settings::for_rule(CheckCode::C410),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c414() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/C414.py"),
&settings::Settings::for_rule(CheckCode::C414),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn c415() -> Result<()> {
let mut checks = check_path(
@@ -955,10 +1021,430 @@ mod tests {
}
#[test]
fn u008() -> Result<()> {
fn d100() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/U008.py"),
&settings::Settings::for_rule(CheckCode::U008),
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D100),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d101() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D101),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d102() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D102),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d103() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D103),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d104() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D104),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d105() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D105),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d106() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D106),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d107() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D107),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d201() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D201),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d202() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D202),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d203() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D203),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d204() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D204),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d205() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D205),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d209() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D209),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d210() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D210),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d211() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D211),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d212() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D212),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d213() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D213),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d300() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D300),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d400() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D400),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d402() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D402),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d403() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D403),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d404() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D404),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d405() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/sections.py"),
&settings::Settings::for_rule(CheckCode::D405),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d406() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/sections.py"),
&settings::Settings::for_rule(CheckCode::D406),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d407() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/sections.py"),
&settings::Settings::for_rule(CheckCode::D407),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d408() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/sections.py"),
&settings::Settings::for_rule(CheckCode::D408),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d409() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/sections.py"),
&settings::Settings::for_rule(CheckCode::D409),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d410() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/sections.py"),
&settings::Settings::for_rule(CheckCode::D410),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d411() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/sections.py"),
&settings::Settings::for_rule(CheckCode::D411),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d412() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/sections.py"),
&settings::Settings::for_rule(CheckCode::D412),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d413() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/sections.py"),
&settings::Settings::for_rule(CheckCode::D413),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d414() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/sections.py"),
&settings::Settings::for_rule(CheckCode::D414),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d415() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D415),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d418() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D418),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn d419() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D419),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
@@ -1073,4 +1559,52 @@ mod tests {
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn u008() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/U008.py"),
&settings::Settings::for_rule(CheckCode::U008),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn m001() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/M001.py"),
&settings::Settings::for_rules(vec![CheckCode::M001, CheckCode::E501, CheckCode::F841]),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn init() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/__init__.py"),
&settings::Settings::for_rules(vec![CheckCode::F821, CheckCode::F822]),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn future_annotations() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/future_annotations.py"),
&settings::Settings::for_rules(vec![CheckCode::F401, CheckCode::F821]),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
}

View File

@@ -1,4 +1,4 @@
use std::io;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::mpsc::channel;
@@ -19,7 +19,7 @@ use ruff::cli::{warn_on, Cli, Warnable};
use ruff::fs::iter_python_files;
use ruff::linter::add_noqa_to_path;
use ruff::linter::autoformat_path;
use ruff::linter::lint_path;
use ruff::linter::{lint_path, lint_stdin};
use ruff::logging::set_up_logging;
use ruff::message::Message;
use ruff::printer::{Printer, SerializationFormat};
@@ -75,6 +75,19 @@ fn show_files(files: &[PathBuf], settings: &Settings) {
}
}
fn read_from_stdin() -> Result<String> {
let mut buffer = String::new();
io::stdin().lock().read_to_string(&mut buffer)?;
Ok(buffer)
}
fn run_once_stdin(settings: &Settings, filename: &Path) -> Result<Vec<Message>> {
let stdin = read_from_stdin()?;
let mut messages = lint_stdin(filename, &stdin, settings)?;
messages.sort_unstable();
Ok(messages)
}
fn run_once(
files: &[PathBuf],
settings: &Settings,
@@ -352,7 +365,17 @@ fn inner_main() -> Result<ExitCode> {
println!("Formatted {modifications} files.");
}
} else {
let messages = run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?;
let messages = if cli.files == vec![PathBuf::from("-")] {
if cli.fix {
eprintln!("Warning: --fix is not enabled when reading from stdin.");
}
let filename = cli.stdin_filename.unwrap_or_else(|| "-".to_string());
let path = Path::new(&filename);
run_once_stdin(&settings, path)?
} else {
run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?
};
if !cli.quiet {
printer.write_once(&messages)?;
}

View File

@@ -1,5 +1,7 @@
pub use assert_false::assert_false;
pub use assert_tuple::assert_tuple;
pub use deprecated_unittest_alias::deprecated_unittest_alias;
pub use duplicate_exceptions::duplicate_exceptions;
pub use if_tuple::if_tuple;
pub use invalid_print_syntax::invalid_print_syntax;
pub use print_call::print_call;
@@ -11,8 +13,10 @@ pub use use_pep604_annotation::use_pep604_annotation;
pub use useless_metaclass_type::useless_metaclass_type;
pub use useless_object_inheritance::useless_object_inheritance;
mod assert_false;
mod assert_tuple;
mod deprecated_unittest_alias;
mod duplicate_exceptions;
mod if_tuple;
mod invalid_print_syntax;
mod print_call;

View File

@@ -0,0 +1,61 @@
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::autofix::fixer;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind, Fix};
use crate::code_gen::SourceGenerator;
fn assertion_error(msg: &Option<Box<Expr>>) -> Stmt {
Stmt::new(
Default::default(),
Default::default(),
StmtKind::Raise {
exc: Some(Box::new(Expr::new(
Default::default(),
Default::default(),
ExprKind::Call {
func: Box::new(Expr::new(
Default::default(),
Default::default(),
ExprKind::Name {
id: "AssertionError".to_string(),
ctx: ExprContext::Load,
},
)),
args: if let Some(msg) = msg {
vec![*msg.clone()]
} else {
vec![]
},
keywords: vec![],
},
))),
cause: None,
},
)
}
pub fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: &Option<Box<Expr>>) {
if let ExprKind::Constant {
value: Constant::Bool(false),
..
} = &test.node
{
let mut check = Check::new(CheckKind::DoNotAssertFalse, Range::from_located(test));
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
let mut generator = SourceGenerator::new();
if let Ok(()) = generator.unparse_stmt(&assertion_error(msg)) {
if let Ok(content) = generator.generate() {
check.amend(Fix {
content,
location: stmt.location,
end_location: stmt.end_location,
applied: false,
})
}
}
}
checker.add_check(check);
}
}

View File

@@ -1,12 +1,12 @@
use rustpython_ast::{Expr, Stmt};
use crate::ast::checks;
use crate::ast::checkers;
use crate::ast::types::{CheckLocator, Range};
use crate::check_ast::Checker;
pub fn assert_tuple(checker: &mut Checker, stmt: &Stmt, test: &Expr) {
if let Some(check) =
checks::check_assert_tuple(test, checker.locate_check(Range::from_located(stmt)))
checkers::assert_tuple(test, checker.locate_check(Range::from_located(stmt)))
{
checker.add_check(check);
}

View File

@@ -0,0 +1,114 @@
use std::collections::BTreeSet;
use itertools::Itertools;
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind, Stmt};
use crate::ast::helpers;
use crate::ast::types::{CheckLocator, Range};
use crate::autofix::fixer;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind, Fix};
use crate::code_gen::SourceGenerator;
fn type_pattern(elts: Vec<&Expr>) -> Expr {
Expr::new(
Default::default(),
Default::default(),
ExprKind::Tuple {
elts: elts.into_iter().cloned().collect(),
ctx: ExprContext::Load,
},
)
}
pub fn duplicate_handler_exceptions(
checker: &mut Checker,
expr: &Expr,
elts: &Vec<Expr>,
) -> BTreeSet<String> {
let mut seen: BTreeSet<String> = Default::default();
let mut duplicates: BTreeSet<String> = Default::default();
let mut unique_elts: Vec<&Expr> = Default::default();
for type_ in elts {
if let Some(name) = helpers::compose_call_path(type_) {
if seen.contains(&name) {
duplicates.insert(name);
} else {
seen.insert(name);
unique_elts.push(type_);
}
}
}
if checker.settings.enabled.contains(&CheckCode::B014) {
// TODO(charlie): Handle "BaseException" and redundant exception aliases.
if !duplicates.is_empty() {
let mut check = Check::new(
CheckKind::DuplicateHandlerException(
duplicates.into_iter().sorted().collect::<Vec<String>>(),
),
checker.locate_check(Range::from_located(expr)),
);
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
// 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 {
content,
location: expr.location,
end_location: expr.end_location,
applied: false,
})
}
}
}
checker.add_check(check);
}
}
seen
}
pub fn duplicate_exceptions(checker: &mut Checker, stmt: &Stmt, handlers: &[Excepthandler]) {
let mut seen: BTreeSet<String> = Default::default();
let mut duplicates: BTreeSet<String> = Default::default();
for handler in handlers {
match &handler.node {
ExcepthandlerKind::ExceptHandler { type_, .. } => {
if let Some(type_) = type_ {
match &type_.node {
ExprKind::Attribute { .. } | ExprKind::Name { .. } => {
if let Some(name) = helpers::compose_call_path(type_) {
if seen.contains(&name) {
duplicates.insert(name);
} else {
seen.insert(name);
}
}
}
ExprKind::Tuple { elts, .. } => {
for name in duplicate_handler_exceptions(checker, type_, elts) {
if seen.contains(&name) {
duplicates.insert(name);
} else {
seen.insert(name);
}
}
}
_ => {}
}
}
}
}
}
if checker.settings.enabled.contains(&CheckCode::B025) {
for duplicate in duplicates.into_iter().sorted() {
checker.add_check(Check::new(
CheckKind::DuplicateTryBlockException(duplicate),
checker.locate_check(Range::from_located(stmt)),
));
}
}
}

View File

@@ -1,13 +1,11 @@
use rustpython_ast::{Expr, Stmt};
use crate::ast::checks;
use crate::ast::checkers;
use crate::ast::types::{CheckLocator, Range};
use crate::check_ast::Checker;
pub fn if_tuple(checker: &mut Checker, stmt: &Stmt, test: &Expr) {
if let Some(check) =
checks::check_if_tuple(test, checker.locate_check(Range::from_located(stmt)))
{
if let Some(check) = checkers::if_tuple(test, checker.locate_check(Range::from_located(stmt))) {
checker.add_check(check);
}
}

View File

@@ -1,13 +1,13 @@
use log::error;
use rustpython_ast::{Expr, Stmt, StmtKind};
use crate::ast::checks;
use crate::ast::checkers;
use crate::autofix::{fixer, fixes};
use crate::check_ast::Checker;
use crate::checks::CheckCode;
pub fn print_call(checker: &mut Checker, expr: &Expr, func: &Expr) {
if let Some(mut check) = checks::check_print_call(
if let Some(mut check) = checkers::print_call(
expr,
func,
checker.settings.enabled.contains(&CheckCode::T201),

View File

@@ -1,6 +1,6 @@
use rustpython_ast::{Expr, Stmt};
use crate::ast::checks;
use crate::ast::{checkers, helpers};
use crate::autofix::{fixer, fixes};
use crate::check_ast::Checker;
@@ -12,14 +12,14 @@ pub fn super_call_with_parameters(
) {
// Only bother going through the super check at all if we're in a `super` call.
// (We check this in `check_super_args` too, so this is just an optimization.)
if checks::is_super_call_with_arguments(func, args) {
if helpers::is_super_call_with_arguments(func, args) {
let scope = checker.current_scope();
let parents: Vec<&Stmt> = checker
.parent_stack
.iter()
.map(|index| checker.parents[*index])
.collect();
if let Some(mut check) = checks::check_super_args(scope, &parents, expr, func, args) {
if let Some(mut check) = checkers::super_args(scope, &parents, expr, func, args) {
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if let Some(fix) = fixes::remove_super_arguments(&mut checker.locator, expr) {
check.amend(fix);

View File

@@ -1,6 +1,6 @@
use rustpython_ast::Expr;
use crate::ast::checks;
use crate::ast::checkers;
use crate::ast::types::{CheckLocator, Range};
use crate::autofix::fixer;
use crate::check_ast::Checker;
@@ -8,7 +8,7 @@ use crate::checks::{CheckKind, Fix};
pub fn type_of_primitive(checker: &mut Checker, expr: &Expr, func: &Expr, args: &Vec<Expr>) {
if let Some(mut check) =
checks::check_type_of_primitive(func, args, checker.locate_check(Range::from_located(expr)))
checkers::type_of_primitive(func, args, checker.locate_check(Range::from_located(expr)))
{
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if let CheckKind::TypeOfPrimitive(primitive) = &check.kind {

View File

@@ -1,17 +1,15 @@
use rustpython_ast::Expr;
use crate::ast::checks;
use crate::ast::checkers;
use crate::ast::types::{CheckLocator, Range};
use crate::autofix::fixer;
use crate::check_ast::Checker;
use crate::checks::Fix;
pub fn unnecessary_abspath(checker: &mut Checker, expr: &Expr, func: &Expr, args: &Vec<Expr>) {
if let Some(mut check) = checks::check_unnecessary_abspath(
func,
args,
checker.locate_check(Range::from_located(expr)),
) {
if let Some(mut check) =
checkers::unnecessary_abspath(func, args, checker.locate_check(Range::from_located(expr)))
{
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
check.amend(Fix {
content: "__file__".to_string(),

View File

@@ -1,7 +1,7 @@
use log::error;
use rustpython_ast::{Expr, Stmt};
use crate::ast::checks;
use crate::ast::checkers;
use crate::ast::types::{CheckLocator, Range};
use crate::autofix::{fixer, fixes};
use crate::check_ast::Checker;
@@ -12,7 +12,7 @@ pub fn useless_metaclass_type(
value: &Expr,
targets: &Vec<Expr>,
) {
if let Some(mut check) = checks::check_useless_metaclass_type(
if let Some(mut check) = checkers::useless_metaclass_type(
targets,
value,
checker.locate_check(Range::from_located(stmt)),

View File

@@ -1,6 +1,6 @@
use rustpython_ast::{Expr, Keyword, Stmt};
use crate::ast::checks;
use crate::ast::checkers;
use crate::autofix::{fixer, fixes};
use crate::check_ast::Checker;
@@ -12,7 +12,7 @@ pub fn useless_object_inheritance(
keywords: &[Keyword],
) {
let scope = checker.current_scope();
if let Some(mut check) = checks::check_useless_object_inheritance(name, bases, scope) {
if let Some(mut check) = checkers::useless_object_inheritance(name, bases, scope) {
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if let Some(fix) = fixes::remove_class_def_base(
&mut checker.locator,

View File

@@ -0,0 +1,37 @@
---
source: src/linter.rs
expression: checks
---
- kind: DoNotAssertFalse
location:
row: 8
column: 8
end_location:
row: 8
column: 13
fix:
content: raise AssertionError()
location:
row: 8
column: 1
end_location:
row: 8
column: 13
applied: false
- kind: DoNotAssertFalse
location:
row: 10
column: 8
end_location:
row: 10
column: 13
fix:
content: "raise AssertionError(\"message\")"
location:
row: 10
column: 1
end_location:
row: 10
column: 24
applied: false

View File

@@ -0,0 +1,59 @@
---
source: src/linter.rs
expression: checks
---
- kind:
DuplicateHandlerException:
- OSError
location:
row: 17
column: 9
end_location:
row: 17
column: 25
fix:
content: "OSError,"
location:
row: 17
column: 9
end_location:
row: 17
column: 25
applied: false
- kind:
DuplicateHandlerException:
- MyError
location:
row: 28
column: 9
end_location:
row: 28
column: 25
fix:
content: "MyError,"
location:
row: 28
column: 9
end_location:
row: 28
column: 25
applied: false
- kind:
DuplicateHandlerException:
- re.error
location:
row: 49
column: 9
end_location:
row: 49
column: 27
fix:
content: "re.error,"
location:
row: 49
column: 9
end_location:
row: 49
column: 27
applied: false

View File

@@ -0,0 +1,41 @@
---
source: src/linter.rs
expression: checks
---
- kind:
DuplicateTryBlockException: ValueError
location:
row: 15
column: 1
end_location:
row: 22
column: 1
fix: ~
- kind:
DuplicateTryBlockException: pickle.PickleError
location:
row: 22
column: 1
end_location:
row: 31
column: 1
fix: ~
- kind:
DuplicateTryBlockException: TypeError
location:
row: 31
column: 1
end_location:
row: 39
column: 1
fix: ~
- kind:
DuplicateTryBlockException: ValueError
location:
row: 31
column: 1
end_location:
row: 39
column: 1
fix: ~

View File

@@ -0,0 +1,32 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UnnecessaryLiteralWithinTupleCall: list
location:
row: 1
column: 6
end_location:
row: 1
column: 19
fix: ~
- kind:
UnnecessaryLiteralWithinTupleCall: tuple
location:
row: 2
column: 6
end_location:
row: 2
column: 19
fix: ~
- kind:
UnnecessaryLiteralWithinTupleCall: list
location:
row: 3
column: 6
end_location:
row: 3
column: 15
fix: ~

View File

@@ -0,0 +1,41 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UnnecessaryLiteralWithinListCall: list
location:
row: 1
column: 6
end_location:
row: 1
column: 18
fix: ~
- kind:
UnnecessaryLiteralWithinListCall: tuple
location:
row: 2
column: 6
end_location:
row: 2
column: 18
fix: ~
- kind:
UnnecessaryLiteralWithinListCall: list
location:
row: 3
column: 6
end_location:
row: 3
column: 14
fix: ~
- kind:
UnnecessaryLiteralWithinListCall: tuple
location:
row: 4
column: 6
end_location:
row: 4
column: 14
fix: ~

View File

@@ -0,0 +1,148 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UnnecessaryDoubleCastOrProcess:
- list
- list
location:
row: 2
column: 1
end_location:
row: 2
column: 14
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- tuple
- list
location:
row: 3
column: 1
end_location:
row: 3
column: 15
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- list
- tuple
location:
row: 4
column: 1
end_location:
row: 4
column: 15
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- tuple
- tuple
location:
row: 5
column: 1
end_location:
row: 5
column: 16
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- set
- set
location:
row: 6
column: 1
end_location:
row: 6
column: 12
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- list
- set
location:
row: 7
column: 1
end_location:
row: 7
column: 13
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- tuple
- set
location:
row: 8
column: 1
end_location:
row: 8
column: 14
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- sorted
- set
location:
row: 9
column: 1
end_location:
row: 9
column: 15
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- reversed
- set
location:
row: 10
column: 1
end_location:
row: 10
column: 17
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- list
- sorted
location:
row: 11
column: 1
end_location:
row: 11
column: 16
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- tuple
- sorted
location:
row: 12
column: 1
end_location:
row: 12
column: 17
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- sorted
- sorted
location:
row: 13
column: 1
end_location:
row: 13
column: 18
fix: ~
- kind:
UnnecessaryDoubleCastOrProcess:
- reversed
- sorted
location:
row: 14
column: 1
end_location:
row: 14
column: 20
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: PublicModule
location:
row: 1
column: 1
end_location:
row: 1
column: 1
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: PublicClass
location:
row: 14
column: 1
end_location:
row: 67
column: 1
fix: ~

View File

@@ -0,0 +1,29 @@
---
source: src/linter.rs
expression: checks
---
- kind: PublicMethod
location:
row: 22
column: 5
end_location:
row: 25
column: 5
fix: ~
- kind: PublicMethod
location:
row: 51
column: 5
end_location:
row: 54
column: 5
fix: ~
- kind: PublicMethod
location:
row: 63
column: 5
end_location:
row: 67
column: 1
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: PublicFunction
location:
row: 395
column: 1
end_location:
row: 396
column: 1
fix: ~

View File

@@ -0,0 +1,6 @@
---
source: src/linter.rs
expression: checks
---
[]

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: MagicMethod
location:
row: 59
column: 5
end_location:
row: 62
column: 5
fix: ~

View File

@@ -0,0 +1,6 @@
---
source: src/linter.rs
expression: checks
---
[]

View File

@@ -0,0 +1,21 @@
---
source: src/linter.rs
expression: checks
---
- kind: PublicInit
location:
row: 55
column: 5
end_location:
row: 58
column: 5
fix: ~
- kind: PublicInit
location:
row: 529
column: 5
end_location:
row: 533
column: 1
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: FitsOnOneLine
location:
row: 124
column: 5
end_location:
row: 126
column: 8
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/linter.rs
expression: checks
---
- kind:
NoBlankLineBeforeFunction: 1
location:
row: 132
column: 5
end_location:
row: 132
column: 25
fix: ~
- kind:
NoBlankLineBeforeFunction: 1
location:
row: 146
column: 5
end_location:
row: 146
column: 38
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/linter.rs
expression: checks
---
- kind:
NoBlankLineAfterFunction: 1
location:
row: 137
column: 5
end_location:
row: 137
column: 25
fix: ~
- kind:
NoBlankLineAfterFunction: 1
location:
row: 146
column: 5
end_location:
row: 146
column: 38
fix: ~

View File

@@ -0,0 +1,32 @@
---
source: src/linter.rs
expression: checks
---
- kind:
OneBlankLineBeforeClass: 0
location:
row: 156
column: 5
end_location:
row: 156
column: 33
fix: ~
- kind:
OneBlankLineBeforeClass: 0
location:
row: 187
column: 5
end_location:
row: 187
column: 46
fix: ~
- kind:
OneBlankLineBeforeClass: 0
location:
row: 521
column: 5
end_location:
row: 527
column: 8
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/linter.rs
expression: checks
---
- kind:
OneBlankLineAfterClass: 0
location:
row: 176
column: 5
end_location:
row: 176
column: 25
fix: ~
- kind:
OneBlankLineAfterClass: 0
location:
row: 187
column: 5
end_location:
row: 187
column: 46
fix: ~

View File

@@ -0,0 +1,21 @@
---
source: src/linter.rs
expression: checks
---
- kind: NoBlankLineAfterSummary
location:
row: 195
column: 5
end_location:
row: 198
column: 8
fix: ~
- kind: NoBlankLineAfterSummary
location:
row: 205
column: 5
end_location:
row: 210
column: 8
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: NewLineAfterLastParagraph
location:
row: 276
column: 5
end_location:
row: 278
column: 20
fix: ~

View File

@@ -0,0 +1,29 @@
---
source: src/linter.rs
expression: checks
---
- kind: NoSurroundingWhitespace
location:
row: 283
column: 5
end_location:
row: 283
column: 34
fix: ~
- kind: NoSurroundingWhitespace
location:
row: 288
column: 5
end_location:
row: 288
column: 38
fix: ~
- kind: NoSurroundingWhitespace
location:
row: 294
column: 5
end_location:
row: 297
column: 8
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/linter.rs
expression: checks
---
- kind:
NoBlankLineBeforeClass: 1
location:
row: 165
column: 5
end_location:
row: 165
column: 30
fix: ~
- kind:
NoBlankLineBeforeClass: 1
location:
row: 176
column: 5
end_location:
row: 176
column: 25
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: MultiLineSummaryFirstLine
location:
row: 124
column: 5
end_location:
row: 126
column: 8
fix: ~

View File

@@ -0,0 +1,133 @@
---
source: src/linter.rs
expression: checks
---
- kind: MultiLineSummarySecondLine
location:
row: 195
column: 5
end_location:
row: 198
column: 8
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 205
column: 5
end_location:
row: 210
column: 8
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 215
column: 5
end_location:
row: 219
column: 8
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 225
column: 5
end_location:
row: 229
column: 8
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 235
column: 5
end_location:
row: 239
column: 4
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 245
column: 5
end_location:
row: 249
column: 8
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 255
column: 5
end_location:
row: 259
column: 12
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 265
column: 5
end_location:
row: 269
column: 8
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 276
column: 5
end_location:
row: 278
column: 20
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 294
column: 5
end_location:
row: 297
column: 8
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 338
column: 5
end_location:
row: 343
column: 8
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 378
column: 5
end_location:
row: 381
column: 8
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 387
column: 5
end_location:
row: 391
column: 8
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 433
column: 37
end_location:
row: 436
column: 8
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 445
column: 5
end_location:
row: 449
column: 8
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 521
column: 5
end_location:
row: 527
column: 8
fix: ~

View File

@@ -0,0 +1,45 @@
---
source: src/linter.rs
expression: checks
---
- kind: UsesTripleQuotes
location:
row: 302
column: 6
end_location:
row: 302
column: 20
fix: ~
- kind: UsesTripleQuotes
location:
row: 307
column: 6
end_location:
row: 307
column: 20
fix: ~
- kind: UsesTripleQuotes
location:
row: 312
column: 6
end_location:
row: 312
column: 16
fix: ~
- kind: UsesTripleQuotes
location:
row: 317
column: 6
end_location:
row: 317
column: 16
fix: ~
- kind: UsesTripleQuotes
location:
row: 323
column: 6
end_location:
row: 323
column: 17
fix: ~

View File

@@ -0,0 +1,133 @@
---
source: src/linter.rs
expression: checks
---
- kind: EndsInPeriod
location:
row: 124
column: 5
end_location:
row: 126
column: 8
fix: ~
- kind: EndsInPeriod
location:
row: 283
column: 5
end_location:
row: 283
column: 34
fix: ~
- kind: EndsInPeriod
location:
row: 288
column: 5
end_location:
row: 288
column: 38
fix: ~
- kind: EndsInPeriod
location:
row: 350
column: 5
end_location:
row: 350
column: 18
fix: ~
- kind: EndsInPeriod
location:
row: 401
column: 25
end_location:
row: 401
column: 40
fix: ~
- kind: EndsInPeriod
location:
row: 405
column: 5
end_location:
row: 405
column: 25
fix: ~
- kind: EndsInPeriod
location:
row: 411
column: 5
end_location:
row: 411
column: 25
fix: ~
- kind: EndsInPeriod
location:
row: 417
column: 35
end_location:
row: 417
column: 50
fix: ~
- kind: EndsInPeriod
location:
row: 424
column: 49
end_location:
row: 424
column: 64
fix: ~
- kind: EndsInPeriod
location:
row: 465
column: 5
end_location:
row: 465
column: 25
fix: ~
- kind: EndsInPeriod
location:
row: 470
column: 5
end_location:
row: 470
column: 25
fix: ~
- kind: EndsInPeriod
location:
row: 475
column: 5
end_location:
row: 475
column: 25
fix: ~
- kind: EndsInPeriod
location:
row: 482
column: 5
end_location:
row: 482
column: 25
fix: ~
- kind: EndsInPeriod
location:
row: 504
column: 5
end_location:
row: 504
column: 35
fix: ~
- kind: EndsInPeriod
location:
row: 509
column: 5
end_location:
row: 509
column: 34
fix: ~
- kind: EndsInPeriod
location:
row: 515
column: 5
end_location:
row: 515
column: 33
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: NoSignature
location:
row: 373
column: 5
end_location:
row: 373
column: 31
fix: ~

View File

@@ -0,0 +1,6 @@
---
source: src/linter.rs
expression: checks
---
[]

View File

@@ -0,0 +1,6 @@
---
source: src/linter.rs
expression: checks
---
[]

View File

@@ -0,0 +1,23 @@
---
source: src/linter.rs
expression: checks
---
- kind:
CapitalizeSectionName: returns
location:
row: 17
column: 5
end_location:
row: 23
column: 8
fix: ~
- kind:
CapitalizeSectionName: Short summary
location:
row: 207
column: 5
end_location:
row: 221
column: 8
fix: ~

View File

@@ -0,0 +1,41 @@
---
source: src/linter.rs
expression: checks
---
- kind:
NewLineAfterSectionName: Returns
location:
row: 30
column: 5
end_location:
row: 36
column: 8
fix: ~
- kind:
NewLineAfterSectionName: Raises
location:
row: 207
column: 5
end_location:
row: 221
column: 8
fix: ~
- kind:
NewLineAfterSectionName: Returns
location:
row: 252
column: 5
end_location:
row: 262
column: 8
fix: ~
- kind:
NewLineAfterSectionName: Raises
location:
row: 252
column: 5
end_location:
row: 262
column: 8
fix: ~

View File

@@ -0,0 +1,50 @@
---
source: src/linter.rs
expression: checks
---
- kind:
DashedUnderlineAfterSection: Returns
location:
row: 42
column: 5
end_location:
row: 47
column: 8
fix: ~
- kind:
DashedUnderlineAfterSection: Returns
location:
row: 54
column: 5
end_location:
row: 58
column: 8
fix: ~
- kind:
DashedUnderlineAfterSection: Raises
location:
row: 207
column: 5
end_location:
row: 221
column: 8
fix: ~
- kind:
DashedUnderlineAfterSection: Returns
location:
row: 252
column: 5
end_location:
row: 262
column: 8
fix: ~
- kind:
DashedUnderlineAfterSection: Raises
location:
row: 252
column: 5
end_location:
row: 262
column: 8
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/linter.rs
expression: checks
---
- kind:
SectionUnderlineAfterName: Returns
location:
row: 85
column: 5
end_location:
row: 92
column: 8
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/linter.rs
expression: checks
---
- kind:
SectionUnderlineMatchesSectionLength: Returns
location:
row: 99
column: 5
end_location:
row: 105
column: 8
fix: ~
- kind:
SectionUnderlineMatchesSectionLength: Returns
location:
row: 207
column: 5
end_location:
row: 221
column: 8
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/linter.rs
expression: checks
---
- kind:
BlankLineAfterSection: Returns
location:
row: 67
column: 5
end_location:
row: 78
column: 8
fix: ~
- kind:
BlankLineAfterSection: Returns
location:
row: 207
column: 5
end_location:
row: 221
column: 8
fix: ~

View File

@@ -0,0 +1,32 @@
---
source: src/linter.rs
expression: checks
---
- kind:
BlankLineBeforeSection: Yields
location:
row: 67
column: 5
end_location:
row: 78
column: 8
fix: ~
- kind:
BlankLineBeforeSection: Returns
location:
row: 122
column: 5
end_location:
row: 129
column: 8
fix: ~
- kind:
BlankLineBeforeSection: Raises
location:
row: 207
column: 5
end_location:
row: 221
column: 8
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/linter.rs
expression: checks
---
- kind:
NoBlankLinesBetweenHeaderAndContent: Short summary
location:
row: 207
column: 5
end_location:
row: 221
column: 8
fix: ~

View File

@@ -0,0 +1,6 @@
---
source: src/linter.rs
expression: checks
---
[]

View File

@@ -0,0 +1,50 @@
---
source: src/linter.rs
expression: checks
---
- kind:
NonEmptySection: Returns
location:
row: 54
column: 5
end_location:
row: 58
column: 8
fix: ~
- kind:
NonEmptySection: Returns
location:
row: 67
column: 5
end_location:
row: 78
column: 8
fix: ~
- kind:
NonEmptySection: Yields
location:
row: 67
column: 5
end_location:
row: 78
column: 8
fix: ~
- kind:
NonEmptySection: Returns
location:
row: 161
column: 5
end_location:
row: 165
column: 8
fix: ~
- kind:
NonEmptySection: Returns
location:
row: 252
column: 5
end_location:
row: 262
column: 8
fix: ~

View File

@@ -0,0 +1,125 @@
---
source: src/linter.rs
expression: checks
---
- kind: EndsInPunctuation
location:
row: 124
column: 5
end_location:
row: 126
column: 8
fix: ~
- kind: EndsInPunctuation
location:
row: 283
column: 5
end_location:
row: 283
column: 34
fix: ~
- kind: EndsInPunctuation
location:
row: 288
column: 5
end_location:
row: 288
column: 38
fix: ~
- kind: EndsInPunctuation
location:
row: 350
column: 5
end_location:
row: 350
column: 18
fix: ~
- kind: EndsInPunctuation
location:
row: 401
column: 25
end_location:
row: 401
column: 40
fix: ~
- kind: EndsInPunctuation
location:
row: 405
column: 5
end_location:
row: 405
column: 25
fix: ~
- kind: EndsInPunctuation
location:
row: 411
column: 5
end_location:
row: 411
column: 25
fix: ~
- kind: EndsInPunctuation
location:
row: 417
column: 35
end_location:
row: 417
column: 50
fix: ~
- kind: EndsInPunctuation
location:
row: 424
column: 49
end_location:
row: 424
column: 64
fix: ~
- kind: EndsInPunctuation
location:
row: 465
column: 5
end_location:
row: 465
column: 25
fix: ~
- kind: EndsInPunctuation
location:
row: 470
column: 5
end_location:
row: 470
column: 25
fix: ~
- kind: EndsInPunctuation
location:
row: 475
column: 5
end_location:
row: 475
column: 25
fix: ~
- kind: EndsInPunctuation
location:
row: 482
column: 5
end_location:
row: 482
column: 25
fix: ~
- kind: EndsInPunctuation
location:
row: 504
column: 5
end_location:
row: 504
column: 35
fix: ~
- kind: EndsInPunctuation
location:
row: 515
column: 5
end_location:
row: 515
column: 33
fix: ~

View File

@@ -0,0 +1,29 @@
---
source: src/linter.rs
expression: checks
---
- kind: SkipDocstring
location:
row: 33
column: 5
end_location:
row: 37
column: 5
fix: ~
- kind: SkipDocstring
location:
row: 85
column: 5
end_location:
row: 89
column: 5
fix: ~
- kind: SkipDocstring
location:
row: 105
column: 1
end_location:
row: 110
column: 1
fix: ~

View File

@@ -0,0 +1,29 @@
---
source: src/linter.rs
expression: checks
---
- kind: NonEmpty
location:
row: 19
column: 9
end_location:
row: 19
column: 15
fix: ~
- kind: NonEmpty
location:
row: 69
column: 5
end_location:
row: 69
column: 12
fix: ~
- kind: NonEmpty
location:
row: 75
column: 9
end_location:
row: 75
column: 11
fix: ~

View File

@@ -4,10 +4,10 @@ expression: checks
---
- kind: LateFutureImport
location:
row: 7
row: 6
column: 1
end_location:
row: 7
row: 6
column: 38
fix: ~

View File

@@ -20,7 +20,8 @@ expression: checks
column: 18
applied: false
- kind:
UnusedNOQA: E501
UnusedNOQA:
- E501
location:
row: 13
column: 10
@@ -37,7 +38,9 @@ expression: checks
column: 24
applied: false
- kind:
UnusedNOQA: E501
UnusedNOQA:
- F841
- E501
location:
row: 16
column: 10
@@ -45,7 +48,7 @@ expression: checks
row: 16
column: 30
fix:
content: " # noqa: F841"
content: ""
location:
row: 16
column: 10
@@ -54,54 +57,74 @@ expression: checks
column: 30
applied: false
- kind:
UnusedNOQA: F841
UnusedNOQA:
- W191
location:
row: 41
row: 19
column: 10
end_location:
row: 19
column: 30
fix:
content: " # noqa: F841"
location:
row: 19
column: 10
end_location:
row: 19
column: 30
applied: false
- kind:
UnusedNOQA:
- F841
location:
row: 44
column: 4
end_location:
row: 41
row: 44
column: 24
fix:
content: " # noqa: E501"
location:
row: 41
row: 44
column: 4
end_location:
row: 41
row: 44
column: 24
applied: false
- kind:
UnusedNOQA: E501
UnusedNOQA:
- E501
location:
row: 49
row: 52
column: 4
end_location:
row: 49
row: 52
column: 18
fix:
content: ""
location:
row: 49
row: 52
column: 4
end_location:
row: 49
row: 52
column: 18
applied: false
- kind:
UnusedNOQA: ~
location:
row: 57
row: 60
column: 4
end_location:
row: 57
row: 60
column: 12
fix:
content: ""
location:
row: 57
row: 60
column: 4
end_location:
row: 57
row: 60
column: 12
applied: false

158
src/visibility.rs Normal file
View File

@@ -0,0 +1,158 @@
//! Abstractions for tracking public and private visibility across modules, classes, and functions.
use std::path::Path;
use rustpython_ast::{Stmt, StmtKind};
use crate::ast::helpers::match_name_or_attr;
use crate::docstrings::types::Documentable;
#[derive(Debug, Clone)]
pub enum Modifier {
Module,
Class,
Function,
}
#[derive(Debug, Clone)]
pub enum Visibility {
Public,
Private,
}
#[derive(Debug, Clone)]
pub struct VisibleScope {
pub modifier: Modifier,
pub visibility: Visibility,
}
/// Returns `true` if a function definition is an `@overload`.
pub fn is_overload(stmt: &Stmt) -> bool {
match &stmt.node {
StmtKind::FunctionDef { decorator_list, .. }
| StmtKind::AsyncFunctionDef { decorator_list, .. } => decorator_list
.iter()
.any(|expr| match_name_or_attr(expr, "overload")),
_ => panic!("Found non-FunctionDef in is_overload"),
}
}
/// Returns `true` if a function is a "magic method".
pub fn is_magic(stmt: &Stmt) -> bool {
match &stmt.node {
StmtKind::FunctionDef { name, .. } | StmtKind::AsyncFunctionDef { name, .. } => {
name.starts_with("__")
&& name.ends_with("__")
&& name != "__init__"
&& name != "__call__"
&& name != "__new__"
}
_ => panic!("Found non-FunctionDef in is_magic"),
}
}
/// Returns `true` if a function is an `__init__`.
pub fn is_init(stmt: &Stmt) -> bool {
match &stmt.node {
StmtKind::FunctionDef { name, .. } | StmtKind::AsyncFunctionDef { name, .. } => {
name == "__init__"
}
_ => panic!("Found non-FunctionDef in is_init"),
}
}
/// Returns `true` if a module name indicates private visibility.
fn is_private_module(module_name: &str) -> bool {
module_name.starts_with('_') || (module_name.starts_with("__") && module_name.ends_with("__"))
}
pub fn module_visibility(path: &Path) -> Visibility {
for component in path.iter().rev() {
if is_private_module(&component.to_string_lossy()) {
return Visibility::Private;
}
}
Visibility::Public
}
fn function_visibility(stmt: &Stmt) -> Visibility {
match &stmt.node {
StmtKind::FunctionDef { name, .. } | StmtKind::AsyncFunctionDef { name, .. } => {
if name.starts_with('_') {
Visibility::Private
} else {
Visibility::Public
}
}
_ => panic!("Found non-FunctionDef in function_visibility"),
}
}
fn method_visibility(stmt: &Stmt) -> Visibility {
match &stmt.node {
StmtKind::FunctionDef { name, .. } | StmtKind::AsyncFunctionDef { name, .. } => {
// Is the method non-private?
if !name.starts_with('_') {
return Visibility::Public;
}
// Is this a magic method?
if name.starts_with("__") && name.ends_with("__") {
return Visibility::Public;
}
Visibility::Private
}
_ => panic!("Found non-FunctionDef in method_visibility"),
}
}
fn class_visibility(stmt: &Stmt) -> Visibility {
match &stmt.node {
StmtKind::ClassDef { name, .. } => {
if name.starts_with('_') {
Visibility::Private
} else {
Visibility::Public
}
}
_ => panic!("Found non-ClassDef in function_visibility"),
}
}
/// Transition a `VisibleScope` based on a new `Documentable` definition.
///
/// `scope` is the current `VisibleScope`, while `Documentable` and `Stmt` describe the current
/// node used to modify visibility.
pub fn transition_scope(scope: &VisibleScope, stmt: &Stmt, kind: &Documentable) -> VisibleScope {
match kind {
Documentable::Function => VisibleScope {
modifier: Modifier::Function,
visibility: match scope {
VisibleScope {
modifier: Modifier::Module,
visibility: Visibility::Public,
} => function_visibility(stmt),
VisibleScope {
modifier: Modifier::Class,
visibility: Visibility::Public,
} => method_visibility(stmt),
_ => Visibility::Private,
},
},
Documentable::Class => VisibleScope {
modifier: Modifier::Class,
visibility: match scope {
VisibleScope {
modifier: Modifier::Module,
visibility: Visibility::Public,
} => class_visibility(stmt),
VisibleScope {
modifier: Modifier::Class,
visibility: Visibility::Public,
} => class_visibility(stmt),
_ => Visibility::Private,
},
},
}
}

47
tests/integration_test.rs Normal file
View File

@@ -0,0 +1,47 @@
use std::str;
use anyhow::Result;
use assert_cmd::{crate_name, Command};
#[test]
fn test_stdin_success() -> Result<()> {
let mut cmd = Command::cargo_bin(crate_name!())?;
cmd.args(&["-"]).write_stdin("").assert().success();
Ok(())
}
#[test]
fn test_stdin_error() -> Result<()> {
let mut cmd = Command::cargo_bin(crate_name!())?;
let output = cmd
.args(&["-"])
.write_stdin("import os\n")
.assert()
.failure();
assert!(str::from_utf8(&output.get_output().stdout)?.contains("-:1:1: F401"));
Ok(())
}
#[test]
fn test_stdin_filename() -> Result<()> {
let mut cmd = Command::cargo_bin(crate_name!())?;
let output = cmd
.args(&["-", "--stdin-filename", "F401.py"])
.write_stdin("import os\n")
.assert()
.failure();
assert!(str::from_utf8(&output.get_output().stdout)?.contains("F401.py:1:1: F401"));
Ok(())
}
#[test]
fn test_stdin_autofix() -> Result<()> {
let mut cmd = Command::cargo_bin(crate_name!())?;
let output = cmd
.args(&["-", "--fix"])
.write_stdin("import os\n")
.assert()
.failure();
assert!(str::from_utf8(&output.get_output().stdout)?.contains("-:1:1: F401"));
Ok(())
}