diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF054.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF054.py new file mode 100644 index 00000000000000..90d332110d1103 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF054.py @@ -0,0 +1,32 @@ +############# Warning ############ +# This file contains form feeds. # +############# Warning ############ + + +# Errors + + + + + +def _(): + pass + +if False: + print('F') + print('T') + + +# No errors + + + + + + + +def _(): + pass + +def f(): + pass diff --git a/crates/ruff_linter/src/checkers/physical_lines.rs b/crates/ruff_linter/src/checkers/physical_lines.rs index 9fba92a5fe4063..f8c6e1492ae40d 100644 --- a/crates/ruff_linter/src/checkers/physical_lines.rs +++ b/crates/ruff_linter/src/checkers/physical_lines.rs @@ -13,6 +13,7 @@ use crate::rules::pycodestyle::rules::{ trailing_whitespace, }; use crate::rules::pylint; +use crate::rules::ruff::rules::indented_form_feed; use crate::settings::LinterSettings; use crate::Locator; @@ -71,6 +72,12 @@ pub(crate) fn check_physical_lines( diagnostics.push(diagnostic); } } + + if settings.rules.enabled(Rule::IndentedFormFeed) { + if let Some(diagnostic) = indented_form_feed(&line) { + diagnostics.push(diagnostic); + } + } } if enforce_no_newline_at_end_of_file { diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index c3e66c99de634a..8891336f9b6a2f 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -1006,6 +1006,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "051") => (RuleGroup::Preview, rules::ruff::rules::IfKeyInDictDel), (Ruff, "052") => (RuleGroup::Preview, rules::ruff::rules::UsedDummyVariable), (Ruff, "053") => (RuleGroup::Preview, rules::ruff::rules::ClassWithMixedTypeVars), + (Ruff, "054") => (RuleGroup::Preview, rules::ruff::rules::IndentedFormFeed), (Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression), (Ruff, "056") => (RuleGroup::Preview, rules::ruff::rules::FalsyDictGetFallback), (Ruff, "057") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRound), diff --git a/crates/ruff_linter/src/registry.rs b/crates/ruff_linter/src/registry.rs index 7cd4eaa2579073..b8de0c47b6fa95 100644 --- a/crates/ruff_linter/src/registry.rs +++ b/crates/ruff_linter/src/registry.rs @@ -253,6 +253,7 @@ impl Rule { Rule::BidirectionalUnicode | Rule::BlankLineWithWhitespace | Rule::DocLineTooLong + | Rule::IndentedFormFeed | Rule::LineTooLong | Rule::MissingCopyrightNotice | Rule::MissingNewlineAtEndOfFile diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index fdb147439f3abf..118f1982ae9653 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -11,11 +11,10 @@ mod tests { use anyhow::Result; use regex::Regex; + use ruff_source_file::SourceFileBuilder; use rustc_hash::FxHashSet; use test_case::test_case; - use ruff_source_file::SourceFileBuilder; - use crate::pyproject_toml::lint_pyproject_toml; use crate::registry::Rule; use crate::settings::types::{ @@ -436,6 +435,7 @@ mod tests { #[test_case(Rule::StarmapZip, Path::new("RUF058_0.py"))] #[test_case(Rule::StarmapZip, Path::new("RUF058_1.py"))] #[test_case(Rule::ClassWithMixedTypeVars, Path::new("RUF053.py"))] + #[test_case(Rule::IndentedFormFeed, Path::new("RUF054.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "preview__{}_{}", diff --git a/crates/ruff_linter/src/rules/ruff/rules/indented_form_feed.rs b/crates/ruff_linter/src/rules/ruff/rules/indented_form_feed.rs new file mode 100644 index 00000000000000..f79ad2a46967c9 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/indented_form_feed.rs @@ -0,0 +1,71 @@ +use memchr::memchr; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_source_file::Line; +use ruff_text_size::{TextRange, TextSize}; + +/// ## What it does +/// Checks for form feed characters preceded by either a space or a tab. +/// +/// ## Why is this bad? +/// [The language reference][lexical-analysis-indentation] states: +/// +/// > A formfeed character may be present at the start of the line; +/// > it will be ignored for the indentation calculations above. +/// > Formfeed characters occurring elsewhere in the leading whitespace +/// > have an undefined effect (for instance, they may reset the space count to zero). +/// +/// ## Example +/// +/// ```python +/// if foo():\n \fbar() +/// ``` +/// +/// Use instead: +/// +/// ```python +/// if foo():\n bar() +/// ``` +/// +/// [lexical-analysis-indentation]: https://docs.python.org/3/reference/lexical_analysis.html#indentation +#[derive(ViolationMetadata)] +pub(crate) struct IndentedFormFeed; + +impl Violation for IndentedFormFeed { + #[derive_message_formats] + fn message(&self) -> String { + "Indented form feed".to_string() + } + + fn fix_title(&self) -> Option { + Some("Remove form feed".to_string()) + } +} + +const FORM_FEED: u8 = b'\x0c'; +const SPACE: u8 = b' '; +const TAB: u8 = b'\t'; + +/// RUF054 +pub(crate) fn indented_form_feed(line: &Line) -> Option { + let index_relative_to_line = memchr(FORM_FEED, line.as_bytes())?; + + if index_relative_to_line == 0 { + return None; + } + + if line[..index_relative_to_line] + .as_bytes() + .iter() + .any(|byte| *byte != SPACE && *byte != TAB) + { + return None; + } + + let relative_index = u32::try_from(index_relative_to_line).ok()?; + let absolute_index = line.start() + TextSize::new(relative_index); + let range = TextRange::at(absolute_index, 1.into()); + + Some(Diagnostic::new(IndentedFormFeed, range)) +} diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index 9113049213d596..18bc32cec58f80 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -13,6 +13,7 @@ pub(crate) use function_call_in_dataclass_default::*; pub(crate) use if_key_in_dict_del::*; pub(crate) use implicit_optional::*; pub(crate) use incorrectly_parenthesized_tuple_in_subscript::*; +pub(crate) use indented_form_feed::*; pub(crate) use invalid_assert_message_literal_argument::*; pub(crate) use invalid_formatter_suppression_comment::*; pub(crate) use invalid_index_type::*; @@ -69,6 +70,7 @@ mod helpers; mod if_key_in_dict_del; mod implicit_optional; mod incorrectly_parenthesized_tuple_in_subscript; +mod indented_form_feed; mod invalid_assert_message_literal_argument; mod invalid_formatter_suppression_comment; mod invalid_index_type; diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF054_RUF054.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF054_RUF054.py.snap new file mode 100644 index 00000000000000..a54d2e2471bd02 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF054_RUF054.py.snap @@ -0,0 +1,39 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF054.py:8:2: RUF054 Indented form feed + | +6 | # Errors +7 | +8 | + | ^ RUF054 + | + = help: Remove form feed + +RUF054.py:10:3: RUF054 Indented form feed + | +10 | + | ^ RUF054 +11 | +12 | def _(): + | + = help: Remove form feed + +RUF054.py:13:2: RUF054 Indented form feed + | +12 | def _(): +13 | pass + | ^ RUF054 +14 | +15 | if False: + | + = help: Remove form feed + +RUF054.py:17:5: RUF054 Indented form feed + | +15 | if False: +16 | print('F') +17 | print('T') + | ^ RUF054 + | + = help: Remove form feed diff --git a/ruff.schema.json b/ruff.schema.json index 83815f2b0d3f89..92e5311a5f678c 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3948,6 +3948,7 @@ "RUF051", "RUF052", "RUF053", + "RUF054", "RUF055", "RUF056", "RUF057", diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index c3d65383d44796..9ab84bb28ba632 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -97,6 +97,7 @@ KNOWN_PARSE_ERRORS = [ "blank-line-with-whitespace", "indentation-with-invalid-multiple-comment", + "indented-form-feed", "missing-newline-at-end-of-file", "mixed-spaces-and-tabs", "no-indented-block",