From adca090d8999ce321450bb34cb1e45340d3faee5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 08:16:41 +0000 Subject: [PATCH 1/5] fix: target first attribute at_sign in stmt_remove_leading_newlines for LocalFunction/ConstFunction When a LocalFunction or ConstFunction has a leading Luau attribute (e.g. @native), the actual first token of the statement is the attribute's at_sign, not the local/const keyword. stmt_remove_leading_newlines was targeting the wrong token, leaving stray newlines before the attribute at the start of a block. Fixes #1109 --- src/formatters/block.rs | 42 +++++++++++++------ tests/inputs-luau/attributes-4.lua | 14 +++++++ .../tests__luau@attributes-4.lua.snap | 16 +++++++ 3 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 tests/inputs-luau/attributes-4.lua create mode 100644 tests/snapshots/tests__luau@attributes-4.lua.snap diff --git a/src/formatters/block.rs b/src/formatters/block.rs index f813f913..8d29ec93 100644 --- a/src/formatters/block.rs +++ b/src/formatters/block.rs @@ -363,12 +363,22 @@ fn stmt_remove_leading_newlines(stmt: Stmt) -> Stmt { local_assignment.local_token(), with_local_token ), - Stmt::LocalFunction(local_function) => update_first_token!( - LocalFunction, - local_function, - local_function.local_token(), - with_local_token - ), + Stmt::LocalFunction(local_function) => { + #[cfg(feature = "luau")] + { + let mut attributes: Vec<_> = local_function.attributes().cloned().collect(); + if !attributes.is_empty() { + let at_sign = attributes[0].at_sign(); + let leading_trivia = + trivia_remove_leading_newlines(at_sign.leading_trivia().collect()); + let new_at_sign = + at_sign.update_leading_trivia(FormatTriviaType::Replace(leading_trivia)); + attributes[0] = attributes[0].clone().with_at_sign(new_at_sign); + return Stmt::LocalFunction(local_function.with_attributes(attributes)); + } + } + update_first_token!(LocalFunction, local_function, local_function.local_token(), with_local_token) + } Stmt::NumericFor(numeric_for) => update_first_token!( NumericFor, numeric_for, @@ -404,12 +414,20 @@ fn stmt_remove_leading_newlines(stmt: Stmt) -> Stmt { with_const_token ), #[cfg(feature = "luau")] - Stmt::ConstFunction(const_function) => update_first_token!( - ConstFunction, - const_function, - const_function.const_token(), - with_const_token - ), + Stmt::ConstFunction(const_function) => { + let mut attributes: Vec<_> = const_function.attributes().cloned().collect(); + if !attributes.is_empty() { + let at_sign = attributes[0].at_sign(); + let leading_trivia = + trivia_remove_leading_newlines(at_sign.leading_trivia().collect()); + let new_at_sign = + at_sign.update_leading_trivia(FormatTriviaType::Replace(leading_trivia)); + attributes[0] = attributes[0].clone().with_at_sign(new_at_sign); + Stmt::ConstFunction(const_function.with_attributes(attributes)) + } else { + update_first_token!(ConstFunction, const_function, const_function.const_token(), with_const_token) + } + } #[cfg(feature = "luau")] Stmt::ExportedTypeDeclaration(exported_type_declaration) => update_first_token!( diff --git a/tests/inputs-luau/attributes-4.lua b/tests/inputs-luau/attributes-4.lua new file mode 100644 index 00000000..c3615452 --- /dev/null +++ b/tests/inputs-luau/attributes-4.lua @@ -0,0 +1,14 @@ +-- leading newlines at start of block should be removed even when local/const function has attributes +do + +@native +local function foo() +end +end + +do + +@native +const function bar() +end +end diff --git a/tests/snapshots/tests__luau@attributes-4.lua.snap b/tests/snapshots/tests__luau@attributes-4.lua.snap new file mode 100644 index 00000000..2811319a --- /dev/null +++ b/tests/snapshots/tests__luau@attributes-4.lua.snap @@ -0,0 +1,16 @@ +--- +source: tests/tests.rs +assertion_line: 36 +expression: "format(&contents, LuaVersion::Luau)" +input_file: tests/inputs-luau/attributes-4.lua +--- +-- leading newlines at start of block should be removed even when local/const function has attributes +do + @native + local function foo() end +end + +do + @native + const function bar() end +end From cb5f1057341dfdcfa83510c25cb75cd018e524bf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 08:23:41 +0000 Subject: [PATCH 2/5] simplify: extract strip_attribute_leading_newlines helper, avoid allocation on no-attribute path - Deduplicate the identical 6-line attribute-stripping logic from LocalFunction and ConstFunction arms into a single helper function - Use peekable iterator so no Vec is allocated when the function has no attributes (the common case) - Make both arms use a consistent if-let pattern instead of early-return vs if/else - Remove redundant comment from test input file --- src/formatters/block.rs | 54 ++++++++++++------- tests/inputs-luau/attributes-4.lua | 1 - .../tests__luau@attributes-4.lua.snap | 1 - 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/formatters/block.rs b/src/formatters/block.rs index 8d29ec93..4df085a8 100644 --- a/src/formatters/block.rs +++ b/src/formatters/block.rs @@ -1,5 +1,7 @@ #[cfg(feature = "luau")] use crate::formatters::general::format_symbol; +#[cfg(feature = "luau")] +use full_moon::ast::luau::LuauAttribute; use crate::{ context::{create_indent_trivia, create_newline_trivia, Context, FormatNode}, fmt_symbol, @@ -317,6 +319,20 @@ fn var_remove_leading_newline(var: Var) -> Var { } } +#[cfg(feature = "luau")] +fn strip_attribute_leading_newlines<'a>( + attributes: impl Iterator, +) -> Option> { + let mut peekable = attributes.peekable(); + peekable.peek()?; + let mut attributes: Vec = peekable.cloned().collect(); + let at_sign = attributes[0].at_sign(); + let leading_trivia = trivia_remove_leading_newlines(at_sign.leading_trivia().collect()); + let new_at_sign = at_sign.update_leading_trivia(FormatTriviaType::Replace(leading_trivia)); + attributes[0] = attributes[0].clone().with_at_sign(new_at_sign); + Some(attributes) +} + fn stmt_remove_leading_newlines(stmt: Stmt) -> Stmt { match stmt { Stmt::Assignment(assignment) => { @@ -365,19 +381,17 @@ fn stmt_remove_leading_newlines(stmt: Stmt) -> Stmt { ), Stmt::LocalFunction(local_function) => { #[cfg(feature = "luau")] + if let Some(attributes) = + strip_attribute_leading_newlines(local_function.attributes()) { - let mut attributes: Vec<_> = local_function.attributes().cloned().collect(); - if !attributes.is_empty() { - let at_sign = attributes[0].at_sign(); - let leading_trivia = - trivia_remove_leading_newlines(at_sign.leading_trivia().collect()); - let new_at_sign = - at_sign.update_leading_trivia(FormatTriviaType::Replace(leading_trivia)); - attributes[0] = attributes[0].clone().with_at_sign(new_at_sign); - return Stmt::LocalFunction(local_function.with_attributes(attributes)); - } + return Stmt::LocalFunction(local_function.with_attributes(attributes)); } - update_first_token!(LocalFunction, local_function, local_function.local_token(), with_local_token) + update_first_token!( + LocalFunction, + local_function, + local_function.local_token(), + with_local_token + ) } Stmt::NumericFor(numeric_for) => update_first_token!( NumericFor, @@ -415,17 +429,17 @@ fn stmt_remove_leading_newlines(stmt: Stmt) -> Stmt { ), #[cfg(feature = "luau")] Stmt::ConstFunction(const_function) => { - let mut attributes: Vec<_> = const_function.attributes().cloned().collect(); - if !attributes.is_empty() { - let at_sign = attributes[0].at_sign(); - let leading_trivia = - trivia_remove_leading_newlines(at_sign.leading_trivia().collect()); - let new_at_sign = - at_sign.update_leading_trivia(FormatTriviaType::Replace(leading_trivia)); - attributes[0] = attributes[0].clone().with_at_sign(new_at_sign); + if let Some(attributes) = + strip_attribute_leading_newlines(const_function.attributes()) + { Stmt::ConstFunction(const_function.with_attributes(attributes)) } else { - update_first_token!(ConstFunction, const_function, const_function.const_token(), with_const_token) + update_first_token!( + ConstFunction, + const_function, + const_function.const_token(), + with_const_token + ) } } diff --git a/tests/inputs-luau/attributes-4.lua b/tests/inputs-luau/attributes-4.lua index c3615452..3abfdfb8 100644 --- a/tests/inputs-luau/attributes-4.lua +++ b/tests/inputs-luau/attributes-4.lua @@ -1,4 +1,3 @@ --- leading newlines at start of block should be removed even when local/const function has attributes do @native diff --git a/tests/snapshots/tests__luau@attributes-4.lua.snap b/tests/snapshots/tests__luau@attributes-4.lua.snap index 2811319a..a7c553ea 100644 --- a/tests/snapshots/tests__luau@attributes-4.lua.snap +++ b/tests/snapshots/tests__luau@attributes-4.lua.snap @@ -4,7 +4,6 @@ assertion_line: 36 expression: "format(&contents, LuaVersion::Luau)" input_file: tests/inputs-luau/attributes-4.lua --- --- leading newlines at start of block should be removed even when local/const function has attributes do @native local function foo() end From c7f6114fb5dfca4df29c9488b8fd4b8f8105b1ef Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 08:26:19 +0000 Subject: [PATCH 3/5] refactor: use next() to own first attribute, eliminating index and clone Replace peek()+collect()+[0]+clone() with cloned().next()? which takes the first element as an owned value so with_at_sign can consume it directly. --- src/formatters/block.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/formatters/block.rs b/src/formatters/block.rs index 4df085a8..b65cda33 100644 --- a/src/formatters/block.rs +++ b/src/formatters/block.rs @@ -323,14 +323,14 @@ fn var_remove_leading_newline(var: Var) -> Var { fn strip_attribute_leading_newlines<'a>( attributes: impl Iterator, ) -> Option> { - let mut peekable = attributes.peekable(); - peekable.peek()?; - let mut attributes: Vec = peekable.cloned().collect(); - let at_sign = attributes[0].at_sign(); + let mut cloned = attributes.cloned(); + let first = cloned.next()?; + let at_sign = first.at_sign(); let leading_trivia = trivia_remove_leading_newlines(at_sign.leading_trivia().collect()); let new_at_sign = at_sign.update_leading_trivia(FormatTriviaType::Replace(leading_trivia)); - attributes[0] = attributes[0].clone().with_at_sign(new_at_sign); - Some(attributes) + let mut result = vec![first.with_at_sign(new_at_sign)]; + result.extend(cloned); + Some(result) } fn stmt_remove_leading_newlines(stmt: Stmt) -> Stmt { From 41af02a77ccc29d7bb0572eb0cc9163d2d622c35 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 08:29:36 +0000 Subject: [PATCH 4/5] style: rustfmt --- src/formatters/block.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/formatters/block.rs b/src/formatters/block.rs index b65cda33..a8aa564c 100644 --- a/src/formatters/block.rs +++ b/src/formatters/block.rs @@ -1,7 +1,5 @@ #[cfg(feature = "luau")] use crate::formatters::general::format_symbol; -#[cfg(feature = "luau")] -use full_moon::ast::luau::LuauAttribute; use crate::{ context::{create_indent_trivia, create_newline_trivia, Context, FormatNode}, fmt_symbol, @@ -20,6 +18,8 @@ use crate::{ }, shape::Shape, }; +#[cfg(feature = "luau")] +use full_moon::ast::luau::LuauAttribute; use full_moon::ast::{ punctuated::Punctuated, Block, Expression, LastStmt, Prefix, Return, Stmt, Var, }; @@ -381,8 +381,7 @@ fn stmt_remove_leading_newlines(stmt: Stmt) -> Stmt { ), Stmt::LocalFunction(local_function) => { #[cfg(feature = "luau")] - if let Some(attributes) = - strip_attribute_leading_newlines(local_function.attributes()) + if let Some(attributes) = strip_attribute_leading_newlines(local_function.attributes()) { return Stmt::LocalFunction(local_function.with_attributes(attributes)); } @@ -429,8 +428,7 @@ fn stmt_remove_leading_newlines(stmt: Stmt) -> Stmt { ), #[cfg(feature = "luau")] Stmt::ConstFunction(const_function) => { - if let Some(attributes) = - strip_attribute_leading_newlines(const_function.attributes()) + if let Some(attributes) = strip_attribute_leading_newlines(const_function.attributes()) { Stmt::ConstFunction(const_function.with_attributes(attributes)) } else { From 54bd23ce68679f851b81b9dd27d138f0d6673cbc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 08:29:57 +0000 Subject: [PATCH 5/5] docs: add changelog entry for #1109 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c333305..0ee38f92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed npm publishing by bumping Node.js from 16 to 22 in CI workflows to support npm trusted publishing +- Luau: Fixed stray leading newlines not being removed from `local function` and `const function` declarations that have attributes (e.g. `@native`) at the start of a block ([#1109](https://github.com/JohnnyMorganz/StyLua/issues/1109)) ## [2.4.1] - 2026-04-06