From 051fdec12447f453f960b5e21ab5a7cfceb6f27c Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:18:23 -0700 Subject: [PATCH 1/7] support if-local and if-const --- Cargo.lock | 4 +- Cargo.toml | 2 +- src/formatters/expression.rs | 26 ++++++++-- src/formatters/luau.rs | 32 ++++++++++-- src/formatters/stmt.rs | 95 +++++++++++++++++++++++++++++------- 5 files changed, 131 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 577c86c0a..4d9413140 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -395,9 +395,9 @@ dependencies = [ [[package]] name = "full_moon" -version = "2.2.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a083d6f598bbe9d70e694f2a5cf78f0354678a8c13528da2df6467b089586404" +checksum = "5bc70b300f9b09c409fa0a68a9f0c3e6b43573e47680d16932a0f8dade11a193" dependencies = [ "bytecount", "cfg-if", diff --git a/Cargo.toml b/Cargo.toml index a0e549eee..1c4042b32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ console = "0.15.7" crossbeam-channel = "0.5.15" ec4rs = { version = "1.0.2", optional = true } env_logger = { version = "0.10.0", default-features = false } -full_moon = "2.2.0" +full_moon = "3.0.0" globset = "0.4.13" ignore = "0.4.20" lazy_static = "1.4.0" diff --git a/src/formatters/expression.rs b/src/formatters/expression.rs index 9f245b093..dc56a85db 100644 --- a/src/formatters/expression.rs +++ b/src/formatters/expression.rs @@ -15,7 +15,7 @@ use std::boxed::Box; #[cfg(feature = "luau")] use crate::formatters::{ assignment::calculate_hang_level, - luau::{format_type_assertion, format_type_instantiation}, + luau::{format_if_condition_binding, format_type_assertion, format_type_instantiation}, stmt::remove_condition_parentheses, trivia_util::HasInlineComments, }; @@ -515,13 +515,17 @@ fn format_else_if_expression_singleline( shape: Shape, ) -> ElseIfExpression { let else_if_token = fmt_symbol!(ctx, else_if_expression.else_if_token(), "elseif ", shape); + let binding = else_if_expression + .binding() + .map(|binding| format_if_condition_binding(ctx, binding, shape)); + let binding_width = binding.as_ref().map_or(0, |binding| binding.to_string().len()); let else_if_condition = remove_condition_parentheses(else_if_expression.condition().to_owned()); - let else_if_condition = format_expression(ctx, &else_if_condition, shape + 7); // 7 = "elseif " + let else_if_condition = format_expression(ctx, &else_if_condition, shape + 7 + binding_width); // 7 = "elseif " let (then_token, expression) = format_token_expression_sequence( ctx, else_if_expression.then_token(), else_if_expression.expression(), - shape.take_first_line(&else_if_condition) + 13, // 13 = "elseif " + " then ", + shape.take_first_line(&else_if_condition) + 13 + binding_width, // 13 = "elseif " + " then ", ); // Add a space before the then token @@ -531,6 +535,7 @@ fn format_else_if_expression_singleline( ElseIfExpression::new(else_if_condition, expression) .with_else_if_token(else_if_token) + .with_binding(binding) .with_then_token(then_token) } @@ -600,6 +605,12 @@ fn format_if_expression(ctx: &Context, if_expression: &IfExpression, shape: Shap let condition = remove_condition_parentheses(if_expression.condition().to_owned()); let if_token = fmt_symbol!(ctx, if_expression.if_token(), "if ", shape); + // Format the `local =` binding (`if local` / `if const`), if present + let binding = if_expression + .binding() + .map(|binding| format_if_condition_binding(ctx, binding, shape)); + let binding_width = binding.as_ref().map_or(0, |binding| binding.to_string().len()); + // Initially format the remainder on a single line let singleline_condition = format_expression(ctx, &condition, shape.with_infinite_width()); let then_token = fmt_symbol!(ctx, if_expression.then_token(), " then ", shape); @@ -634,7 +645,7 @@ fn format_if_expression(ctx: &Context, if_expression: &IfExpression, shape: Shap const ELSE_LENGTH: usize = 6; // " else " // Determine if we need to hang the expression - let singleline_shape = (shape + IF_LENGTH + THEN_LENGTH + ELSE_LENGTH) + let singleline_shape = (shape + IF_LENGTH + THEN_LENGTH + ELSE_LENGTH + binding_width) .take_first_line(&strip_trivia(&singleline_condition)) .take_first_line(&strip_trivia(&singleline_expression)) .take_first_line(&else_ifs.as_ref().map_or(String::new(), |x| { @@ -748,8 +759,13 @@ fn format_if_expression(ctx: &Context, if_expression: &IfExpression, shape: Shap create_indent_trivia(ctx, hanging_shape), ])); + let binding = else_if_expression.binding().map(|binding| { + format_if_condition_binding(ctx, binding, hanging_shape) + }); + ElseIfExpression::new(else_if_condition, expression) .with_else_if_token(else_if_token) + .with_binding(binding) .with_then_token(then_token) } else { singleline_else_if.update_leading_trivia(FormatTriviaType::Append( @@ -776,6 +792,7 @@ fn format_if_expression(ctx: &Context, if_expression: &IfExpression, shape: Shap IfExpression::new(condition, expression, else_expression) .with_if_token(if_token) + .with_binding(binding) .with_then_token(then_token) .with_else_if(else_ifs) .with_else_token(else_token) @@ -797,6 +814,7 @@ fn format_if_expression(ctx: &Context, if_expression: &IfExpression, shape: Shap singleline_else_expression, ) .with_if_token(if_token) + .with_binding(binding) .with_then_token(then_token) .with_else_if(else_ifs) .with_else_token(else_token) diff --git a/src/formatters/luau.rs b/src/formatters/luau.rs index 60b7ab4a5..1f683a515 100644 --- a/src/formatters/luau.rs +++ b/src/formatters/luau.rs @@ -27,9 +27,9 @@ use crate::{ use full_moon::ast::{ luau::{ ExportedTypeDeclaration, ExportedTypeFunction, GenericDeclaration, - GenericDeclarationParameter, GenericParameterInfo, IndexedTypeInfo, LuauAttribute, - TypeArgument, TypeAssertion, TypeDeclaration, TypeField, TypeFieldKey, TypeFunction, - TypeInfo, TypeInstantiation, TypeIntersection, TypeSpecifier, TypeUnion, + GenericDeclarationParameter, GenericParameterInfo, IfConditionBinding, IndexedTypeInfo, + LuauAttribute, TypeArgument, TypeAssertion, TypeDeclaration, TypeField, TypeFieldKey, + TypeFunction, TypeInfo, TypeInstantiation, TypeIntersection, TypeSpecifier, TypeUnion, }, punctuated::Pair, }; @@ -1499,6 +1499,32 @@ pub fn format_type_specifier( .with_type_info(type_info) } +/// Formats an [`IfConditionBinding`] node - the `local =` / `const =` part of an +/// `if local` / `if const` binding, as in `if local player = getPlayer() then ... end`. +/// The formatted binding is ` [: ] = ` (with a trailing space), so it +/// slots directly between the `if `/`elseif ` token and the condition expression. +pub fn format_if_condition_binding( + ctx: &Context, + binding: &IfConditionBinding, + shape: Shape, +) -> IfConditionBinding { + // Preserve the keyword (`local` or `const`), normalising to a single trailing space + let local_token = format_token_reference(ctx, binding.local_token(), shape) + .update_trailing_trivia(FormatTriviaType::Append(vec![Token::new(TokenType::spaces(1))])); + let name = format_token_reference(ctx, binding.name(), shape); + let type_specifier = binding + .type_specifier() + .map(|type_specifier| format_type_specifier(ctx, type_specifier, shape)); + let equal_token = fmt_symbol!(ctx, binding.equal_token(), " = ", shape); + + binding + .to_owned() + .with_local_token(local_token) + .with_name(name) + .with_type_specifier(type_specifier) + .with_equal_token(equal_token) +} + pub fn format_type_instantiation( ctx: &Context, type_instantiation: &TypeInstantiation, diff --git a/src/formatters/stmt.rs b/src/formatters/stmt.rs index b52a39d21..64021ef0c 100644 --- a/src/formatters/stmt.rs +++ b/src/formatters/stmt.rs @@ -8,8 +8,8 @@ use crate::formatters::functions::format_const_function; use crate::formatters::goto::{format_goto, format_goto_no_trivia, format_label}; #[cfg(feature = "luau")] use crate::formatters::luau::{ - format_exported_type_declaration, format_exported_type_function, format_type_declaration_stmt, - format_type_function_stmt, format_type_specifier, + format_exported_type_declaration, format_exported_type_function, format_if_condition_binding, + format_type_declaration_stmt, format_type_function_stmt, format_type_specifier, }; use crate::{ context::{create_indent_trivia, create_newline_trivia, Context, FormatNode}, @@ -346,6 +346,12 @@ fn format_else_if(ctx: &Context, else_if_node: &ElseIf, shape: Shape) -> ElseIf // Remove parentheses around the condition let condition = remove_condition_parentheses(else_if_node.condition().to_owned()); + // Format the `local =` binding (`if local` / `if const`), if present + let binding = else_if_node + .binding() + .map(|binding| format_if_condition_binding(ctx, binding, shape)); + let binding_width = binding.as_ref().map_or(0, |binding| binding.to_string().len()); + // Compute the indent let end_token_type = if should_indent_further(else_if_node.else_if_token().leading_trivia(), shape) { @@ -355,11 +361,12 @@ fn format_else_if(ctx: &Context, else_if_node: &ElseIf, shape: Shape) -> ElseIf }; let elseif_token = format_end_token(ctx, else_if_node.else_if_token(), end_token_type, shape); - let singleline_condition = format_expression(ctx, &condition, shape + 7); + let singleline_condition = format_expression(ctx, &condition, shape + 7 + binding_width); let singleline_then_token = fmt_symbol!(ctx, else_if_node.then_token(), " then", shape); // Determine if we need to hang the condition - let singleline_shape = shape + (7 + 5 + strip_trivia(&singleline_condition).to_string().len()); // 7 = "elseif ", 3 = " then" + let singleline_shape = + shape + (7 + 5 + binding_width + strip_trivia(&singleline_condition).to_string().len()); // 7 = "elseif ", 5 = " then" let require_multiline_expression = singleline_shape.over_budget() || else_if_node .else_if_token() @@ -369,12 +376,25 @@ fn format_else_if(ctx: &Context, else_if_node: &ElseIf, shape: Shape) -> ElseIf .has_leading_comments(CommentSearch::All) || trivia_util::contains_comments(&condition); - let elseif_token = match require_multiline_expression { - true => elseif_token - .update_trailing_trivia(FormatTriviaType::Append(vec![create_newline_trivia(ctx)])), - false => elseif_token.update_trailing_trivia(FormatTriviaType::Append(vec![Token::new( + // When multilining with a binding, the binding stays on the `elseif` line and the condition + // hangs below it, so the newline goes after the binding rather than after `elseif`. + let binding = match (require_multiline_expression, binding) { + (true, Some(binding)) => { + let equal_token = binding.equal_token().update_trailing_trivia( + FormatTriviaType::Replace(vec![create_newline_trivia(ctx)]), + ); + Some(binding.with_equal_token(equal_token)) + } + (_, binding) => binding, + }; + + let elseif_token = if require_multiline_expression && binding.is_none() { + elseif_token + .update_trailing_trivia(FormatTriviaType::Append(vec![create_newline_trivia(ctx)])) + } else { + elseif_token.update_trailing_trivia(FormatTriviaType::Append(vec![Token::new( TokenType::spaces(1), - )])), + )])) } .update_leading_trivia(FormatTriviaType::Append(leading_trivia.to_owned())); @@ -406,6 +426,7 @@ fn format_else_if(ctx: &Context, else_if_node: &ElseIf, shape: Shape) -> ElseIf else_if_node .to_owned() .with_else_if_token(elseif_token) + .with_binding(binding) .with_condition(condition) .with_then_token(then_token) .with_block(block) @@ -441,13 +462,24 @@ pub fn format_if(ctx: &Context, if_node: &If, shape: Shape) -> If { // Remove parentheses around the condition let condition = remove_condition_parentheses(if_node.condition().to_owned()); + // Format the `local =` binding (`if local` / `if const`), if present + #[cfg(feature = "luau")] + let binding = if_node + .binding() + .map(|binding| format_if_condition_binding(ctx, binding, shape)); + #[cfg(feature = "luau")] + let binding_width = binding.as_ref().map_or(0, |binding| binding.to_string().len()); + #[cfg(not(feature = "luau"))] + let binding_width = 0; + let singleline_if_token = fmt_symbol!(ctx, if_node.if_token(), "if ", shape); - let singleline_condition = format_expression(ctx, &condition, shape + IF_LEN + THEN_LEN); + let singleline_condition = + format_expression(ctx, &condition, shape + IF_LEN + THEN_LEN + binding_width); let singleline_then_token = fmt_symbol!(ctx, if_node.then_token(), " then", shape); // Determine if we need to hang the condition - let singleline_shape = - shape + (IF_LEN + THEN_LEN + strip_trivia(&singleline_condition).to_string().len()); + let singleline_shape = shape + + (IF_LEN + THEN_LEN + binding_width + strip_trivia(&singleline_condition).to_string().len()); let require_multiline_expression = singleline_shape.over_budget() || if_node.if_token().has_trailing_comments(CommentSearch::All) || if_node @@ -502,6 +534,9 @@ pub fn format_if(ctx: &Context, if_node: &If, shape: Shape) -> If { .with_block(block) .with_end_token(end_token); + #[cfg(feature = "luau")] + let singleline_if = singleline_if.with_binding(binding.clone()); + // See if it fits under the column width. If it does, bail early and return this singleline if if !shape .add_width(strip_trivia(&singleline_if).to_string().len()) @@ -511,10 +546,29 @@ pub fn format_if(ctx: &Context, if_node: &If, shape: Shape) -> If { } } - let if_token = match require_multiline_expression { - true => fmt_symbol!(ctx, if_node.if_token(), "if", shape) - .update_trailing_trivia(FormatTriviaType::Append(vec![create_newline_trivia(ctx)])), - false => singleline_if_token, + // When multilining with a binding, the binding stays on the `if` line and the condition hangs + // below it, so the newline goes after the binding rather than after `if`. + #[cfg(feature = "luau")] + let binding = match (require_multiline_expression, binding) { + (true, Some(binding)) => { + let equal_token = binding.equal_token().update_trailing_trivia( + FormatTriviaType::Replace(vec![create_newline_trivia(ctx)]), + ); + Some(binding.with_equal_token(equal_token)) + } + (_, binding) => binding, + }; + + #[cfg(feature = "luau")] + let has_binding = binding.is_some(); + #[cfg(not(feature = "luau"))] + let has_binding = false; + + let if_token = if require_multiline_expression && !has_binding { + fmt_symbol!(ctx, if_node.if_token(), "if", shape) + .update_trailing_trivia(FormatTriviaType::Append(vec![create_newline_trivia(ctx)])) + } else { + singleline_if_token } .update_leading_trivia(FormatTriviaType::Append(leading_trivia.to_owned())); @@ -584,7 +638,7 @@ pub fn format_if(ctx: &Context, if_node: &If, shape: Shape) -> If { _ => unreachable!("Got an else token with no else block or vice versa"), }; - if_node + let if_node = if_node .to_owned() .with_if_token(if_token) .with_condition(condition) @@ -593,7 +647,12 @@ pub fn format_if(ctx: &Context, if_node: &If, shape: Shape) -> If { .with_else_if(else_if) .with_else_token(else_token) .with_else(else_block) - .with_end_token(end_token) + .with_end_token(end_token); + + #[cfg(feature = "luau")] + let if_node = if_node.with_binding(binding); + + if_node } /// Format a NumericFor node From a97f297366da4e536b4522af50eff7bf3241cdca Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:56:56 -0700 Subject: [PATCH 2/7] formatting and small fixes --- src/formatters/expression.rs | 8 ++++-- src/formatters/luau.rs | 4 ++- src/formatters/stmt.rs | 53 +++++++++++++++++++++++++++--------- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/src/formatters/expression.rs b/src/formatters/expression.rs index 514efbf84..0e9293332 100644 --- a/src/formatters/expression.rs +++ b/src/formatters/expression.rs @@ -520,7 +520,9 @@ fn format_else_if_expression_singleline( let binding = else_if_expression .binding() .map(|binding| format_if_condition_binding(ctx, binding, shape)); - let binding_width = binding.as_ref().map_or(0, |binding| binding.to_string().len()); + let binding_width = binding + .as_ref() + .map_or(0, |binding| binding.to_string().len()); let else_if_condition = remove_condition_parentheses(else_if_expression.condition().to_owned()); let else_if_condition = format_expression(ctx, &else_if_condition, shape + 7 + binding_width); // 7 = "elseif " let (then_token, expression) = format_token_expression_sequence( @@ -611,7 +613,9 @@ fn format_if_expression(ctx: &Context, if_expression: &IfExpression, shape: Shap let binding = if_expression .binding() .map(|binding| format_if_condition_binding(ctx, binding, shape)); - let binding_width = binding.as_ref().map_or(0, |binding| binding.to_string().len()); + let binding_width = binding + .as_ref() + .map_or(0, |binding| binding.to_string().len()); // Initially format the remainder on a single line let singleline_condition = format_expression(ctx, &condition, shape.with_infinite_width()); diff --git a/src/formatters/luau.rs b/src/formatters/luau.rs index 8e1915cba..1e78e1bb6 100644 --- a/src/formatters/luau.rs +++ b/src/formatters/luau.rs @@ -1544,7 +1544,9 @@ pub fn format_if_condition_binding( ) -> IfConditionBinding { // Preserve the keyword (`local` or `const`), normalising to a single trailing space let local_token = format_token_reference(ctx, binding.local_token(), shape) - .update_trailing_trivia(FormatTriviaType::Append(vec![Token::new(TokenType::spaces(1))])); + .update_trailing_trivia(FormatTriviaType::Append(vec![Token::new( + TokenType::spaces(1), + )])); let name = format_token_reference(ctx, binding.name(), shape); let type_specifier = binding .type_specifier() diff --git a/src/formatters/stmt.rs b/src/formatters/stmt.rs index f760c16d9..0ad4630dc 100644 --- a/src/formatters/stmt.rs +++ b/src/formatters/stmt.rs @@ -346,10 +346,16 @@ fn format_else_if(ctx: &Context, else_if_node: &ElseIf, shape: Shape) -> ElseIf let condition = remove_condition_parentheses(else_if_node.condition().to_owned()); // Format the `local =` binding (`if local` / `if const`), if present + #[cfg(feature = "luau")] let binding = else_if_node .binding() .map(|binding| format_if_condition_binding(ctx, binding, shape)); - let binding_width = binding.as_ref().map_or(0, |binding| binding.to_string().len()); + #[cfg(feature = "luau")] + let binding_width = binding + .as_ref() + .map_or(0, |binding| binding.to_string().len()); + #[cfg(not(feature = "luau"))] + let binding_width = 0; // Compute the indent let end_token_type = @@ -377,17 +383,26 @@ fn format_else_if(ctx: &Context, else_if_node: &ElseIf, shape: Shape) -> ElseIf // When multilining with a binding, the binding stays on the `elseif` line and the condition // hangs below it, so the newline goes after the binding rather than after `elseif`. + #[cfg(feature = "luau")] let binding = match (require_multiline_expression, binding) { (true, Some(binding)) => { - let equal_token = binding.equal_token().update_trailing_trivia( - FormatTriviaType::Replace(vec![create_newline_trivia(ctx)]), - ); + let equal_token = + binding + .equal_token() + .update_trailing_trivia(FormatTriviaType::Replace(vec![ + create_newline_trivia(ctx), + ])); Some(binding.with_equal_token(equal_token)) } (_, binding) => binding, }; - let elseif_token = if require_multiline_expression && binding.is_none() { + #[cfg(feature = "luau")] + let has_binding = binding.is_some(); + #[cfg(not(feature = "luau"))] + let has_binding = false; + + let elseif_token = if require_multiline_expression && !has_binding { elseif_token .update_trailing_trivia(FormatTriviaType::Append(vec![create_newline_trivia(ctx)])) } else { @@ -422,13 +437,17 @@ fn format_else_if(ctx: &Context, else_if_node: &ElseIf, shape: Shape) -> ElseIf let block_shape = shape.reset().increment_block_indent(); let block = format_block(ctx, else_if_node.block(), block_shape); - else_if_node + let else_if_node = else_if_node .to_owned() .with_else_if_token(elseif_token) - .with_binding(binding) .with_condition(condition) .with_then_token(then_token) - .with_block(block) + .with_block(block); + + #[cfg(feature = "luau")] + let else_if_node = else_if_node.with_binding(binding); + + else_if_node } /// Checks to see whether an [`If`] statement matches the structure of an "if guard". @@ -467,7 +486,9 @@ pub fn format_if(ctx: &Context, if_node: &If, shape: Shape) -> If { .binding() .map(|binding| format_if_condition_binding(ctx, binding, shape)); #[cfg(feature = "luau")] - let binding_width = binding.as_ref().map_or(0, |binding| binding.to_string().len()); + let binding_width = binding + .as_ref() + .map_or(0, |binding| binding.to_string().len()); #[cfg(not(feature = "luau"))] let binding_width = 0; @@ -478,7 +499,10 @@ pub fn format_if(ctx: &Context, if_node: &If, shape: Shape) -> If { // Determine if we need to hang the condition let singleline_shape = shape - + (IF_LEN + THEN_LEN + binding_width + strip_trivia(&singleline_condition).to_string().len()); + + (IF_LEN + + THEN_LEN + + binding_width + + strip_trivia(&singleline_condition).to_string().len()); let require_multiline_expression = singleline_shape.over_budget() || if_node.if_token().has_trailing_comments(CommentSearch::All) || if_node @@ -550,9 +574,12 @@ pub fn format_if(ctx: &Context, if_node: &If, shape: Shape) -> If { #[cfg(feature = "luau")] let binding = match (require_multiline_expression, binding) { (true, Some(binding)) => { - let equal_token = binding.equal_token().update_trailing_trivia( - FormatTriviaType::Replace(vec![create_newline_trivia(ctx)]), - ); + let equal_token = + binding + .equal_token() + .update_trailing_trivia(FormatTriviaType::Replace(vec![ + create_newline_trivia(ctx), + ])); Some(binding.with_equal_token(equal_token)) } (_, binding) => binding, From fde9f05c6f4d5eba11f344f6a5bb09af91b7b266 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:01:06 -0700 Subject: [PATCH 3/7] tests --- tests/inputs-luau/if-expression-binding.lua | 25 ++++++ tests/inputs-luau/if-local-if-const.lua | 60 ++++++++++++++ ...tests__luau@if-expression-binding.lua.snap | 38 +++++++++ .../tests__luau@if-local-if-const.lua.snap | 79 +++++++++++++++++++ 4 files changed, 202 insertions(+) create mode 100644 tests/inputs-luau/if-expression-binding.lua create mode 100644 tests/inputs-luau/if-local-if-const.lua create mode 100644 tests/snapshots/tests__luau@if-expression-binding.lua.snap create mode 100644 tests/snapshots/tests__luau@if-local-if-const.lua.snap diff --git a/tests/inputs-luau/if-expression-binding.lua b/tests/inputs-luau/if-expression-binding.lua new file mode 100644 index 000000000..55c332d15 --- /dev/null +++ b/tests/inputs-luau/if-expression-binding.lua @@ -0,0 +1,25 @@ +-- `if local` / `if const` bindings inside if-expressions +local a = if local x = find() then x else default + +local b = if const y = compute() then y else 0 + +-- with a type specifier +local c = if local p: Player = getPlayer() then p.Name else "none" + +-- elseif binding +local d = if local first = tryFirst() then first elseif const second = trySecond() then second else fallback + +-- whitespace normalisation around the binding +local e = if local foo = bar then foo else baz + +-- long binding/branches that must hang +local f = if local reallyLongBindingName: SomeLongType = someExpensiveComputationCall(argumentOne, argumentTwo) then reallyLongBindingName else fallbackValueThatIsAlsoQuiteLong + +local g = if const resolved = resolveSomethingExpensive(inputA, inputB, inputC) then transform(resolved) elseif const other = resolveOther(inputD, inputE) then transform(other) else defaultResultValue + +-- binding condition nested in a table +local thing = makeSomething("Foo", { + Child = if local resolvedChild = resolveChild() then makeSomething("Bar", { + scale = 1, + }) else makeSomething("Baz"), +}) diff --git a/tests/inputs-luau/if-local-if-const.lua b/tests/inputs-luau/if-local-if-const.lua new file mode 100644 index 000000000..e8c92567d --- /dev/null +++ b/tests/inputs-luau/if-local-if-const.lua @@ -0,0 +1,60 @@ +-- `if local` / `if const` statement bindings +if local x = getValue() then + print(x) +end + +if const y = compute() then + print(y) +end + +-- with type specifiers +if local player: Player = getPlayer() then + greet(player) +end + +if const count: number = tally() then + print(count) +end + +-- elseif bindings, mixing local/const and plain conditions +if local a = first() then + useA(a) +elseif const b = second() then + useB(b) +elseif c then + useC() +else + fallback() +end + +-- normalisation of extra whitespace around the binding +if local foo = bar then + baz() +end + +if const qux : string = thing() then + quux() +end + +-- long binding where the condition hangs below (newline goes after the binding `=`) +if local resultValue: ResultType = someLongConditionPredicateFunction() and someOtherLongConditionPredicateFunction() and theThirdConditionPredicate() then + consume(resultValue) +end + +if const aggregatedTotal = firstPredicateCheckFunction() or secondPredicateCheckFunction() or thirdPredicateCheckFunction() or fourthPredicateCheckFunction() then + report(aggregatedTotal) +end + +-- long binding on an elseif branch, condition hangs below +if quickCheck then + fast() +elseif local resolvedEntity: Entity = resolveEntityFromTheRegistry() and validateTheResolvedEntityValue() and entityIsStillActive() then + process(resolvedEntity) +end + +-- nested if-local inside a block +if local outer = openOuter() then + if const inner = openInner(outer) then + combine(outer, inner) + end +end diff --git a/tests/snapshots/tests__luau@if-expression-binding.lua.snap b/tests/snapshots/tests__luau@if-expression-binding.lua.snap new file mode 100644 index 000000000..1b140bc99 --- /dev/null +++ b/tests/snapshots/tests__luau@if-expression-binding.lua.snap @@ -0,0 +1,38 @@ +--- +source: tests/tests.rs +expression: "format(&contents, LuaVersion::Luau)" +input_file: tests/inputs-luau/if-expression-binding.lua +--- +-- `if local` / `if const` bindings inside if-expressions +local a = if local x = find() then x else default + +local b = if const y = compute() then y else 0 + +-- with a type specifier +local c = if local p: Player = getPlayer() then p.Name else "none" + +-- elseif binding +local d = if local first = tryFirst() then first elseif const second = trySecond() then second else fallback + +-- whitespace normalisation around the binding +local e = if local foo = bar then foo else baz + +-- long binding/branches that must hang +local f = if local reallyLongBindingName: SomeLongType = someExpensiveComputationCall(argumentOne, argumentTwo) + then reallyLongBindingName + else fallbackValueThatIsAlsoQuiteLong + +local g = if const resolved = resolveSomethingExpensive(inputA, inputB, inputC) + then transform(resolved) + elseif const other = resolveOther(inputD, inputE) then transform(other) + else defaultResultValue + +-- binding condition nested in a table +local thing = makeSomething("Foo", { + Child = if local resolvedChild = resolveChild() + then makeSomething("Bar", { + scale = 1, + }) + else makeSomething("Baz"), +}) + diff --git a/tests/snapshots/tests__luau@if-local-if-const.lua.snap b/tests/snapshots/tests__luau@if-local-if-const.lua.snap new file mode 100644 index 000000000..7435be83d --- /dev/null +++ b/tests/snapshots/tests__luau@if-local-if-const.lua.snap @@ -0,0 +1,79 @@ +--- +source: tests/tests.rs +expression: "format(&contents, LuaVersion::Luau)" +input_file: tests/inputs-luau/if-local-if-const.lua +--- +-- `if local` / `if const` statement bindings +if local x = getValue() then + print(x) +end + +if const y = compute() then + print(y) +end + +-- with type specifiers +if local player: Player = getPlayer() then + greet(player) +end + +if const count: number = tally() then + print(count) +end + +-- elseif bindings, mixing local/const and plain conditions +if local a = first() then + useA(a) +elseif const b = second() then + useB(b) +elseif c then + useC() +else + fallback() +end + +-- normalisation of extra whitespace around the binding +if local foo = bar then + baz() +end + +if const qux: string = thing() then + quux() +end + +-- long binding where the condition hangs below (newline goes after the binding `=`) +if local resultValue: ResultType = + someLongConditionPredicateFunction() + and someOtherLongConditionPredicateFunction() + and theThirdConditionPredicate() +then + consume(resultValue) +end + +if const aggregatedTotal = + firstPredicateCheckFunction() + or secondPredicateCheckFunction() + or thirdPredicateCheckFunction() + or fourthPredicateCheckFunction() +then + report(aggregatedTotal) +end + +-- long binding on an elseif branch, condition hangs below +if quickCheck then + fast() +elseif local resolvedEntity: Entity = + resolveEntityFromTheRegistry() + and validateTheResolvedEntityValue() + and entityIsStillActive() +then + process(resolvedEntity) +end + +-- nested if-local inside a block +if local outer = openOuter() then + if const inner = openInner(outer) then + combine(outer, inner) + end +end + From 2bbfd2f85e4bc495867d3ebb6e8f47868ba6d895 Mon Sep 17 00:00:00 2001 From: Annie Tang <98965493+annieetang@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:14:35 -0700 Subject: [PATCH 4/7] changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd575604e..98b0bc256 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Luau: Added support for `if local` / `if const` condition bindings in both `if` statements and `if` expressions (e.g. `if local x = getValue() then ... end`) + ### Fixed - Fixed range formatting indenting statements one level too shallow when nested inside a function expression within a table constructor ([#1145](https://github.com/JohnnyMorganz/StyLua/issues/1145)) From 5c9036e2a13c3993e78d6c1a0d8938fe7f0932e3 Mon Sep 17 00:00:00 2001 From: JohnnyMorganz Date: Sat, 10 Oct 2026 15:15:03 +0200 Subject: [PATCH 5/7] Improve handling of formatting in complex situations - Check for comments after `if` / `elseif`, which would format to `if --commentlocal x` - Check for comments after `=` sign - Bug where `elseif local` hanging would go to `elseiflocal` --- src/formatters/expression.rs | 31 ++- src/formatters/luau.rs | 30 ++- src/formatters/stmt.rs | 178 +++++++++++++----- .../if-local-if-const-comments.lua | 27 +++ ...__luau@if-local-if-const-comments.lua.snap | 51 +++++ 5 files changed, 264 insertions(+), 53 deletions(-) create mode 100644 tests/inputs-luau/if-local-if-const-comments.lua create mode 100644 tests/snapshots/tests__luau@if-local-if-const-comments.lua.snap diff --git a/src/formatters/expression.rs b/src/formatters/expression.rs index 0e9293332..6ec35f10d 100644 --- a/src/formatters/expression.rs +++ b/src/formatters/expression.rs @@ -15,7 +15,10 @@ use std::boxed::Box; #[cfg(feature = "luau")] use crate::formatters::{ assignment::calculate_hang_level, - luau::{format_if_condition_binding, format_type_assertion, format_type_instantiation}, + luau::{ + format_if_condition_binding, format_type_assertion, format_type_instantiation, + if_condition_binding_width, + }, stmt::remove_condition_parentheses, trivia_util::HasInlineComments, }; @@ -520,9 +523,7 @@ fn format_else_if_expression_singleline( let binding = else_if_expression .binding() .map(|binding| format_if_condition_binding(ctx, binding, shape)); - let binding_width = binding - .as_ref() - .map_or(0, |binding| binding.to_string().len()); + let binding_width = binding.as_ref().map_or(0, if_condition_binding_width); let else_if_condition = remove_condition_parentheses(else_if_expression.condition().to_owned()); let else_if_condition = format_expression(ctx, &else_if_condition, shape + 7 + binding_width); // 7 = "elseif " let (then_token, expression) = format_token_expression_sequence( @@ -613,9 +614,7 @@ fn format_if_expression(ctx: &Context, if_expression: &IfExpression, shape: Shap let binding = if_expression .binding() .map(|binding| format_if_condition_binding(ctx, binding, shape)); - let binding_width = binding - .as_ref() - .map_or(0, |binding| binding.to_string().len()); + let binding_width = binding.as_ref().map_or(0, if_condition_binding_width); // Initially format the remainder on a single line let singleline_condition = format_expression(ctx, &condition, shape.with_infinite_width()); @@ -682,7 +681,7 @@ fn format_if_expression(ctx: &Context, if_expression: &IfExpression, shape: Shap let condition = hang_expression_trailing_newline( ctx, if_expression.condition(), - shape.increment_additional_indent(), + (shape + binding_width).increment_additional_indent(), Some(1), ); let hanging_shape = shape.reset().increment_additional_indent(); @@ -765,9 +764,23 @@ fn format_if_expression(ctx: &Context, if_expression: &IfExpression, shape: Shap create_indent_trivia(ctx, hanging_shape), ])); + // The condition hangs on the line below the binding, so the binding must + // be separated from `elseif` by a space, and have no trailing space let binding = else_if_expression.binding().map(|binding| { - format_if_condition_binding(ctx, binding, hanging_shape) + let binding = + format_if_condition_binding(ctx, binding, hanging_shape); + let equal_token = binding + .equal_token() + .update_trailing_trivia(FormatTriviaType::Replace(vec![])); + binding.with_equal_token(equal_token) }); + let else_if_token = if binding.is_some() { + else_if_token.update_trailing_trivia(FormatTriviaType::Append( + vec![Token::new(TokenType::spaces(1))], + )) + } else { + else_if_token + }; ElseIfExpression::new(else_if_condition, expression) .with_else_if_token(else_if_token) diff --git a/src/formatters/luau.rs b/src/formatters/luau.rs index 1e78e1bb6..6c079311e 100644 --- a/src/formatters/luau.rs +++ b/src/formatters/luau.rs @@ -1537,6 +1537,24 @@ pub fn format_type_specifier( /// `if local` / `if const` binding, as in `if local player = getPlayer() then ... end`. /// The formatted binding is ` [: ] = ` (with a trailing space), so it /// slots directly between the `if `/`elseif ` token and the condition expression. +/// The width of a formatted [`IfConditionBinding`] on the line it ends on, ignoring comments. +/// Counts characters rather than bytes, and only the last line if the binding spans multiple lines +/// (e.g. due to a multiline type specifier), as the condition continues from there. +pub fn if_condition_binding_width(binding: &IfConditionBinding) -> usize { + let text = format!( + "{} {}{} =", + strip_trivia(binding.local_token()), + strip_trivia(binding.name()), + binding + .type_specifier() + .map_or(String::new(), |specifier| format!( + ": {}", + strip_trivia(specifier.type_info()) + )), + ); + text.lines().last().map_or(0, |line| line.chars().count()) +} + pub fn format_if_condition_binding( ctx: &Context, binding: &IfConditionBinding, @@ -1547,10 +1565,18 @@ pub fn format_if_condition_binding( .update_trailing_trivia(FormatTriviaType::Append(vec![Token::new( TokenType::spaces(1), )])); - let name = format_token_reference(ctx, binding.name(), shape); + // ` ` precedes the name, then the name precedes the type specifier + let name_shape = shape + + (strip_trivia(binding.local_token()) + .to_string() + .chars() + .count() + + 1); + let name = format_token_reference(ctx, binding.name(), name_shape); + let specifier_shape = name_shape + strip_trivia(binding.name()).to_string().chars().count(); let type_specifier = binding .type_specifier() - .map(|type_specifier| format_type_specifier(ctx, type_specifier, shape)); + .map(|type_specifier| format_type_specifier(ctx, type_specifier, specifier_shape)); let equal_token = fmt_symbol!(ctx, binding.equal_token(), " = ", shape); binding diff --git a/src/formatters/stmt.rs b/src/formatters/stmt.rs index 0ad4630dc..94c8d0bdb 100644 --- a/src/formatters/stmt.rs +++ b/src/formatters/stmt.rs @@ -10,6 +10,7 @@ use crate::formatters::goto::{format_goto, format_goto_no_trivia, format_label}; use crate::formatters::luau::{ format_exported_type_declaration, format_exported_type_function, format_if_condition_binding, format_type_declaration_stmt, format_type_function_stmt, format_type_specifier, + if_condition_binding_width, }; use crate::{ context::{create_indent_trivia, create_newline_trivia, Context, FormatNode}, @@ -35,6 +36,8 @@ use crate::{ }, shape::Shape, }; +#[cfg(feature = "luau")] +use full_moon::ast::luau::IfConditionBinding; use full_moon::{ ast::{ punctuated::Punctuated, Block, Call, Do, ElseIf, Expression, FunctionArgs, FunctionCall, @@ -335,6 +338,46 @@ fn should_indent_further<'a>(trivia: impl Iterator, shape: Sha false } +/// Moves the newline after a multilined `if local` / `if const` binding if the condition has to +/// hang below it. The newline is appended rather than replacing the trivia so that comments after `=` +/// are kept. If the keyword token had trailing comments, the binding is moved onto its own +/// indented line. +#[cfg(feature = "luau")] +fn hang_if_condition_binding( + ctx: &Context, + binding: IfConditionBinding, + shape: Shape, + hang_condition: bool, + keyword_has_comments: bool, +) -> IfConditionBinding { + let newline = vec![create_newline_trivia(ctx)]; + let equal_token = if !hang_condition { + binding.equal_token().to_owned() + } else if binding + .equal_token() + .has_trailing_comments(CommentSearch::All) + { + binding + .equal_token() + .update_trailing_trivia(FormatTriviaType::Append(newline)) + } else { + binding + .equal_token() + .update_trailing_trivia(FormatTriviaType::Replace(newline)) + }; + let binding = binding.with_equal_token(equal_token); + + if keyword_has_comments { + let indent = create_indent_trivia(ctx, shape.reset().increment_additional_indent()); + let local_token = binding + .local_token() + .update_leading_trivia(FormatTriviaType::Append(vec![indent])); + binding.with_local_token(local_token) + } else { + binding + } +} + /// Formats an ElseIf node - This must always reside within format_if fn format_else_if(ctx: &Context, else_if_node: &ElseIf, shape: Shape) -> ElseIf { // Calculate trivia @@ -347,13 +390,17 @@ fn format_else_if(ctx: &Context, else_if_node: &ElseIf, shape: Shape) -> ElseIf // Format the `local =` binding (`if local` / `if const`), if present #[cfg(feature = "luau")] + let binding_has_comments = else_if_node + .binding() + .is_some_and(|binding| trivia_util::contains_comments(binding)); + #[cfg(not(feature = "luau"))] + let binding_has_comments = false; + #[cfg(feature = "luau")] let binding = else_if_node .binding() .map(|binding| format_if_condition_binding(ctx, binding, shape)); #[cfg(feature = "luau")] - let binding_width = binding - .as_ref() - .map_or(0, |binding| binding.to_string().len()); + let binding_width = binding.as_ref().map_or(0, if_condition_binding_width); #[cfg(not(feature = "luau"))] let binding_width = 0; @@ -379,22 +426,44 @@ fn format_else_if(ctx: &Context, else_if_node: &ElseIf, shape: Shape) -> ElseIf || else_if_node .then_token() .has_leading_comments(CommentSearch::All) + || binding_has_comments || trivia_util::contains_comments(&condition); - // When multilining with a binding, the binding stays on the `elseif` line and the condition - // hangs below it, so the newline goes after the binding rather than after `elseif`. + // When multilining with a binding, the binding stays on the `elseif` line and the condition hangs + // below it, so the newline goes after the binding rather than after `elseif`. + // If the `elseif` token has trailing comments, the newline must come straight after them, and the + // binding moves onto its own indented line. #[cfg(feature = "luau")] - let binding = match (require_multiline_expression, binding) { - (true, Some(binding)) => { - let equal_token = - binding - .equal_token() - .update_trailing_trivia(FormatTriviaType::Replace(vec![ - create_newline_trivia(ctx), - ])); - Some(binding.with_equal_token(equal_token)) - } - (_, binding) => binding, + let elseif_token_has_comments = else_if_node + .else_if_token() + .has_trailing_comments(CommentSearch::All); + #[cfg(not(feature = "luau"))] + let elseif_token_has_comments = false; + + // Only hang the condition below the binding when needed - a comment on the `elseif` token only + // needs a newline after the comment, not after the binding + #[cfg(feature = "luau")] + let hang_condition = require_multiline_expression + && (binding.is_none() + || singleline_shape.over_budget() + || binding_has_comments + || trivia_util::contains_comments(&condition)); + #[cfg(not(feature = "luau"))] + let hang_condition = require_multiline_expression; + + #[cfg(feature = "luau")] + let binding = if require_multiline_expression { + binding.map(|binding| { + hang_if_condition_binding( + ctx, + binding, + shape, + hang_condition, + elseif_token_has_comments, + ) + }) + } else { + binding }; #[cfg(feature = "luau")] @@ -402,23 +471,27 @@ fn format_else_if(ctx: &Context, else_if_node: &ElseIf, shape: Shape) -> ElseIf #[cfg(not(feature = "luau"))] let has_binding = false; - let elseif_token = if require_multiline_expression && !has_binding { - elseif_token - .update_trailing_trivia(FormatTriviaType::Append(vec![create_newline_trivia(ctx)])) - } else { - elseif_token.update_trailing_trivia(FormatTriviaType::Append(vec![Token::new( - TokenType::spaces(1), - )])) - } - .update_leading_trivia(FormatTriviaType::Append(leading_trivia.to_owned())); + let elseif_token = + if require_multiline_expression && (!has_binding || elseif_token_has_comments) { + elseif_token + .update_trailing_trivia(FormatTriviaType::Append(vec![create_newline_trivia(ctx)])) + } else { + elseif_token.update_trailing_trivia(FormatTriviaType::Append(vec![Token::new( + TokenType::spaces(1), + )])) + } + .update_leading_trivia(FormatTriviaType::Append(leading_trivia.to_owned())); - let condition = match require_multiline_expression { + let condition = match hang_condition { true => { let shape = shape.reset().increment_additional_indent(); hang_expression_trailing_newline(ctx, &condition, shape, None).update_leading_trivia( FormatTriviaType::Append(vec![create_indent_trivia(ctx, shape)]), ) } + // The condition stays on the binding line, but `then` still goes on its own line + false if require_multiline_expression => singleline_condition + .update_trailing_trivia(FormatTriviaType::Append(vec![create_newline_trivia(ctx)])), false => singleline_condition, }; @@ -482,13 +555,17 @@ pub fn format_if(ctx: &Context, if_node: &If, shape: Shape) -> If { // Format the `local =` binding (`if local` / `if const`), if present #[cfg(feature = "luau")] + let binding_has_comments = if_node + .binding() + .is_some_and(|binding| trivia_util::contains_comments(binding)); + #[cfg(not(feature = "luau"))] + let binding_has_comments = false; + #[cfg(feature = "luau")] let binding = if_node .binding() .map(|binding| format_if_condition_binding(ctx, binding, shape)); #[cfg(feature = "luau")] - let binding_width = binding - .as_ref() - .map_or(0, |binding| binding.to_string().len()); + let binding_width = binding.as_ref().map_or(0, if_condition_binding_width); #[cfg(not(feature = "luau"))] let binding_width = 0; @@ -508,6 +585,7 @@ pub fn format_if(ctx: &Context, if_node: &If, shape: Shape) -> If { || if_node .then_token() .has_leading_comments(CommentSearch::All) + || binding_has_comments || trivia_util::contains_comments(&condition); if !require_multiline_expression @@ -571,18 +649,31 @@ pub fn format_if(ctx: &Context, if_node: &If, shape: Shape) -> If { // When multilining with a binding, the binding stays on the `if` line and the condition hangs // below it, so the newline goes after the binding rather than after `if`. + // If the `if` token has trailing comments, the newline must come straight after them, and the + // binding moves onto its own indented line. #[cfg(feature = "luau")] - let binding = match (require_multiline_expression, binding) { - (true, Some(binding)) => { - let equal_token = - binding - .equal_token() - .update_trailing_trivia(FormatTriviaType::Replace(vec![ - create_newline_trivia(ctx), - ])); - Some(binding.with_equal_token(equal_token)) - } - (_, binding) => binding, + let if_token_has_comments = if_node.if_token().has_trailing_comments(CommentSearch::All); + #[cfg(not(feature = "luau"))] + let if_token_has_comments = false; + + // Only hang the condition below the binding when needed - a comment on the `if` token only + // needs a newline after the comment, not after the binding + #[cfg(feature = "luau")] + let hang_condition = require_multiline_expression + && (binding.is_none() + || singleline_shape.over_budget() + || binding_has_comments + || trivia_util::contains_comments(&condition)); + #[cfg(not(feature = "luau"))] + let hang_condition = require_multiline_expression; + + #[cfg(feature = "luau")] + let binding = if require_multiline_expression { + binding.map(|binding| { + hang_if_condition_binding(ctx, binding, shape, hang_condition, if_token_has_comments) + }) + } else { + binding }; #[cfg(feature = "luau")] @@ -590,7 +681,7 @@ pub fn format_if(ctx: &Context, if_node: &If, shape: Shape) -> If { #[cfg(not(feature = "luau"))] let has_binding = false; - let if_token = if require_multiline_expression && !has_binding { + let if_token = if require_multiline_expression && (!has_binding || if_token_has_comments) { fmt_symbol!(ctx, if_node.if_token(), "if", shape) .update_trailing_trivia(FormatTriviaType::Append(vec![create_newline_trivia(ctx)])) } else { @@ -598,13 +689,16 @@ pub fn format_if(ctx: &Context, if_node: &If, shape: Shape) -> If { } .update_leading_trivia(FormatTriviaType::Append(leading_trivia.to_owned())); - let condition = match require_multiline_expression { + let condition = match hang_condition { true => { let shape = shape.reset().increment_additional_indent(); hang_expression_trailing_newline(ctx, &condition, shape, None).update_leading_trivia( FormatTriviaType::Append(vec![create_indent_trivia(ctx, shape)]), ) } + // The condition stays on the binding line, but `then` still goes on its own line + false if require_multiline_expression => singleline_condition + .update_trailing_trivia(FormatTriviaType::Append(vec![create_newline_trivia(ctx)])), false => singleline_condition, }; diff --git a/tests/inputs-luau/if-local-if-const-comments.lua b/tests/inputs-luau/if-local-if-const-comments.lua new file mode 100644 index 000000000..ce6c35532 --- /dev/null +++ b/tests/inputs-luau/if-local-if-const-comments.lua @@ -0,0 +1,27 @@ +if -- note + local x = f() then + print(x) +elseif -- c + local y = g() then + print(y) +end + +if local x = -- why + getValue() then + print(x) +end + +if local x = 1 then print(x) elseif local y = -- why2 + h() then print(y) end + +local w = if a then b elseif local otherBindingName = resolveOtherThingWithAVeryLongFunctionName(argumentNumberOne, argumentNumberTwo) then cccccccccccccccccccc else dddddddddddddddddddd + +if local x = a then + print(x) +elseif local y = b then + print(y) +else + print("none") +end + +local w = if a then b elseif local otherBindingName = resolveOtherThingWithAVeryLongFunctionName(argumentNumberOne, argumentNumberTwo) then cccccccccccccccccccc else dddddddddddddddddddd diff --git a/tests/snapshots/tests__luau@if-local-if-const-comments.lua.snap b/tests/snapshots/tests__luau@if-local-if-const-comments.lua.snap new file mode 100644 index 000000000..bf1953698 --- /dev/null +++ b/tests/snapshots/tests__luau@if-local-if-const-comments.lua.snap @@ -0,0 +1,51 @@ +--- +source: tests/tests.rs +expression: "format(&contents, LuaVersion::Luau)" +input_file: tests/inputs-luau/if-local-if-const-comments.lua +--- +if -- note + local x = f() +then + print(x) +elseif -- c + local y = g() +then + print(y) +end + +if local x = -- why + getValue() +then + print(x) +end + +if local x = 1 then + print(x) +elseif local y = -- why2 + h() +then + print(y) +end + +local w = if a + then b + elseif local otherBindingName = + resolveOtherThingWithAVeryLongFunctionName(argumentNumberOne, argumentNumberTwo) + then cccccccccccccccccccc + else dddddddddddddddddddd + +if local x = a then + print(x) +elseif local y = b then + print(y) +else + print("none") +end + +local w = if a + then b + elseif local otherBindingName = + resolveOtherThingWithAVeryLongFunctionName(argumentNumberOne, argumentNumberTwo) + then cccccccccccccccccccc + else dddddddddddddddddddd + From 1cf19e73e6033b8dd28bdc21d21759ee91eff991 Mon Sep 17 00:00:00 2001 From: JohnnyMorganz Date: Sat, 10 Oct 2026 15:20:07 +0200 Subject: [PATCH 6/7] Simplify comments and code --- src/formatters/expression.rs | 2 -- src/formatters/luau.rs | 13 ++++----- src/formatters/stmt.rs | 52 +++++++++++------------------------- 3 files changed, 21 insertions(+), 46 deletions(-) diff --git a/src/formatters/expression.rs b/src/formatters/expression.rs index 6ec35f10d..d80108d8b 100644 --- a/src/formatters/expression.rs +++ b/src/formatters/expression.rs @@ -764,8 +764,6 @@ fn format_if_expression(ctx: &Context, if_expression: &IfExpression, shape: Shap create_indent_trivia(ctx, hanging_shape), ])); - // The condition hangs on the line below the binding, so the binding must - // be separated from `elseif` by a space, and have no trailing space let binding = else_if_expression.binding().map(|binding| { let binding = format_if_condition_binding(ctx, binding, hanging_shape); diff --git a/src/formatters/luau.rs b/src/formatters/luau.rs index 6c079311e..044385d14 100644 --- a/src/formatters/luau.rs +++ b/src/formatters/luau.rs @@ -1533,13 +1533,7 @@ pub fn format_type_specifier( .with_type_info(type_info) } -/// Formats an [`IfConditionBinding`] node - the `local =` / `const =` part of an -/// `if local` / `if const` binding, as in `if local player = getPlayer() then ... end`. -/// The formatted binding is ` [: ] = ` (with a trailing space), so it -/// slots directly between the `if `/`elseif ` token and the condition expression. -/// The width of a formatted [`IfConditionBinding`] on the line it ends on, ignoring comments. -/// Counts characters rather than bytes, and only the last line if the binding spans multiple lines -/// (e.g. due to a multiline type specifier), as the condition continues from there. +/// The width of the last line of an [`IfConditionBinding`], ignoring comments. pub fn if_condition_binding_width(binding: &IfConditionBinding) -> usize { let text = format!( "{} {}{} =", @@ -1555,6 +1549,10 @@ pub fn if_condition_binding_width(binding: &IfConditionBinding) -> usize { text.lines().last().map_or(0, |line| line.chars().count()) } +/// Formats an [`IfConditionBinding`] node - the `local =` / `const =` part of an +/// `if local` / `if const` binding, as in `if local player = getPlayer() then ... end`. +/// The formatted binding is ` [: ] = ` (with a trailing space), so it +/// slots directly between the `if `/`elseif ` token and the condition expression. pub fn format_if_condition_binding( ctx: &Context, binding: &IfConditionBinding, @@ -1565,7 +1563,6 @@ pub fn format_if_condition_binding( .update_trailing_trivia(FormatTriviaType::Append(vec![Token::new( TokenType::spaces(1), )])); - // ` ` precedes the name, then the name precedes the type specifier let name_shape = shape + (strip_trivia(binding.local_token()) .to_string() diff --git a/src/formatters/stmt.rs b/src/formatters/stmt.rs index 94c8d0bdb..b92367130 100644 --- a/src/formatters/stmt.rs +++ b/src/formatters/stmt.rs @@ -338,10 +338,8 @@ fn should_indent_further<'a>(trivia: impl Iterator, shape: Sha false } -/// Moves the newline after a multilined `if local` / `if const` binding if the condition has to -/// hang below it. The newline is appended rather than replacing the trivia so that comments after `=` -/// are kept. If the keyword token had trailing comments, the binding is moved onto its own -/// indented line. +/// Prepares a multilined `if local` / `if const` binding: puts a newline after `=` when the condition +/// hangs below it, and moves the binding onto its own line when the keyword has trailing comments. #[cfg(feature = "luau")] fn hang_if_condition_binding( ctx: &Context, @@ -429,10 +427,8 @@ fn format_else_if(ctx: &Context, else_if_node: &ElseIf, shape: Shape) -> ElseIf || binding_has_comments || trivia_util::contains_comments(&condition); - // When multilining with a binding, the binding stays on the `elseif` line and the condition hangs - // below it, so the newline goes after the binding rather than after `elseif`. - // If the `elseif` token has trailing comments, the newline must come straight after them, and the - // binding moves onto its own indented line. + // When multilining with a binding, the binding stays on the `elseif` line and the condition + // hangs below it, so the newline goes after the binding rather than after `elseif`. #[cfg(feature = "luau")] let elseif_token_has_comments = else_if_node .else_if_token() @@ -440,8 +436,6 @@ fn format_else_if(ctx: &Context, else_if_node: &ElseIf, shape: Shape) -> ElseIf #[cfg(not(feature = "luau"))] let elseif_token_has_comments = false; - // Only hang the condition below the binding when needed - a comment on the `elseif` token only - // needs a newline after the comment, not after the binding #[cfg(feature = "luau")] let hang_condition = require_multiline_expression && (binding.is_none() @@ -452,19 +446,15 @@ fn format_else_if(ctx: &Context, else_if_node: &ElseIf, shape: Shape) -> ElseIf let hang_condition = require_multiline_expression; #[cfg(feature = "luau")] - let binding = if require_multiline_expression { - binding.map(|binding| { - hang_if_condition_binding( - ctx, - binding, - shape, - hang_condition, - elseif_token_has_comments, - ) - }) - } else { - binding - }; + let binding = binding.map(|binding| { + hang_if_condition_binding( + ctx, + binding, + shape, + hang_condition, + elseif_token_has_comments, + ) + }); #[cfg(feature = "luau")] let has_binding = binding.is_some(); @@ -489,7 +479,6 @@ fn format_else_if(ctx: &Context, else_if_node: &ElseIf, shape: Shape) -> ElseIf FormatTriviaType::Append(vec![create_indent_trivia(ctx, shape)]), ) } - // The condition stays on the binding line, but `then` still goes on its own line false if require_multiline_expression => singleline_condition .update_trailing_trivia(FormatTriviaType::Append(vec![create_newline_trivia(ctx)])), false => singleline_condition, @@ -649,15 +638,11 @@ pub fn format_if(ctx: &Context, if_node: &If, shape: Shape) -> If { // When multilining with a binding, the binding stays on the `if` line and the condition hangs // below it, so the newline goes after the binding rather than after `if`. - // If the `if` token has trailing comments, the newline must come straight after them, and the - // binding moves onto its own indented line. #[cfg(feature = "luau")] let if_token_has_comments = if_node.if_token().has_trailing_comments(CommentSearch::All); #[cfg(not(feature = "luau"))] let if_token_has_comments = false; - // Only hang the condition below the binding when needed - a comment on the `if` token only - // needs a newline after the comment, not after the binding #[cfg(feature = "luau")] let hang_condition = require_multiline_expression && (binding.is_none() @@ -668,13 +653,9 @@ pub fn format_if(ctx: &Context, if_node: &If, shape: Shape) -> If { let hang_condition = require_multiline_expression; #[cfg(feature = "luau")] - let binding = if require_multiline_expression { - binding.map(|binding| { - hang_if_condition_binding(ctx, binding, shape, hang_condition, if_token_has_comments) - }) - } else { - binding - }; + let binding = binding.map(|binding| { + hang_if_condition_binding(ctx, binding, shape, hang_condition, if_token_has_comments) + }); #[cfg(feature = "luau")] let has_binding = binding.is_some(); @@ -696,7 +677,6 @@ pub fn format_if(ctx: &Context, if_node: &If, shape: Shape) -> If { FormatTriviaType::Append(vec![create_indent_trivia(ctx, shape)]), ) } - // The condition stays on the binding line, but `then` still goes on its own line false if require_multiline_expression => singleline_condition .update_trailing_trivia(FormatTriviaType::Append(vec![create_newline_trivia(ctx)])), false => singleline_condition, From ea975ecbbb5a9678f21806cd6ef715c0460d8da1 Mon Sep 17 00:00:00 2001 From: JohnnyMorganz Date: Sat, 10 Oct 2026 15:35:01 +0200 Subject: [PATCH 7/7] Fix clippy issues --- src/formatters/stmt.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/formatters/stmt.rs b/src/formatters/stmt.rs index b92367130..1b0484c5b 100644 --- a/src/formatters/stmt.rs +++ b/src/formatters/stmt.rs @@ -390,7 +390,7 @@ fn format_else_if(ctx: &Context, else_if_node: &ElseIf, shape: Shape) -> ElseIf #[cfg(feature = "luau")] let binding_has_comments = else_if_node .binding() - .is_some_and(|binding| trivia_util::contains_comments(binding)); + .is_some_and(trivia_util::contains_comments); #[cfg(not(feature = "luau"))] let binding_has_comments = false; #[cfg(feature = "luau")] @@ -546,7 +546,7 @@ pub fn format_if(ctx: &Context, if_node: &If, shape: Shape) -> If { #[cfg(feature = "luau")] let binding_has_comments = if_node .binding() - .is_some_and(|binding| trivia_util::contains_comments(binding)); + .is_some_and(trivia_util::contains_comments); #[cfg(not(feature = "luau"))] let binding_has_comments = false; #[cfg(feature = "luau")]