Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### 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`)
- Added `Input` mode for `collapse_simple_statement`, preserving whether simple statements are single-line or multi-line in the input while still formatting their contents ([#1112](https://github.com/JohnnyMorganz/StyLua/pull/1112))

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ StyLua only offers the following options:
| `call_parentheses` | `Always` | Whether parentheses should be applied on function calls with a single string/table argument. Possible options: `Always`, `NoSingleString`, `NoSingleTable`, `None`, `Input`. `Always` applies parentheses in all cases. `NoSingleString` omits parentheses on calls with a single string argument. Similarly, `NoSingleTable` omits parentheses on calls with a single table argument. `None` omits parentheses in both cases. Note: parentheses are still kept in situations where removal can lead to obscurity (e.g. `foo "bar".setup -> foo("bar").setup`, since the index is on the call result, not the string). `Input` removes all automation and preserves parentheses only if they were present in input code: consistency is not enforced. |
| `space_after_function_names` | `Never` | Specify whether to add a space between the function name and parentheses. Possible options: `Never`, `Definitions`, `Calls`, or `Always` |
| `block_newline_gaps` | `Never` | Specify whether to preserve leading and trailing newline gaps for blocks. Possible options: `Never`, `Preserve` |
| `collapse_simple_statement` | `Never` | Specify whether to collapse simple statements. Possible options: `Never`, `FunctionOnly`, `ConditionalOnly`, or `Always` |
| `collapse_simple_statement` | `Never` | Specify whether to collapse simple statements. Possible options: `Never`, `Input`, `FunctionOnly`, `ConditionalOnly`, or `Always` |

Default `stylua.toml`, note you do not need to explicitly specify each option if you want to use the defaults:

Expand Down
16 changes: 15 additions & 1 deletion src/cli/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,9 @@ fn load_overrides(config: Config, opt: &Opt) -> Config {
mod tests {
use super::*;
use clap::StructOpt;
use stylua_lib::{CallParenType, IndentType, LineEndings, LuaVersion, QuoteStyle};
use stylua_lib::{
CallParenType, CollapseSimpleStatement, IndentType, LineEndings, LuaVersion, QuoteStyle,
};

#[test]
fn test_override_syntax() {
Expand Down Expand Up @@ -360,4 +362,16 @@ mod tests {
let config = load_overrides(default_config, &override_opt);
assert_eq!(config.call_parentheses, CallParenType::None);
}

#[test]
fn test_override_collapse_simple_statement() {
let override_opt =
Opt::parse_from(vec!["BINARY_NAME", "--collapse-simple-statement", "Input"]);
let default_config = Config::new();
let config = load_overrides(default_config, &override_opt);
assert_eq!(
config.collapse_simple_statement,
CollapseSimpleStatement::Input
);
}
}
1 change: 1 addition & 0 deletions src/cli/opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ convert_enum!(CallParenType, ArgCallParenType, {

convert_enum!(CollapseSimpleStatement, ArgCollapseSimpleStatement, {
Never,
Input,
FunctionOnly,
ConditionalOnly,
Always,
Expand Down
27 changes: 22 additions & 5 deletions src/context.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{
shape::Shape, BlockNewlineGaps, CallParenType, CollapseSimpleStatement, Config, IndentType,
LineEndings, Range as FormatRange, SpaceAfterFunctionNames,
formatters::trivia_util, shape::Shape, BlockNewlineGaps, CallParenType,
CollapseSimpleStatement, Config, IndentType, LineEndings, Range as FormatRange,
SpaceAfterFunctionNames,
};
use full_moon::{
node::Node,
Expand Down Expand Up @@ -141,17 +142,33 @@ impl Context {
|| self.config().call_parentheses == CallParenType::NoSingleTable
}

pub fn should_collapse_simple_functions(&self) -> bool {
/// Whether a function with a simple body should be collapsed onto a single line
pub fn should_collapse_simple_function(&self, node: &impl Node) -> bool {
matches!(
self.config().collapse_simple_statement,
CollapseSimpleStatement::FunctionOnly | CollapseSimpleStatement::Always
)
) || (self.should_preserve_input_simple_statements()
&& trivia_util::node_spans_single_line(node))
}

/// Whether a function with an empty body should be collapsed onto a single line
pub fn should_collapse_empty_function(&self, node: &impl Node) -> bool {
!self.should_preserve_input_simple_statements() || trivia_util::node_spans_single_line(node)
}

pub fn should_collapse_simple_conditionals(&self) -> bool {
/// Whether an `if` guard with a simple body should be collapsed onto a single line
pub fn should_collapse_simple_conditional(&self, node: &impl Node) -> bool {
matches!(
self.config().collapse_simple_statement,
CollapseSimpleStatement::ConditionalOnly | CollapseSimpleStatement::Always
) || (self.should_preserve_input_simple_statements()
&& trivia_util::node_spans_single_line(node))
}

fn should_preserve_input_simple_statements(&self) -> bool {
matches!(
self.config().collapse_simple_statement,
CollapseSimpleStatement::Input
)
}

Expand Down
13 changes: 13 additions & 0 deletions src/editorconfig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ property_choice! {
property_choice! {
CollapseSimpleStatementChoice, "collapse_simple_statement";
(Never, "never"),
(Input, "input"),
(FunctionOnly, "functiononly"),
(ConditionalOnly, "conditionalonly"),
(Always, "always")
Expand Down Expand Up @@ -162,6 +163,7 @@ fn load(mut config: Config, properties: &Properties) -> Config {
if let Ok(collapse_simple_statement) = properties.get::<CollapseSimpleStatementChoice>() {
config.collapse_simple_statement = match collapse_simple_statement {
CollapseSimpleStatementChoice::Never => CollapseSimpleStatement::Never,
CollapseSimpleStatementChoice::Input => CollapseSimpleStatement::Input,
CollapseSimpleStatementChoice::FunctionOnly => CollapseSimpleStatement::FunctionOnly,
CollapseSimpleStatementChoice::ConditionalOnly => {
CollapseSimpleStatement::ConditionalOnly
Expand Down Expand Up @@ -422,6 +424,17 @@ mod tests {
);
}

#[test]
fn test_collapse_simple_statement_input() {
let mut properties = Properties::new();
properties.insert_raw_for_key("collapse_simple_statement", "Input");
let config = Config::from(&properties);
assert_eq!(
config.collapse_simple_statement,
CollapseSimpleStatement::Input
);
}

#[test]
fn test_collapse_simple_statement_function_only() {
let mut properties = Properties::new();
Expand Down
5 changes: 3 additions & 2 deletions src/formatters/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -781,9 +781,10 @@ pub fn should_collapse_function_body(ctx: &Context, function_body: &FunctionBody
|| trivia_util::contains_comments(function_body.block());

!require_multiline_function
&& (trivia_util::is_block_empty(function_body.block())
&& ((trivia_util::is_block_empty(function_body.block())
&& ctx.should_collapse_empty_function(function_body))
|| (trivia_util::is_block_simple(function_body.block())
&& ctx.should_collapse_simple_functions()
&& ctx.should_collapse_simple_function(function_body)
&& !block_contains_nested_function(function_body.block())))
}

Expand Down
2 changes: 1 addition & 1 deletion src/formatters/stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,8 +578,8 @@ pub fn format_if(ctx: &Context, if_node: &If, shape: Shape) -> If {
|| trivia_util::contains_comments(&condition);

if !require_multiline_expression
&& ctx.should_collapse_simple_conditionals()
&& is_if_guard(if_node)
&& ctx.should_collapse_simple_conditional(if_node)
{
// Rather than deferring to `format_block()`, since we know that there is only a single Stmt or LastStmt in the block, we can format it immediately
// We need to modify the formatted LastStmt, since it will have automatically added leading/trailing trivia we don't want
Expand Down
8 changes: 8 additions & 0 deletions src/formatters/trivia_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ pub fn spans_multiple_lines<T: std::fmt::Display>(item: &T) -> bool {
string.lines().count() > 1
}

/// Whether a node (as written in the input) starts and ends on the same line
pub fn node_spans_single_line(node: &impl full_moon::node::Node) -> bool {
match (node.start_position(), node.end_position()) {
(Some(start), Some(end)) => start.line() == end.line(),
_ => false,
}
}

pub fn can_hang_expression(expression: &Expression) -> bool {
match expression {
Expression::Parentheses { .. } => true, // Can always hang parentheses if necessary
Expand Down
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ pub enum CollapseSimpleStatement {
/// Never collapse
#[default]
Never,
/// Preserve whether simple statements were collapsed in the input
Input,
/// Collapse simple functions onto a single line
FunctionOnly,
/// Collapse simple if guards onto a single line
Expand Down Expand Up @@ -264,7 +266,8 @@ pub struct Config {
/// function is called with only one table or string argument (same as no_call_parentheses).
pub call_parentheses: CallParenType,
/// Whether we should collapse simple structures like functions or guard statements
/// if set to [`CollapseSimpleStatement::None`] structures are never collapsed.
/// if set to [`CollapseSimpleStatement::Never`] structures are never collapsed.
/// if set to [`CollapseSimpleStatement::Input`] simple structures preserve their input shape.
/// if set to [`CollapseSimpleStatement::FunctionOnly`] then simple functions (i.e., functions with a single laststmt) can be collapsed
pub collapse_simple_statement: CollapseSimpleStatement,
/// Whether we should allow blocks to preserve leading and trailing newline gaps.
Expand Down
147 changes: 147 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,153 @@ fn test_collapse_single_statement() {
})
}

#[test]
fn test_collapse_simple_statement_input_preserves_singleline() {
let config = Config {
collapse_simple_statement: CollapseSimpleStatement::Input,
..Config::default()
};

insta::assert_snapshot!(
format_code(
r#"function foo() return bar end
if x then return y end
"#,
config,
None,
OutputVerification::None
)
.unwrap(),
@r###"
function foo() return bar end
if x then return y end
"###
);
}

#[test]
fn test_collapse_simple_statement_input_preserves_multiline() {
let config = Config {
collapse_simple_statement: CollapseSimpleStatement::Input,
..Config::default()
};

insta::assert_snapshot!(
format_code(
r#"function foo()
return bar
end
if x then
return y
end
"#,
config,
None,
OutputVerification::None
)
.unwrap(),
@r###"
function foo()
return bar
end
if x then
return y
end
"###
);
}

#[test]
fn test_collapse_simple_statement_input_preserves_empty_function_shape() {
let config = Config {
collapse_simple_statement: CollapseSimpleStatement::Input,
..Config::default()
};

insta::assert_snapshot!(
format_code(
r#"function singleline() end
function multiline()
end
"#,
config,
None,
OutputVerification::None
)
.unwrap(),
@r###"
function singleline() end
function multiline()
end
"###
);
}

#[test]
fn test_collapse_simple_statement_input_respects_column_width() {
let config = Config {
collapse_simple_statement: CollapseSimpleStatement::Input,
column_width: 40,
..Config::default()
};

insta::assert_snapshot!(
format_code(
r#"if foooooooooooooooooooooooooooooooooooooooo then return bar end
local function foo(aaaaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbbbbbbb, cccccccccccccccc) local x = 1 end
"#,
config,
None,
OutputVerification::None
)
.unwrap(),
@r###"
if
foooooooooooooooooooooooooooooooooooooooo
then
return bar
end
local function foo(
aaaaaaaaaaaaaaaaaaaa,
bbbbbbbbbbbbbbbbbbbb,
cccccccccccccccc
)
local x = 1
end
"###
);
}

#[test]
fn test_collapse_simple_statement_input_preserves_function_argument_hug() {
let config = Config {
collapse_simple_statement: CollapseSimpleStatement::Input,
..Config::default()
};

insta::assert_snapshot!(
format_code(
r#"task.spawn(function()

end)
x.SomeMethod("something", function()

end)
"#,
config,
None,
OutputVerification::None
)
.unwrap(),
@r###"
task.spawn(function()
end)
x.SomeMethod("something", function()
end)
"###
);
}

#[test]
fn test_preserve_block_newline_gaps() {
insta::glob!("inputs-preserve-block-newline-gaps/*.lua", |path| {
Expand Down
Loading