Skip to content

Commit f367aa8

Browse files
[ruff] Indented form feeds (RUF054) (#16049)
## Summary Resolves #12321. The physical-line-based `RUF054` checks for form feed characters that are preceded by only tabs and spaces, but not any other characters, including form feeds. ## Test Plan `cargo nextest run` and `cargo insta test`.
1 parent 9ae98d4 commit f367aa8

10 files changed

Lines changed: 157 additions & 2 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
############# Warning ############
2+
# This file contains form feeds. #
3+
############# Warning ############
4+
5+
6+
# Errors
7+
8+
9+
10+
11+
12+
def _():
13+
pass
14+
15+
if False:
16+
print('F')
17+
print('T')
18+
19+
20+
# No errors
21+
22+
23+
24+
25+
26+
27+
28+
def _():
29+
pass
30+
31+
def f():
32+
pass

crates/ruff_linter/src/checkers/physical_lines.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use crate::rules::pycodestyle::rules::{
1313
trailing_whitespace,
1414
};
1515
use crate::rules::pylint;
16+
use crate::rules::ruff::rules::indented_form_feed;
1617
use crate::settings::LinterSettings;
1718
use crate::Locator;
1819

@@ -71,6 +72,12 @@ pub(crate) fn check_physical_lines(
7172
diagnostics.push(diagnostic);
7273
}
7374
}
75+
76+
if settings.rules.enabled(Rule::IndentedFormFeed) {
77+
if let Some(diagnostic) = indented_form_feed(&line) {
78+
diagnostics.push(diagnostic);
79+
}
80+
}
7481
}
7582

7683
if enforce_no_newline_at_end_of_file {

crates/ruff_linter/src/codes.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
10061006
(Ruff, "051") => (RuleGroup::Preview, rules::ruff::rules::IfKeyInDictDel),
10071007
(Ruff, "052") => (RuleGroup::Preview, rules::ruff::rules::UsedDummyVariable),
10081008
(Ruff, "053") => (RuleGroup::Preview, rules::ruff::rules::ClassWithMixedTypeVars),
1009+
(Ruff, "054") => (RuleGroup::Preview, rules::ruff::rules::IndentedFormFeed),
10091010
(Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression),
10101011
(Ruff, "056") => (RuleGroup::Preview, rules::ruff::rules::FalsyDictGetFallback),
10111012
(Ruff, "057") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRound),

