From 53fbb305dc3307d820a17751736e723baee19d8b Mon Sep 17 00:00:00 2001 From: "oss-engineer (agent)" Date: Fri, 30 Oct 2026 07:48:50 +0000 Subject: [PATCH 1/4] Split concat chains starting with a multiline long string when over width (#1174) --- CHANGELOG.md | 4 +++ src/formatters/assignment.rs | 11 ++++++ .../inputs/concat-multiline-string-chain.lua | 11 ++++++ ...ard@concat-multiline-string-chain.lua.snap | 35 +++++++++++++++++++ 4 files changed, 61 insertions(+) create mode 100644 tests/inputs/concat-multiline-string-chain.lua create mode 100644 tests/snapshots/tests__standard@concat-multiline-string-chain.lua.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ac6dc18..01aea407 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] +### Fixed + +- Fixed a `..` concatenation chain starting with a multiline `[[ ]]` string never being split when over the column width ([#1174](https://github.com/JohnnyMorganz/StyLua/issues/1174)) + ## [2.6.0] - 2026-10-10 ### Added diff --git a/src/formatters/assignment.rs b/src/formatters/assignment.rs index 3983e7c4..b343569e 100644 --- a/src/formatters/assignment.rs +++ b/src/formatters/assignment.rs @@ -297,8 +297,19 @@ fn attempt_assignment_tactics( let hanging_expr_list = hang_punctuated_list(ctx, expressions, shape); let hanging_shape = shape.take_first_line(&strip_trivia(&hanging_expr_list)); + // A multiline expression (e.g. one starting with a `[[ ]]` string) may have a short first line + // whilst a later line is over budget. In that case, hang if it makes the longest line shorter + let expr_string = strip_trailing_trivia(&expr_list).to_string(); + let longest_line = |string: &str| string.lines().map(str::len).max().unwrap_or(0); + let unhung_longest_line = longest_line(&expr_string); + let later_line_over_budget = expr_string.lines().count() > 1 + && shape.reset().add_width(unhung_longest_line).over_budget() + && longest_line(&strip_trivia(&hanging_expr_list).to_string()) + < unhung_longest_line; + if expression.has_inline_comments() || hanging_shape.used_width() < formatting_shape.used_width() + || later_line_over_budget { (hanging_expr_list, equal_token) } else { diff --git a/tests/inputs/concat-multiline-string-chain.lua b/tests/inputs/concat-multiline-string-chain.lua new file mode 100644 index 00000000..f78ad809 --- /dev/null +++ b/tests/inputs/concat-multiline-string-chain.lua @@ -0,0 +1,11 @@ +local x = [[ +box[0,0;1,1;] +dropdown[4,0.75;6.4;contributor_grouping;]] .. S("Group by name") .. "," .. S("Group by contribution") .. ";" .. mail.selected_idxs.contributor_grouping[name] .. [[;true] +]] +local y = "dropdown[4,0.75;6.4;contributor_grouping;" .. S("Group by name") .. "," .. S("Group by contribution") .. ";" .. mail.selected_idxs.contributor_grouping[name] .. ";true]" +local z = [[ +short]] .. foo .. [[bar]] +x = [[ +box[0,0;1,1;] +dropdown[4,0.75;6.4;contributor_grouping;]] .. S("Group by name") .. "," .. S("Group by contribution") .. ";" .. mail.selected_idxs.contributor_grouping[name] .. [[;true] +]] diff --git a/tests/snapshots/tests__standard@concat-multiline-string-chain.lua.snap b/tests/snapshots/tests__standard@concat-multiline-string-chain.lua.snap new file mode 100644 index 00000000..17e0f3c6 --- /dev/null +++ b/tests/snapshots/tests__standard@concat-multiline-string-chain.lua.snap @@ -0,0 +1,35 @@ +--- +source: tests/tests.rs +expression: "format(&contents, LuaVersion::Lua51)" +input_file: tests/inputs/concat-multiline-string-chain.lua +--- +local x = [[ +box[0,0;1,1;] +dropdown[4,0.75;6.4;contributor_grouping;]] + .. S("Group by name") + .. "," + .. S("Group by contribution") + .. ";" + .. mail.selected_idxs.contributor_grouping[name] + .. [[;true] +]] +local y = "dropdown[4,0.75;6.4;contributor_grouping;" + .. S("Group by name") + .. "," + .. S("Group by contribution") + .. ";" + .. mail.selected_idxs.contributor_grouping[name] + .. ";true]" +local z = [[ +short]] .. foo .. [[bar]] +x = [[ +box[0,0;1,1;] +dropdown[4,0.75;6.4;contributor_grouping;]] + .. S("Group by name") + .. "," + .. S("Group by contribution") + .. ";" + .. mail.selected_idxs.contributor_grouping[name] + .. [[;true] +]] + From 3439bae07e6cd99669ef8dbc574317b79616eb55 Mon Sep 17 00:00:00 2001 From: "oss-engineer (agent)" Date: Fri, 30 Oct 2026 08:06:55 +0000 Subject: [PATCH 2/4] Restrict #1174 hang to concat chains led by a multiline long string --- CHANGELOG.md | 4 -- src/formatters/assignment.rs | 40 +++++++++++++------ .../inputs/concat-multiline-string-chain.lua | 10 +++++ ...ard@concat-multiline-string-chain.lua.snap | 10 +++++ 4 files changed, 48 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01aea407..2ac6dc18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Fixed - -- Fixed a `..` concatenation chain starting with a multiline `[[ ]]` string never being split when over the column width ([#1174](https://github.com/JohnnyMorganz/StyLua/issues/1174)) - ## [2.6.0] - 2026-10-10 ### Added diff --git a/src/formatters/assignment.rs b/src/formatters/assignment.rs index b343569e..24bfc46f 100644 --- a/src/formatters/assignment.rs +++ b/src/formatters/assignment.rs @@ -18,7 +18,7 @@ use crate::{ context::{create_indent_trivia, create_newline_trivia, Context}, fmt_symbol, formatters::{ - expression::{format_expression, format_var, hang_expression}, + expression::{format_expression, format_var, hang_expression, is_brackets_string}, general::{ format_punctuated, format_punctuated_multiline, format_token_reference, try_format_punctuated, @@ -201,6 +201,29 @@ pub fn hang_at_equals_due_to_comments( (equal_token, expression) } +/// Whether the expression is a `..` chain whose leftmost operand is a multiline long string (`[[ ]]` / `[=[ ]=]`) +fn is_multiline_long_string_concat(expression: &Expression) -> bool { + fn leftmost(expression: &Expression) -> &Expression { + match expression { + Expression::BinaryOperator { lhs, .. } => leftmost(lhs), + _ => expression, + } + } + + matches!( + expression, + Expression::BinaryOperator { + binop: full_moon::ast::BinOp::TwoDots(_), + .. + } + ) && match leftmost(expression) { + Expression::String(token) => { + is_brackets_string(leftmost(expression)) && token.token().to_string().contains('\n') + } + _ => false, + } +} + /// Attempts different formatting tactics on an expression list being assigned (`= foo, bar`), to find the best /// formatting output. fn attempt_assignment_tactics( @@ -297,19 +320,12 @@ fn attempt_assignment_tactics( let hanging_expr_list = hang_punctuated_list(ctx, expressions, shape); let hanging_shape = shape.take_first_line(&strip_trivia(&hanging_expr_list)); - // A multiline expression (e.g. one starting with a `[[ ]]` string) may have a short first line - // whilst a later line is over budget. In that case, hang if it makes the longest line shorter - let expr_string = strip_trailing_trivia(&expr_list).to_string(); - let longest_line = |string: &str| string.lines().map(str::len).max().unwrap_or(0); - let unhung_longest_line = longest_line(&expr_string); - let later_line_over_budget = expr_string.lines().count() > 1 - && shape.reset().add_width(unhung_longest_line).over_budget() - && longest_line(&strip_trivia(&hanging_expr_list).to_string()) - < unhung_longest_line; - + // A `..` chain led by a multiline long string (e.g. `[[ ]]`) has a short first line, so the + // comparison above never hangs it, even when a later line is over budget if expression.has_inline_comments() || hanging_shape.used_width() < formatting_shape.used_width() - || later_line_over_budget + || (is_multiline_long_string_concat(expression) + && shape.test_over_budget(&strip_trailing_trivia(&expr_list))) { (hanging_expr_list, equal_token) } else { diff --git a/tests/inputs/concat-multiline-string-chain.lua b/tests/inputs/concat-multiline-string-chain.lua index f78ad809..aebad576 100644 --- a/tests/inputs/concat-multiline-string-chain.lua +++ b/tests/inputs/concat-multiline-string-chain.lua @@ -9,3 +9,13 @@ x = [[ box[0,0;1,1;] dropdown[4,0.75;6.4;contributor_grouping;]] .. S("Group by name") .. "," .. S("Group by contribution") .. ";" .. mail.selected_idxs.contributor_grouping[name] .. [[;true] ]] +-- regression: function-led multiline RHS must be unchanged +local f = (function() + return 1 +end)() .. "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .. b +-- regression: call-led multiline RHS (reduced from jest-lua TestScheduler) must be unchanged +local function g() + local message = ("%sYour test suite is leaking memory. Please ensure all references are cleaned.\n"):format( + tostring(chalk.red:bold("EXPERIMENTAL FEATURE!\n")) + ) .. "\n" .. "There is a number of things that can leak memory:\n" .. " - Async operations that have not finished (e.g. fs.readFile).\n" .. " - Keeping references to the global scope." +end diff --git a/tests/snapshots/tests__standard@concat-multiline-string-chain.lua.snap b/tests/snapshots/tests__standard@concat-multiline-string-chain.lua.snap index 17e0f3c6..e697f55a 100644 --- a/tests/snapshots/tests__standard@concat-multiline-string-chain.lua.snap +++ b/tests/snapshots/tests__standard@concat-multiline-string-chain.lua.snap @@ -32,4 +32,14 @@ dropdown[4,0.75;6.4;contributor_grouping;]] .. mail.selected_idxs.contributor_grouping[name] .. [[;true] ]] +-- regression: function-led multiline RHS must be unchanged +local f = (function() + return 1 +end)() .. "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .. b +-- regression: call-led multiline RHS (reduced from jest-lua TestScheduler) must be unchanged +local function g() + local message = ("%sYour test suite is leaking memory. Please ensure all references are cleaned.\n"):format( + tostring(chalk.red:bold("EXPERIMENTAL FEATURE!\n")) + ) .. "\n" .. "There is a number of things that can leak memory:\n" .. " - Async operations that have not finished (e.g. fs.readFile).\n" .. " - Keeping references to the global scope." +end From 1cea4ff3c604d5542c0c51a88cd12917f12ad969 Mon Sep 17 00:00:00 2001 From: "oss-engineer (agent)" Date: Fri, 30 Oct 2026 08:10:50 +0000 Subject: [PATCH 3/4] Only hang [[ ]]-led concat chains when hanging brings them within budget --- src/formatters/assignment.rs | 5 +++-- tests/inputs/concat-multiline-string-chain.lua | 4 ++++ .../tests__standard@concat-multiline-string-chain.lua.snap | 4 ++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/formatters/assignment.rs b/src/formatters/assignment.rs index 24bfc46f..ab47a2d9 100644 --- a/src/formatters/assignment.rs +++ b/src/formatters/assignment.rs @@ -321,11 +321,12 @@ fn attempt_assignment_tactics( let hanging_shape = shape.take_first_line(&strip_trivia(&hanging_expr_list)); // A `..` chain led by a multiline long string (e.g. `[[ ]]`) has a short first line, so the - // comparison above never hangs it, even when a later line is over budget + // comparison above never hangs it, even when a later line is over budget. Only hang if that fixes it if expression.has_inline_comments() || hanging_shape.used_width() < formatting_shape.used_width() || (is_multiline_long_string_concat(expression) - && shape.test_over_budget(&strip_trailing_trivia(&expr_list))) + && shape.test_over_budget(&strip_trailing_trivia(&expr_list)) + && !shape.test_over_budget(&strip_trivia(&hanging_expr_list))) { (hanging_expr_list, equal_token) } else { diff --git a/tests/inputs/concat-multiline-string-chain.lua b/tests/inputs/concat-multiline-string-chain.lua index aebad576..d2675747 100644 --- a/tests/inputs/concat-multiline-string-chain.lua +++ b/tests/inputs/concat-multiline-string-chain.lua @@ -19,3 +19,7 @@ local function g() tostring(chalk.red:bold("EXPERIMENTAL FEATURE!\n")) ) .. "\n" .. "There is a number of things that can leak memory:\n" .. " - Async operations that have not finished (e.g. fs.readFile).\n" .. " - Keeping references to the global scope." end +-- regression: over-long line inside the long string itself; hanging cannot help, so must be unchanged +local s = [[ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +]] .. a .. b diff --git a/tests/snapshots/tests__standard@concat-multiline-string-chain.lua.snap b/tests/snapshots/tests__standard@concat-multiline-string-chain.lua.snap index e697f55a..c3809c15 100644 --- a/tests/snapshots/tests__standard@concat-multiline-string-chain.lua.snap +++ b/tests/snapshots/tests__standard@concat-multiline-string-chain.lua.snap @@ -42,4 +42,8 @@ local function g() tostring(chalk.red:bold("EXPERIMENTAL FEATURE!\n")) ) .. "\n" .. "There is a number of things that can leak memory:\n" .. " - Async operations that have not finished (e.g. fs.readFile).\n" .. " - Keeping references to the global scope." end +-- regression: over-long line inside the long string itself; hanging cannot help, so must be unchanged +local s = [[ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +]] .. a .. b From 5e26e6dd7cad636e8d2b3fb98afeb3b3d97f0b5e Mon Sep 17 00:00:00 2001 From: "oss-engineer (agent)" Date: Fri, 30 Oct 2026 08:28:11 +0000 Subject: [PATCH 4/4] Base the #1174 hang decision on the formatted expression so parenthesised leads are idempotent --- src/formatters/assignment.rs | 3 ++- tests/inputs/concat-multiline-string-chain.lua | 5 +++++ ...__standard@concat-multiline-string-chain.lua.snap | 12 ++++++++++++ tests/test_concat_idempotence.rs | 10 ++++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 tests/test_concat_idempotence.rs diff --git a/src/formatters/assignment.rs b/src/formatters/assignment.rs index ab47a2d9..c284c9d2 100644 --- a/src/formatters/assignment.rs +++ b/src/formatters/assignment.rs @@ -324,7 +324,8 @@ fn attempt_assignment_tactics( // comparison above never hangs it, even when a later line is over budget. Only hang if that fixes it if expression.has_inline_comments() || hanging_shape.used_width() < formatting_shape.used_width() - || (is_multiline_long_string_concat(expression) + // Check the formatted expression (e.g. with parentheses removed), as that is what is output + || (is_multiline_long_string_concat(expr_list.iter().next().unwrap()) && shape.test_over_budget(&strip_trailing_trivia(&expr_list)) && !shape.test_over_budget(&strip_trivia(&hanging_expr_list))) { diff --git a/tests/inputs/concat-multiline-string-chain.lua b/tests/inputs/concat-multiline-string-chain.lua index d2675747..c2a2f42a 100644 --- a/tests/inputs/concat-multiline-string-chain.lua +++ b/tests/inputs/concat-multiline-string-chain.lua @@ -23,3 +23,8 @@ end local s = [[ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ]] .. a .. b +-- parenthesised long string lead: parentheses are removed, so the chain must hang in a single pass +local p = ([[ +foo +]]) .. "dropdown[4,0.75;6.4;contributor_grouping;" .. S("Group by name") .. "," .. S("Group by contribution") .. ";" .. mail.selected_idxs.contributor_grouping[name] .. [[;true] +]] diff --git a/tests/snapshots/tests__standard@concat-multiline-string-chain.lua.snap b/tests/snapshots/tests__standard@concat-multiline-string-chain.lua.snap index c3809c15..bcf0c13e 100644 --- a/tests/snapshots/tests__standard@concat-multiline-string-chain.lua.snap +++ b/tests/snapshots/tests__standard@concat-multiline-string-chain.lua.snap @@ -46,4 +46,16 @@ end local s = [[ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ]] .. a .. b +-- parenthesised long string lead: parentheses are removed, so the chain must hang in a single pass +local p = [[ +foo +]] + .. "dropdown[4,0.75;6.4;contributor_grouping;" + .. S("Group by name") + .. "," + .. S("Group by contribution") + .. ";" + .. mail.selected_idxs.contributor_grouping[name] + .. [[;true] +]] diff --git a/tests/test_concat_idempotence.rs b/tests/test_concat_idempotence.rs new file mode 100644 index 00000000..6bffaa77 --- /dev/null +++ b/tests/test_concat_idempotence.rs @@ -0,0 +1,10 @@ +use stylua_lib::{format_code, Config, OutputVerification}; + +#[test] +fn test_concat_multiline_string_chain_idempotent() { + let input = include_str!("inputs/concat-multiline-string-chain.lua"); + let format = + |code: &str| format_code(code, Config::default(), None, OutputVerification::None).unwrap(); + let first = format(input); + assert_eq!(format(&first), first); +}