diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b3b91e0..8667b999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 4f106e54..4d0279e3 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/src/cli/config.rs b/src/cli/config.rs index 4b8591a2..b344c0f1 100644 --- a/src/cli/config.rs +++ b/src/cli/config.rs @@ -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() { @@ -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 + ); + } } diff --git a/src/cli/opt.rs b/src/cli/opt.rs index b9d8499b..d071da64 100644 --- a/src/cli/opt.rs +++ b/src/cli/opt.rs @@ -278,6 +278,7 @@ convert_enum!(CallParenType, ArgCallParenType, { convert_enum!(CollapseSimpleStatement, ArgCollapseSimpleStatement, { Never, + Input, FunctionOnly, ConditionalOnly, Always, diff --git a/src/context.rs b/src/context.rs index ee34794f..d74fc41a 100644 --- a/src/context.rs +++ b/src/context.rs @@ -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, @@ -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 ) } diff --git a/src/editorconfig.rs b/src/editorconfig.rs index 1dfb4fc8..4d31c73a 100644 --- a/src/editorconfig.rs +++ b/src/editorconfig.rs @@ -77,6 +77,7 @@ property_choice! { property_choice! { CollapseSimpleStatementChoice, "collapse_simple_statement"; (Never, "never"), + (Input, "input"), (FunctionOnly, "functiononly"), (ConditionalOnly, "conditionalonly"), (Always, "always") @@ -162,6 +163,7 @@ fn load(mut config: Config, properties: &Properties) -> Config { if let Ok(collapse_simple_statement) = properties.get::() { config.collapse_simple_statement = match collapse_simple_statement { CollapseSimpleStatementChoice::Never => CollapseSimpleStatement::Never, + CollapseSimpleStatementChoice::Input => CollapseSimpleStatement::Input, CollapseSimpleStatementChoice::FunctionOnly => CollapseSimpleStatement::FunctionOnly, CollapseSimpleStatementChoice::ConditionalOnly => { CollapseSimpleStatement::ConditionalOnly @@ -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(); diff --git a/src/formatters/functions.rs b/src/formatters/functions.rs index 290d2619..ab3244e1 100644 --- a/src/formatters/functions.rs +++ b/src/formatters/functions.rs @@ -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()))) } diff --git a/src/formatters/stmt.rs b/src/formatters/stmt.rs index 1b0484c5..1f2b2a6d 100644 --- a/src/formatters/stmt.rs +++ b/src/formatters/stmt.rs @@ -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 diff --git a/src/formatters/trivia_util.rs b/src/formatters/trivia_util.rs index 54937d4c..2a103741 100644 --- a/src/formatters/trivia_util.rs +++ b/src/formatters/trivia_util.rs @@ -151,6 +151,14 @@ pub fn spans_multiple_lines(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 diff --git a/src/lib.rs b/src/lib.rs index 7ca7a77f..62db706f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 @@ -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. diff --git a/tests/tests.rs b/tests/tests.rs index 95f8e0a5..908a390f 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -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| {