Merge branch 'main' into PYI050

This commit is contained in:
Charlie Marsh 2023-06-06 21:27:59 -04:00
commit ddd3ededec
74 changed files with 1560 additions and 466 deletions

View File

@ -34,6 +34,7 @@ jobs:
args: --out dist args: --out dist
- name: "Test sdist" - name: "Test sdist"
run: | run: |
rustup default $(cat rust-toolchain)
pip install dist/${{ env.PACKAGE_NAME }}-*.tar.gz --force-reinstall pip install dist/${{ env.PACKAGE_NAME }}-*.tar.gz --force-reinstall
ruff --help ruff --help
python -m ruff --help python -m ruff --help

View File

@ -1,4 +1,12 @@
fail_fast: true fail_fast: true
exclude: |
(?x)^(
crates/ruff/resources/.*|
crates/ruff_python_formatter/resources/.*|
crates/ruff_python_formatter/src/snapshots/.*
)$
repos: repos:
- repo: https://github.com/abravalheri/validate-pyproject - repo: https://github.com/abravalheri/validate-pyproject
rev: v0.12.1 rev: v0.12.1
@ -19,7 +27,7 @@ repos:
- id: markdownlint-fix - id: markdownlint-fix
- repo: https://github.com/crate-ci/typos - repo: https://github.com/crate-ci/typos
rev: v1.14.8 rev: v1.14.12
hooks: hooks:
- id: typos - id: typos

6
Cargo.lock generated
View File

