diff --git a/CHANGELOG.md b/CHANGELOG.md index bd575604..98b0bc25 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)) diff --git a/src/formatters/expression.rs b/src/formatters/expression.rs index d5d0a594..d80108d8 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_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, }; @@ -517,13 +520,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, 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); // 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 @@ -533,6 +540,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) } @@ -602,6 +610,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, if_condition_binding_width); + // 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); @@ -636,7 +650,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| { @@ -667,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(); @@ -750,8 +764,25 @@ 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| { + 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) + .with_binding(binding) .with_then_token(then_token) } else { singleline_else_if.update_leading_trivia(FormatTriviaType::Append( @@ -778,6 +809,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) @@ -799,6 +831,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 9204dbad..044385d1 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, }; @@ -1533,6 +1533,57 @@ pub fn format_type_specifier( .with_type_info(type_info) } +/// The width of the last line of an [`IfConditionBinding`], ignoring comments. +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()) +} + +/// 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_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, 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 8857565d..1b0484c5 100644 --- a/src/formatters/stmt.rs +++ b/src/formatters/stmt.rs @@ -8,8 +8,9 @@ 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, + 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,44 @@ fn should_indent_further<'a>(trivia: impl Iterator, shape: Sha false } +/// 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, + 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 @@ -345,6 +386,22 @@ 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 + #[cfg(feature = "luau")] + let binding_has_comments = else_if_node + .binding() + .is_some_and(trivia_util::contains_comments); + #[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, if_condition_binding_width); + #[cfg(not(feature = "luau"))] + let binding_width = 0; + // Compute the indent let end_token_type = if should_indent_further(else_if_node.else_if_token().leading_trivia(), shape) { @@ -354,11 +411,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() @@ -366,24 +424,63 @@ 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); - 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( - TokenType::spaces(1), - )])), - } - .update_leading_trivia(FormatTriviaType::Append(leading_trivia.to_owned())); + // 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() + .has_trailing_comments(CommentSearch::All); + #[cfg(not(feature = "luau"))] + let elseif_token_has_comments = false; - let condition = match require_multiline_expression { + #[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 = 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(); + #[cfg(not(feature = "luau"))] + let has_binding = false; + + 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 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)]), ) } + false if require_multiline_expression => singleline_condition + .update_trailing_trivia(FormatTriviaType::Append(vec![create_newline_trivia(ctx)])), false => singleline_condition, }; @@ -402,12 +499,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_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". @@ -440,18 +542,39 @@ 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_has_comments = if_node + .binding() + .is_some_and(trivia_util::contains_comments); + #[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, if_condition_binding_width); + #[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 .then_token() .has_leading_comments(CommentSearch::All) + || binding_has_comments || trivia_util::contains_comments(&condition); if !require_multiline_expression @@ -501,6 +624,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()) @@ -510,20 +636,49 @@ 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 if_token_has_comments = if_node.if_token().has_trailing_comments(CommentSearch::All); + #[cfg(not(feature = "luau"))] + let if_token_has_comments = false; + + #[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 = 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(); + #[cfg(not(feature = "luau"))] + let has_binding = false; + + 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 { + singleline_if_token } .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)]), ) } + false if require_multiline_expression => singleline_condition + .update_trailing_trivia(FormatTriviaType::Append(vec![create_newline_trivia(ctx)])), false => singleline_condition, }; @@ -583,7 +738,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) @@ -592,7 +747,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 diff --git a/tests/inputs-luau/if-expression-binding.lua b/tests/inputs-luau/if-expression-binding.lua new file mode 100644 index 00000000..55c332d1 --- /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-comments.lua b/tests/inputs-luau/if-local-if-const-comments.lua new file mode 100644 index 00000000..ce6c3553 --- /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/inputs-luau/if-local-if-const.lua b/tests/inputs-luau/if-local-if-const.lua new file mode 100644 index 00000000..e8c92567 --- /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 00000000..1b140bc9 --- /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-comments.lua.snap b/tests/snapshots/tests__luau@if-local-if-const-comments.lua.snap new file mode 100644 index 00000000..bf195369 --- /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 + 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 00000000..7435be83 --- /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 +