Compare commits

..

15 Commits

Author SHA1 Message Date
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
42 changed files with 2295 additions and 280 deletions

71
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"
@@ -1685,6 +1711,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 +1960,10 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.68"
version = "0.0.70"
dependencies = [
"anyhow",
"assert_cmd",
"bincode",
"cacache",
"chrono",
@@ -2345,6 +2399,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"
@@ -2593,6 +2653,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.68"
version = "0.0.70"
edition = "2021"
[lib]
@@ -40,6 +40,7 @@ strum_macros = "0.24.3"
num-bigint = "0.4.3"
[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.70
hooks:
- id: lint
```
@@ -215,10 +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/) (11/16)
- [`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/) (6/47)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (partial)
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/) (25/48)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (8/34)
Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis Flake8:
@@ -281,7 +281,7 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| 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 | | 🛠 |
| 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 | | |
@@ -293,6 +293,7 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| 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 | | 🛠 |
@@ -304,12 +305,31 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| 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)` | | 🛠 |
| D200 | OneLinerDocstring | One-line docstring should fit on one line | | |
| D205 | BlankLineAfterSummary | 1 blank line required between summary line and description | | |
| 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 | | |
| D400 | DocstringEndsInNonPeriod | First line should end with a period | | |
| D419 | EmptyDocstring | Docstring is empty | | |
| 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 | | |
| D415 | EndsInPunctuation | First line should end with a period, question mark, or exclamation point | | |
| 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 | | |
| M001 | UnusedNOQA | Unused `noqa` directive | | 🛠 |
## Integrations

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

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

@@ -1004,6 +1004,68 @@ pub fn unnecessary_literal_within_list_call(
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 {

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

@@ -21,17 +21,18 @@ use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::{checkers, helpers, operations, visitor};
use crate::autofix::{fixer, fixes};
use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::{Docstring, DocstringKind};
use crate::docstrings::{Definition, DefinitionKind, Documentable};
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};
use crate::{docstrings, plugins};
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,
@@ -39,7 +40,7 @@ pub struct Checker<'a> {
// Computed checks.
checks: Vec<Check>,
// Docstring tracking.
docstrings: Vec<Docstring<'a>>,
docstrings: Vec<(Definition<'a>, Visibility)>,
// Edit tracking.
// TODO(charlie): Instead of exposing deletions, wrap in a public API.
pub(crate) deletions: BTreeSet<usize>,
@@ -52,11 +53,11 @@ 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.
pub(crate) initial: bool,
visible_scope: VisibleScope,
in_f_string: Option<Range>,
in_annotation: bool,
in_literal: bool,
@@ -91,7 +92,10 @@ impl<'a> Checker<'a> {
deferred_functions: Default::default(),
deferred_lambdas: Default::default(),
deferred_assignments: Default::default(),
initial: true,
visible_scope: VisibleScope {
modifier: Modifier::Module,
visibility: module_visibility(path),
},
in_f_string: None,
in_annotation: Default::default(),
in_literal: Default::default(),
@@ -125,36 +129,8 @@ where
StmtKind::Import { .. } => {
self.futures_allowed = false;
}
StmtKind::Expr { value } => {
// Track all docstrings: module-, class-, and function-level.
let mut is_module_docstring = false;
if matches!(
&value.node,
ExprKind::Constant {
value: Constant::Str(_),
..
}
) {
if let Some(docstring) = docstrings::extract(self, stmt, value) {
if matches!(&docstring.kind, DocstringKind::Module) {
is_module_docstring = true;
}
self.docstrings.push(docstring);
}
}
if !is_module_docstring {
if !self.seen_import_boundary
&& !operations::in_nested_block(&self.parent_stack, &self.parents)
{
self.seen_import_boundary = true;
}
self.futures_allowed = false;
}
}
node => {
self.futures_allowed = false;
if !self.seen_import_boundary
&& !helpers::is_assignment_to_a_dunder(node)
&& !operations::in_nested_block(&self.parent_stack, &self.parents)
@@ -590,18 +566,31 @@ where
StmtKind::Delete { .. } => {}
_ => {}
}
self.initial = false;
// Recurse.
let prev_visibile_scope = self.visible_scope.clone();
match &stmt.node {
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
StmtKind::FunctionDef { body, .. } | StmtKind::AsyncFunctionDef { body, .. } => {
let definition =
docstrings::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 =
docstrings::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);
}
@@ -629,6 +618,7 @@ where
}
_ => visitor::walk_stmt(self, stmt),
};
self.visible_scope = prev_visibile_scope;
// Post-visit.
if let StmtKind::ClassDef { name, .. } = &stmt.node {
@@ -837,6 +827,15 @@ where
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) = checkers::unnecessary_subscript_reversal(expr, func, args)
{
@@ -1643,6 +1642,25 @@ impl<'a> Checker<'a> {
}
}
fn visit_docstring<'b>(&mut self, python_ast: &'b Suite) -> bool
where
'b: 'a,
{
let docstring = docstrings::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;
@@ -1695,9 +1713,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 {
@@ -1889,10 +1908,27 @@ impl<'a> Checker<'a> {
}
fn check_docstrings(&mut self) {
while let Some(docstring) = self.docstrings.pop() {
while let Some((docstring, visibility)) = self.docstrings.pop() {
if !docstrings::not_empty(self, &docstring) {
continue;
}
if !docstrings::not_missing(self, &docstring, &visibility) {
continue;
}
if self.settings.enabled.contains(&CheckCode::D200) {
docstrings::one_liner(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D201)
|| self.settings.enabled.contains(&CheckCode::D202)
{
docstrings::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)
{
docstrings::blank_before_after_class(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D205) {
docstrings::blank_after_summary(self, &docstring);
}
@@ -1902,11 +1938,25 @@ impl<'a> Checker<'a> {
if self.settings.enabled.contains(&CheckCode::D210) {
docstrings::no_surrounding_whitespace(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D212)
|| self.settings.enabled.contains(&CheckCode::D213)
{
docstrings::multi_line_summary_start(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D300) {
docstrings::triple_quotes(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D400) {
docstrings::ends_with_period(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D419) {
docstrings::not_empty(self, &docstring);
if self.settings.enabled.contains(&CheckCode::D402) {
docstrings::no_signature(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D403) {
docstrings::capitalized(self, &docstring);
}
if self.settings.enabled.contains(&CheckCode::D415) {
docstrings::ends_with_punctuation(self, &docstring);
}
}
}
@@ -1960,6 +2010,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);

View File

@@ -137,6 +137,7 @@ pub enum CheckCode {
C408,
C409,
C410,
C414,
C415,
// flake8-print
T201,
@@ -151,12 +152,31 @@ pub enum CheckCode {
U007,
U008,
// pydocstyle
D100,
D101,
D102,
D103,
D104,
D105,
D106,
D107,
D200,
D205,
D209,
D210,
D212,
D213,
D300,
D400,
D402,
D403,
D415,
D419,
D201,
D202,
D211,
D203,
D204,
// Meta
M001,
}
@@ -240,6 +260,7 @@ pub enum CheckKind {
UnnecessaryCollectionCall(String),
UnnecessaryLiteralWithinTupleCall(String),
UnnecessaryLiteralWithinListCall(String),
UnnecessaryDoubleCastOrProcess(String, String),
UnnecessarySubscriptReversal(String),
// flake8-print
PrintFound,
@@ -254,12 +275,31 @@ pub enum CheckKind {
UsePEP604Annotation,
SuperCallWithParameters,
// pydocstyle
OneLinerDocstring,
BlankLineAfterSummary,
EndsInPeriod,
EndsInPunctuation,
FirstLineCapitalized,
FitsOnOneLine,
MultiLineSummaryFirstLine,
MultiLineSummarySecondLine,
NewLineAfterLastParagraph,
NoBlankLineAfterSummary,
NoSurroundingWhitespace,
EmptyDocstring,
DocstringEndsInNonPeriod,
NonEmpty,
UsesTripleQuotes,
NoSignature,
NoBlankLineBeforeFunction(usize),
NoBlankLineAfterFunction(usize),
NoBlankLineBeforeClass(usize),
OneBlankLineBeforeClass(usize),
OneBlankLineAfterClass(usize),
PublicModule,
PublicClass,
PublicMethod,
PublicFunction,
PublicPackage,
MagicMethod,
PublicNestedClass,
PublicInit,
// Meta
UnusedNOQA(Option<Vec<String>>),
}
@@ -350,6 +390,10 @@ impl CheckCode {
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())
}
@@ -369,12 +413,31 @@ impl CheckCode {
CheckCode::U007 => CheckKind::UsePEP604Annotation,
CheckCode::U008 => CheckKind::SuperCallWithParameters,
// pydocstyle
CheckCode::D200 => CheckKind::OneLinerDocstring,
CheckCode::D205 => CheckKind::BlankLineAfterSummary,
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::D205 => CheckKind::NoBlankLineAfterSummary,
CheckCode::D209 => CheckKind::NewLineAfterLastParagraph,
CheckCode::D210 => CheckKind::NoSurroundingWhitespace,
CheckCode::D400 => CheckKind::DocstringEndsInNonPeriod,
CheckCode::D419 => CheckKind::EmptyDocstring,
CheckCode::D400 => CheckKind::EndsInPeriod,
CheckCode::D419 => CheckKind::NonEmpty,
CheckCode::D212 => CheckKind::MultiLineSummaryFirstLine,
CheckCode::D213 => CheckKind::MultiLineSummarySecondLine,
CheckCode::D300 => CheckKind::UsesTripleQuotes,
CheckCode::D402 => CheckKind::NoSignature,
CheckCode::D403 => CheckKind::FirstLineCapitalized,
CheckCode::D415 => CheckKind::EndsInPunctuation,
CheckCode::D201 => CheckKind::NoBlankLineBeforeFunction(1),
CheckCode::D202 => CheckKind::NoBlankLineAfterFunction(1),
CheckCode::D211 => CheckKind::NoBlankLineBeforeClass(1),
CheckCode::D203 => CheckKind::OneBlankLineBeforeClass(0),
CheckCode::D204 => CheckKind::OneBlankLineAfterClass(0),
// Meta
CheckCode::M001 => CheckKind::UnusedNOQA(None),
}
@@ -449,6 +512,7 @@ impl CheckKind {
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,
@@ -463,12 +527,31 @@ impl CheckKind {
CheckKind::UselessObjectInheritance(_) => &CheckCode::U004,
CheckKind::SuperCallWithParameters => &CheckCode::U008,
// pydocstyle
CheckKind::OneLinerDocstring => &CheckCode::D200,
CheckKind::BlankLineAfterSummary => &CheckCode::D205,
CheckKind::PublicModule => &CheckCode::D100,
CheckKind::PublicClass => &CheckCode::D101,
CheckKind::PublicMethod => &CheckCode::D102,
CheckKind::PublicFunction => &CheckCode::D103,
CheckKind::PublicPackage => &CheckCode::D104,
CheckKind::MagicMethod => &CheckCode::D105,
CheckKind::PublicNestedClass => &CheckCode::D106,
CheckKind::PublicInit => &CheckCode::D107,
CheckKind::FitsOnOneLine => &CheckCode::D200,
CheckKind::NoBlankLineAfterSummary => &CheckCode::D205,
CheckKind::NewLineAfterLastParagraph => &CheckCode::D209,
CheckKind::NoSurroundingWhitespace => &CheckCode::D210,
CheckKind::DocstringEndsInNonPeriod => &CheckCode::D400,
CheckKind::EmptyDocstring => &CheckCode::D419,
CheckKind::EndsInPeriod => &CheckCode::D400,
CheckKind::NonEmpty => &CheckCode::D419,
CheckKind::MultiLineSummaryFirstLine => &CheckCode::D212,
CheckKind::MultiLineSummarySecondLine => &CheckCode::D213,
CheckKind::UsesTripleQuotes => &CheckCode::D300,
CheckKind::NoSignature => &CheckCode::D402,
CheckKind::FirstLineCapitalized => &CheckCode::D403,
CheckKind::EndsInPunctuation => &CheckCode::D415,
CheckKind::NoBlankLineBeforeFunction(_) => &CheckCode::D201,
CheckKind::NoBlankLineAfterFunction(_) => &CheckCode::D202,
CheckKind::NoBlankLineBeforeClass(_) => &CheckCode::D211,
CheckKind::OneBlankLineBeforeClass(_) => &CheckCode::D203,
CheckKind::OneBlankLineAfterClass(_) => &CheckCode::D204,
// Meta
CheckKind::UnusedNOQA(_) => &CheckCode::M001,
}
@@ -625,7 +708,7 @@ impl CheckKind {
CheckKind::DuplicateHandlerException(names) => {
if names.len() == 1 {
let name = &names[0];
format!("Exception handler with duplicate exception: `{name}")
format!("Exception handler with duplicate exception: `{name}`")
} else {
let names = names.iter().map(|name| format!("`{name}`")).join(", ");
format!("Exception handler with duplicate exceptions: {names}")
@@ -681,6 +764,9 @@ impl CheckKind {
)
}
}
CheckKind::UnnecessaryDoubleCastOrProcess(inner, outer) => {
format!("Unnecessary {inner} call within {outer}().")
}
CheckKind::UnnecessarySubscriptReversal(func) => {
format!("Unnecessary subscript reversal of iterable within {func}()")
}
@@ -713,8 +799,8 @@ impl CheckKind {
"Use `super()` instead of `super(__class__, self)`".to_string()
}
// pydocstyle
CheckKind::OneLinerDocstring => "One-line docstring should fit on one line".to_string(),
CheckKind::BlankLineAfterSummary => {
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 => {
@@ -723,10 +809,48 @@ impl CheckKind {
CheckKind::NoSurroundingWhitespace => {
"No whitespaces allowed surrounding docstring text".to_string()
}
CheckKind::DocstringEndsInNonPeriod => {
"First line should end with a period".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::EmptyDocstring => "Docstring is empty".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(),
// Meta
CheckKind::UnusedNOQA(codes) => match codes {
None => "Unused `noqa` directive".to_string(),

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

@@ -1,198 +1,633 @@
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, CheckKind};
use rustpython_ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
use crate::checks::{Check, CheckCode, CheckKind};
use crate::visibility::{is_init, is_magic, is_overload, Modifier, Visibility, VisibleScope};
#[derive(Debug)]
pub enum DocstringKind {
pub enum DefinitionKind<'a> {
Module,
Function,
Class,
Package,
Class(&'a Stmt),
NestedClass(&'a Stmt),
Function(&'a Stmt),
NestedFunction(&'a Stmt),
Method(&'a Stmt),
}
#[derive(Debug)]
pub struct Docstring<'a> {
pub kind: DocstringKind,
pub parent: Option<&'a Stmt>,
pub expr: &'a Expr,
pub struct Definition<'a> {
pub kind: DefinitionKind<'a>,
pub docstring: Option<&'a Expr>,
}
/// Extract a docstring from an expression.
pub fn extract<'a, 'b>(
checker: &'a Checker,
stmt: &'b Stmt,
expr: &'b Expr,
) -> Option<Docstring<'b>> {
let defined_in = checker
.binding_context()
.defined_in
.map(|index| checker.parents[index]);
pub enum Documentable {
Class,
Function,
}
match defined_in {
None => {
if checker.initial {
return Some(Docstring {
kind: DocstringKind::Module,
parent: None,
expr,
});
}
}
Some(parent) => {
if let StmtKind::FunctionDef { body, .. }
| StmtKind::AsyncFunctionDef { body, .. }
| StmtKind::ClassDef { body, .. } = &parent.node
{
if body.first().map(|node| node == stmt).unwrap_or_default() {
return Some(Docstring {
kind: if matches!(&parent.node, StmtKind::ClassDef { .. }) {
DocstringKind::Class
} else {
DocstringKind::Function
},
parent: None,
expr,
});
/// 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
}
pub fn one_liner(checker: &mut Checker, docstring: &Docstring) {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.expr.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 {
return;
}
}
if non_empty_line_count == 1 && line_count > 1 {
checker.add_check(Check::new(
CheckKind::OneLinerDocstring,
Range::from_located(docstring.expr),
));
}
/// 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,
},
},
}
}
pub fn blank_after_summary(checker: &mut Checker, docstring: &Docstring) {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.expr.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;
/// Extract the source code range for a docstring.
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 {
break;
if checker.settings.enabled.contains(&CheckCode::D103) {
checker.add_check(Check::new(
CheckKind::PublicFunction,
Range::from_located(stmt),
));
}
false
}
}
if lines_count > 1 && blanks_count != 1 {
checker.add_check(Check::new(
CheckKind::BlankLineAfterSummary,
Range::from_located(docstring.expr),
));
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
}
}
}
}
pub fn newline_after_last_paragraph(checker: &mut Checker, docstring: &Docstring) {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.expr.node
{
let mut line_count = 0;
for line in string.lines() {
if !line.trim().is_empty() {
/// 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 line_count > 1 {
let content = checker
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
.slice_source_code_range(&Range::from_located(docstring.expr));
if let Some(line) = content.lines().last() {
let line = line.trim();
if line != "\"\"\"" && line != "'''" {
.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::NewLineAfterLastParagraph,
Range::from_located(docstring.expr),
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),
));
}
}
return;
}
}
}
}
pub fn no_surrounding_whitespace(checker: &mut Checker, docstring: &Docstring) {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.expr.node
{
let mut lines = string.lines();
if let Some(line) = lines.next() {
if line.trim().is_empty() {
return;
}
/// 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 line.starts_with(' ') || (matches!(lines.next(), None) && line.ends_with(' ')) {
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::NoSurroundingWhitespace,
Range::from_located(docstring.expr),
CheckKind::NoBlankLineAfterSummary,
range_for(docstring),
));
}
}
}
}
pub fn not_empty(checker: &mut Checker, docstring: &Docstring) {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.expr.node
{
if string.trim().is_empty() {
checker.add_check(Check::new(
CheckKind::EmptyDocstring,
Range::from_located(docstring.expr),
));
/// 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;
}
}
}
}
}
pub fn ends_with_period(checker: &mut Checker, docstring: &Docstring) {
if let ExprKind::Constant {
value: Constant::Str(string),
..
} = &docstring.expr.node
{
if let Some(string) = string.lines().next() {
if !string.ends_with('.') {
/// 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::DocstringEndsInNonPeriod,
Range::from_located(docstring.expr),
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),
));
}
}
}
}
}
}
/// 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),
));
}
}
}
}
}
/// 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
}