@ -691,7 +691,7 @@ dependencies = [
[[package]] [[package]]
name = "flake8-to-ruff" name = "flake8-to-ruff"
version = "0.0.270" version = "0.0.271"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1725,7 +1725,7 @@ dependencies = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.0.270" version = "0.0.271"
dependencies = [ dependencies = [
"annotate-snippets 0.9.1", "annotate-snippets 0.9.1",
"anyhow", "anyhow",
@ -1818,7 +1818,7 @@ dependencies = [
[[package]] [[package]]
name = "ruff_cli" name = "ruff_cli"
version = "0.0.270" version = "0.0.271"
dependencies = [ dependencies = [
"annotate-snippets 0.9.1", "annotate-snippets 0.9.1",
"anyhow", "anyhow",

View File

@ -139,7 +139,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml ```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.0.270 rev: v0.0.271
hooks: hooks:
- id: ruff - id: ruff
``` ```

View File

@ -1,5 +1,5 @@
[files] [files]
extend-exclude = ["snapshots", "black"] extend-exclude = ["resources", "snapshots"]
[default.extend-words] [default.extend-words]
trivias = "trivias" trivias = "trivias"

View File

@ -1,6 +1,6 @@
[package] [package]
name = "flake8-to-ruff" name = "flake8-to-ruff"
version = "0.0.270" version = "0.0.271"
edition = { workspace = true } edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }

View File

@ -1,6 +1,6 @@
[package] [package]
name = "ruff" name = "ruff"
version = "0.0.270" version = "0.0.271"
authors.workspace = true authors.workspace = true
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true

View File

@ -2,3 +2,6 @@
"{bar}{}".format(1, bar=2, spam=3) # F522 "{bar}{}".format(1, bar=2, spam=3) # F522
"{bar:{spam}}".format(bar=2, spam=3) # No issues "{bar:{spam}}".format(bar=2, spam=3) # No issues
"{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522 "{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
# Not fixable
(''
.format(x=2))

View File

@ -83,6 +83,11 @@ def f():
pass pass
def f():
with (Nested(m)) as (cm):
pass
def f(): def f():
toplevel = tt = lexer.get_token() toplevel = tt = lexer.get_token()
if not tt: if not tt:

View File

@ -3,12 +3,6 @@
for item in {"apples", "lemons", "water"}: # flags in-line set literals for item in {"apples", "lemons", "water"}: # flags in-line set literals
print(f"I like {item}.") print(f"I like {item}.")
for item in set(("apples", "lemons", "water")): # flags set() calls
print(f"I like {item}.")
for number in {i for i in range(10)}: # flags set comprehensions
print(number)
numbers_list = [i for i in {1, 2, 3}] # flags sets in list comprehensions numbers_list = [i for i in {1, 2, 3}] # flags sets in list comprehensions
numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions
@ -36,3 +30,9 @@ numbers_set = {i for i in (1, 2, 3)} # tuples in comprehensions are fine
numbers_dict = {str(i): i for i in [1, 2, 3]} # lists in dict comprehensions are fine numbers_dict = {str(i): i for i in [1, 2, 3]} # lists in dict comprehensions are fine
numbers_gen = (i for i in (1, 2, 3)) # tuples in generator expressions are fine numbers_gen = (i for i in (1, 2, 3)) # tuples in generator expressions are fine
for item in set(("apples", "lemons", "water")): # set constructor is fine
print(f"I like {item}.")
for number in {i for i in range(10)}: # set comprehensions are fine
print(number)

View File

@ -17,3 +17,30 @@ def f():
def f(): def f():
nonlocal y nonlocal y
def f():
x = 1
def g():
nonlocal x
del x
def f():
def g():
nonlocal x
del x
def f():
try:
pass
except Exception as x:
pass
def g():
nonlocal x
x = 2

View File

@ -307,9 +307,18 @@ where
stmt.range(), stmt.range(),
ExecutionContext::Runtime, ExecutionContext::Runtime,
); );
} else { }
// Ensure that every nonlocal has an existing binding from a parent scope.
if self.enabled(Rule::NonlocalWithoutBinding) { // Ensure that every nonlocal has an existing binding from a parent scope.
if self.enabled(Rule::NonlocalWithoutBinding) {
if self
.semantic_model
.scopes
.ancestors(self.semantic_model.scope_id)
.skip(1)
.take_while(|scope| !scope.kind.is_module())
.all(|scope| !scope.declares(name.as_str()))
{
self.diagnostics.push(Diagnostic::new( self.diagnostics.push(Diagnostic::new(
pylint::rules::NonlocalWithoutBinding { pylint::rules::NonlocalWithoutBinding {
name: name.to_string(), name: name.to_string(),
@ -4042,7 +4051,7 @@ where
let name_range = let name_range =
helpers::excepthandler_name_range(excepthandler, self.locator).unwrap(); helpers::excepthandler_name_range(excepthandler, self.locator).unwrap();
if self.semantic_model.scope().defines(name) { if self.semantic_model.scope().has(name) {
self.handle_node_store( self.handle_node_store(
name, name,
&Expr::Name(ast::ExprName { &Expr::Name(ast::ExprName {
@ -4067,7 +4076,7 @@ where
if let Some(binding_id) = { if let Some(binding_id) = {
let scope = self.semantic_model.scope_mut(); let scope = self.semantic_model.scope_mut();
scope.remove(name) scope.delete(name)
} { } {
if !self.semantic_model.is_used(binding_id) { if !self.semantic_model.is_used(binding_id) {
if self.enabled(Rule::UnusedVariable) { if self.enabled(Rule::UnusedVariable) {
@ -4697,19 +4706,16 @@ impl<'a> Checker<'a> {
} }
let scope = self.semantic_model.scope_mut(); let scope = self.semantic_model.scope_mut();
if scope.remove(id.as_str()).is_some() { if scope.delete(id.as_str()).is_none() {
return; if self.enabled(Rule::UndefinedName) {
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedName {
name: id.to_string(),
},
expr.range(),
));
}
} }
if !self.enabled(Rule::UndefinedName) {
return;
}
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedName {
name: id.to_string(),
},
expr.range(),
));
} }
fn check_deferred_future_type_definitions(&mut self) { fn check_deferred_future_type_definitions(&mut self) {
@ -4977,7 +4983,7 @@ impl<'a> Checker<'a> {
.collect(); .collect();
if !sources.is_empty() { if !sources.is_empty() {
for (name, range) in &exports { for (name, range) in &exports {
if !scope.defines(name) { if !scope.has(name) {
diagnostics.push(Diagnostic::new( diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedLocalWithImportStarUsage { pyflakes::rules::UndefinedLocalWithImportStarUsage {
name: (*name).to_string(), name: (*name).to_string(),

View File

@ -386,7 +386,9 @@ pub struct StringDotFormatExtraNamedArguments {
missing: Vec<String>, missing: Vec<String>,
} }
impl AlwaysAutofixableViolation for StringDotFormatExtraNamedArguments { impl Violation for StringDotFormatExtraNamedArguments {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
let StringDotFormatExtraNamedArguments { missing } = self; let StringDotFormatExtraNamedArguments { missing } = self;
@ -394,10 +396,10 @@ impl AlwaysAutofixableViolation for StringDotFormatExtraNamedArguments {
format!("`.format` call has unused named argument(s): {message}") format!("`.format` call has unused named argument(s): {message}")
} }
fn autofix_title(&self) -> String { fn autofix_title(&self) -> Option<String> {
let StringDotFormatExtraNamedArguments { missing } = self; let StringDotFormatExtraNamedArguments { missing } = self;
let message = missing.join(", "); let message = missing.join(", ");
format!("Remove extra named arguments: {message}") Some(format!("Remove extra named arguments: {message}"))
} }
} }

View File

@ -51,7 +51,7 @@ impl Violation for UndefinedExport {
pub(crate) fn undefined_export(name: &str, range: TextRange, scope: &Scope) -> Vec<Diagnostic> { pub(crate) fn undefined_export(name: &str, range: TextRange, scope: &Scope) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new(); let mut diagnostics = Vec::new();
if !scope.uses_star_imports() { if !scope.uses_star_imports() {
if !scope.defines(name) { if !scope.has(name) {
diagnostics.push(Diagnostic::new( diagnostics.push(Diagnostic::new(
UndefinedExport { UndefinedExport {
name: (*name).to_string(), name: (*name).to_string(),

View File

@ -47,7 +47,7 @@ impl Violation for UndefinedLocal {
pub(crate) fn undefined_local(checker: &mut Checker, name: &str) { pub(crate) fn undefined_local(checker: &mut Checker, name: &str) {
// If the name hasn't already been defined in the current scope... // If the name hasn't already been defined in the current scope...
let current = checker.semantic_model().scope(); let current = checker.semantic_model().scope();
if !current.kind.is_any_function() || current.defines(name) { if !current.kind.is_any_function() || current.has(name) {
return; return;
} }

View File

@ -1,5 +1,5 @@
use itertools::Itertools; use itertools::Itertools;
use ruff_text_size::TextRange; use ruff_text_size::{TextRange, TextSize};
use rustpython_parser::ast::{self, Ranged, Stmt}; use rustpython_parser::ast::{self, Ranged, Stmt};
use rustpython_parser::{lexer, Mode, Tok}; use rustpython_parser::{lexer, Mode, Tok};
@ -61,21 +61,37 @@ impl Violation for UnusedVariable {
} }
} }
/// Return the [`TextRange`] of the token after the next match of /// Return the [`TextRange`] of the token before the next match of the predicate
/// the predicate, skipping over any bracketed expressions. fn match_token_before<F>(location: TextSize, locator: &Locator, f: F) -> Option<TextRange>
fn match_token_after<F, T>(located: &T, locator: &Locator, f: F) -> TextRange
where where
F: Fn(Tok) -> bool, F: Fn(Tok) -> bool,
T: Ranged,
{ {
let contents = locator.after(located.start()); let contents = locator.after(location);
for ((_, range), (tok, _)) in lexer::lex_starts_at(contents, Mode::Module, location)
.flatten()
.tuple_windows()
{
if f(tok) {
return Some(range);
}
}
None
}
/// Return the [`TextRange`] of the token after the next match of the predicate, skipping over
/// any bracketed expressions.
fn match_token_after<F>(location: TextSize, locator: &Locator, f: F) -> Option<TextRange>
where
F: Fn(Tok) -> bool,
{
let contents = locator.after(location);
// Track the bracket depth. // Track the bracket depth.
let mut par_count = 0u32; let mut par_count = 0u32;
let mut sqb_count = 0u32; let mut sqb_count = 0u32;
let mut brace_count = 0u32; let mut brace_count = 0u32;
for ((tok, _), (_, range)) in lexer::lex_starts_at(contents, Mode::Module, located.start()) for ((tok, _), (_, range)) in lexer::lex_starts_at(contents, Mode::Module, location)
.flatten() .flatten()
.tuple_windows() .tuple_windows()
{ {
@ -91,97 +107,83 @@ where
} }
Tok::Rpar => { Tok::Rpar => {
par_count = par_count.saturating_sub(1); par_count = par_count.saturating_sub(1);
// If this is a closing bracket, continue.
if par_count == 0 {
continue;
}
} }
Tok::Rsqb => { Tok::Rsqb => {
sqb_count = sqb_count.saturating_sub(1); sqb_count = sqb_count.saturating_sub(1);
// If this is a closing bracket, continue.
if sqb_count == 0 {
continue;
}
} }
Tok::Rbrace => { Tok::Rbrace => {
brace_count = brace_count.saturating_sub(1); brace_count = brace_count.saturating_sub(1);
// If this is a closing bracket, continue.
if brace_count == 0 {
continue;
}
} }
_ => {} _ => {}
} }
// If we're in nested brackets, continue. // If we're in nested brackets, continue.
if par_count > 0 || sqb_count > 0 || brace_count > 0 { if par_count > 0 || sqb_count > 0 || brace_count > 0 {
continue; continue;
} }
if f(tok) { if f(tok) {
return range; return Some(range);
} }
} }
unreachable!("No token after matched"); None
} }
/// Return the [`TextRange`] of the token matching the predicate, /// Return the [`TextRange`] of the token matching the predicate or the first mismatched
/// skipping over any bracketed expressions. /// bracket, skipping over any bracketed expressions.
fn match_token<F, T>(located: &T, locator: &Locator, f: F) -> TextRange fn match_token_or_closing_brace<F>(location: TextSize, locator: &Locator, f: F) -> Option<TextRange>
where where
F: Fn(Tok) -> bool, F: Fn(Tok) -> bool,
T: Ranged,
{ {
let contents = locator.after(located.start()); let contents = locator.after(location);
// Track the bracket depth. // Track the bracket depth.
let mut par_count = 0; let mut par_count = 0u32;
let mut sqb_count = 0; let mut sqb_count = 0u32;
let mut brace_count = 0; let mut brace_count = 0u32;
for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, located.start()).flatten() { for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, location).flatten() {
match tok { match tok {
Tok::Lpar => { Tok::Lpar => {
par_count += 1; par_count = par_count.saturating_add(1);
} }
Tok::Lsqb => { Tok::Lsqb => {
sqb_count += 1; sqb_count = sqb_count.saturating_add(1);
} }
Tok::Lbrace => { Tok::Lbrace => {
brace_count += 1; brace_count = brace_count.saturating_add(1);
} }
Tok::Rpar => { Tok::Rpar => {
par_count -= 1;
// If this is a closing bracket, continue.
if par_count == 0 { if par_count == 0 {
continue; return Some(range);
} }
par_count = par_count.saturating_sub(1);
} }
Tok::Rsqb => { Tok::Rsqb => {
sqb_count -= 1;
// If this is a closing bracket, continue.
if sqb_count == 0 { if sqb_count == 0 {
continue; return Some(range);
} }
sqb_count = sqb_count.saturating_sub(1);
} }
Tok::Rbrace => { Tok::Rbrace => {
brace_count -= 1;
// If this is a closing bracket, continue.
if brace_count == 0 { if brace_count == 0 {
continue; return Some(range);
} }
brace_count = brace_count.saturating_sub(1);
} }
_ => {} _ => {}
} }
// If we're in nested brackets, continue. // If we're in nested brackets, continue.
if par_count > 0 || sqb_count > 0 || brace_count > 0 { if par_count > 0 || sqb_count > 0 || brace_count > 0 {
continue; continue;
} }
if f(tok) { if f(tok) {
return range; return Some(range);
} }
} }
unreachable!("No token after matched"); None
} }
/// Generate a [`Edit`] to remove an unused variable assignment, given the /// Generate a [`Edit`] to remove an unused variable assignment, given the
@ -201,10 +203,10 @@ fn remove_unused_variable(
{ {
// If the expression is complex (`x = foo()`), remove the assignment, // If the expression is complex (`x = foo()`), remove the assignment,
// but preserve the right-hand side. // but preserve the right-hand side.
let edit = Edit::deletion( let start = target.start();
target.start(), let end =
match_token_after(target, checker.locator, |tok| tok == Tok::Equal).start(), match_token_after(start, checker.locator, |tok| tok == Tok::Equal)?.start();
); let edit = Edit::deletion(start, end);
Some(Fix::suggested(edit)) Some(Fix::suggested(edit))
} else { } else {
// If (e.g.) assigning to a constant (`x = 1`), delete the entire statement. // If (e.g.) assigning to a constant (`x = 1`), delete the entire statement.
@ -232,10 +234,10 @@ fn remove_unused_variable(
return if contains_effect(value, |id| checker.semantic_model().is_builtin(id)) { return if contains_effect(value, |id| checker.semantic_model().is_builtin(id)) {
// If the expression is complex (`x = foo()`), remove the assignment, // If the expression is complex (`x = foo()`), remove the assignment,
// but preserve the right-hand side. // but preserve the right-hand side.
let edit = Edit::deletion( let start = stmt.start();
stmt.start(), let end =
match_token_after(stmt, checker.locator, |tok| tok == Tok::Equal).start(), match_token_after(start, checker.locator, |tok| tok == Tok::Equal)?.start();
); let edit = Edit::deletion(start, end);
Some(Fix::suggested(edit)) Some(Fix::suggested(edit))
} else { } else {
// If (e.g.) assigning to a constant (`x = 1`), delete the entire statement. // If (e.g.) assigning to a constant (`x = 1`), delete the entire statement.
@ -258,15 +260,20 @@ fn remove_unused_variable(
for item in items { for item in items {
if let Some(optional_vars) = &item.optional_vars { if let Some(optional_vars) = &item.optional_vars {
if optional_vars.range() == range { if optional_vars.range() == range {
let edit = Edit::deletion( // Find the first token before the `as` keyword.
item.context_expr.end(), let start =
// The end of the `Withitem` is the colon, comma, or closing match_token_before(item.context_expr.start(), checker.locator, |tok| {
// parenthesis following the `optional_vars`. tok == Tok::As
match_token(&item.context_expr, checker.locator, |tok| { })?
tok == Tok::Colon || tok == Tok::Comma || tok == Tok::Rpar .end();
})
.start(), // Find the first colon, comma, or closing bracket after the `as` keyword.
); let end = match_token_or_closing_brace(start, checker.locator, |tok| {
tok == Tok::Colon || tok == Tok::Comma
})?
.start();
let edit = Edit::deletion(start, end);
return Some(Fix::suggested(edit)); return Some(Fix::suggested(edit));
} }
} }

View File

@ -33,6 +33,7 @@ F522.py:2:1: F522 [*] `.format` call has unused named argument(s): spam
2 |+"{bar}{}".format(1, bar=2, ) # F522 2 |+"{bar}{}".format(1, bar=2, ) # F522
3 3 | "{bar:{spam}}".format(bar=2, spam=3) # No issues 3 3 | "{bar:{spam}}".format(bar=2, spam=3) # No issues
4 4 | "{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522 4 4 | "{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
5 5 | # Not fixable
F522.py:4:1: F522 [*] `.format` call has unused named argument(s): eggs, ham F522.py:4:1: F522 [*] `.format` call has unused named argument(s): eggs, ham
| |
@ -40,6 +41,8 @@ F522.py:4:1: F522 [*] `.format` call has unused named argument(s): eggs, ham
5 | "{bar:{spam}}".format(bar=2, spam=3) # No issues 5 | "{bar:{spam}}".format(bar=2, spam=3) # No issues
6 | "{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522 6 | "{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F522 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F522
7 | # Not fixable
8 | (''
| |
= help: Remove extra named arguments: eggs, ham = help: Remove extra named arguments: eggs, ham
@ -49,5 +52,19 @@ F522.py:4:1: F522 [*] `.format` call has unused named argument(s): eggs, ham
3 3 | "{bar:{spam}}".format(bar=2, spam=3) # No issues 3 3 | "{bar:{spam}}".format(bar=2, spam=3) # No issues
4 |-"{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522 4 |-"{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
4 |+"{bar:{spam}}".format(bar=2, spam=3, ) # F522 4 |+"{bar:{spam}}".format(bar=2, spam=3, ) # F522
5 5 | # Not fixable
6 6 | (''
7 7 | .format(x=2))
F522.py:6:2: F522 `.format` call has unused named argument(s): x
|
6 | "{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
7 | # Not fixable
8 | (''
| __^
9 | | .format(x=2))
| |_____________^ F522
|
= help: Remove extra named arguments: x

View File

@ -377,126 +377,145 @@ F841_3.py:77:25: F841 [*] Local variable `cm` is assigned to but never used
79 79 | 79 79 |
80 80 | 80 80 |
F841_3.py:87:5: F841 [*] Local variable `toplevel` is assigned to but never used F841_3.py:87:26: F841 [*] Local variable `cm` is assigned to but never used
| |
87 | def f(): 87 | def f():
88 | toplevel = tt = lexer.get_token() 88 | with (Nested(m)) as (cm):
| ^^^^^^^^ F841 | ^^ F841
89 | if not tt: 89 | pass
90 | break
| |
= help: Remove assignment to unused variable `toplevel` = help: Remove assignment to unused variable `cm`
Suggested fix Suggested fix
84 84 | 84 84 |
85 85 | 85 85 |
86 86 | def f(): 86 86 | def f():
87 |- toplevel = tt = lexer.get_token() 87 |- with (Nested(m)) as (cm):
87 |+ tt = lexer.get_token() 87 |+ with (Nested(m)):
88 88 | if not tt: 88 88 | pass
89 89 | break 89 89 |
90 90 | 90 90 |
F841_3.py:93:5: F841 [*] Local variable `toplevel` is assigned to but never used F841_3.py:92:5: F841 [*] Local variable `toplevel` is assigned to but never used
| |
93 | def f(): 92 | def f():
94 | toplevel = tt = lexer.get_token() 93 | toplevel = tt = lexer.get_token()
| ^^^^^^^^ F841
94 | if not tt:
95 | break
|
= help: Remove assignment to unused variable `toplevel`
Suggested fix
89 89 |
90 90 |
91 91 | def f():
92 |- toplevel = tt = lexer.get_token()
92 |+ tt = lexer.get_token()
93 93 | if not tt:
94 94 | break
95 95 |
F841_3.py:98:5: F841 [*] Local variable `toplevel` is assigned to but never used
|
98 | def f():
99 | toplevel = tt = lexer.get_token()
| ^^^^^^^^ F841 | ^^^^^^^^ F841
| |
= help: Remove assignment to unused variable `toplevel` = help: Remove assignment to unused variable `toplevel`
Suggested fix Suggested fix
90 90 |
91 91 |
92 92 | def f():
93 |- toplevel = tt = lexer.get_token()
93 |+ tt = lexer.get_token()
94 94 |
95 95 | 95 95 |
96 96 | def f(): 96 96 |
97 97 | def f():
98 |- toplevel = tt = lexer.get_token()
98 |+ tt = lexer.get_token()
99 99 |
100 100 |
101 101 | def f():
F841_3.py:93:16: F841 [*] Local variable `tt` is assigned to but never used F841_3.py:98:16: F841 [*] Local variable `tt` is assigned to but never used
| |
93 | def f(): 98 | def f():
94 | toplevel = tt = lexer.get_token() 99 | toplevel = tt = lexer.get_token()
| ^^ F841 | ^^ F841
| |
= help: Remove assignment to unused variable `tt` = help: Remove assignment to unused variable `tt`
Suggested fix Suggested fix
90 90 |
91 91 |
92 92 | def f():
93 |- toplevel = tt = lexer.get_token()
93 |+ toplevel = lexer.get_token()
94 94 |
95 95 | 95 95 |
96 96 | def f(): 96 96 |
97 97 | def f():
F841_3.py:97:5: F841 [*] Local variable `toplevel` is assigned to but never used 98 |- toplevel = tt = lexer.get_token()
| 98 |+ toplevel = lexer.get_token()
97 | def f():
98 | toplevel = (a, b) = lexer.get_token()
| ^^^^^^^^ F841
|
= help: Remove assignment to unused variable `toplevel`
Suggested fix
94 94 |
95 95 |
96 96 | def f():
97 |- toplevel = (a, b) = lexer.get_token()
97 |+ (a, b) = lexer.get_token()
98 98 |
99 99 | 99 99 |
100 100 | def f(): 100 100 |
101 101 | def f():
F841_3.py:101:14: F841 [*] Local variable `toplevel` is assigned to but never used F841_3.py:102:5: F841 [*] Local variable `toplevel` is assigned to but never used
| |
101 | def f(): 102 | def f():
102 | (a, b) = toplevel = lexer.get_token() 103 | toplevel = (a, b) = lexer.get_token()
| ^^^^^^^^ F841
|
= help: Remove assignment to unused variable `toplevel`
Suggested fix
98 98 |
99 99 |
100 100 | def f():
101 |- (a, b) = toplevel = lexer.get_token()
101 |+ (a, b) = lexer.get_token()
102 102 |
103 103 |
104 104 | def f():
F841_3.py:105:5: F841 [*] Local variable `toplevel` is assigned to but never used
|
105 | def f():
106 | toplevel = tt = 1
| ^^^^^^^^ F841 | ^^^^^^^^ F841
| |
= help: Remove assignment to unused variable `toplevel` = help: Remove assignment to unused variable `toplevel`
Suggested fix Suggested fix
102 102 | 99 99 |
100 100 |
101 101 | def f():
102 |- toplevel = (a, b) = lexer.get_token()
102 |+ (a, b) = lexer.get_token()
103 103 | 103 103 |
104 104 | def f(): 104 104 |
105 |- toplevel = tt = 1 105 105 | def f():
105 |+ tt = 1
F841_3.py:105:16: F841 [*] Local variable `tt` is assigned to but never used F841_3.py:106:14: F841 [*] Local variable `toplevel` is assigned to but never used
| |
105 | def f(): 106 | def f():
106 | toplevel = tt = 1 107 | (a, b) = toplevel = lexer.get_token()
| ^^^^^^^^ F841
|
= help: Remove assignment to unused variable `toplevel`
Suggested fix
103 103 |
104 104 |
105 105 | def f():
106 |- (a, b) = toplevel = lexer.get_token()
106 |+ (a, b) = lexer.get_token()
107 107 |
108 108 |
109 109 | def f():
F841_3.py:110:5: F841 [*] Local variable `toplevel` is assigned to but never used
|
110 | def f():
111 | toplevel = tt = 1
| ^^^^^^^^ F841
|
= help: Remove assignment to unused variable `toplevel`
Suggested fix
107 107 |
108 108 |
109 109 | def f():
110 |- toplevel = tt = 1
110 |+ tt = 1
F841_3.py:110:16: F841 [*] Local variable `tt` is assigned to but never used
|
110 | def f():
111 | toplevel = tt = 1
| ^^ F841 | ^^ F841
| |
= help: Remove assignment to unused variable `tt` = help: Remove assignment to unused variable `tt`
Suggested fix Suggested fix
102 102 | 107 107 |
103 103 | 108 108 |
104 104 | def f(): 109 109 | def f():
105 |- toplevel = tt = 1 110 |- toplevel = tt = 1
105 |+ toplevel = 1 110 |+ toplevel = 1

View File

@ -1,4 +1,4 @@
use rustpython_parser::ast::{Expr, ExprName, Ranged}; use rustpython_parser::ast::{Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
@ -6,7 +6,7 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
/// ## What it does /// ## What it does
/// Checks for iterations over `set` literals and comprehensions. /// Checks for iterations over `set` literals.
/// ///
/// ## Why is this bad? /// ## Why is this bad?
/// Iterating over a `set` is less efficient than iterating over a sequence /// Iterating over a `set` is less efficient than iterating over a sequence
@ -38,23 +38,7 @@ impl Violation for IterationOverSet {
/// PLC0208 /// PLC0208
pub(crate) fn iteration_over_set(checker: &mut Checker, expr: &Expr) { pub(crate) fn iteration_over_set(checker: &mut Checker, expr: &Expr) {
let is_set = match expr { if expr.is_set_expr() {
// Ex) `for i in {1, 2, 3}`
Expr::Set(_) => true,
// Ex)` for i in {n for n in range(1, 4)}`
Expr::SetComp(_) => true,
// Ex) `for i in set(1, 2, 3)`
Expr::Call(call) => {
if let Expr::Name(ExprName { id, .. }) = call.func.as_ref() {
id.as_str() == "set" && checker.semantic_model().is_builtin("set")
} else {
false
}
}
_ => false,
};
if is_set {
checker checker
.diagnostics .diagnostics
.push(Diagnostic::new(IterationOverSet, expr.range())); .push(Diagnostic::new(IterationOverSet, expr.range()));

View File

@ -10,62 +10,44 @@ iteration_over_set.py:3:13: PLC0208 Use a sequence type instead of a `set` when
6 | print(f"I like {item}.") 6 | print(f"I like {item}.")
| |
iteration_over_set.py:6:13: PLC0208 Use a sequence type instead of a `set` when iterating over values iteration_over_set.py:6:28: PLC0208 Use a sequence type instead of a `set` when iterating over values
|
6 | print(f"I like {item}.")
7 |
8 | for item in set(("apples", "lemons", "water")): # flags set() calls
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0208
9 | print(f"I like {item}.")
|
iteration_over_set.py:9:15: PLC0208 Use a sequence type instead of a `set` when iterating over values
| |
9 | print(f"I like {item}.") 6 | print(f"I like {item}.")
10 | 7 |
11 | for number in {i for i in range(10)}: # flags set comprehensions 8 | numbers_list = [i for i in {1, 2, 3}] # flags sets in list comprehensions
| ^^^^^^^^^^^^^^^^^^^^^^ PLC0208
12 | print(number)
|
iteration_over_set.py:12:28: PLC0208 Use a sequence type instead of a `set` when iterating over values
|
12 | print(number)
13 |
14 | numbers_list = [i for i in {1, 2, 3}] # flags sets in list comprehensions
| ^^^^^^^^^ PLC0208 | ^^^^^^^^^ PLC0208
15 | 9 |
16 | numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions 10 | numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions
| |
iteration_over_set.py:14:27: PLC0208 Use a sequence type instead of a `set` when iterating over values iteration_over_set.py:8:27: PLC0208 Use a sequence type instead of a `set` when iterating over values
| |
14 | numbers_list = [i for i in {1, 2, 3}] # flags sets in list comprehensions 8 | numbers_list = [i for i in {1, 2, 3}] # flags sets in list comprehensions
15 | 9 |
16 | numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions 10 | numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions
| ^^^^^^^^^ PLC0208 | ^^^^^^^^^ PLC0208
17 | 11 |
18 | numbers_dict = {str(i): i for i in {1, 2, 3}} # flags sets in dict comprehensions 12 | numbers_dict = {str(i): i for i in {1, 2, 3}} # flags sets in dict comprehensions
| |
iteration_over_set.py:16:36: PLC0208 Use a sequence type instead of a `set` when iterating over values iteration_over_set.py:10:36: PLC0208 Use a sequence type instead of a `set` when iterating over values
| |
16 | numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions 10 | numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions
17 | 11 |
18 | numbers_dict = {str(i): i for i in {1, 2, 3}} # flags sets in dict comprehensions 12 | numbers_dict = {str(i): i for i in {1, 2, 3}} # flags sets in dict comprehensions
| ^^^^^^^^^ PLC0208 | ^^^^^^^^^ PLC0208
19 | 13 |
20 | numbers_gen = (i for i in {1, 2, 3}) # flags sets in generator expressions 14 | numbers_gen = (i for i in {1, 2, 3}) # flags sets in generator expressions
| |
iteration_over_set.py:18:27: PLC0208 Use a sequence type instead of a `set` when iterating over values iteration_over_set.py:12:27: PLC0208 Use a sequence type instead of a `set` when iterating over values
| |
18 | numbers_dict = {str(i): i for i in {1, 2, 3}} # flags sets in dict comprehensions 12 | numbers_dict = {str(i): i for i in {1, 2, 3}} # flags sets in dict comprehensions
19 | 13 |
20 | numbers_gen = (i for i in {1, 2, 3}) # flags sets in generator expressions 14 | numbers_gen = (i for i in {1, 2, 3}) # flags sets in generator expressions
| ^^^^^^^^^ PLC0208 | ^^^^^^^^^ PLC0208
21 | 15 |
22 | # Non-errors 16 | # Non-errors
| |

View File

@ -1,6 +1,6 @@
[package] [package]
name = "ruff_cli" name = "ruff_cli"
version = "0.0.270" version = "0.0.271"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"] authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = { workspace = true } edition = { workspace = true }
rust-version = { workspace = true } rust-version = { workspace = true }

View File

@ -0,0 +1,114 @@
use crate::node::AnyNodeRef;
use ruff_text_size::TextRange;
use rustpython_ast::{
Arguments, Expr, Identifier, Ranged, StmtAsyncFunctionDef, StmtFunctionDef, Suite,
};
/// Enum that represents any python function definition.
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum AnyFunctionDefinition<'a> {
FunctionDefinition(&'a StmtFunctionDef),
AsyncFunctionDefinition(&'a StmtAsyncFunctionDef),
}
impl<'a> AnyFunctionDefinition<'a> {
pub const fn cast_ref(reference: AnyNodeRef<'a>) -> Option<Self> {
match reference {
AnyNodeRef::StmtAsyncFunctionDef(definition) => {
Some(Self::AsyncFunctionDefinition(definition))
}
AnyNodeRef::StmtFunctionDef(definition) => Some(Self::FunctionDefinition(definition)),
_ => None,
}
}
/// Returns `Some` if this is a [`StmtFunctionDef`] and `None` otherwise.
pub const fn as_function_definition(self) -> Option<&'a StmtFunctionDef> {
if let Self::FunctionDefinition(definition) = self {
Some(definition)
} else {
None
}
}
/// Returns `Some` if this is a [`StmtAsyncFunctionDef`] and `None` otherwise.
pub const fn as_async_function_definition(self) -> Option<&'a StmtAsyncFunctionDef> {
if let Self::AsyncFunctionDefinition(definition) = self {
Some(definition)
} else {
None
}
}
/// Returns the function's name
pub const fn name(self) -> &'a Identifier {
match self {
Self::FunctionDefinition(definition) => &definition.name,
Self::AsyncFunctionDefinition(definition) => &definition.name,
}
}
/// Returns the function arguments (parameters).
pub fn arguments(self) -> &'a Arguments {
match self {
Self::FunctionDefinition(definition) => definition.args.as_ref(),
Self::AsyncFunctionDefinition(definition) => definition.args.as_ref(),
}
}
/// Returns the function's body
pub const fn body(self) -> &'a Suite {
match self {
Self::FunctionDefinition(definition) => &definition.body,
Self::AsyncFunctionDefinition(definition) => &definition.body,
}
}
/// Returns the decorators attributing the function.
pub fn decorators(self) -> &'a [Expr] {
match self {
Self::FunctionDefinition(definition) => &definition.decorator_list,
Self::AsyncFunctionDefinition(definition) => &definition.decorator_list,
}
}
pub fn returns(self) -> Option<&'a Expr> {
match self {
Self::FunctionDefinition(definition) => definition.returns.as_deref(),
Self::AsyncFunctionDefinition(definition) => definition.returns.as_deref(),
}
}
pub fn type_comments(self) -> Option<&'a str> {
match self {
Self::FunctionDefinition(definition) => definition.type_comment.as_deref(),
Self::AsyncFunctionDefinition(definition) => definition.type_comment.as_deref(),
}
}
/// Returns `true` if this is [`Self::AsyncFunctionDefinition`]
pub const fn is_async(self) -> bool {
matches!(self, Self::AsyncFunctionDefinition(_))
}
}
impl Ranged for AnyFunctionDefinition<'_> {
fn range(&self) -> TextRange {
match self {
AnyFunctionDefinition::FunctionDefinition(definition) => definition.range(),
AnyFunctionDefinition::AsyncFunctionDefinition(definition) => definition.range(),
}
}
}
impl<'a> From<&'a StmtFunctionDef> for AnyFunctionDefinition<'a> {
fn from(value: &'a StmtFunctionDef) -> Self {
Self::FunctionDefinition(value)
}
}
impl<'a> From<&'a StmtAsyncFunctionDef> for AnyFunctionDefinition<'a> {
fn from(value: &'a StmtAsyncFunctionDef) -> Self {
Self::AsyncFunctionDefinition(value)
}
}

View File

@ -2,6 +2,7 @@ pub mod all;
pub mod call_path; pub mod call_path;
pub mod cast; pub mod cast;
pub mod comparable; pub mod comparable;
pub mod function;
pub mod hashable; pub mod hashable;
pub mod helpers; pub mod helpers;
pub mod imports; pub mod imports;

View File

@ -0,0 +1,43 @@
(aaaaaaaa
+ # trailing operator comment
b # trailing right comment
)
(aaaaaaaa # trailing left comment
+ # trailing operator comment
# leading right comment
b
)
# Black breaks the right side first for the following expressions:
aaaaaaaaaaaaaa + caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaal(argument1, argument2, argument3)
aaaaaaaaaaaaaa + [bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eeeeeee]
aaaaaaaaaaaaaa + (bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eeeeeee)
aaaaaaaaaaaaaa + { key1:bbbbbbbbbbbbbbbbbbbbbb, key2: ccccccccccccccccccccc, key3: dddddddddddddddd, key4: eeeeeee }
aaaaaaaaaaaaaa + { bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eeeeeee }
aaaaaaaaaaaaaa + [a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ]
aaaaaaaaaaaaaa + (a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb )
aaaaaaaaaaaaaa + {a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb}
# Wraps it in parentheses if it needs to break both left and right
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + [
bbbbbbbbbbbbbbbbbbbbbb,
ccccccccccccccccccccc,
dddddddddddddddd,
eee
] # comment
# But only for expressions that have a statement parent.
not (aaaaaaaaaaaaaa + {a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb})
[a + [bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] in c ]
# leading comment
(
# comment
content + b
)

View File

@ -12,7 +12,7 @@ pub(crate) trait PyFormatterExtensions<'ast, 'buf> {
/// ///
/// * [`NodeLevel::Module`]: Up to two empty lines /// * [`NodeLevel::Module`]: Up to two empty lines
/// * [`NodeLevel::CompoundStatement`]: Up to one empty line /// * [`NodeLevel::CompoundStatement`]: Up to one empty line
/// * [`NodeLevel::Parenthesized`]: No empty lines /// * [`NodeLevel::Expression`]: No empty lines
fn join_nodes<'fmt>(&'fmt mut self, level: NodeLevel) -> JoinNodesBuilder<'fmt, 'ast, 'buf>; fn join_nodes<'fmt>(&'fmt mut self, level: NodeLevel) -> JoinNodesBuilder<'fmt, 'ast, 'buf>;
} }
@ -48,18 +48,18 @@ impl<'fmt, 'ast, 'buf> JoinNodesBuilder<'fmt, 'ast, 'buf> {
{ {
let node_level = self.node_level; let node_level = self.node_level;
let separator = format_with(|f: &mut PyFormatter| match node_level { let separator = format_with(|f: &mut PyFormatter| match node_level {
NodeLevel::TopLevel => match lines_before(f.context().contents(), node.start()) { NodeLevel::TopLevel => match lines_before(node.start(), f.context().contents()) {
0 | 1 => hard_line_break().fmt(f), 0 | 1 => hard_line_break().fmt(f),
2 => empty_line().fmt(f), 2 => empty_line().fmt(f),
_ => write!(f, [empty_line(), empty_line()]), _ => write!(f, [empty_line(), empty_line()]),
}, },
NodeLevel::CompoundStatement => { NodeLevel::CompoundStatement => {
match lines_before(f.context().contents(), node.start()) { match lines_before(node.start(), f.context().contents()) {
0 | 1 => hard_line_break().fmt(f), 0 | 1 => hard_line_break().fmt(f),
_ => empty_line().fmt(f), _ => empty_line().fmt(f),
} }
} }
NodeLevel::Parenthesized => hard_line_break().fmt(f), NodeLevel::Expression => hard_line_break().fmt(f),
}); });
self.entry_with_separator(&separator, content); self.entry_with_separator(&separator, content);
@ -200,7 +200,7 @@ no_leading_newline = 30"#
// Removes all empty lines // Removes all empty lines
#[test] #[test]
fn ranged_builder_parenthesized_level() { fn ranged_builder_parenthesized_level() {
let printed = format_ranged(NodeLevel::Parenthesized); let printed = format_ranged(NodeLevel::Expression);
assert_eq!( assert_eq!(
&printed, &printed,

View File

@ -39,7 +39,7 @@ impl Format<PyFormatContext<'_>> for FormatLeadingComments<'_> {
for comment in leading_comments { for comment in leading_comments {
let slice = comment.slice(); let slice = comment.slice();
let lines_after_comment = lines_after(f.context().contents(), slice.end()); let lines_after_comment = lines_after(slice.end(), f.context().contents());
write!( write!(
f, f,
[format_comment(comment), empty_lines(lines_after_comment)] [format_comment(comment), empty_lines(lines_after_comment)]
@ -80,7 +80,7 @@ impl Format<PyFormatContext<'_>> for FormatLeadingAlternateBranchComments<'_> {
if let Some(first_leading) = self.comments.first() { if let Some(first_leading) = self.comments.first() {
// Leading comments only preserves the lines after the comment but not before. // Leading comments only preserves the lines after the comment but not before.
// Insert the necessary lines. // Insert the necessary lines.
if lines_before(f.context().contents(), first_leading.slice().start()) > 1 { if lines_before(first_leading.slice().start(), f.context().contents()) > 1 {
write!(f, [empty_line()])?; write!(f, [empty_line()])?;
} }
@ -88,7 +88,7 @@ impl Format<PyFormatContext<'_>> for FormatLeadingAlternateBranchComments<'_> {
} else if let Some(last_preceding) = self.last_node { } else if let Some(last_preceding) = self.last_node {
// The leading comments formatting ensures that it preserves the right amount of lines after // The leading comments formatting ensures that it preserves the right amount of lines after
// We need to take care of this ourselves, if there's no leading `else` comment. // We need to take care of this ourselves, if there's no leading `else` comment.
if lines_after(f.context().contents(), last_preceding.end()) > 1 { if lines_after(last_preceding.end(), f.context().contents()) > 1 {
write!(f, [empty_line()])?; write!(f, [empty_line()])?;
} }
} }
@ -132,7 +132,7 @@ impl Format<PyFormatContext<'_>> for FormatTrailingComments<'_> {
has_trailing_own_line_comment |= trailing.position().is_own_line(); has_trailing_own_line_comment |= trailing.position().is_own_line();
if has_trailing_own_line_comment { if has_trailing_own_line_comment {
let lines_before_comment = lines_before(f.context().contents(), slice.start()); let lines_before_comment = lines_before(slice.start(), f.context().contents());
// A trailing comment at the end of a body or list // A trailing comment at the end of a body or list
// ```python // ```python
@ -175,20 +175,26 @@ pub(crate) fn dangling_node_comments<T>(node: &T) -> FormatDanglingComments
where where
T: AstNode, T: AstNode,
{ {
FormatDanglingComments { FormatDanglingComments::Node(node.as_any_node_ref())
node: node.as_any_node_ref(),
}
} }
pub(crate) struct FormatDanglingComments<'a> { pub(crate) fn dangling_comments(comments: &[SourceComment]) -> FormatDanglingComments {
node: AnyNodeRef<'a>, FormatDanglingComments::Comments(comments)
}
pub(crate) enum FormatDanglingComments<'a> {
Node(AnyNodeRef<'a>),
Comments(&'a [SourceComment]),
} }
impl Format<PyFormatContext<'_>> for FormatDanglingComments<'_> { impl Format<PyFormatContext<'_>> for FormatDanglingComments<'_> {
fn fmt(&self, f: &mut Formatter<PyFormatContext>) -> FormatResult<()> { fn fmt(&self, f: &mut Formatter<PyFormatContext>) -> FormatResult<()> {
let comments = f.context().comments().clone(); let comments = f.context().comments().clone();
let dangling_comments = comments.dangling_comments(self.node); let dangling_comments = match self {
Self::Comments(comments) => comments,
Self::Node(node) => comments.dangling_comments(*node),
};
let mut first = true; let mut first = true;
for comment in dangling_comments { for comment in dangling_comments {
@ -200,7 +206,7 @@ impl Format<PyFormatContext<'_>> for FormatDanglingComments<'_> {
f, f,
[ [
format_comment(comment), format_comment(comment),
empty_lines(lines_after(f.context().contents(), comment.slice().end())) empty_lines(lines_after(comment.slice().end(), f.context().contents()))
] ]
)?; )?;
@ -301,7 +307,7 @@ impl Format<PyFormatContext<'_>> for FormatEmptyLines {
}, },
// Remove all whitespace in parenthesized expressions // Remove all whitespace in parenthesized expressions
NodeLevel::Parenthesized => write!(f, [hard_line_break()]), NodeLevel::Expression => write!(f, [hard_line_break()]),
} }
} }
} }

View File

@ -103,8 +103,8 @@ use crate::comments::map::MultiMap;
use crate::comments::node_key::NodeRefEqualityKey; use crate::comments::node_key::NodeRefEqualityKey;
use crate::comments::visitor::CommentsVisitor; use crate::comments::visitor::CommentsVisitor;
pub(crate) use format::{ pub(crate) use format::{
dangling_node_comments, leading_alternate_branch_comments, leading_node_comments, dangling_comments, dangling_node_comments, leading_alternate_branch_comments,
trailing_comments, trailing_node_comments, leading_node_comments, trailing_comments, trailing_node_comments,
}; };
use ruff_formatter::{SourceCode, SourceCodeSlice}; use ruff_formatter::{SourceCode, SourceCodeSlice};
use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::node::AnyNodeRef;

View File

@ -520,8 +520,8 @@ fn handle_trailing_end_of_line_condition_comment<'a>(
if preceding.ptr_eq(last_before_colon) { if preceding.ptr_eq(last_before_colon) {
let mut start = preceding.end(); let mut start = preceding.end();
while let Some((offset, c)) = find_first_non_trivia_character_in_range( while let Some((offset, c)) = find_first_non_trivia_character_in_range(
locator.contents(),
TextRange::new(start, following.start()), TextRange::new(start, following.start()),
locator.contents(),
) { ) {
match c { match c {
':' => { ':' => {
@ -655,7 +655,7 @@ fn handle_trailing_binary_expression_left_or_operator_comment<'a>(
); );
let operator_offset = loop { let operator_offset = loop {
match find_first_non_trivia_character_in_range(locator.contents(), between_operands_range) { match find_first_non_trivia_character_in_range(between_operands_range, locator.contents()) {
// Skip over closing parens // Skip over closing parens
Some((offset, ')')) => { Some((offset, ')')) => {
between_operands_range = between_operands_range =
@ -733,17 +733,17 @@ fn find_pos_only_slash_offset(
locator: &Locator, locator: &Locator,
) -> Option<TextSize> { ) -> Option<TextSize> {
// First find the comma separating the two arguments // First find the comma separating the two arguments
find_first_non_trivia_character_in_range(locator.contents(), between_arguments_range).and_then( find_first_non_trivia_character_in_range(between_arguments_range, locator.contents()).and_then(
|(comma_offset, comma)| { |(comma_offset, comma)| {
debug_assert_eq!(comma, ','); debug_assert_eq!(comma, ',');
// Then find the position of the `/` operator // Then find the position of the `/` operator
find_first_non_trivia_character_in_range( find_first_non_trivia_character_in_range(
locator.contents(),
TextRange::new( TextRange::new(
comma_offset + TextSize::new(1), comma_offset + TextSize::new(1),
between_arguments_range.end(), between_arguments_range.end(),
), ),
locator.contents(),
) )
.map(|(offset, c)| { .map(|(offset, c)| {
debug_assert_eq!(c, '/'); debug_assert_eq!(c, '/');

View File

@ -82,5 +82,5 @@ pub(crate) enum NodeLevel {
CompoundStatement, CompoundStatement,
/// Formatting nodes that are enclosed in a parenthesized expression. /// Formatting nodes that are enclosed in a parenthesized expression.
Parenthesized, Expression,
} }

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprAttribute; use rustpython_parser::ast::ExprAttribute;
@ -10,3 +13,9 @@ impl FormatNodeRule<ExprAttribute> for FormatExprAttribute {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprAttribute {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
default_expression_needs_parentheses(self.into(), parenthesize, source)
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprAwait; use rustpython_parser::ast::ExprAwait;
@ -10,3 +13,9 @@ impl FormatNodeRule<ExprAwait> for FormatExprAwait {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprAwait {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
default_expression_needs_parentheses(self.into(), parenthesize, source)
}
}

View File

@ -1,12 +1,215 @@
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::comments::trailing_comments;
use ruff_formatter::{write, Buffer, FormatResult}; use crate::expression::parentheses::{
use rustpython_parser::ast::ExprBinOp; default_expression_needs_parentheses, NeedsParentheses, Parenthesize,
};
use crate::expression::Parentheses;
use crate::prelude::*;
use crate::FormatNodeRule;
use ruff_formatter::{
format_args, write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions,
};
use ruff_python_ast::node::AstNode;
use rustpython_parser::ast::{
Constant, Expr, ExprAttribute, ExprBinOp, ExprConstant, ExprUnaryOp, Operator, Unaryop,
};
#[derive(Default)] #[derive(Default)]
pub struct FormatExprBinOp; pub struct FormatExprBinOp {
parentheses: Option<Parentheses>,
}
impl FormatRuleWithOptions<ExprBinOp, PyFormatContext<'_>> for FormatExprBinOp {
type Options = Option<Parentheses>;
fn with_options(mut self, options: Self::Options) -> Self {
self.parentheses = options;
self
}
}
impl FormatNodeRule<ExprBinOp> for FormatExprBinOp { impl FormatNodeRule<ExprBinOp> for FormatExprBinOp {
fn fmt_fields(&self, item: &ExprBinOp, f: &mut PyFormatter) -> FormatResult<()> { fn fmt_fields(&self, item: &ExprBinOp, f: &mut PyFormatter) -> FormatResult<()> {
write!(f, [verbatim_text(item.range)]) let ExprBinOp {
left,
right,
op,
range: _,
} = item;
let should_break_right = self.parentheses == Some(Parentheses::Custom);
if should_break_right {
let left = left.format().memoized();
let right = right.format().memoized();
write!(
f,
[best_fitting![
// The whole expression on a single line
format_args![left, space(), op.format(), space(), right],
// Break the right, but keep the left flat
format_args![
left,
space(),
op.format(),
space(),
group(&right).should_expand(true),
],
// Break after the operator, try to keep the right flat, otherwise expand it
format_args![
text("("),
block_indent(&format_args![
left,
hard_line_break(),
op.format(),
space(),
group(&right),
]),
text(")")
],
]]
)
} else {
let comments = f.context().comments().clone();
let operator_comments = comments.dangling_comments(item.as_any_node_ref());
let needs_space = !is_simple_power_expression(item);
let before_operator_space = if needs_space {
soft_line_break_or_space()
} else {
soft_line_break()
};
write!(
f,
[
left.format(),
before_operator_space,
op.format(),
trailing_comments(operator_comments),
]
)?;
// Format the operator on its own line if the operator has trailing comments and the right side has leading comments.
if !operator_comments.is_empty() && comments.has_leading_comments(right.as_ref().into())
{
write!(f, [hard_line_break()])?;
} else if needs_space {
write!(f, [space()])?;
}
write!(f, [group(&right.format())])
}
}
fn fmt_dangling_comments(&self, _node: &ExprBinOp, _f: &mut PyFormatter) -> FormatResult<()> {
// Handled inside of `fmt_fields`
Ok(())
}
}
const fn is_simple_power_expression(expr: &ExprBinOp) -> bool {
expr.op.is_pow() && is_simple_power_operand(&expr.left) && is_simple_power_operand(&expr.right)
}
/// Return `true` if an [`Expr`] adheres to [Black's definition](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#line-breaks-binary-operators)
/// of a non-complex expression, in the context of a power operation.
const fn is_simple_power_operand(expr: &Expr) -> bool {
match expr {
Expr::UnaryOp(ExprUnaryOp {
op: Unaryop::Not, ..
}) => false,
Expr::Constant(ExprConstant {
value: Constant::Complex { .. } | Constant::Float(_) | Constant::Int(_),
..
}) => true,
Expr::Name(_) => true,
Expr::UnaryOp(ExprUnaryOp { operand, .. }) => is_simple_power_operand(operand),
Expr::Attribute(ExprAttribute { value, .. }) => is_simple_power_operand(value),
_ => false,
}
}
#[derive(Copy, Clone)]
pub struct FormatOperator;
impl<'ast> AsFormat<PyFormatContext<'ast>> for Operator {
type Format<'a> = FormatRefWithRule<'a, Operator, FormatOperator, PyFormatContext<'ast>>;
fn format(&self) -> Self::Format<'_> {
FormatRefWithRule::new(self, FormatOperator)
}
}
impl<'ast> IntoFormat<PyFormatContext<'ast>> for Operator {
type Format = FormatOwnedWithRule<Operator, FormatOperator, PyFormatContext<'ast>>;
fn into_format(self) -> Self::Format {
FormatOwnedWithRule::new(self, FormatOperator)
}
}
impl FormatRule<Operator, PyFormatContext<'_>> for FormatOperator {
fn fmt(&self, item: &Operator, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
let operator = match item {
Operator::Add => "+",
Operator::Sub => "-",
Operator::Mult => "*",
Operator::MatMult => "@",
Operator::Div => "/",
Operator::Mod => "%",
Operator::Pow => "**",
Operator::LShift => "<<",
Operator::RShift => ">>",
Operator::BitOr => "|",
Operator::BitXor => "^",
Operator::BitAnd => "&",
Operator::FloorDiv => "//",
};
text(operator).fmt(f)
}
}
impl NeedsParentheses for ExprBinOp {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
match default_expression_needs_parentheses(self.into(), parenthesize, source) {
Parentheses::Optional => {
if should_binary_break_right_side_first(self) {
Parentheses::Custom
} else {
Parentheses::Optional
}
}
parentheses => parentheses,
}
}
}
pub(super) fn should_binary_break_right_side_first(expr: &ExprBinOp) -> bool {
use ruff_python_ast::prelude::*;
if expr.left.is_bin_op_expr() {
false
} else {
match expr.right.as_ref() {
Expr::Tuple(ExprTuple {
elts: expressions, ..
})
| Expr::List(ExprList {
elts: expressions, ..
})
| Expr::Set(ExprSet {
elts: expressions, ..
})
| Expr::Dict(ExprDict {
values: expressions,
..
}) => !expressions.is_empty(),
Expr::Call(ExprCall { args, keywords, .. }) => !args.is_empty() && !keywords.is_empty(),
Expr::ListComp(_) | Expr::SetComp(_) | Expr::DictComp(_) | Expr::GeneratorExp(_) => {
true
}
_ => false,
}
} }
} }

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprBoolOp; use rustpython_parser::ast::ExprBoolOp;
@ -10,3 +13,9 @@ impl FormatNodeRule<ExprBoolOp> for FormatExprBoolOp {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprBoolOp {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
default_expression_needs_parentheses(self.into(), parenthesize, source)
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprCall; use rustpython_parser::ast::ExprCall;
@ -10,3 +13,12 @@ impl FormatNodeRule<ExprCall> for FormatExprCall {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprCall {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
match default_expression_needs_parentheses(self.into(), parenthesize, source) {
Parentheses::Optional => Parentheses::Never,
parentheses => parentheses,
}
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprCompare; use rustpython_parser::ast::ExprCompare;
@ -10,3 +13,9 @@ impl FormatNodeRule<ExprCompare> for FormatExprCompare {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprCompare {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
default_expression_needs_parentheses(self.into(), parenthesize, source)
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprConstant; use rustpython_parser::ast::ExprConstant;
@ -10,3 +13,9 @@ impl FormatNodeRule<ExprConstant> for FormatExprConstant {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprConstant {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
default_expression_needs_parentheses(self.into(), parenthesize, source)
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprDict; use rustpython_parser::ast::ExprDict;
@ -10,3 +13,12 @@ impl FormatNodeRule<ExprDict> for FormatExprDict {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprDict {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
match default_expression_needs_parentheses(self.into(), parenthesize, source) {
Parentheses::Optional => Parentheses::Never,
parentheses => parentheses,
}
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprDictComp; use rustpython_parser::ast::ExprDictComp;
@ -10,3 +13,12 @@ impl FormatNodeRule<ExprDictComp> for FormatExprDictComp {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprDictComp {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
match default_expression_needs_parentheses(self.into(), parenthesize, source) {
Parentheses::Optional => Parentheses::Never,
parentheses => parentheses,
}
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprFormattedValue; use rustpython_parser::ast::ExprFormattedValue;
@ -10,3 +13,9 @@ impl FormatNodeRule<ExprFormattedValue> for FormatExprFormattedValue {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprFormattedValue {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
default_expression_needs_parentheses(self.into(), parenthesize, source)
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprGeneratorExp; use rustpython_parser::ast::ExprGeneratorExp;
@ -10,3 +13,12 @@ impl FormatNodeRule<ExprGeneratorExp> for FormatExprGeneratorExp {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprGeneratorExp {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
match default_expression_needs_parentheses(self.into(), parenthesize, source) {
Parentheses::Optional => Parentheses::Never,
parentheses => parentheses,
}
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprIfExp; use rustpython_parser::ast::ExprIfExp;
@ -10,3 +13,9 @@ impl FormatNodeRule<ExprIfExp> for FormatExprIfExp {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprIfExp {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
default_expression_needs_parentheses(self.into(), parenthesize, source)
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprJoinedStr; use rustpython_parser::ast::ExprJoinedStr;
@ -10,3 +13,9 @@ impl FormatNodeRule<ExprJoinedStr> for FormatExprJoinedStr {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprJoinedStr {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
default_expression_needs_parentheses(self.into(), parenthesize, source)
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprLambda; use rustpython_parser::ast::ExprLambda;
@ -10,3 +13,9 @@ impl FormatNodeRule<ExprLambda> for FormatExprLambda {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprLambda {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
default_expression_needs_parentheses(self.into(), parenthesize, source)
}
}

View File

@ -1,5 +1,10 @@
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::comments::dangling_comments;
use ruff_formatter::{write, Buffer, FormatResult}; use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::prelude::*;
use crate::FormatNodeRule;
use ruff_formatter::{format_args, write};
use rustpython_parser::ast::ExprList; use rustpython_parser::ast::ExprList;
#[derive(Default)] #[derive(Default)]
@ -7,6 +12,55 @@ pub struct FormatExprList;
impl FormatNodeRule<ExprList> for FormatExprList { impl FormatNodeRule<ExprList> for FormatExprList {
fn fmt_fields(&self, item: &ExprList, f: &mut PyFormatter) -> FormatResult<()> { fn fmt_fields(&self, item: &ExprList, f: &mut PyFormatter) -> FormatResult<()> {
write!(f, [verbatim_text(item.range)]) let ExprList {
range: _,
elts,
ctx: _,
} = item;
let items = format_with(|f| {
let mut iter = elts.iter();
if let Some(first) = iter.next() {
write!(f, [first.format()])?;
}
for item in iter {
write!(f, [text(","), soft_line_break_or_space(), item.format()])?;
}
if !elts.is_empty() {
write!(f, [if_group_breaks(&text(","))])?;
}
Ok(())
});
let comments = f.context().comments().clone();
let dangling = comments.dangling_comments(item.into());
write!(
f,
[group(&format_args![
text("["),
dangling_comments(dangling),
soft_block_indent(&items),
text("]")
])]
)
}
fn fmt_dangling_comments(&self, _node: &ExprList, _f: &mut PyFormatter) -> FormatResult<()> {
// Handled as part of `fmt_fields`
Ok(())
}
}
impl NeedsParentheses for ExprList {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
match default_expression_needs_parentheses(self.into(), parenthesize, source) {
Parentheses::Optional => Parentheses::Never,
parentheses => parentheses,
}
} }
} }

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprListComp; use rustpython_parser::ast::ExprListComp;
@ -10,3 +13,12 @@ impl FormatNodeRule<ExprListComp> for FormatExprListComp {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprListComp {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
match default_expression_needs_parentheses(self.into(), parenthesize, source) {
Parentheses::Optional => Parentheses::Never,
parentheses => parentheses,
}
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::prelude::*; use crate::prelude::*;
use crate::FormatNodeRule; use crate::FormatNodeRule;
use ruff_formatter::{write, FormatContext}; use ruff_formatter::{write, FormatContext};
@ -22,6 +25,12 @@ impl FormatNodeRule<ExprName> for FormatExprName {
} }
} }
impl NeedsParentheses for ExprName {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
default_expression_needs_parentheses(self.into(), parenthesize, source)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprNamedExpr; use rustpython_parser::ast::ExprNamedExpr;
@ -10,3 +13,9 @@ impl FormatNodeRule<ExprNamedExpr> for FormatExprNamedExpr {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprNamedExpr {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
default_expression_needs_parentheses(self.into(), parenthesize, source)
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprSet; use rustpython_parser::ast::ExprSet;
@ -10,3 +13,12 @@ impl FormatNodeRule<ExprSet> for FormatExprSet {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprSet {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
match default_expression_needs_parentheses(self.into(), parenthesize, source) {
Parentheses::Optional => Parentheses::Never,
parentheses => parentheses,
}
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprSetComp; use rustpython_parser::ast::ExprSetComp;
@ -10,3 +13,12 @@ impl FormatNodeRule<ExprSetComp> for FormatExprSetComp {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprSetComp {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
match default_expression_needs_parentheses(self.into(), parenthesize, source) {
Parentheses::Optional => Parentheses::Never,
parentheses => parentheses,
}
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprSlice; use rustpython_parser::ast::ExprSlice;
@ -10,3 +13,9 @@ impl FormatNodeRule<ExprSlice> for FormatExprSlice {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprSlice {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
default_expression_needs_parentheses(self.into(), parenthesize, source)
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprStarred; use rustpython_parser::ast::ExprStarred;
@ -10,3 +13,9 @@ impl FormatNodeRule<ExprStarred> for FormatExprStarred {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprStarred {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
default_expression_needs_parentheses(self.into(), parenthesize, source)
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprSubscript; use rustpython_parser::ast::ExprSubscript;
@ -10,3 +13,9 @@ impl FormatNodeRule<ExprSubscript> for FormatExprSubscript {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprSubscript {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
default_expression_needs_parentheses(self.into(), parenthesize, source)
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprTuple; use rustpython_parser::ast::ExprTuple;
@ -10,3 +13,12 @@ impl FormatNodeRule<ExprTuple> for FormatExprTuple {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprTuple {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
match default_expression_needs_parentheses(self.into(), parenthesize, source) {
Parentheses::Optional => Parentheses::Never,
parentheses => parentheses,
}
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprUnaryOp; use rustpython_parser::ast::ExprUnaryOp;
@ -10,3 +13,9 @@ impl FormatNodeRule<ExprUnaryOp> for FormatExprUnaryOp {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprUnaryOp {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
default_expression_needs_parentheses(self.into(), parenthesize, source)
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprYield; use rustpython_parser::ast::ExprYield;
@ -10,3 +13,9 @@ impl FormatNodeRule<ExprYield> for FormatExprYield {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprYield {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
default_expression_needs_parentheses(self.into(), parenthesize, source)
}
}

View File

@ -1,3 +1,6 @@
use crate::expression::parentheses::{
default_expression_needs_parentheses, NeedsParentheses, Parentheses, Parenthesize,
};
use crate::{verbatim_text, FormatNodeRule, PyFormatter}; use crate::{verbatim_text, FormatNodeRule, PyFormatter};
use ruff_formatter::{write, Buffer, FormatResult}; use ruff_formatter::{write, Buffer, FormatResult};
use rustpython_parser::ast::ExprYieldFrom; use rustpython_parser::ast::ExprYieldFrom;
@ -10,3 +13,9 @@ impl FormatNodeRule<ExprYieldFrom> for FormatExprYieldFrom {
write!(f, [verbatim_text(item.range)]) write!(f, [verbatim_text(item.range)])
} }
} }
impl NeedsParentheses for ExprYieldFrom {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
default_expression_needs_parentheses(self.into(), parenthesize, source)
}
}

View File

@ -1,53 +0,0 @@
use crate::context::NodeLevel;
use crate::prelude::*;
use ruff_formatter::{format_args, write};
use rustpython_parser::ast::Expr;
/// Formats the passed expression. Adds parentheses if the expression doesn't fit on a line.
pub(crate) const fn maybe_parenthesize(expression: &Expr) -> MaybeParenthesize {
MaybeParenthesize { expression }
}
pub(crate) struct MaybeParenthesize<'a> {
expression: &'a Expr,
}
impl Format<PyFormatContext<'_>> for MaybeParenthesize<'_> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
let saved_level = f.context().node_level();
f.context_mut().set_node_level(NodeLevel::Parenthesized);
let result = if needs_parentheses(self.expression) {
write!(
f,
[group(&format_args![
if_group_breaks(&text("(")),
soft_block_indent(&self.expression.format()),
if_group_breaks(&text(")"))
])]
)
} else {
// Don't add parentheses around expressions that have parentheses on their own (e.g. list, dict, tuple, call expression)
self.expression.format().fmt(f)
};
f.context_mut().set_node_level(saved_level);
result
}
}
const fn needs_parentheses(expr: &Expr) -> bool {
!matches!(
expr,
Expr::Tuple(_)
| Expr::List(_)
| Expr::Set(_)
| Expr::Dict(_)
| Expr::ListComp(_)
| Expr::SetComp(_)
| Expr::DictComp(_)
| Expr::GeneratorExp(_)
| Expr::Call(_)
)
}

View File

@ -1,5 +1,9 @@
use crate::context::NodeLevel;
use crate::expression::parentheses::{NeedsParentheses, Parentheses, Parenthesize};
use crate::prelude::*; use crate::prelude::*;
use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule, FormatRule}; use ruff_formatter::{
format_args, write, FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions,
};
use rustpython_parser::ast::Expr; use rustpython_parser::ast::Expr;
pub(crate) mod expr_attribute; pub(crate) mod expr_attribute;
@ -29,17 +33,30 @@ pub(crate) mod expr_tuple;
pub(crate) mod expr_unary_op; pub(crate) mod expr_unary_op;
pub(crate) mod expr_yield; pub(crate) mod expr_yield;
pub(crate) mod expr_yield_from; pub(crate) mod expr_yield_from;
pub(crate) mod maybe_parenthesize; pub(crate) mod parentheses;
#[derive(Default)] #[derive(Default)]
pub struct FormatExpr; pub struct FormatExpr {
parenthesize: Parenthesize,
}
impl FormatRuleWithOptions<Expr, PyFormatContext<'_>> for FormatExpr {
type Options = Parenthesize;
fn with_options(mut self, options: Self::Options) -> Self {
self.parenthesize = options;
self
}
}
impl FormatRule<Expr, PyFormatContext<'_>> for FormatExpr { impl FormatRule<Expr, PyFormatContext<'_>> for FormatExpr {
fn fmt(&self, item: &Expr, f: &mut PyFormatter) -> FormatResult<()> { fn fmt(&self, item: &Expr, f: &mut PyFormatter) -> FormatResult<()> {
match item { let parentheses = item.needs_parentheses(self.parenthesize, f.context().contents());
let format_expr = format_with(|f| match item {
Expr::BoolOp(expr) => expr.format().fmt(f), Expr::BoolOp(expr) => expr.format().fmt(f),
Expr::NamedExpr(expr) => expr.format().fmt(f), Expr::NamedExpr(expr) => expr.format().fmt(f),
Expr::BinOp(expr) => expr.format().fmt(f), Expr::BinOp(expr) => expr.format().with_options(Some(parentheses)).fmt(f),
Expr::UnaryOp(expr) => expr.format().fmt(f), Expr::UnaryOp(expr) => expr.format().fmt(f),
Expr::Lambda(expr) => expr.format().fmt(f), Expr::Lambda(expr) => expr.format().fmt(f),
Expr::IfExp(expr) => expr.format().fmt(f), Expr::IfExp(expr) => expr.format().fmt(f),
@ -64,6 +81,72 @@ impl FormatRule<Expr, PyFormatContext<'_>> for FormatExpr {
Expr::List(expr) => expr.format().fmt(f), Expr::List(expr) => expr.format().fmt(f),
Expr::Tuple(expr) => expr.format().fmt(f), Expr::Tuple(expr) => expr.format().fmt(f),
Expr::Slice(expr) => expr.format().fmt(f), Expr::Slice(expr) => expr.format().fmt(f),
});
let saved_level = f.context().node_level();
f.context_mut().set_node_level(NodeLevel::Expression);
let result = match parentheses {
Parentheses::Always => {
write!(
f,
[group(&format_args![
text("("),
soft_block_indent(&format_expr),
text(")")
])]
)
}
// Add optional parentheses. Ignore if the item renders parentheses itself.
Parentheses::Optional => {
write!(
f,
[group(&format_args![
if_group_breaks(&text("(")),
soft_block_indent(&format_expr),
if_group_breaks(&text(")"))
])]
)
}
Parentheses::Custom | Parentheses::Never => Format::fmt(&format_expr, f),
};
f.context_mut().set_node_level(saved_level);
result
}
}
impl NeedsParentheses for Expr {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses {
match self {
Expr::BoolOp(expr) => expr.needs_parentheses(parenthesize, source),
Expr::NamedExpr(expr) => expr.needs_parentheses(parenthesize, source),
Expr::BinOp(expr) => expr.needs_parentheses(parenthesize, source),
Expr::UnaryOp(expr) => expr.needs_parentheses(parenthesize, source),
Expr::Lambda(expr) => expr.needs_parentheses(parenthesize, source),
Expr::IfExp(expr) => expr.needs_parentheses(parenthesize, source),
Expr::Dict(expr) => expr.needs_parentheses(parenthesize, source),
Expr::Set(expr) => expr.needs_parentheses(parenthesize, source),
Expr::ListComp(expr) => expr.needs_parentheses(parenthesize, source),
Expr::SetComp(expr) => expr.needs_parentheses(parenthesize, source),
Expr::DictComp(expr) => expr.needs_parentheses(parenthesize, source),
Expr::GeneratorExp(expr) => expr.needs_parentheses(parenthesize, source),
Expr::Await(expr) => expr.needs_parentheses(parenthesize, source),
Expr::Yield(expr) => expr.needs_parentheses(parenthesize, source),
Expr::YieldFrom(expr) => expr.needs_parentheses(parenthesize, source),
Expr::Compare(expr) => expr.needs_parentheses(parenthesize, source),
Expr::Call(expr) => expr.needs_parentheses(parenthesize, source),
Expr::FormattedValue(expr) => expr.needs_parentheses(parenthesize, source),
Expr::JoinedStr(expr) => expr.needs_parentheses(parenthesize, source),
Expr::Constant(expr) => expr.needs_parentheses(parenthesize, source),
Expr::Attribute(expr) => expr.needs_parentheses(parenthesize, source),
Expr::Subscript(expr) => expr.needs_parentheses(parenthesize, source),
Expr::Starred(expr) => expr.needs_parentheses(parenthesize, source),
Expr::Name(expr) => expr.needs_parentheses(parenthesize, source),
Expr::List(expr) => expr.needs_parentheses(parenthesize, source),
Expr::Tuple(expr) => expr.needs_parentheses(parenthesize, source),
Expr::Slice(expr) => expr.needs_parentheses(parenthesize, source),
} }
} }
} }

View File

@ -0,0 +1,93 @@
use crate::trivia::{
find_first_non_trivia_character_after, find_first_non_trivia_character_before,
};
use ruff_python_ast::node::AnyNodeRef;
pub(crate) trait NeedsParentheses {
fn needs_parentheses(&self, parenthesize: Parenthesize, source: &str) -> Parentheses;
}
pub(super) fn default_expression_needs_parentheses(
node: AnyNodeRef,
parenthesize: Parenthesize,
source: &str,
) -> Parentheses {
debug_assert!(
node.is_expression(),
"Should only be called for expressions"
);
// `Optional` or `Preserve` and expression has parentheses in source code.
if !parenthesize.is_if_breaks() && is_expression_parenthesized(node, source) {
Parentheses::Always
}
// `Optional` or `IfBreaks`: Add parentheses if the expression doesn't fit on a line
else if !parenthesize.is_preserve() {
Parentheses::Optional
} else {
//`Preserve` and expression has no parentheses in the source code
Parentheses::Never
}
}
/// Configures if the expression should be parenthesized.
#[derive(Copy, Clone, Debug, Default)]
pub enum Parenthesize {
/// Parenthesize the expression if it has parenthesis in the source.
#[default]
Preserve,
/// Parenthesizes the expression if it doesn't fit on a line OR if the expression is parenthesized in the source code.
Optional,
/// Parenthesizes the expression only if it doesn't fit on a line.
IfBreaks,
}
impl Parenthesize {
const fn is_if_breaks(self) -> bool {
matches!(self, Parenthesize::IfBreaks)
}
const fn is_preserve(self) -> bool {
matches!(self, Parenthesize::Preserve)
}
}
/// Whether it is necessary to add parentheses around an expression.
/// This is different from [`Parenthesize`] in that it is the resolved representation: It takes into account
/// whether there are parentheses in the source code or not.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Parentheses {
/// Always create parentheses
Always,
/// Only add parentheses when necessary because the expression breaks over multiple lines.
Optional,
/// Custom handling by the node's formatter implementation
Custom,
/// Never add parentheses
Never,
}
fn is_expression_parenthesized(expr: AnyNodeRef, contents: &str) -> bool {
use rustpython_parser::ast::Ranged;
debug_assert!(
expr.is_expression(),
"Should only be called for expressions"
);
// Search backwards to avoid ambiguity with `(a, )` and because it's faster
matches!(
find_first_non_trivia_character_after(expr.end(), contents),
Some((_, ')'))
)
// Search forwards to confirm that this is not a nested expression `(5 + d * 3)`
&& matches!(
find_first_non_trivia_character_before(expr.start(), contents),
Some((_, '('))
)
}

View File

@ -51,7 +51,7 @@ where
self.fmt_node(node, f)?; self.fmt_node(node, f)?;
self.fmt_dangling_comments(node, f)?; self.fmt_dangling_comments(node, f)?;
self.fmt_trailing_comments(node, f)?; self.fmt_trailing_comments(node, f)?;
write!(f, [source_position(node.start())]) write!(f, [source_position(node.end())])
} }
/// Formats the node without comments. Ignores any suppression comments. /// Formats the node without comments. Ignores any suppression comments.
@ -225,8 +225,11 @@ if True:
let formatted_code = printed.as_code(); let formatted_code = printed.as_code();
let reformatted = let reformatted = format_module(formatted_code).unwrap_or_else(|err| {
format_module(formatted_code).expect("Expected formatted code to be valid syntax"); panic!(
"Formatted code resulted introduced a syntax error {err:#?}. Code:\n{formatted_code}"
)
});
if reformatted.as_code() != formatted_code { if reformatted.as_code() != formatted_code {
let diff = TextDiff::from_lines(formatted_code, reformatted.as_code()) let diff = TextDiff::from_lines(formatted_code, reformatted.as_code())
@ -314,7 +317,7 @@ Formatted twice:
let formatted_code = printed.as_code(); let formatted_code = printed.as_code();
let reformatted = let reformatted =
format_module(formatted_code).expect("Expected formatted code to be valid syntax"); format_module(formatted_code).unwrap_or_else(|err| panic!("Expected formatted code to be valid syntax but it contains syntax errors: {err}\n{formatted_code}"));
if reformatted.as_code() != formatted_code { if reformatted.as_code() != formatted_code {
let diff = TextDiff::from_lines(formatted_code, reformatted.as_code()) let diff = TextDiff::from_lines(formatted_code, reformatted.as_code())
@ -378,7 +381,9 @@ other
// Uncomment the `dbg` to print the IR. // Uncomment the `dbg` to print the IR.
// Use `dbg_write!(f, []) instead of `write!(f, [])` in your formatting code to print some IR // Use `dbg_write!(f, []) instead of `write!(f, [])` in your formatting code to print some IR
// inside of a `Format` implementation // inside of a `Format` implementation
// dbg!(formatted.document()); // dbg!(formatted
// .document()
// .display(formatted.context().source_code()));
let printed = formatted.print().unwrap(); let printed = formatted.print().unwrap();

View File

@ -168,7 +168,7 @@ if True:
- 2, - 2,
- 3, - 3,
-] -]
+[1, 2, 3,] +[1, 2, 3]
-division_result_tuple = (6 / 2,) -division_result_tuple = (6 / 2,)
+division_result_tuple = (6/2,) +division_result_tuple = (6/2,)
@ -250,7 +250,7 @@ for x in (1,):
for (x,) in (1,), (2,), (3,): for (x,) in (1,), (2,), (3,):
pass pass
[1, 2, 3,] [1, 2, 3]
division_result_tuple = (6/2,) division_result_tuple = (6/2,)
print("foo %r", (foo.bar,)) print("foo %r", (foo.bar,))

View File

@ -109,7 +109,21 @@ async def wat():
```diff ```diff
--- Black --- Black
+++ Ruff +++ Ruff
@@ -19,14 +19,9 @@ @@ -4,10 +4,12 @@
#
# Has many lines. Many, many lines.
# Many, many, many lines.
-"""Module docstring.
+(
+ """Module docstring.
Possibly also many, many lines.
"""
+)
import os.path
import sys
@@ -19,9 +21,6 @@
import fast import fast
except ImportError: except ImportError:
import slow as fast import slow as fast
@ -117,16 +131,9 @@ async def wat():
- -
-# Some comment before a function. -# Some comment before a function.
y = 1 y = 1
-( (
- # some strings # some strings
- y # type: ignore @@ -93,4 +92,4 @@
-)
+# some strings
+y # type: ignore
def function(default=None):
@@ -93,4 +88,4 @@
# Some closing comments. # Some closing comments.
# Maybe Vim or Emacs directives for formatting. # Maybe Vim or Emacs directives for formatting.
@ -144,10 +151,12 @@ async def wat():
# #
# Has many lines. Many, many lines. # Has many lines. Many, many lines.
# Many, many, many lines. # Many, many, many lines.
"""Module docstring. (
"""Module docstring.
Possibly also many, many lines. Possibly also many, many lines.
""" """
)
import os.path import os.path
import sys import sys
@ -160,8 +169,10 @@ try:
except ImportError: except ImportError:
import slow as fast import slow as fast
y = 1 y = 1
# some strings (
y # type: ignore # some strings
y # type: ignore
)
def function(default=None): def function(default=None):

View File

@ -276,17 +276,7 @@ last_call()
Name Name
None None
True True
@@ -22,119 +23,83 @@ @@ -30,56 +31,39 @@
v1 << 2
1 >> v2
1 % finished
-1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8
-((1 + v2) - (v3 * 4)) ^ (((5**v6) / 7) // 8)
+1 + v2 - v3 * 4 ^ 5 ** v6 / 7 // 8
+((1 + v2) - (v3 * 4)) ^ (((5 ** v6) / 7) // 8)
not great
~great
+value
-1 -1
~int and not v1 ^ 123 + v2 | True ~int and not v1 ^ 123 + v2 | True
(~int) and (not ((v1 ^ (123 + v2)) | True)) (~int) and (not ((v1 ^ (123 + v2)) | True))
@ -312,14 +302,14 @@ last_call()
(str or None) if True else (str or bytes or None) (str or None) if True else (str or bytes or None)
str or None if (1 if True else 2) else str or bytes or None str or None if (1 if True else 2) else str or bytes or None
(str or None) if (1 if True else 2) else (str or bytes or None) (str or None) if (1 if True else 2) else (str or bytes or None)
-( (
- (super_long_variable_name or None) - (super_long_variable_name or None)
- if (1 if super_long_test_name else 2) - if (1 if super_long_test_name else 2)
- else (str or bytes or None) - else (str or bytes or None)
-) + (super_long_variable_name or None) if (1 if super_long_test_name else 2) else (str or bytes or None)
)
-{"2.7": dead, "3.7": (long_live or die_hard)} -{"2.7": dead, "3.7": (long_live or die_hard)}
-{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}} -{"2.7": dead, "3.7": (long_live or die_hard), **{"3.6": verygood}}
+(super_long_variable_name or None) if (1 if super_long_test_name else 2) else (str or bytes or None)
+{'2.7': dead, '3.7': (long_live or die_hard)} +{'2.7': dead, '3.7': (long_live or die_hard)}
+{'2.7': dead, '3.7': (long_live or die_hard), **{'3.6': verygood}} +{'2.7': dead, '3.7': (long_live or die_hard), **{'3.6': verygood}}
{**a, **b, **c} {**a, **b, **c}
@ -338,7 +328,7 @@ last_call()
- 2, - 2,
- 3, - 3,
-] -]
+[1, 2, 3,] +[1, 2, 3]
[*a] [*a]
[*range(10)] [*range(10)]
-[ -[
@ -351,15 +341,14 @@ last_call()
- *a, - *a,
- 5, - 5,
-] -]
-[ +[*a, 4, 5]
- this_is_a_very_long_variable_which_will_force_a_delimiter_split, +[4, *a, 5]
- element, [
- another, this_is_a_very_long_variable_which_will_force_a_delimiter_split,
- *more, element,
-] @@ -87,54 +71,44 @@
+[*a, 4, 5,] *more,
+[4, *a, 5,] ]
+[this_is_a_very_long_variable_which_will_force_a_delimiter_split, element, another, *more]
{i for i in (1, 2, 3)} {i for i in (1, 2, 3)}
-{(i**2) for i in (1, 2, 3)} -{(i**2) for i in (1, 2, 3)}
-{(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))} -{(i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))}
@ -417,18 +406,21 @@ last_call()
dict[str, int] dict[str, int]
tuple[str, ...] tuple[str, ...]
-tuple[str, int, float, dict[str, int]] -tuple[str, int, float, dict[str, int]]
tuple[ -tuple[
- str, - str,
- int, - int,
- float, - float,
- dict[str, int], - dict[str, int],
+(
+ tuple[
+ str, int, float, dict[str, int] + str, int, float, dict[str, int]
] ]
+)
+tuple[str, int, float, dict[str, int],] +tuple[str, int, float, dict[str, int],]
very_long_variable_name_filters: t.List[ very_long_variable_name_filters: t.List[
t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]],
] ]
@@ -144,9 +109,9 @@ @@ -144,9 +118,9 @@
xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore
sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__) sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)
) )
@ -441,7 +433,7 @@ last_call()
slice[0] slice[0]
slice[0:1] slice[0:1]
slice[0:1:2] slice[0:1:2]
@@ -174,57 +139,29 @@ @@ -174,57 +148,29 @@
numpy[:, ::-1] numpy[:, ::-1]
numpy[np.newaxis, :] numpy[np.newaxis, :]
(str or None) if (sys.version_info[0] > (3,)) else (str or bytes or None) (str or None) if (sys.version_info[0] > (3,)) else (str or bytes or None)
@ -450,9 +442,8 @@ last_call()
+{'2.7': dead, '3.7': long_live or die_hard} +{'2.7': dead, '3.7': long_live or die_hard}
+{'2.7', '3.6', '3.7', '3.8', '3.9', '4.0' if gilectomy else '3.10'} +{'2.7', '3.6', '3.7', '3.8', '3.9', '4.0' if gilectomy else '3.10'}
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C] [1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C]
-(SomeName) (SomeName)
SomeName SomeName
+SomeName
(Good, Bad, Ugly) (Good, Bad, Ugly)
(i for i in (1, 2, 3)) (i for i in (1, 2, 3))
-((i**2) for i in (1, 2, 3)) -((i**2) for i in (1, 2, 3))
@ -511,7 +502,7 @@ last_call()
Ø = set() Ø = set()
authors.łukasz.say_thanks() authors.łukasz.say_thanks()
mapping = { mapping = {
@@ -237,134 +174,86 @@ @@ -237,114 +183,80 @@
def gen(): def gen():
yield from outside_of_generator yield from outside_of_generator
@ -655,30 +646,7 @@ last_call()
+ ~ aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h ^ aaaaaaaaaaaaaaaa.i << aaaaaaaaaaaaaaaa.k >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n + ~ aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h ^ aaaaaaaaaaaaaaaa.i << aaaaaaaaaaaaaaaa.k >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n
): ):
return True return True
-( (
- aaaaaaaaaaaaaaaa
- + aaaaaaaaaaaaaaaa
- - aaaaaaaaaaaaaaaa
- * (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa)
- / (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa)
-)
+aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa - aaaaaaaaaaaaaaaa * (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa) / (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa)
aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa
-(
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- >> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- << aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-)
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa >> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa << aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
bbbb >> bbbb * bbbb
-(
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- ^ bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- ^ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-)
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ^bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa^aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
last_call()
# standalone comment at ENDMARKER
``` ```
## Ruff Output ## Ruff Output
@ -709,8 +677,8 @@ Name1 or Name2 and Name3 or Name4
v1 << 2 v1 << 2
1 >> v2 1 >> v2
1 % finished 1 % finished
1 + v2 - v3 * 4 ^ 5 ** v6 / 7 // 8 1 + v2 - v3 * 4 ^ 5**v6 / 7 // 8
((1 + v2) - (v3 * 4)) ^ (((5 ** v6) / 7) // 8) ((1 + v2) - (v3 * 4)) ^ (((5**v6) / 7) // 8)
not great not great
~great ~great
+value +value
@ -731,7 +699,9 @@ str or None if True else str or bytes or None
(str or None) if True else (str or bytes or None) (str or None) if True else (str or bytes or None)
str or None if (1 if True else 2) else str or bytes or None str or None if (1 if True else 2) else str or bytes or None
(str or None) if (1 if True else 2) else (str or bytes or None) (str or None) if (1 if True else 2) else (str or bytes or None)
(super_long_variable_name or None) if (1 if super_long_test_name else 2) else (str or bytes or None) (
(super_long_variable_name or None) if (1 if super_long_test_name else 2) else (str or bytes or None)
)
{'2.7': dead, '3.7': (long_live or die_hard)} {'2.7': dead, '3.7': (long_live or die_hard)}
{'2.7': dead, '3.7': (long_live or die_hard), **{'3.6': verygood}} {'2.7': dead, '3.7': (long_live or die_hard), **{'3.6': verygood}}
{**a, **b, **c} {**a, **b, **c}
@ -743,12 +713,17 @@ str or None if (1 if True else 2) else str or bytes or None
(1, 2, 3) (1, 2, 3)
[] []
[1, 2, 3, 4, 5, 6, 7, 8, 9, (10 or A), (11 or B), (12 or C)] [1, 2, 3, 4, 5, 6, 7, 8, 9, (10 or A), (11 or B), (12 or C)]
[1, 2, 3,] [1, 2, 3]
[*a] [*a]
[*range(10)] [*range(10)]
[*a, 4, 5,] [*a, 4, 5]
[4, *a, 5,] [4, *a, 5]
[this_is_a_very_long_variable_which_will_force_a_delimiter_split, element, another, *more] [
this_is_a_very_long_variable_which_will_force_a_delimiter_split,
element,
another,
*more,
]
{i for i in (1, 2, 3)} {i for i in (1, 2, 3)}
{(i ** 2) for i in (1, 2, 3)} {(i ** 2) for i in (1, 2, 3)}
{(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))} {(i ** 2) for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))}
@ -782,9 +757,11 @@ call.me(maybe)
list[str] list[str]
dict[str, int] dict[str, int]
tuple[str, ...] tuple[str, ...]
tuple[ (
tuple[
str, int, float, dict[str, int] str, int, float, dict[str, int]
] ]
)
tuple[str, int, float, dict[str, int],] tuple[str, int, float, dict[str, int],]
very_long_variable_name_filters: t.List[ very_long_variable_name_filters: t.List[
t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]],
@ -828,7 +805,7 @@ numpy[np.newaxis, :]
{'2.7': dead, '3.7': long_live or die_hard} {'2.7': dead, '3.7': long_live or die_hard}
{'2.7', '3.6', '3.7', '3.8', '3.9', '4.0' if gilectomy else '3.10'} {'2.7', '3.6', '3.7', '3.8', '3.9', '4.0' if gilectomy else '3.10'}
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C] [1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C]
SomeName (SomeName)
SomeName SomeName
(Good, Bad, Ugly) (Good, Bad, Ugly)
(i for i in (1, 2, 3)) (i for i in (1, 2, 3))
@ -936,11 +913,25 @@ if (
~ aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h ^ aaaaaaaaaaaaaaaa.i << aaaaaaaaaaaaaaaa.k >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n ~ aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e | aaaaaaaaaaaaaaaa.f & aaaaaaaaaaaaaaaa.g % aaaaaaaaaaaaaaaa.h ^ aaaaaaaaaaaaaaaa.i << aaaaaaaaaaaaaaaa.k >> aaaaaaaaaaaaaaaa.l ** aaaaaaaaaaaaaaaa.m // aaaaaaaaaaaaaaaa.n
): ):
return True return True
aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa - aaaaaaaaaaaaaaaa * (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa) / (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa) (
aaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaa
- aaaaaaaaaaaaaaaa
* (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa)
/ (aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa)
)
aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa >> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa << aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
>> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
<< aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
)
bbbb >> bbbb * bbbb bbbb >> bbbb * bbbb
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ^bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa^aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
^ bbbb.a & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
^ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
)
last_call() last_call()
# standalone comment at ENDMARKER # standalone comment at ENDMARKER
``` ```

View File

@ -76,16 +76,51 @@ x[
```diff ```diff
--- Black --- Black
+++ Ruff +++ Ruff
@@ -56,4 +56,8 @@ @@ -31,14 +31,17 @@
ham[lower + offset : upper + offset]
slice[::, ::]
-slice[
+(
+ slice[
# A
:
# B
:
# C
]
-slice[
+)
+(
+ slice[
# A
1:
# B
@@ -46,8 +49,10 @@
# C
3
]
+)
-slice[
+(
+ slice[
# A
1
+ 2 :
@@ -56,4 +61,11 @@
# C # C
4 4
] ]
-x[1:2:3] # A # B # C -x[1:2:3] # A # B # C
+x[ +)
+(
+ x[
+ 1: # A + 1: # A
+ 2: # B + 2: # B
+ 3 # C + 3 # C
+] +]
+)
``` ```
## Ruff Output ## Ruff Output
@ -124,14 +159,17 @@ ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)]
ham[lower + offset : upper + offset] ham[lower + offset : upper + offset]
slice[::, ::] slice[::, ::]
slice[ (
slice[
# A # A
: :
# B # B
: :
# C # C
] ]
slice[ )
(
slice[
# A # A
1: 1:
# B # B
@ -139,8 +177,10 @@ slice[
# C # C
3 3
] ]
)
slice[ (
slice[
# A # A
1 1
+ 2 : + 2 :
@ -149,11 +189,14 @@ slice[
# C # C
4 4
] ]
x[ )
(
x[
1: # A 1: # A
2: # B 2: # B
3 # C 3 # C
] ]
)
``` ```
## Black Output ## Black Output

View File

@ -42,14 +42,13 @@ assert (
```diff ```diff
--- Black --- Black
+++ Ruff +++ Ruff
@@ -1,58 +1,33 @@ @@ -2,18 +2,13 @@
importA (
-( ()
- () << 0
- << 0
- ** 101234234242352525425252352352525234890264906820496920680926538059059209922523523525 - ** 101234234242352525425252352352525234890264906820496920680926538059059209922523523525
-) # + **101234234242352525425252352352525234890264906820496920680926538059059209922523523525
+() << 0 ** 101234234242352525425252352352525234890264906820496920680926538059059209922523523525 # ) #
assert sort_by_dependency( assert sort_by_dependency(
{ {
@ -65,12 +64,7 @@ assert (
} }
) == ["2a", "2b", "2", "3a", "3b", "3", "1"] ) == ["2a", "2b", "2", "3a", "3b", "3", "1"]
importA @@ -25,34 +20,18 @@
0
-0 ^ 0 #
+0^0 #
class A: class A:
def foo(self): def foo(self):
for _ in range(10): for _ in range(10):
@ -120,7 +114,11 @@ assert (
```py ```py
importA importA
() << 0 ** 101234234242352525425252352352525234890264906820496920680926538059059209922523523525 # (
()
<< 0
**101234234242352525425252352352525234890264906820496920680926538059059209922523523525
) #
assert sort_by_dependency( assert sort_by_dependency(
{ {
@ -131,7 +129,7 @@ assert sort_by_dependency(
importA importA
0 0
0^0 # 0 ^ 0 #
class A: class A:

View File

@ -0,0 +1,123 @@
---
source: crates/ruff_python_formatter/src/lib.rs
expression: snapshot
---
## Input
```py
(aaaaaaaa
+ # trailing operator comment
b # trailing right comment
)
(aaaaaaaa # trailing left comment
+ # trailing operator comment
# leading right comment
b
)
# Black breaks the right side first for the following expressions:
aaaaaaaaaaaaaa + caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaal(argument1, argument2, argument3)
aaaaaaaaaaaaaa + [bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eeeeeee]
aaaaaaaaaaaaaa + (bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eeeeeee)
aaaaaaaaaaaaaa + { key1:bbbbbbbbbbbbbbbbbbbbbb, key2: ccccccccccccccccccccc, key3: dddddddddddddddd, key4: eeeeeee }
aaaaaaaaaaaaaa + { bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eeeeeee }
aaaaaaaaaaaaaa + [a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ]
aaaaaaaaaaaaaa + (a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb )
aaaaaaaaaaaaaa + {a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb}
# Wraps it in parentheses if it needs to break both left and right
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + [
bbbbbbbbbbbbbbbbbbbbbb,
ccccccccccccccccccccc,
dddddddddddddddd,
eee
] # comment
# But only for expressions that have a statement parent.
not (aaaaaaaaaaaaaa + {a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb})
[a + [bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] in c ]
# leading comment
(
# comment
content + b
)
```
## Output
```py
(
aaaaaaaa
+ b # trailing operator comment # trailing right comment
)
(
aaaaaaaa # trailing left comment
+ # trailing operator comment
# leading right comment
b
)
# Black breaks the right side first for the following expressions:
(
aaaaaaaaaaaaaa
+ caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaal(argument1, argument2, argument3)
)
aaaaaaaaaaaaaa + [
bbbbbbbbbbbbbbbbbbbbbb,
ccccccccccccccccccccc,
dddddddddddddddd,
eeeeeee,
]
(
aaaaaaaaaaaaaa
+ (bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eeeeeee)
)
(
aaaaaaaaaaaaaa
+ { key1:bbbbbbbbbbbbbbbbbbbbbb, key2: ccccccccccccccccccccc, key3: dddddddddddddddd, key4: eeeeeee }
)
(
aaaaaaaaaaaaaa
+ { bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eeeeeee }
)
(
aaaaaaaaaaaaaa
+ [a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ]
)
(
aaaaaaaaaaaaaa
+ (a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb )
)
(
aaaaaaaaaaaaaa
+ {a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb}
)
# Wraps it in parentheses if it needs to break both left and right
(
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ [bbbbbbbbbbbbbbbbbbbbbb, ccccccccccccccccccccc, dddddddddddddddd, eee]
) # comment
# But only for expressions that have a statement parent.
(
not (aaaaaaaaaaaaaa + {a for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb})
)
[
a + [bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] in c,
]
# leading comment
(
# comment
content
+ b
)
```

View File

@ -1,3 +1,4 @@
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*; use crate::prelude::*;
use crate::FormatNodeRule; use crate::FormatNodeRule;
use rustpython_parser::ast::StmtExpr; use rustpython_parser::ast::StmtExpr;
@ -9,6 +10,6 @@ impl FormatNodeRule<StmtExpr> for FormatStmtExpr {
fn fmt_fields(&self, item: &StmtExpr, f: &mut PyFormatter) -> FormatResult<()> { fn fmt_fields(&self, item: &StmtExpr, f: &mut PyFormatter) -> FormatResult<()> {
let StmtExpr { value, .. } = item; let StmtExpr { value, .. } = item;
value.format().fmt(f) value.format().with_options(Parenthesize::Optional).fmt(f)
} }
} }

View File

@ -1,5 +1,5 @@
use crate::comments::{leading_alternate_branch_comments, trailing_comments}; use crate::comments::{leading_alternate_branch_comments, trailing_comments};
use crate::expression::maybe_parenthesize::maybe_parenthesize; use crate::expression::parentheses::Parenthesize;
use crate::prelude::*; use crate::prelude::*;
use crate::FormatNodeRule; use crate::FormatNodeRule;
use ruff_formatter::write; use ruff_formatter::write;
@ -33,7 +33,7 @@ impl FormatNodeRule<StmtWhile> for FormatStmtWhile {
[ [
text("while"), text("while"),
space(), space(),
maybe_parenthesize(test), test.format().with_options(Parenthesize::IfBreaks),
text(":"), text(":"),
trailing_comments(trailing_condition_comments), trailing_comments(trailing_condition_comments),
block_indent(&body.format()) block_indent(&body.format())

View File

@ -10,8 +10,8 @@ use ruff_text_size::{TextLen, TextRange, TextSize};
/// ///
/// Returns `None` if the range is empty or only contains trivia (whitespace or comments). /// Returns `None` if the range is empty or only contains trivia (whitespace or comments).
pub(crate) fn find_first_non_trivia_character_in_range( pub(crate) fn find_first_non_trivia_character_in_range(
code: &str,
range: TextRange, range: TextRange,
code: &str,
) -> Option<(TextSize, char)> { ) -> Option<(TextSize, char)> {
let rest = &code[range]; let rest = &code[range];
let mut char_iter = rest.chars(); let mut char_iter = rest.chars();
@ -40,8 +40,63 @@ pub(crate) fn find_first_non_trivia_character_in_range(
None None
} }
pub(crate) fn find_first_non_trivia_character_after(
offset: TextSize,
code: &str,
) -> Option<(TextSize, char)> {
find_first_non_trivia_character_in_range(TextRange::new(offset, code.text_len()), code)
}
pub(crate) fn find_first_non_trivia_character_before(
offset: TextSize,
code: &str,
) -> Option<(TextSize, char)> {
let head = &code[TextRange::up_to(offset)];
let mut char_iter = head.chars();
while let Some(c) = char_iter.next_back() {
match c {
c if is_python_whitespace(c) => {
continue;
}
// Empty comment
'#' => continue,
non_trivia_character => {
// Non trivia character but we don't know if it is a comment or not. Consume all characters
// until the start of the line and track if the last non-whitespace character was a `#`.
let mut is_comment = false;
let first_non_trivia_offset = char_iter.as_str().text_len();
while let Some(c) = char_iter.next_back() {
match c {
'#' => {
is_comment = true;
}
'\n' | '\r' => {
if !is_comment {
return Some((first_non_trivia_offset, non_trivia_character));
}
}
c => {
if !is_python_whitespace(c) {
is_comment = false;
}
}
}
}
}
}
}
None
}
/// Returns the number of newlines between `offset` and the first non whitespace character in the source code. /// Returns the number of newlines between `offset` and the first non whitespace character in the source code.
pub(crate) fn lines_before(code: &str, offset: TextSize) -> u32 { pub(crate) fn lines_before(offset: TextSize, code: &str) -> u32 {
let head = &code[TextRange::up_to(offset)]; let head = &code[TextRange::up_to(offset)];
let mut newlines = 0u32; let mut newlines = 0u32;
@ -68,7 +123,7 @@ pub(crate) fn lines_before(code: &str, offset: TextSize) -> u32 {
} }
/// Counts the empty lines between `offset` and the first non-whitespace character. /// Counts the empty lines between `offset` and the first non-whitespace character.
pub(crate) fn lines_after(code: &str, offset: TextSize) -> u32 { pub(crate) fn lines_after(offset: TextSize, code: &str) -> u32 {
let rest = &code[usize::from(offset)..]; let rest = &code[usize::from(offset)..];
let mut newlines = 0; let mut newlines = 0;
@ -98,33 +153,33 @@ mod tests {
#[test] #[test]
fn lines_before_empty_string() { fn lines_before_empty_string() {
assert_eq!(lines_before("", TextSize::new(0)), 0); assert_eq!(lines_before(TextSize::new(0), ""), 0);
} }
#[test] #[test]
fn lines_before_in_the_middle_of_a_line() { fn lines_before_in_the_middle_of_a_line() {
assert_eq!(lines_before("a = 20", TextSize::new(4)), 0); assert_eq!(lines_before(TextSize::new(4), "a = 20"), 0);
} }
#[test] #[test]
fn lines_before_on_a_new_line() { fn lines_before_on_a_new_line() {
assert_eq!(lines_before("a = 20\nb = 10", TextSize::new(7)), 1); assert_eq!(lines_before(TextSize::new(7), "a = 20\nb = 10"), 1);
} }
#[test] #[test]
fn lines_before_multiple_leading_newlines() { fn lines_before_multiple_leading_newlines() {
assert_eq!(lines_before("a = 20\n\r\nb = 10", TextSize::new(9)), 2); assert_eq!(lines_before(TextSize::new(9), "a = 20\n\r\nb = 10"), 2);
} }
#[test] #[test]
fn lines_before_with_comment_offset() { fn lines_before_with_comment_offset() {
assert_eq!(lines_before("a = 20\n# a comment", TextSize::new(8)), 0); assert_eq!(lines_before(TextSize::new(8), "a = 20\n# a comment"), 0);
} }
#[test] #[test]
fn lines_before_with_trailing_comment() { fn lines_before_with_trailing_comment() {
assert_eq!( assert_eq!(
lines_before("a = 20 # some comment\nb = 10", TextSize::new(22)), lines_before(TextSize::new(22), "a = 20 # some comment\nb = 10"),
1 1
); );
} }
@ -132,40 +187,40 @@ mod tests {
#[test] #[test]
fn lines_before_with_comment_only_line() { fn lines_before_with_comment_only_line() {
assert_eq!( assert_eq!(
lines_before("a = 20\n# some comment\nb = 10", TextSize::new(22)), lines_before(TextSize::new(22), "a = 20\n# some comment\nb = 10"),
1 1
); );
} }
#[test] #[test]
fn lines_after_empty_string() { fn lines_after_empty_string() {
assert_eq!(lines_after("", TextSize::new(0)), 0); assert_eq!(lines_after(TextSize::new(0), ""), 0);
} }
#[test] #[test]
fn lines_after_in_the_middle_of_a_line() { fn lines_after_in_the_middle_of_a_line() {
assert_eq!(lines_after("a = 20", TextSize::new(4)), 0); assert_eq!(lines_after(TextSize::new(4), "a = 20"), 0);
} }
#[test] #[test]
fn lines_after_before_a_new_line() { fn lines_after_before_a_new_line() {
assert_eq!(lines_after("a = 20\nb = 10", TextSize::new(6)), 1); assert_eq!(lines_after(TextSize::new(6), "a = 20\nb = 10"), 1);
} }
#[test] #[test]
fn lines_after_multiple_newlines() { fn lines_after_multiple_newlines() {
assert_eq!(lines_after("a = 20\n\r\nb = 10", TextSize::new(6)), 2); assert_eq!(lines_after(TextSize::new(6), "a = 20\n\r\nb = 10"), 2);
} }
#[test] #[test]
fn lines_after_before_comment_offset() { fn lines_after_before_comment_offset() {
assert_eq!(lines_after("a = 20 # a comment\n", TextSize::new(7)), 0); assert_eq!(lines_after(TextSize::new(7), "a = 20 # a comment\n"), 0);
} }
#[test] #[test]
fn lines_after_with_comment_only_line() { fn lines_after_with_comment_only_line() {
assert_eq!( assert_eq!(
lines_after("a = 20\n# some comment\nb = 10", TextSize::new(6)), lines_after(TextSize::new(6), "a = 20\n# some comment\nb = 10"),
1 1
); );
} }

View File

@ -23,6 +23,8 @@ pub struct Scope<'a> {
bindings: FxHashMap<&'a str, BindingId>, bindings: FxHashMap<&'a str, BindingId>,
/// A map from binding ID to binding ID that it shadows. /// A map from binding ID to binding ID that it shadows.
shadowed_bindings: HashMap<BindingId, BindingId, BuildNoHashHasher<BindingId>>, shadowed_bindings: HashMap<BindingId, BindingId, BuildNoHashHasher<BindingId>>,
/// A list of all names that have been deleted in this scope.
deleted_symbols: Vec<&'a str>,
/// Index into the globals arena, if the scope contains any globally-declared symbols. /// Index into the globals arena, if the scope contains any globally-declared symbols.
globals_id: Option<GlobalsId>, globals_id: Option<GlobalsId>,
} }
@ -36,6 +38,7 @@ impl<'a> Scope<'a> {
star_imports: Vec::default(), star_imports: Vec::default(),
bindings: FxHashMap::default(), bindings: FxHashMap::default(),
shadowed_bindings: IntMap::default(), shadowed_bindings: IntMap::default(),
deleted_symbols: Vec::default(),
globals_id: None, globals_id: None,
} }
} }
@ -48,6 +51,7 @@ impl<'a> Scope<'a> {
star_imports: Vec::default(), star_imports: Vec::default(),
bindings: FxHashMap::default(), bindings: FxHashMap::default(),
shadowed_bindings: IntMap::default(), shadowed_bindings: IntMap::default(),
deleted_symbols: Vec::default(),
globals_id: None, globals_id: None,
} }
} }
@ -67,14 +71,23 @@ impl<'a> Scope<'a> {
} }
} }
/// Returns `true` if this scope defines a binding with the given name. /// Removes the binding with the given name.
pub fn defines(&self, name: &str) -> bool { pub fn delete(&mut self, name: &'a str) -> Option<BindingId> {
self.deleted_symbols.push(name);
self.bindings.remove(name)
}
/// Returns `true` if this scope has a binding with the given name.
pub fn has(&self, name: &str) -> bool {
self.bindings.contains_key(name) self.bindings.contains_key(name)
} }
/// Removes the binding with the given name /// Returns `true` if the scope declares a symbol with the given name.
pub fn remove(&mut self, name: &str) -> Option<BindingId> { ///
self.bindings.remove(name) /// Unlike [`Scope::has`], the name may no longer be bound to a value (e.g., it could be
/// deleted).
pub fn declares(&self, name: &str) -> bool {
self.has(name) || self.deleted_symbols.contains(&name)
} }
/// Returns the ids of all bindings defined in this scope. /// Returns the ids of all bindings defined in this scope.

View File

@ -242,7 +242,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be
```yaml ```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.0.270 rev: v0.0.271
hooks: hooks:
- id: ruff - id: ruff
``` ```

View File

@ -22,7 +22,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml ```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.0.270 rev: v0.0.271
hooks: hooks:
- id: ruff - id: ruff
``` ```
@ -32,7 +32,7 @@ Or, to enable autofix:
```yaml ```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.0.270 rev: v0.0.271
hooks: hooks:
- id: ruff - id: ruff
args: [ --fix, --exit-non-zero-on-fix ] args: [ --fix, --exit-non-zero-on-fix ]

View File

@ -5,7 +5,7 @@ build-backend = "maturin"
[project] [project]
name = "ruff" name = "ruff"
version = "0.0.270" version = "0.0.271"
description = "An extremely fast Python linter, written in Rust." description = "An extremely fast Python linter, written in Rust."
authors = [{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" }] authors = [{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" }]
maintainers = [{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" }] maintainers = [{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" }]