diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 32ab1bef3a..117b8d4133 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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. diff --git a/Cargo.lock b/Cargo.lock index 687f7432aa..5c52832e86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 614abe900f..f9a69585c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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] diff --git a/resources/test/fixtures/pycodestyle/E11.py b/resources/test/fixtures/pycodestyle/E11.py new file mode 100644 index 0000000000..f8ace8030b --- /dev/null +++ b/resources/test/fixtures/pycodestyle/E11.py @@ -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() diff --git a/resources/test/fixtures/pycodestyle/E20.py b/resources/test/fixtures/pycodestyle/E20.py new file mode 100644 index 0000000000..20c6dfd805 --- /dev/null +++ b/resources/test/fixtures/pycodestyle/E20.py @@ -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] +#: diff --git a/resources/test/fixtures/pycodestyle/E22.py b/resources/test/fixtures/pycodestyle/E22.py new file mode 100644 index 0000000000..7ea27927e5 --- /dev/null +++ b/resources/test/fixtures/pycodestyle/E22.py @@ -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", +} +#: diff --git a/ruff_macros/Cargo.toml b/ruff_macros/Cargo.toml index 8e46d3ab33..5655d6c19d 100644 --- a/ruff_macros/Cargo.toml +++ b/ruff_macros/Cargo.toml @@ -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" } diff --git a/src/checkers/logical_lines.rs b/src/checkers/logical_lines.rs new file mode 100644 index 0000000000..4e4fe461cc --- /dev/null +++ b/src/checkers/logical_lines.rs @@ -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 { + 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 = lexer::make_tokenizer(contents).collect(); + let locator = Locator::new(contents); + let actual: Vec = 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 = lexer::make_tokenizer(contents).collect(); + let locator = Locator::new(contents); + let actual: Vec = 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 = lexer::make_tokenizer(contents).collect(); + let locator = Locator::new(contents); + let actual: Vec = 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 = lexer::make_tokenizer(contents).collect(); + let locator = Locator::new(contents); + let actual: Vec = 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 = lexer::make_tokenizer(contents).collect(); + let locator = Locator::new(contents); + let actual: Vec = 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); + } +} diff --git a/src/checkers/mod.rs b/src/checkers/mod.rs index cec54e376b..0befee33ce 100644 --- a/src/checkers/mod.rs +++ b/src/checkers/mod.rs @@ -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; diff --git a/src/checkers/lines.rs b/src/checkers/physical_lines.rs similarity index 97% rename from src/checkers/lines.rs rename to src/checkers/physical_lines.rs index 72a1eef03e..4c16d3b02a 100644 --- a/src/checkers/lines.rs +++ b/src/checkers/physical_lines.rs @@ -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, diff --git a/src/linter.rs b/src/linter.rs index 88de2d3889..45d45a6509 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -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, diff --git a/src/registry.rs b/src/registry.rs index 1403e4cc9d..c75d1ab3b7 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -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, } } diff --git a/src/rules/pycodestyle/logical_lines.rs b/src/rules/pycodestyle/logical_lines.rs new file mode 100644 index 0000000000..1d798e9b14 --- /dev/null +++ b/src/rules/pycodestyle/logical_lines.rs @@ -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 { + 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 +} diff --git a/src/rules/pycodestyle/mod.rs b/src/rules/pycodestyle/mod.rs index ea74ce6e86..37168d233d 100644 --- a/src/rules/pycodestyle/mod.rs +++ b/src/rules/pycodestyle/mod.rs @@ -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( diff --git a/src/rules/pycodestyle/rules/extraneous_whitespace.rs b/src/rules/pycodestyle/rules/extraneous_whitespace.rs new file mode 100644 index 0000000000..de75f3d7bc --- /dev/null +++ b/src/rules/pycodestyle/rules/extraneous_whitespace.rs @@ -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 = + 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![] +} diff --git a/src/rules/pycodestyle/rules/indentation.rs b/src/rules/pycodestyle/rules/indentation.rs new file mode 100644 index 0000000000..acb3df89ac --- /dev/null +++ b/src/rules/pycodestyle/rules/indentation.rs @@ -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, + 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, + _indent_size: usize, +) -> Vec<(usize, DiagnosticKind)> { + vec![] +} diff --git a/src/rules/pycodestyle/rules/mod.rs b/src/rules/pycodestyle/rules/mod.rs index 07cda0d565..c4c7c9057a 100644 --- a/src/rules/pycodestyle/rules/mod.rs +++ b/src/rules/pycodestyle/rules/mod.rs @@ -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; diff --git a/src/rules/pycodestyle/rules/space_around_operator.rs b/src/rules/pycodestyle/rules/space_around_operator.rs new file mode 100644 index 0000000000..9e215f9032 --- /dev/null +++ b/src/rules/pycodestyle/rules/space_around_operator.rs @@ -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 = + 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![] +} diff --git a/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E111_E11.py.snap b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E111_E11.py.snap new file mode 100644 index 0000000000..3b430934eb --- /dev/null +++ b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E111_E11.py.snap @@ -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: ~ + diff --git a/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E112_E11.py.snap b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E112_E11.py.snap new file mode 100644 index 0000000000..619e248de1 --- /dev/null +++ b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E112_E11.py.snap @@ -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: ~ + diff --git a/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E113_E11.py.snap b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E113_E11.py.snap new file mode 100644 index 0000000000..879ffc0206 --- /dev/null +++ b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E113_E11.py.snap @@ -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: ~ + diff --git a/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E114_E11.py.snap b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E114_E11.py.snap new file mode 100644 index 0000000000..07a9ccd9e9 --- /dev/null +++ b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E114_E11.py.snap @@ -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: ~ + diff --git a/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E115_E11.py.snap b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E115_E11.py.snap new file mode 100644 index 0000000000..6be19a0006 --- /dev/null +++ b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E115_E11.py.snap @@ -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: ~ + diff --git a/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E116_E11.py.snap b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E116_E11.py.snap new file mode 100644 index 0000000000..30f7203bcf --- /dev/null +++ b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E116_E11.py.snap @@ -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: ~ + diff --git a/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E117_E11.py.snap b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E117_E11.py.snap new file mode 100644 index 0000000000..7f2c8fd96a --- /dev/null +++ b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E117_E11.py.snap @@ -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: ~ + diff --git a/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E201_E20.py.snap b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E201_E20.py.snap new file mode 100644 index 0000000000..046e0c18c6 --- /dev/null +++ b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E201_E20.py.snap @@ -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: ~ + diff --git a/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E202_E20.py.snap b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E202_E20.py.snap new file mode 100644 index 0000000000..a45c87919a --- /dev/null +++ b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E202_E20.py.snap @@ -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: ~ + diff --git a/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E203_E20.py.snap b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E203_E20.py.snap new file mode 100644 index 0000000000..8456faacbf --- /dev/null +++ b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E203_E20.py.snap @@ -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: ~ + diff --git a/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E221_E22.py.snap b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E221_E22.py.snap new file mode 100644 index 0000000000..3716ba8c04 --- /dev/null +++ b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E221_E22.py.snap @@ -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: ~ + diff --git a/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E222_E22.py.snap b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E222_E22.py.snap new file mode 100644 index 0000000000..20c046646d --- /dev/null +++ b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E222_E22.py.snap @@ -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: ~ + diff --git a/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E223_E22.py.snap b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E223_E22.py.snap new file mode 100644 index 0000000000..61e968130a --- /dev/null +++ b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E223_E22.py.snap @@ -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: ~ + diff --git a/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E224_E22.py.snap b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E224_E22.py.snap new file mode 100644 index 0000000000..0f21097e75 --- /dev/null +++ b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E224_E22.py.snap @@ -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: ~ + diff --git a/src/source_code/stylist.rs b/src/source_code/stylist.rs index bdaac5c162..d4f7438d61 100644 --- a/src/source_code/stylist.rs +++ b/src/source_code/stylist.rs @@ -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 {