Skip to content

Commit

Permalink
Implement whitespace-around-keywords (E271, E272, E273, E274)
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Feb 8, 2023
1 parent 8261d06 commit 3f4a72d
Show file tree
Hide file tree
Showing 11 changed files with 434 additions and 2 deletions.
58 changes: 58 additions & 0 deletions crates/ruff/resources/test/fixtures/pycodestyle/E27.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#: Okay
True and False
#: E271
True and False
#: E272
True and False
#: E271
if 1:
#: E273
True and False
#: E273 E274
True and False
#: E271
a and b
#: E271
1 and b
#: E271
a and 2
#: E271 E272
1 and b
#: E271 E272
a and 2
#: E272
this and False
#: E273
a and b
#: E274
a and b
#: E273 E274
this and False
#: Okay
from u import (a, b)
from v import c, d
#: E271
from w import (e, f)
#: E275
from w import(e, f)
#: E275
from importable.module import(e, f)
#: E275
try:
from importable.module import(e, f)
except ImportError:
pass
#: E275
if(foo):
pass
else:
pass
#: Okay
matched = {"true": True, "false": False}
#: E275:2:11
if True:
assert(1)
#: Okay
def f():
print((yield))
x = (yield)
19 changes: 18 additions & 1 deletion crates/ruff/src/checkers/logical_lines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ 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::rules::pycodestyle::rules::{
extraneous_whitespace, indentation, space_around_operator, whitespace_around_keywords,
};
use crate::settings::Settings;
use crate::source_code::{Locator, Stylist};