View File

@@ -28,6 +28,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,
@@ -966,6 +996,18 @@ mod tests {
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(
@@ -979,10 +1021,142 @@ mod tests {
}
#[test]
fn d200() -> Result<()> {
fn d100() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/D.py"),
&settings::Settings::for_rule(CheckCode::D200),
&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);
@@ -1026,6 +1200,54 @@ mod tests {
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(
@@ -1038,6 +1260,42 @@ mod tests {
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 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 d419() -> Result<()> {
let mut checks = check_path(

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

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

@@ -2,10 +2,10 @@
source: src/linter.rs
expression: checks
---
- kind: OneLinerDocstring
- kind: FitsOnOneLine
location:
row: 124
column: 6
column: 5
end_location:
row: 126
column: 8

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

@@ -2,18 +2,18 @@
source: src/linter.rs
expression: checks
---
- kind: BlankLineAfterSummary
- kind: NoBlankLineAfterSummary
location:
row: 195
column: 6
column: 5
end_location:
row: 198
column: 8
fix: ~
- kind: BlankLineAfterSummary
- kind: NoBlankLineAfterSummary
location:
row: 205
column: 6
column: 5
end_location:
row: 210
column: 8

View File

@@ -5,7 +5,7 @@ expression: checks
- kind: NewLineAfterLastParagraph
location:
row: 276
column: 6
column: 5
end_location:
row: 278
column: 20

View File

@@ -5,7 +5,7 @@ expression: checks
- kind: NoSurroundingWhitespace
location:
row: 283
column: 6
column: 5
end_location:
row: 283
column: 34
@@ -13,7 +13,7 @@ expression: checks
- kind: NoSurroundingWhitespace
location:
row: 288
column: 6
column: 5
end_location:
row: 288
column: 38
@@ -21,7 +21,7 @@ expression: checks
- kind: NoSurroundingWhitespace
location:
row: 294
column: 6
column: 5
end_location:
row: 297
column: 8

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

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

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

@@ -2,26 +2,26 @@
source: src/linter.rs
expression: checks
---
- kind: EmptyDocstring
- kind: NonEmpty
location:
row: 19
column: 10
column: 9
end_location:
row: 19
column: 15
fix: ~
- kind: EmptyDocstring
- kind: NonEmpty
location:
row: 69
column: 6
column: 5
end_location:
row: 69
column: 12
fix: ~
- kind: EmptyDocstring
- kind: NonEmpty
location:
row: 75
column: 10
column: 9
end_location:
row: 75
column: 11

149
src/visibility.rs Normal file
View File

@@ -0,0 +1,149 @@
use std::path::Path;
use crate::ast::helpers::match_name_or_attr;
use rustpython_ast::{Stmt, StmtKind};
use crate::docstrings::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,
}
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"),
}
}
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"),
}
}
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"),
}
}
fn is_private_name(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_name(&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.
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(())
}