Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add let_chain_style configuration option #5983

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions Configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,72 @@ macro_rules! foo {

See also [`format_macro_matchers`](#format_macro_matchers).

## `let_chain_style`

Controls how rustfmt treats let-chains

- **Default value**: `LegibleBindings`
- **Possible values**: `LegibleBindings`, `Tall`
- **Stability**: No (tracking issue: N/A)

#### `LegibleBindings` (default):

let-chain items are formatted on their own line to disambiguate the new bindings.
The let-chain may be arranged horizontally when the chain:
1. Only contains two items
2. The first item is an identifier
3. The second item is a let expressions.

```rust
fn main() {
if let Some(x) = y
&& a
{}

if let Some(x) = y
&& let Some(a) = b
{}

if let Ok(name) = str::from_utf8(name)
&& is_dyn_sym(name)
{}

if condition()
&& let Some(binding) = expr
&& condition2(binding)
&& condition3()
&& let Some(binding2) = expr2
&& condition4(binding, binding2)
{
body();
}
}
```

#### `Tall`:

let-chain items are placed horizontally when there is sufficient space, otherwise the chain is formatted vertically.

```rust
fn main() {
if let Some(x) = y && a {}

if let Some(x) = y && let Some(a) = b {}

if let Ok(name) = str::from_utf8(name) && is_dyn_sym(name) {}

if condition()
&& let Some(binding) = expr
&& condition2(binding)
&& condition3()
&& let Some(binding2) = expr2
&& condition4(binding, binding2)
{
body();
}
}
```

## `skip_macro_invocations`

Skip formatting the bodies of macro invocations with the following names.
Expand Down
3 changes: 3 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ create_config! {
"Write an item and its attribute on the same line \
if their combined width is below a threshold";
format_generated_files: bool, true, false, "Format generated files";
let_chain_style: LetChainStyle, LetChainStyle::LegibleBindings, false, "Controls how rustfmt \
lays out let-chains";

// Options that can change the source code beyond whitespace/blocks (somewhat linty things)
merge_derives: bool, true, true, "Merge multiple `#[derive(...)]` into a single one";
Expand Down Expand Up @@ -680,6 +682,7 @@ edition = "2015"
version = "One"
inline_attribute_width = 0
format_generated_files = true
let_chain_style = "LegibleBindings"
merge_derives = true
use_try_shorthand = false
use_field_init_shorthand = false
Expand Down
14 changes: 14 additions & 0 deletions src/config/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -494,3 +494,17 @@ pub enum StyleEdition {
/// [Edition 2024]().
Edition2024,
}

/// Controls how rustfmt treats let-chains
#[config_type]
pub enum LetChainStyle {
/// let-chain items are formatted on their own line to disambiguate the new bindings.
/// The let-chain may be arranged horizontally when the chain:
/// 1. Only contains two items
/// 2. The first item is an identifier
/// 3. The second item is a let expressions.
LegibleBindings,
/// let-chain items are placed horizontally when there is sufficient space, otherwise the chain
/// is formatted vertically.
Tall,
}
22 changes: 14 additions & 8 deletions src/pairs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use rustc_ast::ast;

use crate::config::lists::*;
use crate::config::IndentStyle;
use crate::config::LetChainStyle;
use crate::rewrite::{Rewrite, RewriteContext};
use crate::shape::Shape;
use crate::utils::{
Expand Down Expand Up @@ -42,7 +43,7 @@ pub(crate) fn rewrite_all_pairs(
context: &RewriteContext<'_>,
) -> Option<String> {
expr.flatten(context, shape).and_then(|list| {
if list.let_chain_count() > 0 && !list.can_rewrite_let_chain_single_line() {
if list.let_chain_count() > 0 && !list.can_rewrite_let_chain_single_line(context) {
rewrite_pairs_multiline(&list, shape, context)
} else {
// First we try formatting on one line.
Expand Down Expand Up @@ -278,15 +279,20 @@ impl<'a, 'b> PairList<'a, 'b, ast::Expr> {
.count()
}

fn can_rewrite_let_chain_single_line(&self) -> bool {
if self.list.len() != 2 {
return false;
}
fn can_rewrite_let_chain_single_line(&self, context: &RewriteContext<'_>) -> bool {
match context.config.let_chain_style() {
LetChainStyle::Tall => true,
LetChainStyle::LegibleBindings => {
if self.list.len() != 2 {
return false;
}

let fist_item_is_ident = is_ident(self.list[0].0);
let second_item_is_let_chain = matches!(self.list[1].0.kind, ast::ExprKind::Let(..));
let fist_is_ident = is_ident(self.list[0].0);
let second_is_let_expr = matches!(self.list[1].0.kind, ast::ExprKind::Let(..));

fist_item_is_ident && second_item_is_let_chain
fist_is_ident && second_is_let_expr
}
}
}
}

Expand Down
123 changes: 123 additions & 0 deletions tests/source/configs/let_chain_style/legible_bindings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// rustfmt-let_chain_style: LegibleBindings

fn main() {
if let x = x && x {}

if xxx && let x = x {}

if aaaaaaaaaaaaaaaaaaaaa && aaaaaaaaaaaaaaa && aaaaaaaaa && let Some(x) = xxxxxxxxxxxx && aaaaaaa && let None = aaaaaaaaaa {}

if aaaaaaaaaaaaaaaaaaaaa && aaaaaaaaaaaaaaa && aaaaaaaaa && let Some(x) = xxxxxxxxxxxx && aaaaaaa && let None = aaaaaaaaaa {}

if let Some(Struct { x:TS(1,2) }) = path::to::<_>(hehe)
&& let [Simple, people] = /* get ready */ create_universe(/* hi */ GreatPowers).initialize_badminton().populate_swamps() &&
let everybody = (Loops { hi /*hi*/ , ..loopy() }) && summons::triumphantly() { todo!() }

if let XXXXXXXXX { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx, yyyyyyyyyyyyy, zzzzzzzzzzzzz} = xxxxxxx()
&& let Foo = bar() { todo!() }
}

fn test_single_line_let_chain() {
// first item in let-chain is an ident
if a && let Some(b) = foo() {
}

// first item in let-chain is a unary ! with an ident
let unary_not = if !from_hir_call
&& let Some(p) = parent
{
};

// first item in let-chain is a unary * with an ident
let unary_deref = if *some_deref
&& let Some(p) = parent
{
};

// first item in let-chain is a unary - (neg) with an ident
let unary_neg = if -some_ident
&& let Some(p) = parent
{
};

// first item in let-chain is a try (?) with an ident
let try_ = if some_try?
&& let Some(p) = parent
{
};

// first item in let-chain is an ident wrapped in parens
let in_parens = if (some_ident)
&& let Some(p) = parent
{
};

// first item in let-chain is a ref & with an ident
let _ref = if &some_ref
&& let Some(p) = parent
{
};

// first item in let-chain is a ref &mut with an ident
let mut_ref = if &mut some_ref
&& let Some(p) = parent
{
};

// chain unary ref and try
let chain_of_unary_ref_and_try = if !&*some_ref?
&& let Some(p) = parent {
};
}

fn test_multi_line_let_chain() {
// Can only single line the let-chain if the first item is an ident
if let Some(x) = y && a {

}

// More than one let-chain must be formatted on multiple lines
if let Some(x) = y && let Some(a) = b {

}

// The ident isn't long enough so we don't wrap the first let-chain
if a && let Some(x) = y && let Some(a) = b {

}

// The ident is long enough so both let-chains are wrapped
if aaa && let Some(x) = y && let Some(a) = b {

}

// function call
if a() && let Some(x) = y {

}

// bool literal
if true && let Some(x) = y {

}

// cast to a bool
if 1 as bool && let Some(x) = y {

}

// matches! macro call
if matches!(a, some_type) && let Some(x) = y {

}

// block expression returning bool
if { true } && let Some(x) = y {

}

// field access
if a.x && let Some(x) = y {

}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// rustfmt-let_chain_style: Tall

fn main() {
if let x = x && x {}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// rustfmt-let_chain_style: LegibleBindings

fn main() {
if let x = x
&& x
Expand Down
108 changes: 108 additions & 0 deletions tests/target/configs/let_chain_style/tall.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// rustfmt-let_chain_style: Tall

fn main() {
if let x = x && x {}

if xxx && let x = x {}

if aaaaaaaaaaaaaaaaaaaaa
&& aaaaaaaaaaaaaaa
&& aaaaaaaaa
&& let Some(x) = xxxxxxxxxxxx
&& aaaaaaa
&& let None = aaaaaaaaaa
{}

if aaaaaaaaaaaaaaaaaaaaa
&& aaaaaaaaaaaaaaa
&& aaaaaaaaa
&& let Some(x) = xxxxxxxxxxxx
&& aaaaaaa
&& let None = aaaaaaaaaa
{}

if let Some(Struct { x: TS(1, 2) }) = path::to::<_>(hehe)
&& let [Simple, people] = /* get ready */
create_universe(/* hi */ GreatPowers)
.initialize_badminton()
.populate_swamps()
&& let everybody = (Loops {
hi, /*hi*/
..loopy()
})
&& summons::triumphantly()
{
todo!()
}

if let XXXXXXXXX {
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
yyyyyyyyyyyyy,
zzzzzzzzzzzzz,
} = xxxxxxx()
&& let Foo = bar()
{
todo!()
}
}

fn test_single_line_let_chain() {
// first item in let-chain is an ident
if a && let Some(b) = foo() {}

// first item in let-chain is a unary ! with an ident
let unary_not = if !from_hir_call && let Some(p) = parent {};

// first item in let-chain is a unary * with an ident
let unary_deref = if *some_deref && let Some(p) = parent {};

// first item in let-chain is a unary - (neg) with an ident
let unary_neg = if -some_ident && let Some(p) = parent {};

// first item in let-chain is a try (?) with an ident
let try_ = if some_try? && let Some(p) = parent {};

// first item in let-chain is an ident wrapped in parens
let in_parens = if (some_ident) && let Some(p) = parent {};

// first item in let-chain is a ref & with an ident
let _ref = if &some_ref && let Some(p) = parent {};

// first item in let-chain is a ref &mut with an ident
let mut_ref = if &mut some_ref && let Some(p) = parent {};

// chain unary ref and try
let chain_of_unary_ref_and_try = if !&*some_ref? && let Some(p) = parent {};
}

fn test_multi_line_let_chain() {
// Can only single line the let-chain if the first item is an ident
if let Some(x) = y && a {}

// More than one let-chain must be formatted on multiple lines
if let Some(x) = y && let Some(a) = b {}

// The ident isn't long enough so we don't wrap the first let-chain
if a && let Some(x) = y && let Some(a) = b {}

// The ident is long enough so both let-chains are wrapped
if aaa && let Some(x) = y && let Some(a) = b {}

// function call
if a() && let Some(x) = y {}

// bool literal
if true && let Some(x) = y {}

// cast to a bool
if 1 as bool && let Some(x) = y {}

// matches! macro call
if matches!(a, some_type) && let Some(x) = y {}

// block expression returning bool
if { true } && let Some(x) = y {}

// field access
if a.x && let Some(x) = y {}
}
Loading