diff --git a/README.md b/README.md index d46a703445..9934b70e48 100644 --- a/README.md +++ b/README.md @@ -596,6 +596,7 @@ For more, see [pycodestyle](https://pypi.org/project/pycodestyle/2.9.1/) on PyPI #### Error (E) | Code | Name | Message | Fix | | ---- | ---- | ------- | --- | +| E101 | mixed-spaces-and-tabs | Indentation contains mixed spaces and tabs | | | E401 | multiple-imports-on-one-line | Multiple imports on one line | | | E402 | module-import-not-at-top-of-file | Module level import not at top of file | | | E501 | line-too-long | Line too long ({length} > {limit} characters) | | diff --git a/resources/test/fixtures/pycodestyle/E101.py b/resources/test/fixtures/pycodestyle/E101.py new file mode 100644 index 0000000000..73d8913e4d --- /dev/null +++ b/resources/test/fixtures/pycodestyle/E101.py @@ -0,0 +1,19 @@ +def func_all_spaces(): + # No error + print("spaces") + +def func_tabs(): + # No error + print("tabs") + +def func_mixed_start_with_tab(): + # E101 + print("mixed starts with tab") + +def func_mixed_start_with_space(): + # E101 + print("mixed starts with space") + +def xyz(): + # E101 + print("xyz"); diff --git a/ruff.schema.json b/ruff.schema.json index f661594724..79ae9df202 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1311,6 +1311,9 @@ "DTZ011", "DTZ012", "E", + "E1", + "E10", + "E101", "E4", "E40", "E401", diff --git a/src/checkers/lines.rs b/src/checkers/lines.rs index eccbdf6c63..987e504b5d 100644 --- a/src/checkers/lines.rs +++ b/src/checkers/lines.rs @@ -2,7 +2,7 @@ use crate::registry::{Diagnostic, Rule}; use crate::rules::pycodestyle::rules::{ - doc_line_too_long, line_too_long, no_newline_at_end_of_file, + doc_line_too_long, line_too_long, mixed_spaces_and_tabs, no_newline_at_end_of_file, }; use crate::rules::pygrep_hooks::rules::{blanket_noqa, blanket_type_ignore}; use crate::rules::pyupgrade::rules::unnecessary_coding_comment; @@ -25,6 +25,7 @@ pub fn check_lines( let enforce_unnecessary_coding_comment = settings .rules .enabled(&Rule::PEP3120UnnecessaryCodingComment); + let enforce_mixed_spaces_and_tabs = settings.rules.enabled(&Rule::MixedSpacesAndTabs); let mut commented_lines_iter = commented_lines.iter().peekable(); let mut doc_lines_iter = doc_lines.iter().peekable(); @@ -72,6 +73,12 @@ pub fn check_lines( } } + if enforce_mixed_spaces_and_tabs { + if let Some(diagnostic) = mixed_spaces_and_tabs(index, line) { + diagnostics.push(diagnostic); + } + } + if enforce_line_too_long { if let Some(diagnostic) = line_too_long(index, line, settings) { diagnostics.push(diagnostic); diff --git a/src/registry.rs b/src/registry.rs index b6b2fb17f1..0788fd6978 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -14,6 +14,7 @@ use crate::{rules, violations}; ruff_macros::define_rule_mapping!( // pycodestyle errors + E101 => violations::MixedSpacesAndTabs, E401 => violations::MultipleImportsOnOneLine, E402 => violations::ModuleImportNotAtTopOfFile, E501 => violations::LineTooLong, @@ -556,7 +557,8 @@ impl Rule { | Rule::DocLineTooLong | Rule::PEP3120UnnecessaryCodingComment | Rule::BlanketTypeIgnore - | Rule::BlanketNOQA => &LintSource::Lines, + | Rule::BlanketNOQA + | Rule::MixedSpacesAndTabs => &LintSource::Lines, Rule::AmbiguousUnicodeCharacterComment | Rule::AmbiguousUnicodeCharacterDocstring | Rule::AmbiguousUnicodeCharacterString diff --git a/src/rules/pycodestyle/mod.rs b/src/rules/pycodestyle/mod.rs index d5320c2b9c..9fdb329cf6 100644 --- a/src/rules/pycodestyle/mod.rs +++ b/src/rules/pycodestyle/mod.rs @@ -36,6 +36,7 @@ mod tests { #[test_case(Rule::NoNewLineAtEndOfFile, Path::new("W292_4.py"))] #[test_case(Rule::InvalidEscapeSequence, Path::new("W605_0.py"))] #[test_case(Rule::InvalidEscapeSequence, Path::new("W605_1.py"))] + #[test_case(Rule::MixedSpacesAndTabs, Path::new("E101.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/src/rules/pycodestyle/rules.rs b/src/rules/pycodestyle/rules.rs index bba5f51892..4b85fef21e 100644 --- a/src/rules/pycodestyle/rules.rs +++ b/src/rules/pycodestyle/rules.rs @@ -78,6 +78,23 @@ pub fn line_too_long(lineno: usize, line: &str, settings: &Settings) -> Option Option { + let indent = leading_space(line); + + if indent.contains(' ') && indent.contains('\t') { + Some(Diagnostic::new( + violations::MixedSpacesAndTabs, + Range::new( + Location::new(lineno + 1, 0), + Location::new(lineno + 1, indent.chars().count()), + ), + )) + } else { + None + } +} + /// W505 pub fn doc_line_too_long(lineno: usize, line: &str, settings: &Settings) -> Option { let Some(limit) = settings.pycodestyle.max_doc_length else { diff --git a/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E101_E101.py.snap b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E101_E101.py.snap new file mode 100644 index 0000000000..b78ef86463 --- /dev/null +++ b/src/rules/pycodestyle/snapshots/ruff__rules__pycodestyle__tests__E101_E101.py.snap @@ -0,0 +1,35 @@ +--- +source: src/rules/pycodestyle/mod.rs +expression: diagnostics +--- +- kind: + MixedSpacesAndTabs: ~ + location: + row: 11 + column: 0 + end_location: + row: 11 + column: 3 + fix: ~ + parent: ~ +- kind: + MixedSpacesAndTabs: ~ + location: + row: 15 + column: 0 + end_location: + row: 15 + column: 11 + fix: ~ + parent: ~ +- kind: + MixedSpacesAndTabs: ~ + location: + row: 19 + column: 0 + end_location: + row: 19 + column: 4 + fix: ~ + parent: ~ + diff --git a/src/violations.rs b/src/violations.rs index 96534ff8fb..ff53ffff71 100644 --- a/src/violations.rs +++ b/src/violations.rs @@ -48,6 +48,16 @@ impl Violation for LineTooLong { } } +define_violation!( + pub struct MixedSpacesAndTabs; +); +impl Violation for MixedSpacesAndTabs { + #[derive_message_formats] + fn message(&self) -> String { + format!("Indentation contains mixed spaces and tabs") + } +} + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum EqCmpop { Eq,