diff --git a/crates/ruff/resources/test/fixtures/flake8_bugbear/B006_4.py b/crates/ruff/resources/test/fixtures/flake8_bugbear/B006_4.py new file mode 100644 index 0000000000..4da2641c12 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_bugbear/B006_4.py @@ -0,0 +1,9 @@ +# formfeed indent +# https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458825 +# This is technically a stylist bug (and has a test there), but it surfaced in B006 + + +class FormFeedIndent: + def __init__(self, a=[]): + print(a) + diff --git a/crates/ruff/src/rules/flake8_bugbear/mod.rs b/crates/ruff/src/rules/flake8_bugbear/mod.rs index 71ec3b130a..be5b06a88c 100644 --- a/crates/ruff/src/rules/flake8_bugbear/mod.rs +++ b/crates/ruff/src/rules/flake8_bugbear/mod.rs @@ -36,6 +36,7 @@ mod tests { #[test_case(Rule::MutableArgumentDefault, Path::new("B006_1.py"))] #[test_case(Rule::MutableArgumentDefault, Path::new("B006_2.py"))] #[test_case(Rule::MutableArgumentDefault, Path::new("B006_3.py"))] + #[test_case(Rule::MutableArgumentDefault, Path::new("B006_4.py"))] #[test_case(Rule::NoExplicitStacklevel, Path::new("B028.py"))] #[test_case(Rule::RaiseLiteral, Path::new("B016.py"))] #[test_case(Rule::RaiseWithoutFromInsideExcept, Path::new("B904.py"))] diff --git a/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B006_B006_4.py.snap b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B006_B006_4.py.snap new file mode 100644 index 0000000000..7d7b16b041 --- /dev/null +++ b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B006_B006_4.py.snap @@ -0,0 +1,24 @@ +--- +source: crates/ruff/src/rules/flake8_bugbear/mod.rs +--- +B006_4.py:7:26: B006 [*] Do not use mutable data structures for argument defaults + | +6 | class FormFeedIndent: +7 | def __init__(self, a=[]): + | ^^ B006 +8 | print(a) + | + = help: Replace with `None`; initialize within function + +ℹ Possible fix +4 4 | +5 5 | +6 6 | class FormFeedIndent: +7 |- def __init__(self, a=[]): + 7 |+ def __init__(self, a=None): + 8 |+ if a is None: + 9 |+ a = [] +8 10 | print(a) +9 11 | + + diff --git a/crates/ruff_python_codegen/src/stylist.rs b/crates/ruff_python_codegen/src/stylist.rs index 087d82a29a..4e4f92227b 100644 --- a/crates/ruff_python_codegen/src/stylist.rs +++ b/crates/ruff_python_codegen/src/stylist.rs @@ -84,7 +84,21 @@ fn detect_indention(tokens: &[LexResult], locator: &Locator) -> Indentation { }); if let Some(indent_range) = indent_range { - let whitespace = locator.slice(*indent_range); + let mut whitespace = locator.slice(*indent_range); + // https://docs.python.org/3/reference/lexical_analysis.html#indentation + // > 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). + // So there's UB in python lexer -.- + // In practice, they just reset the indentation: + // https://github.com/python/cpython/blob/df8b3a46a7aa369f246a09ffd11ceedf1d34e921/Parser/tokenizer.c#L1819-L1821 + // https://github.com/astral-sh/ruff/blob/a41bb2733fe75a71f4cf6d4bb21e659fc4630b30/crates/ruff_python_parser/src/lexer.rs#L664-L667 + // We also reset the indentation when we see a formfeed character. + // See also https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458825 + if let Some((_before, after)) = whitespace.rsplit_once('\x0C') { + whitespace = after; + } Indentation(whitespace.to_string()) } else { @@ -228,6 +242,19 @@ x = ( Stylist::from_tokens(&tokens, &locator).indentation(), &Indentation::default() ); + + // formfeed indent, see `detect_indention` comment. + let contents = r#" +class FormFeedIndent: + def __init__(self, a=[]): + print(a) +"#; + let locator = Locator::new(contents); + let tokens: Vec<_> = lex(contents, Mode::Module).collect(); + assert_eq!( + Stylist::from_tokens(&tokens, &locator).indentation(), + &Indentation(" ".to_string()) + ); } #[test]