Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e08e1caf71 | ||
|
|
0072dfd81e | ||
|
|
6ffe02ee05 | ||
|
|
688fc0cd02 | ||
|
|
f30e5e45ab | ||
|
|
1a68a38306 | ||
|
|
590aa92ead | ||
|
|
8868f57a74 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1960,7 +1960,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.69"
|
||||
version = "0.0.70"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.69"
|
||||
version = "0.0.70"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
21
README.md
21
README.md
@@ -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,9 +215,9 @@ 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/) (11/47)
|
||||
- [`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:
|
||||
@@ -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,6 +305,14 @@ 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)` | | 🛠 |
|
||||
| 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 | | |
|
||||
@@ -312,9 +321,15 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
|
||||
| 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
14
resources/test/fixtures/C414.py
vendored
Normal 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))
|
||||
@@ -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 {
|
||||
|
||||
@@ -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],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
119
src/check_ast.rs
119
src/check_ast.rs
@@ -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,13 +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);
|
||||
}
|
||||
@@ -1916,6 +1949,9 @@ impl<'a> Checker<'a> {
|
||||
if self.settings.enabled.contains(&CheckCode::D400) {
|
||||
docstrings::ends_with_period(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);
|
||||
}
|
||||
@@ -1974,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);
|
||||
|
||||
@@ -137,6 +137,7 @@ pub enum CheckCode {
|
||||
C408,
|
||||
C409,
|
||||
C410,
|
||||
C414,
|
||||
C415,
|
||||
// flake8-print
|
||||
T201,
|
||||
@@ -151,6 +152,14 @@ pub enum CheckCode {
|
||||
U007,
|
||||
U008,
|
||||
// pydocstyle
|
||||
D100,
|
||||
D101,
|
||||
D102,
|
||||
D103,
|
||||
D104,
|
||||
D105,
|
||||
D106,
|
||||
D107,
|
||||
D200,
|
||||
D205,
|
||||
D209,
|
||||
@@ -159,9 +168,15 @@ pub enum CheckCode {
|
||||
D213,
|
||||
D300,
|
||||
D400,
|
||||
D402,
|
||||
D403,
|
||||
D415,
|
||||
D419,
|
||||
D201,
|
||||
D202,
|
||||
D211,
|
||||
D203,
|
||||
D204,
|
||||
// Meta
|
||||
M001,
|
||||
}
|
||||
@@ -245,6 +260,7 @@ pub enum CheckKind {
|
||||
UnnecessaryCollectionCall(String),
|
||||
UnnecessaryLiteralWithinTupleCall(String),
|
||||
UnnecessaryLiteralWithinListCall(String),
|
||||
UnnecessaryDoubleCastOrProcess(String, String),
|
||||
UnnecessarySubscriptReversal(String),
|
||||
// flake8-print
|
||||
PrintFound,
|
||||
@@ -270,6 +286,20 @@ pub enum CheckKind {
|
||||
NoSurroundingWhitespace,
|
||||
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>>),
|
||||
}
|
||||
@@ -360,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())
|
||||
}
|
||||
@@ -379,6 +413,14 @@ impl CheckCode {
|
||||
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::D205 => CheckKind::NoBlankLineAfterSummary,
|
||||
CheckCode::D209 => CheckKind::NewLineAfterLastParagraph,
|
||||
@@ -388,8 +430,14 @@ impl CheckCode {
|
||||
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),
|
||||
}
|
||||
@@ -464,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,
|
||||
@@ -478,6 +527,14 @@ impl CheckKind {
|
||||
CheckKind::UselessObjectInheritance(_) => &CheckCode::U004,
|
||||
CheckKind::SuperCallWithParameters => &CheckCode::U008,
|
||||
// pydocstyle
|
||||
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,
|
||||
@@ -487,8 +544,14 @@ impl CheckKind {
|
||||
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,
|
||||
}
|
||||
@@ -701,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}()")
|
||||
}
|
||||
@@ -759,6 +825,32 @@ impl CheckKind {
|
||||
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(),
|
||||
|
||||
@@ -1,175 +1,395 @@
|
||||
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::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 `Expr`.
|
||||
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]);
|
||||
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,
|
||||
});
|
||||
pub enum Documentable {
|
||||
Class,
|
||||
Function,
|
||||
}
|
||||
|
||||
/// 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 the source code range for a `Docstring`.
|
||||
fn range_for(docstring: &Docstring) -> Range {
|
||||
/// 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.
|
||||
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.expr.location.row(),
|
||||
docstring.expr.location.column() - 1,
|
||||
),
|
||||
end_location: docstring.expr.end_location,
|
||||
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, 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;
|
||||
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 {
|
||||
return;
|
||||
|
||||
if non_empty_line_count == 1 && line_count > 1 {
|
||||
checker.add_check(Check::new(CheckKind::FitsOnOneLine, range_for(docstring)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, 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;
|
||||
} 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, 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() {
|
||||
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),
|
||||
));
|
||||
}
|
||||
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;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// D210
|
||||
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;
|
||||
}
|
||||
if line.starts_with(' ') || (matches!(lines.next(), None) && line.ends_with(' ')) {
|
||||
if lines_count > 1 && blanks_count != 1 {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::NoSurroundingWhitespace,
|
||||
CheckKind::NoBlankLineAfterSummary,
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
@@ -177,39 +397,90 @@ pub fn no_surrounding_whitespace(checker: &mut Checker, docstring: &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, docstring: &Docstring) {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(string),
|
||||
..
|
||||
} = &docstring.expr.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 == "'''"
|
||||
|| first_line == "u\"\"\""
|
||||
|| first_line == "u'''"
|
||||
|| first_line == "r\"\"\""
|
||||
|| first_line == "r'''"
|
||||
|| first_line == "ur\"\"\""
|
||||
|| first_line == "ur'''"
|
||||
{
|
||||
if checker.settings.enabled.contains(&CheckCode::D212) {
|
||||
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::MultiLineSummaryFirstLine,
|
||||
CheckKind::MultiLineSummarySecondLine,
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
} else if checker.settings.enabled.contains(&CheckCode::D213) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::MultiLineSummarySecondLine,
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,70 +488,123 @@ pub fn multi_line_summary_start(checker: &mut Checker, docstring: &Docstring) {
|
||||
}
|
||||
|
||||
/// D300
|
||||
pub fn triple_quotes(checker: &mut Checker, docstring: &Docstring) {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(string),
|
||||
..
|
||||
} = &docstring.expr.node
|
||||
{
|
||||
let content = checker
|
||||
.locator
|
||||
.slice_source_code_range(&range_for(docstring));
|
||||
if string.contains("\"\"\"") {
|
||||
if !content.starts_with("'''") {
|
||||
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),
|
||||
));
|
||||
}
|
||||
} else if !content.starts_with("\"\"\"") {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::UsesTripleQuotes,
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// D400
|
||||
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('.') {
|
||||
checker.add_check(Check::new(CheckKind::EndsInPeriod, range_for(docstring)));
|
||||
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, docstring: &Docstring) {
|
||||
if !matches!(docstring.kind, DocstringKind::Function) {
|
||||
pub fn capitalized(checker: &mut Checker, definition: &Definition) {
|
||||
if !matches!(definition.kind, DefinitionKind::Function(_)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(string),
|
||||
..
|
||||
} = &docstring.expr.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 != '\'' {
|
||||
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),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(first_char) = first_word.chars().next() {
|
||||
if !first_char.is_uppercase() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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::FirstLineCapitalized,
|
||||
CheckKind::EndsInPunctuation,
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
@@ -289,36 +613,20 @@ pub fn capitalized(checker: &mut Checker, docstring: &Docstring) {
|
||||
}
|
||||
}
|
||||
|
||||
/// D415
|
||||
pub fn ends_with_punctuation(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('.') || string.ends_with('!') || string.ends_with('?')) {
|
||||
checker.add_check(Check::new(
|
||||
CheckKind::EndsInPunctuation,
|
||||
range_for(docstring),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// D419
|
||||
pub fn not_empty(checker: &mut Checker, docstring: &Docstring) -> bool {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(string),
|
||||
..
|
||||
} = &docstring.expr.node
|
||||
{
|
||||
if string.trim().is_empty() {
|
||||
if checker.settings.enabled.contains(&CheckCode::D419) {
|
||||
checker.add_check(Check::new(CheckKind::NonEmpty, range_for(docstring)));
|
||||
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;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
172
src/linter.rs
172
src/linter.rs
@@ -996,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(
|
||||
@@ -1009,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);
|
||||
@@ -1056,6 +1200,18 @@ 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(
|
||||
@@ -1104,6 +1260,18 @@ 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(
|
||||
|
||||
148
src/snapshots/ruff__linter__tests__c414.snap
Normal file
148
src/snapshots/ruff__linter__tests__c414.snap
Normal 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: ~
|
||||
|
||||
13
src/snapshots/ruff__linter__tests__d100.snap
Normal file
13
src/snapshots/ruff__linter__tests__d100.snap
Normal 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: ~
|
||||
|
||||
13
src/snapshots/ruff__linter__tests__d101.snap
Normal file
13
src/snapshots/ruff__linter__tests__d101.snap
Normal 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: ~
|
||||
|
||||
29
src/snapshots/ruff__linter__tests__d102.snap
Normal file
29
src/snapshots/ruff__linter__tests__d102.snap
Normal 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: ~
|
||||
|
||||
13
src/snapshots/ruff__linter__tests__d103.snap
Normal file
13
src/snapshots/ruff__linter__tests__d103.snap
Normal 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: ~
|
||||
|
||||
6
src/snapshots/ruff__linter__tests__d104.snap
Normal file
6
src/snapshots/ruff__linter__tests__d104.snap
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
[]
|
||||
|
||||
13
src/snapshots/ruff__linter__tests__d105.snap
Normal file
13
src/snapshots/ruff__linter__tests__d105.snap
Normal 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: ~
|
||||
|
||||
6
src/snapshots/ruff__linter__tests__d106.snap
Normal file
6
src/snapshots/ruff__linter__tests__d106.snap
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
[]
|
||||
|
||||
21
src/snapshots/ruff__linter__tests__d107.snap
Normal file
21
src/snapshots/ruff__linter__tests__d107.snap
Normal 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: ~
|
||||
|
||||
23
src/snapshots/ruff__linter__tests__d201.snap
Normal file
23
src/snapshots/ruff__linter__tests__d201.snap
Normal 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: ~
|
||||
|
||||
23
src/snapshots/ruff__linter__tests__d202.snap
Normal file
23
src/snapshots/ruff__linter__tests__d202.snap
Normal 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: ~
|
||||
|
||||
32
src/snapshots/ruff__linter__tests__d203.snap
Normal file
32
src/snapshots/ruff__linter__tests__d203.snap
Normal 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: ~
|
||||
|
||||
23
src/snapshots/ruff__linter__tests__d204.snap
Normal file
23
src/snapshots/ruff__linter__tests__d204.snap
Normal 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: ~
|
||||
|
||||
23
src/snapshots/ruff__linter__tests__d211.snap
Normal file
23
src/snapshots/ruff__linter__tests__d211.snap
Normal 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: ~
|
||||
|
||||
13
src/snapshots/ruff__linter__tests__d402.snap
Normal file
13
src/snapshots/ruff__linter__tests__d402.snap
Normal 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: ~
|
||||
|
||||
149
src/visibility.rs
Normal file
149
src/visibility.rs
Normal 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user