[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`.
This commit is contained in:
InSync 2025-02-10 07:23:48 +07:00 committed by GitHub
parent 9ae98d4a09
commit f367aa8367
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 157 additions and 2 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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),

View file

@ -253,6 +253,7 @@ impl Rule {
Rule::BidirectionalUnicode
| Rule::BlankLineWithWhitespace
| Rule::DocLineTooLong
| Rule::IndentedFormFeed
| Rule::LineTooLong
| Rule::MissingCopyrightNotice
| Rule::MissingNewlineAtEndOfFile

View file

@ -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__{}_{}",

View file

@ -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<String> {
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<Diagnostic> {
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))
}

View file

@ -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;

View file

@ -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

1
ruff.schema.json generated
View file

@ -3948,6 +3948,7 @@
"RUF051",
"RUF052",
"RUF053",
"RUF054",
"RUF055",
"RUF056",
"RUF057",

View file

@ -97,6 +97,7 @@ KNOWN_FORMATTING_VIOLATIONS = [
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",