diff --git a/crates/ruff_python_formatter/src/core/helpers.rs b/crates/ruff_python_formatter/src/core/helpers.rs index fce76027c3..e6130fd59d 100644 --- a/crates/ruff_python_formatter/src/core/helpers.rs +++ b/crates/ruff_python_formatter/src/core/helpers.rs @@ -23,9 +23,17 @@ pub fn trailing_quote(content: &str) -> Option<&&str> { .find(|&pattern| content.ends_with(pattern)) } +pub fn is_radix_literal(content: &str) -> bool { + content.starts_with("0b") + || content.starts_with("0o") + || content.starts_with("0x") + || content.starts_with("0B") + || content.starts_with("0O") + || content.starts_with("0X") +} + #[cfg(test)] mod tests { - #[test] fn test_prefixes() { let prefixes = ruff_python::str::TRIPLE_QUOTE_PREFIXES diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 9a263fc41a..738abb1dd2 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -22,6 +22,9 @@ pub mod shared_traits; pub mod trivia; pub fn fmt(contents: &str) -> Result> { + // Create a reusable locator. + let locator = Locator::new(contents); + // Tokenize once. let tokens: Vec = ruff_rustpython::tokenize(contents); @@ -37,7 +40,7 @@ pub fn fmt(contents: &str) -> Result> { // Attach trivia. attach(&mut python_cst, trivia); normalize_newlines(&mut python_cst); - normalize_parentheses(&mut python_cst); + normalize_parentheses(&mut python_cst, &locator); format!( ASTFormatContext::new( @@ -45,7 +48,7 @@ pub fn fmt(contents: &str) -> Result> { indent_style: IndentStyle::Space(4), line_width: 88.try_into().unwrap(), }, - Locator::new(contents) + locator, ), [format::builders::block(&python_cst)] ) diff --git a/crates/ruff_python_formatter/src/parentheses.rs b/crates/ruff_python_formatter/src/parentheses.rs index 80b5dd7170..ba9ecabf47 100644 --- a/crates/ruff_python_formatter/src/parentheses.rs +++ b/crates/ruff_python_formatter/src/parentheses.rs @@ -1,7 +1,11 @@ +use crate::core::helpers::is_radix_literal; +use crate::core::locator::Locator; +use crate::core::types::Range; use crate::core::visitor; use crate::core::visitor::Visitor; use crate::cst::{Expr, ExprKind, Stmt, StmtKind}; use crate::trivia::{Parenthesize, TriviaKind}; +use rustpython_parser::ast::Constant; /// Modify an [`Expr`] to infer parentheses, rather than respecting any user-provided trivia. fn use_inferred_parens(expr: &mut Expr) { @@ -22,9 +26,11 @@ fn use_inferred_parens(expr: &mut Expr) { } } -struct ParenthesesNormalizer {} +struct ParenthesesNormalizer<'a> { + locator: &'a Locator<'a>, +} -impl<'a> Visitor<'a> for ParenthesesNormalizer { +impl<'a> Visitor<'a> for ParenthesesNormalizer<'_> { fn visit_stmt(&mut self, stmt: &'a mut Stmt) { // Always remove parentheses around statements, unless it's an expression statement, // in which case, remove parentheses around the expression. @@ -134,7 +140,29 @@ impl<'a> Visitor<'a> for ParenthesesNormalizer { ExprKind::FormattedValue { .. } => {} ExprKind::JoinedStr { .. } => {} ExprKind::Constant { .. } => {} - ExprKind::Attribute { .. } => {} + ExprKind::Attribute { value, .. } => { + if matches!( + value.node, + ExprKind::Constant { + value: Constant::Float(..), + .. + }, + ) { + value.parentheses = Parenthesize::Always; + } else if matches!( + value.node, + ExprKind::Constant { + value: Constant::Int(..), + .. + }, + ) { + let (source, start, end) = self.locator.slice(Range::from_located(value)); + // TODO(charlie): Encode this in the AST via separate node types. + if !is_radix_literal(&source[start..end]) { + value.parentheses = Parenthesize::Always; + } + } + } ExprKind::Subscript { value, slice, .. } => { // If the slice isn't manually parenthesized, ensure that we _never_ parenthesize // the value. @@ -166,7 +194,7 @@ impl<'a> Visitor<'a> for ParenthesesNormalizer { /// /// TODO(charlie): It's weird that we have both `TriviaKind::Parentheses` (which aren't used /// during formatting) and `Parenthesize` (which are used during formatting). -pub fn normalize_parentheses(python_cst: &mut [Stmt]) { - let mut normalizer = ParenthesesNormalizer {}; +pub fn normalize_parentheses(python_cst: &mut [Stmt], locator: &Locator) { + let mut normalizer = ParenthesesNormalizer { locator }; normalizer.visit_body(python_cst); } diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap deleted file mode 100644 index f22cbaca7c..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap +++ /dev/null @@ -1,128 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/attribute_access_on_number_literals.py ---- -## Input - -```py -x = 123456789 .bit_count() -x = (123456).__abs__() -x = .1.is_integer() -x = 1. .imag -x = 1E+1.imag -x = 1E-1.real -x = 123456789.123456789.hex() -x = 123456789.123456789E123456789 .real -x = 123456789E123456789 .conjugate() -x = 123456789J.real -x = 123456789.123456789J.__add__(0b1011.bit_length()) -x = 0XB1ACC.conjugate() -x = 0B1011 .conjugate() -x = 0O777 .real -x = 0.000000006 .hex() -x = -100.0000J - -if 10 .real: - ... - -y = 100[no] -y = 100(no) -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,22 +1,22 @@ --x = (123456789).bit_count() -+x = 123456789.bit_count() - x = (123456).__abs__() --x = (0.1).is_integer() --x = (1.0).imag --x = (1e1).imag --x = (1e-1).real --x = (123456789.123456789).hex() --x = (123456789.123456789e123456789).real --x = (123456789e123456789).conjugate() -+x = 0.1.is_integer() -+x = 1.0.imag -+x = 1e1.imag -+x = 1e-1.real -+x = 123456789.123456789.hex() -+x = 123456789.123456789e123456789.real -+x = 123456789e123456789.conjugate() - x = 123456789j.real - x = 123456789.123456789j.__add__(0b1011.bit_length()) - x = 0xB1ACC.conjugate() - x = 0b1011.conjugate() - x = 0o777.real --x = (0.000000006).hex() -+x = 0.000000006.hex() - x = -100.0000j - --if (10).real: -+if 10.real: - ... - - y = 100[no] --y = 100(no) -+y = 100((no)) -``` - -## Ruff Output - -```py -x = 123456789.bit_count() -x = (123456).__abs__() -x = 0.1.is_integer() -x = 1.0.imag -x = 1e1.imag -x = 1e-1.real -x = 123456789.123456789.hex() -x = 123456789.123456789e123456789.real -x = 123456789e123456789.conjugate() -x = 123456789j.real -x = 123456789.123456789j.__add__(0b1011.bit_length()) -x = 0xB1ACC.conjugate() -x = 0b1011.conjugate() -x = 0o777.real -x = 0.000000006.hex() -x = -100.0000j - -if 10.real: - ... - -y = 100[no] -y = 100((no)) -``` - -## Black Output - -```py -x = (123456789).bit_count() -x = (123456).__abs__() -x = (0.1).is_integer() -x = (1.0).imag -x = (1e1).imag -x = (1e-1).real -x = (123456789.123456789).hex() -x = (123456789.123456789e123456789).real -x = (123456789e123456789).conjugate() -x = 123456789j.real -x = 123456789.123456789j.__add__(0b1011.bit_length()) -x = 0xB1ACC.conjugate() -x = 0b1011.conjugate() -x = 0o777.real -x = (0.000000006).hex() -x = -100.0000j - -if (10).real: - ... - -y = 100[no] -y = 100(no) -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap index 1f5941e5e9..34ab0f3c2b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap @@ -292,17 +292,6 @@ last_call() } Python3 > Python2 > COBOL Life is Life -@@ -122,8 +125,8 @@ - call(b, **self.screen_kwargs) - lukasz.langa.pl - call.me(maybe) --(1).real --(1.0).real -+1.real -+1.0.real - ....__class__ - list[str] - dict[str, int] @@ -138,33 +141,33 @@ very_long_variable_name_filters: t.List[ t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], @@ -310,14 +299,14 @@ last_call() -xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore - sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) +xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = ( -+ classmethod(sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)) # type: ignore -+) -+xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = ( + classmethod(sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)) # type: ignore ) -xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore - sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) +xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = ( ++ classmethod(sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)) # type: ignore ++) ++xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = ( + classmethod(sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)) ) -xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( @@ -583,8 +572,8 @@ call(**self.screen_kwargs) call(b, **self.screen_kwargs) lukasz.langa.pl call.me(maybe) -1.real -1.0.real +(1).real +(1.0).real ....__class__ list[str] dict[str, int] diff --git a/crates/ruff_python_formatter/src/trivia.rs b/crates/ruff_python_formatter/src/trivia.rs index e10834d2f1..7c5f2f1c46 100644 --- a/crates/ruff_python_formatter/src/trivia.rs +++ b/crates/ruff_python_formatter/src/trivia.rs @@ -183,7 +183,14 @@ pub fn extract_trivia_tokens(lxr: &[LexResult]) -> Vec { if matches!(tok, Tok::Lpar) { if prev_tok.map_or(true, |(_, prev_tok, _)| { - !matches!(prev_tok, Tok::Name { .. }) + !matches!( + prev_tok, + Tok::Name { .. } + | Tok::Int { .. } + | Tok::Float { .. } + | Tok::Complex { .. } + | Tok::String { .. } + ) }) { parens.push((start, true)); } else {