Expand Down Expand Up @@ -87,6 +89,21 @@ pub fn check_logical_lines(
}
}
}
if line.keyword {
for (index, kind) in whitespace_around_keywords(&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,
Expand Down
14 changes: 13 additions & 1 deletion crates/ruff/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ ruff_macros::define_rule_mapping!(
E223 => rules::pycodestyle::rules::TabBeforeOperator,
#[cfg(feature = "logical_lines")]
E224 => rules::pycodestyle::rules::TabAfterOperator,
#[cfg(feature = "logical_lines")]
E271 => rules::pycodestyle::rules::MultipleSpacesAfterKeyword,
#[cfg(feature = "logical_lines")]
E272 => rules::pycodestyle::rules::MultipleSpacesBeforeKeyword,
#[cfg(feature = "logical_lines")]
E273 => rules::pycodestyle::rules::TabAfterKeyword,
#[cfg(feature = "logical_lines")]
E274 => rules::pycodestyle::rules::TabBeforeKeyword,
E401 => rules::pycodestyle::rules::MultipleImportsOnOneLine,
E402 => rules::pycodestyle::rules::ModuleImportNotAtTopOfFile,
E501 => rules::pycodestyle::rules::LineTooLong,
Expand Down Expand Up @@ -760,7 +768,11 @@ impl Rule {
| Rule::UnexpectedIndentationComment
| Rule::WhitespaceAfterOpenBracket
| Rule::WhitespaceBeforeCloseBracket
| Rule::WhitespaceBeforePunctuation => &LintSource::LogicalLines,
| Rule::WhitespaceBeforePunctuation
| Rule::MultipleSpacesAfterKeyword
| Rule::MultipleSpacesBeforeKeyword
| Rule::TabAfterKeyword
| Rule::TabBeforeKeyword => &LintSource::LogicalLines,
_ => &LintSource::Ast,
}
}
Expand Down
45 changes: 45 additions & 0 deletions crates/ruff/src/rules/pycodestyle/logical_lines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ pub struct LogicalLine {
pub bracket: bool,
/// Whether the logical line contains a punctuation mark.
pub punctuation: bool,
/// Whether the logical line contains a keyword.
pub keyword: bool,
}

impl LogicalLine {
Expand All @@ -27,6 +29,7 @@ fn build_line(tokens: &[(Location, &Tok, Location)], locator: &Locator) -> Logic
let mut operator = false;
let mut bracket = false;
let mut punctuation = false;
let mut keyword = false;
let mut mapping = Vec::new();
let mut prev: Option<&Location> = None;
let mut length = 0;
Expand Down Expand Up @@ -90,6 +93,47 @@ fn build_line(tokens: &[(Location, &Tok, Location)], locator: &Locator) -> Logic
punctuation |= matches!(tok, Tok::Comma | Tok::Semi | Tok::Colon);
}

if !keyword {
keyword |= matches!(
tok,
Tok::False
| Tok::None
| Tok::True
| Tok::And
| Tok::As
| Tok::Assert
| Tok::Async
| Tok::Await
| Tok::Break
| Tok::Class
| Tok::Continue
| Tok::Def
| Tok::Del
| Tok::Elif
| Tok::Else
| Tok::Except
| Tok::Finally
| Tok::For
| Tok::From
| Tok::Global
| Tok::If
| Tok::Import
| Tok::In
| Tok::Is
| Tok::Lambda
| Tok::Nonlocal
| Tok::Not
| Tok::Or
| Tok::Pass
| Tok::Raise
| Tok::Return
| Tok::Try
| Tok::While
| Tok::With
| Tok::Yield
);
}

// TODO(charlie): "Mute" strings.
let text = if let Tok::String { .. } = tok {
"\"xxx\""
Expand Down Expand Up @@ -133,6 +177,7 @@ fn build_line(tokens: &[(Location, &Tok, Location)], locator: &Locator) -> Logic
operator,
bracket,
punctuation,
keyword,
mapping,
}
}
Expand Down
4 changes: 4 additions & 0 deletions crates/ruff/src/rules/pycodestyle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,16 @@ mod tests {
#[cfg(feature = "logical_lines")]
#[test_case(Rule::IndentationWithInvalidMultiple, Path::new("E11.py"))]
#[test_case(Rule::IndentationWithInvalidMultipleComment, Path::new("E11.py"))]
#[test_case(Rule::MultipleSpacesAfterKeyword, Path::new("E27.py"))]
#[test_case(Rule::MultipleSpacesAfterOperator, Path::new("E22.py"))]
#[test_case(Rule::MultipleSpacesBeforeKeyword, Path::new("E27.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::TabAfterKeyword, Path::new("E27.py"))]
#[test_case(Rule::TabAfterOperator, Path::new("E22.py"))]
#[test_case(Rule::TabBeforeKeyword, Path::new("E27.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"))]
Expand Down
5 changes: 5 additions & 0 deletions crates/ruff/src/rules/pycodestyle/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ pub use space_around_operator::{
TabAfterOperator, TabBeforeOperator,
};
pub use type_comparison::{type_comparison, TypeComparison};
pub use whitespace_around_keywords::{
whitespace_around_keywords, MultipleSpacesAfterKeyword, MultipleSpacesBeforeKeyword,
TabAfterKeyword, TabBeforeKeyword,
};

mod ambiguous_class_name;
mod ambiguous_function_name;
Expand All @@ -48,3 +52,4 @@ mod no_newline_at_end_of_file;
mod not_tests;
mod space_around_operator;
mod type_comparison;
mod whitespace_around_keywords;
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#![allow(dead_code)]

use once_cell::sync::Lazy;
use regex::Regex;

use ruff_macros::{define_violation, derive_message_formats};

use crate::registry::DiagnosticKind;
use crate::violation::Violation;

define_violation!(
pub struct MultipleSpacesAfterKeyword;
);
impl Violation for MultipleSpacesAfterKeyword {
#[derive_message_formats]
fn message(&self) -> String {
format!("Multiple spaces after keyword")
}
}

define_violation!(
pub struct MultipleSpacesBeforeKeyword;
);
impl Violation for MultipleSpacesBeforeKeyword {
#[derive_message_formats]
fn message(&self) -> String {
format!("Multiple spaces before keyword")
}
}

define_violation!(
pub struct TabAfterKeyword;
);
impl Violation for TabAfterKeyword {
#[derive_message_formats]
fn message(&self) -> String {
format!("Tab after keyword")
}
}

define_violation!(
pub struct TabBeforeKeyword;
);
impl Violation for TabBeforeKeyword {
#[derive_message_formats]
fn message(&self) -> String {
format!("Tab before keyword")
}
}

static KEYWORD_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(\s*)\b(?:False|None|True|and|as|assert|async|await|break|class|continue|def|del|elif|else|except|finally|for|from|global|if|import|in|is|lambda|nonlocal|not|or|pass|raise|return|try|while|with|yield)\b(\s*)").unwrap()
});

/// E271, E272, E273, E274
#[cfg(feature = "logical_lines")]
pub fn whitespace_around_keywords(line: &str) -> Vec<(usize, DiagnosticKind)> {
let mut diagnostics = vec![];
for line_match in KEYWORD_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(), TabBeforeKeyword.into()));
} else if before.as_str().len() > 1 {
diagnostics.push((before.start(), MultipleSpacesBeforeKeyword.into()));
}

if after.as_str().contains('\t') {
diagnostics.push((after.start(), TabAfterKeyword.into()));
} else if after.as_str().len() > 1 {
diagnostics.push((after.start(), MultipleSpacesAfterKeyword.into()));
}
}
diagnostics
}

#[cfg(not(feature = "logical_lines"))]
pub fn whitespace_around_keywords(_line: &str) -> Vec<(usize, DiagnosticKind)> {
vec![]
}
Loading

0 comments on commit 3f4a72d

Please sign in to comment.