Implement pycodestyle's logical line detection (#1130)

Along with the logical line detection, this adds 14 of the missing `pycodestyle` rules.

For now, this is all gated behind a `logical_lines` feature that's off-by-default, which will let us implement all rules prior to shipping, since we want to couple the release of these rules with new defaults and instructions.
This commit is contained in:
Charlie Marsh 2023-02-05 15:06:02 -05:00 committed by GitHub
parent f03c8fff14
commit e3dfa2e04e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1680 additions and 14 deletions

View File

@ -69,13 +69,13 @@ jobs:
- name: "Run tests (Ubuntu)"
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
cargo insta test --all --delete-unreferenced-snapshots
cargo insta test --all --all-features --delete-unreferenced-snapshots
git diff --exit-code
- name: "Run tests (Windows)"
if: ${{ matrix.os == 'windows-latest' }}
shell: bash
run: |
cargo insta test --all
cargo insta test --all --all-features
git diff --exit-code
- run: cargo test --package ruff_cli --test black_compatibility_test -- --ignored
# Check for broken links in the documentation.

7
Cargo.lock generated
View File

@ -126,6 +126,12 @@ dependencies = [
"serde",
]
[[package]]
name = "bisection"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "021e079a1bab0ecce6cf4b4b74c0c37afa4a697136eb3b127875c84a8f04a8c3"
[[package]]
name = "bit-set"
version = "0.5.3"
@ -1887,6 +1893,7 @@ name = "ruff"
version = "0.0.241"
dependencies = [
"anyhow",
"bisection",
"bitflags",
"cfg-if",
"chrono",

View File

@ -25,6 +25,7 @@ doctest = false
[dependencies]
anyhow = { version = "1.0.66" }
bisection = { version = "0.1.0" }
bitflags = { version = "1.3.2" }
cfg-if = { version = "1.0.0" }
chrono = { version = "0.4.21", default-features = false, features = ["clock"] }
@ -63,6 +64,10 @@ thiserror = { version = "1.0" }
titlecase = { version = "2.2.1" }
toml = { version = "0.6.0" }
[features]
default = []
logical_lines = []
# https://docs.rs/getrandom/0.2.7/getrandom/#webassembly-support
# For (future) wasm-pack support
[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]

View File

@ -0,0 +1,42 @@
#: E111
if x > 2:
print(x)
#: E111 E117
if True:
print()
#: E112
if False:
print()
#: E113
print()
print()
#: E114 E116
mimetype = 'application/x-directory'
# 'httpd/unix-directory'
create_date = False
#: E116 E116 E116
def start(self):
if True:
self.master.start()
# try:
# self.master.start()
# except MasterExit:
# self.shutdown()
# finally:
# sys.exit()
#: E115 E115 E115 E115 E115 E115
def start(self):
if True:
# try:
# self.master.start()
# except MasterExit:
# self.shutdown()
# finally:
# sys.exit()
self.master.start()
#: E117
def start():
print()
#: E117 W191
def start():
print()

View File

@ -0,0 +1,78 @@
#: E201:1:6
spam( ham[1], {eggs: 2})
#: E201:1:10
spam(ham[ 1], {eggs: 2})
#: E201:1:15
spam(ham[1], { eggs: 2})
#: E201:1:6
spam( ham[1], {eggs: 2})
#: E201:1:10
spam(ham[ 1], {eggs: 2})
#: E201:1:15
spam(ham[1], { eggs: 2})
#: Okay
spam(ham[1], {eggs: 2})
#:
#: E202:1:23
spam(ham[1], {eggs: 2} )
#: E202:1:22
spam(ham[1], {eggs: 2 })
#: E202:1:11
spam(ham[1 ], {eggs: 2})
#: E202:1:23
spam(ham[1], {eggs: 2} )
#: E202:1:22
spam(ham[1], {eggs: 2 })
#: E202:1:11
spam(ham[1 ], {eggs: 2})
#: Okay
spam(ham[1], {eggs: 2})
result = func(
arg1='some value',
arg2='another value',
)
result = func(
arg1='some value',
arg2='another value'
)
result = [
item for item in items
if item > 5
]
#:
#: E203:1:10
if x == 4 :
print x, y
x, y = y, x
#: E203:1:10
if x == 4 :
print x, y
x, y = y, x
#: E203:2:15 E702:2:16
if x == 4:
print x, y ; x, y = y, x
#: E203:2:15 E702:2:16
if x == 4:
print x, y ; x, y = y, x
#: E203:3:13
if x == 4:
print x, y
x, y = y , x
#: E203:3:13
if x == 4:
print x, y
x, y = y , x
#: Okay
if x == 4:
print x, y
x, y = y, x
a[b1, :] == a[b1, ...]
b = a[:, b1]
#:

View File

@ -0,0 +1,171 @@
#: E221
a = 12 + 3
b = 4 + 5
#: E221 E221
x = 1
y = 2
long_variable = 3
#: E221 E221
x[0] = 1
x[1] = 2
long_variable = 3
#: E221 E221
x = f(x) + 1
y = long_variable + 2
z = x[0] + 3
#: E221:3:14
text = """
bar
foo %s""" % rofl
#: Okay
x = 1
y = 2
long_variable = 3
#:
#: E222
a = a + 1
b = b + 10
#: E222 E222
x = -1
y = -2
long_variable = 3
#: E222 E222
x[0] = 1
x[1] = 2
long_variable = 3
#:
#: E223
foobart = 4
a = 3 # aligned with tab
#:
#: E224
a += 1
b += 1000
#:
#: E225
submitted +=1
#: E225
submitted+= 1
#: E225
c =-1
#: E225
x = x /2 - 1
#: E225
c = alpha -4
#: E225
c = alpha- 4
#: E225
z = x **y
#: E225
z = (x + 1) **y
#: E225
z = (x + 1)** y
#: E225
_1kB = _1MB >>10
#: E225
_1kB = _1MB>> 10
#: E225 E225
i=i+ 1
#: E225 E225
i=i +1
#: E225
i = 1and 1
#: E225
i = 1or 0
#: E225
1is 1
#: E225
1in []
#: E225
i = 1 @2
#: E225
i = 1@ 2
#: E225 E226
i=i+1
#: E225 E226
i =i+1
#: E225 E226
i= i+1
#: E225 E226
c = (a +b)*(a - b)
#: E225 E226
c = (a+ b)*(a - b)
#:
#: E226
z = 2//30
#: E226 E226
c = (a+b) * (a-b)
#: E226
norman = True+False
#: E226
x = x*2 - 1
#: E226
x = x/2 - 1
#: E226 E226
hypot2 = x*x + y*y
#: E226
c = (a + b)*(a - b)
#: E226
def halves(n):
return (i//2 for i in range(n))
#: E227
_1kB = _1MB>>10
#: E227
_1MB = _1kB<<10
#: E227
a = b|c
#: E227
b = c&a
#: E227
c = b^a
#: E228
a = b%c
#: E228
msg = fmt%(errno, errmsg)
#: E228
msg = "Error %d occurred"%errno
#:
#: Okay
i = i + 1
submitted += 1
x = x * 2 - 1
hypot2 = x * x + y * y
c = (a + b) * (a - b)
_1MiB = 2 ** 20
_1TiB = 2**30
foo(bar, key='word', *args, **kwargs)
baz(**kwargs)
negative = -1
spam(-1)
-negative
func1(lambda *args, **kw: (args, kw))
func2(lambda a, b=h[:], c=0: (a, b, c))
if not -5 < x < +5:
print >>sys.stderr, "x is out of range."
print >> sys.stdout, "x is an integer."
x = x / 2 - 1
x = 1 @ 2
if alpha[:-i]:
*a, b = (1, 2, 3)
def squares(n):
return (i**2 for i in range(n))
ENG_PREFIXES = {
-6: "\u03bc", # Greek letter mu
-3: "m",
}
#:

View File

@ -11,5 +11,5 @@ doctest = false
once_cell = { version = "1.17.0" }
proc-macro2 = { version = "1.0.47" }
quote = { version = "1.0.21" }
syn = { version = "1.0.103", features = ["derive", "parsing"] }
syn = { version = "1.0.103", features = ["derive", "parsing", "extra-traits"] }
textwrap = { version = "0.16.0" }

View File

@ -0,0 +1,206 @@
use bisection::bisect_left;
use itertools::Itertools;
use rustpython_ast::Location;
use rustpython_parser::lexer::LexResult;
use crate::ast::types::Range;
use crate::registry::Diagnostic;
use crate::rules::pycodestyle::logical_lines::iter_logical_lines;
use crate::rules::pycodestyle::rules::{extraneous_whitespace, indentation, space_around_operator};
use crate::settings::Settings;
use crate::source_code::{Locator, Stylist};
/// Return the amount of indentation, expanding tabs to the next multiple of 8.
fn expand_indent(mut line: &str) -> usize {
while line.ends_with("\n\r") {
line = &line[..line.len() - 2];
}
if !line.contains('\t') {
return line.len() - line.trim_start().len();
}
let mut indent = 0;
for c in line.chars() {
if c == '\t' {
indent = (indent / 8) * 8 + 8;
} else if c == ' ' {
indent += 1;
} else {
break;
}
}
indent
}
pub fn check_logical_lines(
tokens: &[LexResult],
locator: &Locator,
stylist: &Stylist,
settings: &Settings,
) -> Vec<Diagnostic> {
let mut diagnostics = vec![];
let indent_char = stylist.indentation().as_char();
let mut prev_line = None;
let mut prev_indent_level = None;
for line in iter_logical_lines(tokens, locator) {
if line.mapping.is_empty() {
continue;
}
// Extract the indentation level.
let start_loc = line.mapping[0].1;
let start_line = locator
.slice_source_code_range(&Range::new(Location::new(start_loc.row(), 0), start_loc));
let indent_level = expand_indent(start_line);
let indent_size = 4;
// Generate mapping from logical to physical offsets.
let mapping_offsets = line.mapping.iter().map(|(offset, _)| *offset).collect_vec();
if line.operator {
for (index, kind) in space_around_operator(&line.text) {
let (token_offset, pos) = line.mapping[bisect_left(&mapping_offsets, &index)];
let location = Location::new(pos.row(), pos.column() + index - token_offset);
if settings.rules.enabled(kind.rule()) {
diagnostics.push(Diagnostic {
kind,
location,
end_location: location,
fix: None,
parent: None,
});
}
}
}
if line.bracket || line.punctuation {
for (index, kind) in extraneous_whitespace(&line.text) {
let (token_offset, pos) = line.mapping[bisect_left(&mapping_offsets, &index)];
let location = Location::new(pos.row(), pos.column() + index - token_offset);
if settings.rules.enabled(kind.rule()) {
diagnostics.push(Diagnostic {
kind,
location,
end_location: location,
fix: None,
parent: None,
});
}
}
}
for (index, kind) in indentation(
&line,
prev_line.as_ref(),
indent_char,
indent_level,
prev_indent_level,
indent_size,
) {
let (token_offset, pos) = line.mapping[bisect_left(&mapping_offsets, &index)];
let location = Location::new(pos.row(), pos.column() + index - token_offset);
if settings.rules.enabled(kind.rule()) {
diagnostics.push(Diagnostic {
kind,
location,
end_location: location,
fix: None,
parent: None,
});
}
}
if !line.is_comment() {
prev_line = Some(line);
prev_indent_level = Some(indent_level);
}
}
diagnostics
}
#[cfg(test)]
mod tests {
use rustpython_parser::lexer;
use rustpython_parser::lexer::LexResult;
use crate::checkers::logical_lines::iter_logical_lines;
use crate::source_code::Locator;
#[test]
fn split_logical_lines() {
let contents = r#"
x = 1
y = 2
z = x + 1"#;
let lxr: Vec<LexResult> = lexer::make_tokenizer(contents).collect();
let locator = Locator::new(contents);
let actual: Vec<String> = iter_logical_lines(&lxr, &locator)
.into_iter()
.map(|line| line.text)
.collect();
let expected = vec![
"x = 1".to_string(),
"y = 2".to_string(),
"z = x + 1".to_string(),
];
assert_eq!(actual, expected);
let contents = r#"
x = [
1,
2,
3,
]
y = 2
z = x + 1"#;
let lxr: Vec<LexResult> = lexer::make_tokenizer(contents).collect();
let locator = Locator::new(contents);
let actual: Vec<String> = iter_logical_lines(&lxr, &locator)
.into_iter()
.map(|line| line.text)
.collect();
let expected = vec![
"x = [1, 2, 3, ]".to_string(),
"y = 2".to_string(),
"z = x + 1".to_string(),
];
assert_eq!(actual, expected);
let contents = "x = 'abc'";
let lxr: Vec<LexResult> = lexer::make_tokenizer(contents).collect();
let locator = Locator::new(contents);
let actual: Vec<String> = iter_logical_lines(&lxr, &locator)
.into_iter()
.map(|line| line.text)
.collect();
let expected = vec!["x = \"xxx\"".to_string()];
assert_eq!(actual, expected);
let contents = r#"
def f():
x = 1
f()"#;
let lxr: Vec<LexResult> = lexer::make_tokenizer(contents).collect();
let locator = Locator::new(contents);
let actual: Vec<String> = iter_logical_lines(&lxr, &locator)
.into_iter()
.map(|line| line.text)
.collect();
let expected = vec!["def f():", "x = 1", "f()"];
assert_eq!(actual, expected);
let contents = r#"
def f():
"""Docstring goes here."""
# Comment goes here.
x = 1
f()"#;
let lxr: Vec<LexResult> = lexer::make_tokenizer(contents).collect();
let locator = Locator::new(contents);
let actual: Vec<String> = iter_logical_lines(&lxr, &locator)
.into_iter()
.map(|line| line.text)
.collect();
let expected = vec!["def f():", "\"xxx\"", "", "x = 1", "f()"];
assert_eq!(actual, expected);
}
}

View File

@ -1,6 +1,7 @@
pub mod ast;
pub mod filesystem;
pub mod imports;
pub mod lines;
pub mod logical_lines;
pub mod noqa;
pub mod physical_lines;
pub mod tokens;

View File

@ -1,4 +1,4 @@
//! Lint rules based on checking raw physical lines.
//! Lint rules based on checking physical lines.
use std::path::Path;
@ -15,7 +15,7 @@ use crate::rules::pyupgrade::rules::unnecessary_coding_comment;
use crate::settings::{flags, Settings};
use crate::source_code::Stylist;
pub fn check_lines(
pub fn check_physical_lines(
path: &Path,
stylist: &Stylist,
contents: &str,
@ -164,7 +164,7 @@ mod tests {
use std::path::Path;
use super::check_lines;
use super::check_physical_lines;
use crate::registry::Rule;
use crate::settings::{flags, Settings};
use crate::source_code::{Locator, Stylist};
@ -176,7 +176,7 @@ mod tests {
let stylist = Stylist::from_contents(line, &locator);
let check_with_max_line_length = |line_length: usize| {
check_lines(
check_physical_lines(
Path::new("foo.py"),
&stylist,
line,

View File

@ -10,8 +10,9 @@ use crate::autofix::fix_file;
use crate::checkers::ast::check_ast;
use crate::checkers::filesystem::check_file_path;
use crate::checkers::imports::check_imports;
use crate::checkers::lines::check_lines;
use crate::checkers::logical_lines::check_logical_lines;
use crate::checkers::noqa::check_noqa;
use crate::checkers::physical_lines::check_physical_lines;
use crate::checkers::tokens::check_tokens;
use crate::directives::Directives;
use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens};
@ -89,6 +90,15 @@ pub fn check_path(
diagnostics.extend(check_file_path(path, package, settings));
}
// Run the logical line-based rules.
if settings
.rules
.iter_enabled()
.any(|rule_code| matches!(rule_code.lint_source(), LintSource::LogicalLines))
{
diagnostics.extend(check_logical_lines(&tokens, locator, stylist, settings));
}
// Run the AST-based rules.
let use_ast = settings
.rules
@ -152,9 +162,9 @@ pub fn check_path(
if settings
.rules
.iter_enabled()
.any(|rule_code| matches!(rule_code.lint_source(), LintSource::Lines))
.any(|rule_code| matches!(rule_code.lint_source(), LintSource::PhysicalLines))
{
diagnostics.extend(check_lines(
diagnostics.extend(check_physical_lines(
path,
stylist,
contents,

View File

@ -13,6 +13,34 @@ use crate::violation::Violation;
ruff_macros::define_rule_mapping!(
// pycodestyle errors
E101 => rules::pycodestyle::rules::MixedSpacesAndTabs,
#[cfg(feature = "logical_lines")]
E111 => rules::pycodestyle::rules::IndentationWithInvalidMultiple,
#[cfg(feature = "logical_lines")]
E112 => rules::pycodestyle::rules::NoIndentedBlock,
#[cfg(feature = "logical_lines")]
E113 => rules::pycodestyle::rules::UnexpectedIndentation,
#[cfg(feature = "logical_lines")]
E114 => rules::pycodestyle::rules::IndentationWithInvalidMultipleComment,
#[cfg(feature = "logical_lines")]
E115 => rules::pycodestyle::rules::NoIndentedBlockComment,
#[cfg(feature = "logical_lines")]
E116 => rules::pycodestyle::rules::UnexpectedIndentationComment,
#[cfg(feature = "logical_lines")]
E117 => rules::pycodestyle::rules::OverIndented,
#[cfg(feature = "logical_lines")]
E201 => rules::pycodestyle::rules::WhitespaceAfterOpenBracket,
#[cfg(feature = "logical_lines")]
E202 => rules::pycodestyle::rules::WhitespaceBeforeCloseBracket,
#[cfg(feature = "logical_lines")]
E203 => rules::pycodestyle::rules::WhitespaceBeforePunctuation,
#[cfg(feature = "logical_lines")]
E221 => rules::pycodestyle::rules::MultipleSpacesBeforeOperator,
#[cfg(feature = "logical_lines")]
E222 => rules::pycodestyle::rules::MultipleSpacesAfterOperator,
#[cfg(feature = "logical_lines")]
E223 => rules::pycodestyle::rules::TabBeforeOperator,
#[cfg(feature = "logical_lines")]
E224 => rules::pycodestyle::rules::TabAfterOperator,
E401 => rules::pycodestyle::rules::MultipleImportsOnOneLine,
E402 => rules::pycodestyle::rules::ModuleImportNotAtTopOfFile,
E501 => rules::pycodestyle::rules::LineTooLong,
@ -671,7 +699,8 @@ impl Linter {
pub enum LintSource {
Ast,
Io,
Lines,
PhysicalLines,
LogicalLines,
Tokens,
Imports,
NoQa,
@ -695,7 +724,7 @@ impl Rule {
| Rule::ShebangNotExecutable
| Rule::ShebangNewline
| Rule::ShebangPython
| Rule::ShebangWhitespace => &LintSource::Lines,
| Rule::ShebangWhitespace => &LintSource::PhysicalLines,
Rule::AmbiguousUnicodeCharacterComment
| Rule::AmbiguousUnicodeCharacterDocstring
| Rule::AmbiguousUnicodeCharacterString
@ -714,6 +743,21 @@ impl Rule {
Rule::IOError => &LintSource::Io,
Rule::UnsortedImports | Rule::MissingRequiredImport => &LintSource::Imports,
Rule::ImplicitNamespacePackage => &LintSource::Filesystem,
#[cfg(feature = "logical_lines")]
Rule::IndentationWithInvalidMultiple
| Rule::IndentationWithInvalidMultipleComment
| Rule::MultipleSpacesAfterOperator
| Rule::MultipleSpacesBeforeOperator
| Rule::NoIndentedBlock
| Rule::NoIndentedBlockComment
| Rule::OverIndented
| Rule::TabAfterOperator
| Rule::TabBeforeOperator
| Rule::UnexpectedIndentation
| Rule::UnexpectedIndentationComment
| Rule::WhitespaceAfterOpenBracket
| Rule::WhitespaceBeforeCloseBracket
| Rule::WhitespaceBeforePunctuation => &LintSource::LogicalLines,
_ => &LintSource::Ast,
}
}

View File

@ -0,0 +1,171 @@
use rustpython_ast::Location;
use rustpython_parser::lexer::{LexResult, Tok};
use crate::ast::types::Range;
use crate::source_code::Locator;
#[derive(Debug)]
pub struct LogicalLine {
pub text: String,
pub mapping: Vec<(usize, Location)>,
/// Whether the logical line contains an operator.
pub operator: bool,
/// Whether the logical line contains a comment.
pub bracket: bool,
/// Whether the logical line contains a punctuation mark.
pub punctuation: bool,
}
impl LogicalLine {
pub fn is_comment(&self) -> bool {
self.text.is_empty()
}
}
fn build_line(tokens: &[(Location, &Tok, Location)], locator: &Locator) -> LogicalLine {
let mut logical = String::with_capacity(88);
let mut operator = false;
let mut bracket = false;
let mut punctuation = false;
let mut mapping = Vec::new();
let mut prev: Option<&Location> = None;
let mut length = 0;
for (start, tok, end) in tokens {
if matches!(
tok,
Tok::Newline | Tok::NonLogicalNewline | Tok::Indent | Tok::Dedent
) {
continue;
}
if mapping.is_empty() {
mapping.push((0, *start));
}
if matches!(tok, Tok::Comment { .. }) {
continue;
}
if !operator {
operator |= matches!(
tok,
Tok::Amper
| Tok::AmperEqual
| Tok::CircumFlex
| Tok::CircumflexEqual
| Tok::Colon
| Tok::ColonEqual
| Tok::DoubleSlash
| Tok::DoubleSlashEqual
| Tok::DoubleStar
| Tok::Equal
| Tok::Greater
| Tok::GreaterEqual
| Tok::Less
| Tok::LessEqual
| Tok::Minus
| Tok::MinusEqual
| Tok::NotEqual
| Tok::Percent
| Tok::PercentEqual
| Tok::Plus
| Tok::PlusEqual
| Tok::Slash
| Tok::SlashEqual
| Tok::Star
| Tok::StarEqual
| Tok::Vbar
| Tok::VbarEqual
);
}
if !bracket {
bracket |= matches!(
tok,
Tok::Lpar | Tok::Lsqb | Tok::Lbrace | Tok::Rpar | Tok::Rsqb | Tok::Rbrace
);
}
if !punctuation {
punctuation |= matches!(tok, Tok::Comma | Tok::Semi | Tok::Colon);
}
// TODO(charlie): "Mute" strings.
let text = if let Tok::String { .. } = tok {
"\"xxx\""
} else {
locator.slice_source_code_range(&Range {
location: *start,
end_location: *end,
})
};
if let Some(prev) = prev {
if prev.row() != start.row() {
let prev_text = locator.slice_source_code_range(&Range {
location: Location::new(prev.row(), prev.column() - 1),
end_location: Location::new(prev.row(), prev.column()),
});
if prev_text == ","
|| ((prev_text != "{" && prev_text != "[" && prev_text != "(")
&& (text != "}" && text != "]" && text != ")"))
{
logical.push(' ');
length += 1;
}
} else if prev.column() != start.column() {
let prev_text = locator.slice_source_code_range(&Range {
location: *prev,
end_location: *start,
});
logical.push_str(prev_text);
length += prev_text.len();
}
}
logical.push_str(text);
length += text.len();
mapping.push((length, *end));
prev = Some(end);
}
LogicalLine {
text: logical,
operator,
bracket,
punctuation,
mapping,
}
}
pub fn iter_logical_lines(tokens: &[LexResult], locator: &Locator) -> Vec<LogicalLine> {
let mut parens = 0;
let mut accumulator = Vec::with_capacity(32);
let mut lines = Vec::with_capacity(128);
for &(start, ref tok, end) in tokens.iter().flatten() {
accumulator.push((start, tok, end));
if matches!(tok, Tok::Lbrace | Tok::Lpar | Tok::Lsqb) {
parens += 1;
} else if matches!(tok, Tok::Rbrace | Tok::Rpar | Tok::Rsqb) {
parens -= 1;
} else if parens == 0 {
if matches!(
tok,
Tok::Newline | Tok::NonLogicalNewline | Tok::Comment(..)
) {
if matches!(tok, Tok::Newline) {
lines.push(build_line(&accumulator, locator));
accumulator.drain(..);
} else if tokens.len() == 1 {
accumulator.remove(0);
} else {
lines.push(build_line(&accumulator, locator));
accumulator.drain(..);
}
}
}
}
if !accumulator.is_empty() {
lines.push(build_line(&accumulator, locator));
}
lines
}

View File

@ -2,7 +2,8 @@
pub(crate) mod rules;
pub mod settings;
pub mod helpers;
pub(crate) mod helpers;
pub(crate) mod logical_lines;
#[cfg(test)]
mod tests {
@ -49,6 +50,31 @@ mod tests {
Ok(())
}
#[cfg(feature = "logical_lines")]
#[test_case(Rule::IndentationWithInvalidMultiple, Path::new("E11.py"))]
#[test_case(Rule::IndentationWithInvalidMultipleComment, Path::new("E11.py"))]
#[test_case(Rule::MultipleSpacesAfterOperator, Path::new("E22.py"))]
#[test_case(Rule::MultipleSpacesBeforeOperator, Path::new("E22.py"))]
#[test_case(Rule::NoIndentedBlock, Path::new("E11.py"))]
#[test_case(Rule::NoIndentedBlockComment, Path::new("E11.py"))]
#[test_case(Rule::OverIndented, Path::new("E11.py"))]
#[test_case(Rule::TabAfterOperator, Path::new("E22.py"))]
#[test_case(Rule::TabBeforeOperator, Path::new("E22.py"))]
#[test_case(Rule::UnexpectedIndentation, Path::new("E11.py"))]
#[test_case(Rule::UnexpectedIndentationComment, Path::new("E11.py"))]
#[test_case(Rule::WhitespaceAfterOpenBracket, Path::new("E20.py"))]
#[test_case(Rule::WhitespaceBeforeCloseBracket, Path::new("E20.py"))]
#[test_case(Rule::WhitespaceBeforePunctuation, Path::new("E20.py"))]
fn logical(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("pycodestyle").join(path).as_path(),
&settings::Settings::for_rule(rule_code),
)?;
assert_yaml_snapshot!(snapshot, diagnostics);
Ok(())
}
#[test]
fn constant_literals() -> Result<()> {
let diagnostics = test_path(

View File

@ -0,0 +1,71 @@
#![allow(dead_code)]
use once_cell::sync::Lazy;
use regex::Regex;
use ruff_macros::derive_message_formats;
use crate::define_violation;
use crate::registry::DiagnosticKind;
use crate::violation::Violation;
define_violation!(
pub struct WhitespaceAfterOpenBracket;
);
impl Violation for WhitespaceAfterOpenBracket {
#[derive_message_formats]
fn message(&self) -> String {
format!("Whitespace after '('")
}
}
define_violation!(
pub struct WhitespaceBeforeCloseBracket;
);
impl Violation for WhitespaceBeforeCloseBracket {
#[derive_message_formats]
fn message(&self) -> String {
format!("Whitespace before ')'")
}
}
define_violation!(
pub struct WhitespaceBeforePunctuation;
);
impl Violation for WhitespaceBeforePunctuation {
#[derive_message_formats]
fn message(&self) -> String {
format!("Whitespace before ',', ';', or ':'")
}
}
// TODO(charlie): Pycodestyle has a negative lookahead on the end.
static EXTRANEOUS_WHITESPACE_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"([\[({][ \t]|[ \t][]}),;:])").unwrap());
/// E201, E202, E203
#[cfg(feature = "logical_lines")]
pub fn extraneous_whitespace(line: &str) -> Vec<(usize, DiagnosticKind)> {
let mut diagnostics = vec![];
for line_match in EXTRANEOUS_WHITESPACE_REGEX.captures_iter(line) {
let match_ = line_match.get(1).unwrap();
let text = match_.as_str();
let char = text.trim();
let found = match_.start();
if text.chars().last().unwrap().is_ascii_whitespace() {
diagnostics.push((found + 1, WhitespaceAfterOpenBracket.into()));
} else if line.chars().nth(found - 1).map_or(false, |c| c != ',') {
if char == "}" || char == "]" || char == ")" {
diagnostics.push((found, WhitespaceBeforeCloseBracket.into()));
} else {
diagnostics.push((found, WhitespaceBeforePunctuation.into()));
}
}
}
diagnostics
}
#[cfg(not(feature = "logical_lines"))]
pub fn extraneous_whitespace(_line: &str) -> Vec<(usize, DiagnosticKind)> {
vec![]
}

View File

@ -0,0 +1,150 @@
#![allow(dead_code)]
use crate::define_violation;
use crate::registry::DiagnosticKind;
use crate::rules::pycodestyle::logical_lines::LogicalLine;
use crate::violation::Violation;
use ruff_macros::derive_message_formats;
define_violation!(
pub struct IndentationWithInvalidMultiple {
pub indent_size: usize,
}
);
impl Violation for IndentationWithInvalidMultiple {
#[derive_message_formats]
fn message(&self) -> String {
let Self { indent_size } = self;
format!("Indentation is not a multiple of {indent_size}")
}
}
define_violation!(
pub struct IndentationWithInvalidMultipleComment {
pub indent_size: usize,
}
);
impl Violation for IndentationWithInvalidMultipleComment {
#[derive_message_formats]
fn message(&self) -> String {
let Self { indent_size } = self;
format!("Indentation is not a multiple of {indent_size} (comment)")
}
}
define_violation!(
pub struct NoIndentedBlock;
);
impl Violation for NoIndentedBlock {
#[derive_message_formats]
fn message(&self) -> String {
format!("Expected an indented block")
}
}
define_violation!(
pub struct NoIndentedBlockComment;
);
impl Violation for NoIndentedBlockComment {
#[derive_message_formats]
fn message(&self) -> String {
format!("Expected an indented block (comment)")
}
}
define_violation!(
pub struct UnexpectedIndentation;
);
impl Violation for UnexpectedIndentation {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unexpected indentation")
}
}
define_violation!(
pub struct UnexpectedIndentationComment;
);
impl Violation for UnexpectedIndentationComment {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unexpected indentation (comment)")
}
}
define_violation!(
pub struct OverIndented;
);
impl Violation for OverIndented {
#[derive_message_formats]
fn message(&self) -> String {
format!("Over-indented")
}
}
/// E111
#[cfg(feature = "logical_lines")]
pub fn indentation(
logical_line: &LogicalLine,
prev_logical_line: Option<&LogicalLine>,
indent_char: char,
indent_level: usize,
prev_indent_level: Option<usize>,
indent_size: usize,
) -> Vec<(usize, DiagnosticKind)> {
let mut diagnostics = vec![];
if indent_level % indent_size != 0 {
diagnostics.push((
0,
if logical_line.is_comment() {
IndentationWithInvalidMultipleComment { indent_size }.into()
} else {
IndentationWithInvalidMultiple { indent_size }.into()
},
));
}
let indent_expect = prev_logical_line.map_or(false, |prev_logical_line| {
prev_logical_line.text.ends_with(':')
});
if indent_expect && indent_level <= prev_indent_level.unwrap_or(0) {
diagnostics.push((
0,
if logical_line.is_comment() {
NoIndentedBlockComment.into()
} else {
NoIndentedBlock.into()
},
));
} else if !indent_expect
&& prev_indent_level.map_or(false, |prev_indent_level| indent_level > prev_indent_level)
{
diagnostics.push((
0,
if logical_line.is_comment() {
UnexpectedIndentationComment.into()
} else {
UnexpectedIndentation.into()
},
));
}
if indent_expect {
let expected_indent_amount = if indent_char == '\t' { 8 } else { 4 };
let expected_indent_level = prev_indent_level.unwrap_or(0) + expected_indent_amount;
if indent_level > expected_indent_level {
diagnostics.push((0, OverIndented.into()));
}
}
diagnostics
}
#[cfg(not(feature = "logical_lines"))]
pub fn indentation(
_logical_line: &LogicalLine,
_prev_logical_line: Option<&LogicalLine>,
_indent_char: char,
_indent_level: usize,
_prev_indent_level: Option<usize>,
_indent_size: usize,
) -> Vec<(usize, DiagnosticKind)> {
vec![]
}

View File

@ -5,16 +5,29 @@ pub use do_not_assign_lambda::{do_not_assign_lambda, DoNotAssignLambda};
pub use do_not_use_bare_except::{do_not_use_bare_except, DoNotUseBareExcept};
pub use doc_line_too_long::{doc_line_too_long, DocLineTooLong};
pub use errors::{syntax_error, IOError, SyntaxError};
pub use extraneous_whitespace::{
extraneous_whitespace, WhitespaceAfterOpenBracket, WhitespaceBeforeCloseBracket,
WhitespaceBeforePunctuation,
};
pub use imports::{
module_import_not_at_top_of_file, multiple_imports_on_one_line, ModuleImportNotAtTopOfFile,
MultipleImportsOnOneLine,
};
pub use indentation::{
indentation, IndentationWithInvalidMultiple, IndentationWithInvalidMultipleComment,
NoIndentedBlock, NoIndentedBlockComment, OverIndented, UnexpectedIndentation,
UnexpectedIndentationComment,
};
pub use invalid_escape_sequence::{invalid_escape_sequence, InvalidEscapeSequence};
pub use line_too_long::{line_too_long, LineTooLong};
pub use literal_comparisons::{literal_comparisons, NoneComparison, TrueFalseComparison};
pub use mixed_spaces_and_tabs::{mixed_spaces_and_tabs, MixedSpacesAndTabs};
pub use no_newline_at_end_of_file::{no_newline_at_end_of_file, NoNewLineAtEndOfFile};
pub use not_tests::{not_tests, NotInTest, NotIsTest};
pub use space_around_operator::{
space_around_operator, MultipleSpacesAfterOperator, MultipleSpacesBeforeOperator,
TabAfterOperator, TabBeforeOperator,
};
pub use type_comparison::{type_comparison, TypeComparison};
mod ambiguous_class_name;
@ -24,11 +37,14 @@ mod do_not_assign_lambda;
mod do_not_use_bare_except;
mod doc_line_too_long;
mod errors;
mod extraneous_whitespace;
mod imports;
mod indentation;
mod invalid_escape_sequence;
mod line_too_long;
mod literal_comparisons;
mod mixed_spaces_and_tabs;
mod no_newline_at_end_of_file;
mod not_tests;
mod space_around_operator;
mod type_comparison;

View File

@ -0,0 +1,81 @@
#![allow(dead_code)]
use once_cell::sync::Lazy;
use regex::Regex;
use ruff_macros::derive_message_formats;
use crate::define_violation;
use crate::registry::DiagnosticKind;
use crate::violation::Violation;
define_violation!(
pub struct TabBeforeOperator;
);
impl Violation for TabBeforeOperator {
#[derive_message_formats]
fn message(&self) -> String {
format!("Tab before operator")
}
}
define_violation!(
pub struct MultipleSpacesBeforeOperator;
);
impl Violation for MultipleSpacesBeforeOperator {
#[derive_message_formats]
fn message(&self) -> String {
format!("Multiple spaces before operator")
}
}
define_violation!(
pub struct TabAfterOperator;
);
impl Violation for TabAfterOperator {
#[derive_message_formats]
fn message(&self) -> String {
format!("Tab after operator")
}
}
define_violation!(
pub struct MultipleSpacesAfterOperator;
);
impl Violation for MultipleSpacesAfterOperator {
#[derive_message_formats]
fn message(&self) -> String {
format!("Multiple spaces after operator")
}
}
static OPERATOR_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"[^,\s](\s*)(?:[-+*/|!<=>%&^]+|:=)(\s*)").unwrap());
/// E221, E222, E223, E224
#[cfg(feature = "logical_lines")]
pub fn space_around_operator(line: &str) -> Vec<(usize, DiagnosticKind)> {
let mut diagnostics = vec![];
for line_match in OPERATOR_REGEX.captures_iter(line) {
let before = line_match.get(1).unwrap();
let after = line_match.get(2).unwrap();
if before.as_str().contains('\t') {
diagnostics.push((before.start(), TabBeforeOperator.into()));
} else if before.as_str().len() > 1 {
diagnostics.push((before.start(), MultipleSpacesBeforeOperator.into()));
}
if after.as_str().contains('\t') {
diagnostics.push((after.start(), TabAfterOperator.into()));
} else if after.as_str().len() > 1 {
diagnostics.push((after.start(), MultipleSpacesAfterOperator.into()));
}
}
diagnostics
}
#[cfg(not(feature = "logical_lines"))]
pub fn space_around_operator(_line: &str) -> Vec<(usize, DiagnosticKind)> {
vec![]
}

View File

@ -0,0 +1,27 @@
---
source: src/rules/pycodestyle/mod.rs
expression: diagnostics
---
- kind:
IndentationWithInvalidMultiple:
indent_size: 4
location:
row: 3
column: 2
end_location:
row: 3
column: 2
fix: ~
parent: ~
- kind:
IndentationWithInvalidMultiple:
indent_size: 4
location:
row: 6
column: 5
end_location:
row: 6
column: 5
fix: ~
parent: ~

View File

@ -0,0 +1,15 @@
---
source: src/rules/pycodestyle/mod.rs
expression: diagnostics
---
- kind:
NoIndentedBlock: ~
location:
row: 9
column: 0
end_location:
row: 9
column: 0
fix: ~
parent: ~

View File

@ -0,0 +1,15 @@
---
source: src/rules/pycodestyle/mod.rs
expression: diagnostics
---
- kind:
UnexpectedIndentation: ~
location:
row: 12
column: 4
end_location:
row: 12
column: 4
fix: ~
parent: ~

View File

@ -0,0 +1,16 @@
---
source: src/rules/pycodestyle/mod.rs
expression: diagnostics
---
- kind:
IndentationWithInvalidMultipleComment:
indent_size: 4
location:
row: 15
column: 5
end_location:
row: 15
column: 5
fix: ~
parent: ~

View File

@ -0,0 +1,65 @@
---
source: src/rules/pycodestyle/mod.rs
expression: diagnostics
---
- kind:
NoIndentedBlockComment: ~
location:
row: 30
column: 0
end_location:
row: 30
column: 0
fix: ~
parent: ~
- kind:
NoIndentedBlockComment: ~
location:
row: 31
column: 0
end_location:
row: 31
column: 0
fix: ~
parent: ~
- kind:
NoIndentedBlockComment: ~
location:
row: 32
column: 0
end_location:
row: 32
column: 0
fix: ~
parent: ~
- kind:
NoIndentedBlockComment: ~
location:
row: 33
column: 0
end_location:
row: 33
column: 0
fix: ~
parent: ~
- kind:
NoIndentedBlockComment: ~
location:
row: 34
column: 0
end_location:
row: 34
column: 0
fix: ~
parent: ~
- kind:
NoIndentedBlockComment: ~
location:
row: 35
column: 0
end_location:
row: 35
column: 0
fix: ~
parent: ~

View File

@ -0,0 +1,45 @@
---
source: src/rules/pycodestyle/mod.rs
expression: diagnostics
---
- kind:
UnexpectedIndentationComment: ~
location:
row: 15
column: 5
end_location:
row: 15
column: 5
fix: ~
parent: ~
- kind:
UnexpectedIndentationComment: ~
location:
row: 22
column: 12
end_location:
row: 22
column: 12
fix: ~
parent: ~
- kind:
UnexpectedIndentationComment: ~
location:
row: 24
column: 12
end_location:
row: 24
column: 12
fix: ~
parent: ~
- kind:
UnexpectedIndentationComment: ~
location:
row: 26
column: 12
end_location:
row: 26
column: 12
fix: ~
parent: ~

View File

@ -0,0 +1,35 @@
---
source: src/rules/pycodestyle/mod.rs
expression: diagnostics
---
- kind:
OverIndented: ~
location:
row: 6
column: 5
end_location:
row: 6
column: 5
fix: ~
parent: ~
- kind:
OverIndented: ~
location:
row: 39
column: 8
end_location:
row: 39
column: 8
fix: ~
parent: ~
- kind:
OverIndented: ~
location:
row: 42
column: 2
end_location:
row: 42
column: 2
fix: ~
parent: ~

View File

@ -0,0 +1,65 @@
---
source: src/rules/pycodestyle/mod.rs
expression: diagnostics
---
- kind:
WhitespaceAfterOpenBracket: ~
location:
row: 2
column: 5
end_location:
row: 2
column: 5
fix: ~
parent: ~
- kind:
WhitespaceAfterOpenBracket: ~
location:
row: 4
column: 9
end_location:
row: 4
column: 9
fix: ~
parent: ~
- kind:
WhitespaceAfterOpenBracket: ~
location:
row: 6
column: 14
end_location:
row: 6
column: 14
fix: ~
parent: ~
- kind:
WhitespaceAfterOpenBracket: ~
location:
row: 8
column: 5
end_location:
row: 8
column: 5
fix: ~
parent: ~
- kind:
WhitespaceAfterOpenBracket: ~
location:
row: 10
column: 9
end_location:
row: 10
column: 9
fix: ~
parent: ~
- kind:
WhitespaceAfterOpenBracket: ~
location:
row: 12
column: 14
end_location:
row: 12
column: 14
fix: ~
parent: ~

View File

@ -0,0 +1,65 @@
---
source: src/rules/pycodestyle/mod.rs
expression: diagnostics
---
- kind:
WhitespaceBeforeCloseBracket: ~
location:
row: 19
column: 22
end_location:
row: 19
column: 22
fix: ~
parent: ~
- kind:
WhitespaceBeforeCloseBracket: ~
location:
row: 21
column: 21
end_location:
row: 21
column: 21
fix: ~
parent: ~
- kind:
WhitespaceBeforeCloseBracket: ~
location:
row: 23
column: 10
end_location:
row: 23
column: 10
fix: ~
parent: ~
- kind:
WhitespaceBeforeCloseBracket: ~
location:
row: 25
column: 22
end_location:
row: 25
column: 22
fix: ~
parent: ~
- kind:
WhitespaceBeforeCloseBracket: ~
location:
row: 27
column: 21
end_location:
row: 27
column: 21
fix: ~
parent: ~
- kind:
WhitespaceBeforeCloseBracket: ~
location:
row: 29
column: 10
end_location:
row: 29
column: 10
fix: ~
parent: ~

View File

@ -0,0 +1,65 @@
---
source: src/rules/pycodestyle/mod.rs
expression: diagnostics
---
- kind:
WhitespaceBeforePunctuation: ~
location:
row: 51
column: 9
end_location:
row: 51
column: 9
fix: ~
parent: ~
- kind:
WhitespaceBeforePunctuation: ~
location:
row: 55
column: 9
end_location:
row: 55
column: 9
fix: ~
parent: ~
- kind:
WhitespaceBeforePunctuation: ~
location:
row: 60
column: 14
end_location:
row: 60
column: 14
fix: ~
parent: ~
- kind:
WhitespaceBeforePunctuation: ~
location:
row: 63
column: 14
end_location:
row: 63
column: 14
fix: ~
parent: ~
- kind:
WhitespaceBeforePunctuation: ~
location:
row: 67
column: 12
end_location:
row: 67
column: 12
fix: ~
parent: ~
- kind:
WhitespaceBeforePunctuation: ~
location:
row: 71
column: 12
end_location:
row: 71
column: 12
fix: ~
parent: ~

View File

@ -0,0 +1,85 @@
---
source: src/rules/pycodestyle/mod.rs
expression: diagnostics
---
- kind:
MultipleSpacesBeforeOperator: ~
location:
row: 3
column: 5
end_location:
row: 3
column: 5
fix: ~
parent: ~
- kind:
MultipleSpacesBeforeOperator: ~
location:
row: 5
column: 1
end_location:
row: 5
column: 1
fix: ~
parent: ~
- kind:
MultipleSpacesBeforeOperator: ~
location:
row: 6
column: 1
end_location:
row: 6
column: 1
fix: ~
parent: ~
- kind:
MultipleSpacesBeforeOperator: ~
location:
row: 9
column: 4
end_location:
row: 9
column: 4
fix: ~
parent: ~
- kind:
MultipleSpacesBeforeOperator: ~
location:
row: 10
column: 4
end_location:
row: 10
column: 4
fix: ~
parent: ~
- kind:
MultipleSpacesBeforeOperator: ~
location:
row: 13
column: 8
end_location:
row: 13
column: 8
fix: ~
parent: ~
- kind:
MultipleSpacesBeforeOperator: ~
location:
row: 15
column: 8
end_location:
row: 15
column: 8
fix: ~
parent: ~
- kind:
MultipleSpacesBeforeOperator: ~
location:
row: 19
column: 13
end_location:
row: 19
column: 13
fix: ~
parent: ~

View File

@ -0,0 +1,55 @@
---
source: src/rules/pycodestyle/mod.rs
expression: diagnostics
---
- kind:
MultipleSpacesAfterOperator: ~
location:
row: 28
column: 7
end_location:
row: 28
column: 7
fix: ~
parent: ~
- kind:
MultipleSpacesAfterOperator: ~
location:
row: 31
column: 3
end_location:
row: 31
column: 3
fix: ~
parent: ~
- kind:
MultipleSpacesAfterOperator: ~
location:
row: 32
column: 3
end_location:
row: 32
column: 3
fix: ~
parent: ~
- kind:
MultipleSpacesAfterOperator: ~
location:
row: 35
column: 6
end_location:
row: 35
column: 6
fix: ~
parent: ~
- kind:
MultipleSpacesAfterOperator: ~
location:
row: 36
column: 6
end_location:
row: 36
column: 6
fix: ~
parent: ~

View File

@ -0,0 +1,15 @@
---
source: src/rules/pycodestyle/mod.rs
expression: diagnostics
---
- kind:
TabBeforeOperator: ~
location:
row: 43
column: 1
end_location:
row: 43
column: 1
fix: ~
parent: ~

View File

@ -0,0 +1,15 @@
---
source: src/rules/pycodestyle/mod.rs
expression: diagnostics
---
- kind:
TabAfterOperator: ~
location:
row: 48
column: 4
end_location:
row: 48
column: 4
fix: ~
parent: ~

View File

@ -112,6 +112,10 @@ impl Indentation {
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn as_char(&self) -> char {
self.0.chars().next().unwrap()
}
}
impl Deref for Indentation {