crates/ruff_linter/src/registry.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ impl Rule {
253253
Rule::BidirectionalUnicode
254254
| Rule::BlankLineWithWhitespace
255255
| Rule::DocLineTooLong
256+
| Rule::IndentedFormFeed
256257
| Rule::LineTooLong
257258
| Rule::MissingCopyrightNotice
258259
| Rule::MissingNewlineAtEndOfFile

crates/ruff_linter/src/rules/ruff/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,10 @@ mod tests {
1111

1212
use anyhow::Result;
1313
use regex::Regex;
14+
use ruff_source_file::SourceFileBuilder;
1415
use rustc_hash::FxHashSet;
1516
use test_case::test_case;
1617

17-
use ruff_source_file::SourceFileBuilder;
18-
1918
use crate::pyproject_toml::lint_pyproject_toml;
2019
use crate::registry::Rule;
2120
use crate::settings::types::{
@@ -436,6 +435,7 @@ mod tests {
436435
#[test_case(Rule::StarmapZip, Path::new("RUF058_0.py"))]
437436
#[test_case(Rule::StarmapZip, Path::new("RUF058_1.py"))]
438437
#[test_case(Rule::ClassWithMixedTypeVars, Path::new("RUF053.py"))]
438+
#[test_case(Rule::IndentedFormFeed, Path::new("RUF054.py"))]
439439
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
440440
let snapshot = format!(
441441
"preview__{}_{}",
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
use memchr::memchr;
2+
3+
use ruff_diagnostics::{Diagnostic, Violation};
4+
use ruff_macros::{derive_message_formats, ViolationMetadata};
5+
use ruff_source_file::Line;
6+
use ruff_text_size::{TextRange, TextSize};
7+
8+
/// ## What it does
9+
/// Checks for form feed characters preceded by either a space or a tab.
10+
///
11+
/// ## Why is this bad?
12+
/// [The language reference][lexical-analysis-indentation] states:
13+
///
14+
/// > A formfeed character may be present at the start of the line;
15+
/// > it will be ignored for the indentation calculations above.
16+
/// > Formfeed characters occurring elsewhere in the leading whitespace
17+
/// > have an undefined effect (for instance, they may reset the space count to zero).
18+
///
19+
/// ## Example
20+
///
21+
/// ```python
22+
/// if foo():\n \fbar()
23+
/// ```
24+
///
25+
/// Use instead:
26+
///
27+
/// ```python
28+
/// if foo():\n bar()
29+
/// ```
30+
///
31+
/// [lexical-analysis-indentation]: https://docs.python.org/3/reference/lexical_analysis.html#indentation
32+
#[derive(ViolationMetadata)]
33+
pub(crate) struct IndentedFormFeed;
34+
35+
impl Violation for IndentedFormFeed {
36+
#[derive_message_formats]
37+
fn message(&self) -> String {
38+
"Indented form feed".to_string()
39+
}
40+
41+
fn fix_title(&self) -> Option<String> {
42+
Some("Remove form feed".to_string())
43+
}
44+
}
45+
46+
const FORM_FEED: u8 = b'\x0c';
47+
const SPACE: u8 = b' ';
48+
const TAB: u8 = b'\t';
49+
50+
/// RUF054
51+
pub(crate) fn indented_form_feed(line: &Line) -> Option<Diagnostic> {
52+
let index_relative_to_line = memchr(FORM_FEED, line.as_bytes())?;
53+
54+
if index_relative_to_line == 0 {
55+
return None;
56+
}
57+
58+
if line[..index_relative_to_line]
59+
.as_bytes()
60+
.iter()
61+
.any(|byte| *byte != SPACE && *byte != TAB)
62+
{
63+
return None;
64+
}
65+
66+
let relative_index = u32::try_from(index_relative_to_line).ok()?;
67+
let absolute_index = line.start() + TextSize::new(relative_index);
68+
let range = TextRange::at(absolute_index, 1.into());
69+
70+
Some(Diagnostic::new(IndentedFormFeed, range))
71+
}

crates/ruff_linter/src/rules/ruff/rules/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub(crate) use function_call_in_dataclass_default::*;
1313
pub(crate) use if_key_in_dict_del::*;
1414
pub(crate) use implicit_optional::*;
1515
pub(crate) use incorrectly_parenthesized_tuple_in_subscript::*;
16+
pub(crate) use indented_form_feed::*;
1617
pub(crate) use invalid_assert_message_literal_argument::*;
1718
pub(crate) use invalid_formatter_suppression_comment::*;
1819
pub(crate) use invalid_index_type::*;
@@ -69,6 +70,7 @@ mod helpers;
6970
mod if_key_in_dict_del;
7071
mod implicit_optional;
7172
mod incorrectly_parenthesized_tuple_in_subscript;
73+
mod indented_form_feed;
7274
mod invalid_assert_message_literal_argument;
7375
mod invalid_formatter_suppression_comment;
7476
mod invalid_index_type;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
source: crates/ruff_linter/src/rules/ruff/mod.rs
3+
---
4+
RUF054.py:8:2: RUF054 Indented form feed
5+
|
6+
6 | # Errors
7+
7 |
8+
8 |
9+
| ^ RUF054
10+
|
11+
= help: Remove form feed
12+
13+
RUF054.py:10:3: RUF054 Indented form feed
14+
|
15+
10 |
16+
| ^ RUF054
17+
11 |
18+
12 | def _():
19+
|
20+
= help: Remove form feed
21+
22+
RUF054.py:13:2: RUF054 Indented form feed
23+
|
24+
12 | def _():
25+
13 | pass
26+
| ^ RUF054
27+
14 |
28+
15 | if False:
29+
|
30+
= help: Remove form feed
31+
32+
RUF054.py:17:5: RUF054 Indented form feed
33+
|
34+
15 | if False:
35+
16 | print('F')
36+
17 | print('T')
37+
| ^ RUF054
38+
|
39+
= help: Remove form feed

ruff.schema.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/check_docs_formatted.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
KNOWN_PARSE_ERRORS = [
9898
"blank-line-with-whitespace",
9999
"indentation-with-invalid-multiple-comment",
100+
"indented-form-feed",
100101
"missing-newline-at-end-of-file",
101102
"mixed-spaces-and-tabs",
102103
"no-indented-block",

0 commit comments

Comments
 (0)