diff --git a/CHANGELOG.md b/CHANGELOG.md index 59fd2f6b..ea8d3a2a 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 + +- Added configuration option `space_after_function_names` to specify whether to include a space between a function name and parentheses ([#839](https://github.com/JohnnyMorganz/StyLua/issues/839)) + ## [0.20.0] - 2024-01-20 ### Added diff --git a/README.md b/README.md index d4207e3a..90a3ef2c 100644 --- a/README.md +++ b/README.md @@ -251,15 +251,16 @@ If a project uses the default configuration of StyLua without a configuration fi StyLua only offers the following options: -| Option | Default | Description | -| --------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `column_width` | `120` | Approximate line length for printing. Used as a guide for line wrapping - this is not a hard requirement: lines may fall under or over the limit. | -| `line_endings` | `Unix` | Line endings type. Possible options: `Unix` (LF) or `Windows` (CRLF) | -| `indent_type` | `Tabs` | Indent type. Possible options: `Tabs` or `Spaces` | -| `indent_width` | `4` | Character size of single indentation. If `indent_type` is set to `Tabs`, this option is used as a heuristic to determine column width only. | -| `quote_style` | `AutoPreferDouble` | Quote style for string literals. Possible options: `AutoPreferDouble`, `AutoPreferSingle`, `ForceDouble`, `ForceSingle`. `AutoPrefer` styles will prefer the specified quote style, but fall back to the alternative if it has fewer string escapes. `Force` styles always use the specified style regardless of escapes. | -| `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. | -| `collapse_simple_statement` | `Never` | Specify whether to collapse simple statements. Possible options: `Never`, `FunctionOnly`, `ConditionalOnly`, or `Always` | +| Option | Default | Description | +| ---------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `column_width` | `120` | Approximate line length for printing. Used as a guide for line wrapping - this is not a hard requirement: lines may fall under or over the limit. | +| `line_endings` | `Unix` | Line endings type. Possible options: `Unix` (LF) or `Windows` (CRLF) | +| `indent_type` | `Tabs` | Indent type. Possible options: `Tabs` or `Spaces` | +| `indent_width` | `4` | Character size of single indentation. If `indent_type` is set to `Tabs`, this option is used as a heuristic to determine column width only. | +| `quote_style` | `AutoPreferDouble` | Quote style for string literals. Possible options: `AutoPreferDouble`, `AutoPreferSingle`, `ForceDouble`, `ForceSingle`. `AutoPrefer` styles will prefer the specified quote style, but fall back to the alternative if it has fewer string escapes. `Force` styles always use the specified style regardless of escapes. | +| `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` | +| `collapse_simple_statement` | `Never` | Specify whether to collapse simple statements. Possible options: `Never`, `FunctionOnly`, `ConditionalOnly`, or `Always` | Default `stylua.toml`, note you do not need to explicitly specify each option if you want to use the defaults: @@ -271,6 +272,7 @@ indent_width = 4 quote_style = "AutoPreferDouble" call_parentheses = "Always" collapse_simple_statement = "Never" +space_after_function_names = "Never" [sort_requires] enabled = false diff --git a/src/cli/config.rs b/src/cli/config.rs index 1b7ae4af..c3fe9bb2 100644 --- a/src/cli/config.rs +++ b/src/cli/config.rs @@ -174,6 +174,9 @@ pub fn load_overrides(config: Config, opt: &Opt) -> Config { if let Some(call_parentheses) = opt.format_opts.call_parentheses { new_config.call_parentheses = call_parentheses.into(); }; + if let Some(space_after_function_names) = opt.format_opts.space_after_function_names { + new_config.space_after_function_names = space_after_function_names.into(); + }; if let Some(collapse_simple_statement) = opt.format_opts.collapse_simple_statement { new_config.collapse_simple_statement = collapse_simple_statement.into(); } diff --git a/src/cli/opt.rs b/src/cli/opt.rs index 6a75f470..5ba37150 100644 --- a/src/cli/opt.rs +++ b/src/cli/opt.rs @@ -1,6 +1,9 @@ use clap::{ArgEnum, StructOpt}; use std::path::PathBuf; -use stylua_lib::{CallParenType, CollapseSimpleStatement, IndentType, LineEndings, QuoteStyle}; +use stylua_lib::{ + CallParenType, CollapseSimpleStatement, IndentType, LineEndings, QuoteStyle, + SpaceAfterFunctionNames, +}; lazy_static::lazy_static! { static ref NUM_CPUS: String = num_cpus::get().to_string(); @@ -183,6 +186,8 @@ pub struct FormatOpts { /// Enable requires sorting #[structopt(long)] pub sort_requires: bool, + #[structopt(long, arg_enum, ignore_case = true)] + pub space_after_function_names: Option, } // Convert [`stylua_lib::Config`] enums into clap-friendly enums @@ -250,6 +255,13 @@ convert_enum!(CollapseSimpleStatement, ArgCollapseSimpleStatement, { Always, }); +convert_enum!(SpaceAfterFunctionNames, ArgSpaceAfterFunctionNames, { + Never, + Definitions, + Calls, + Always, +}); + #[cfg(test)] mod tests { use super::Opt; diff --git a/src/context.rs b/src/context.rs index d6d21314..ea2a1066 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,6 +1,6 @@ use crate::{ shape::Shape, CallParenType, CollapseSimpleStatement, Config, IndentType, LineEndings, - Range as FormatRange, + Range as FormatRange, SpaceAfterFunctionNames, }; use full_moon::{ node::Node, @@ -181,6 +181,30 @@ pub fn create_plain_indent_trivia(ctx: &Context, indent_level: usize) -> Token { } } +/// Creates a new Token containing whitespace used after function declarations +pub fn create_function_definition_trivia(ctx: &Context) -> Token { + match ctx.config().space_after_function_names { + SpaceAfterFunctionNames::Always | SpaceAfterFunctionNames::Definitions => { + Token::new(TokenType::spaces(1)) + } + SpaceAfterFunctionNames::Never | SpaceAfterFunctionNames::Calls => { + Token::new(TokenType::spaces(0)) + } + } +} + +/// Creates a new Token containing whitespace used after function calls +pub fn create_function_call_trivia(ctx: &Context) -> Token { + match ctx.config().space_after_function_names { + SpaceAfterFunctionNames::Always | SpaceAfterFunctionNames::Calls => { + Token::new(TokenType::spaces(1)) + } + SpaceAfterFunctionNames::Never | SpaceAfterFunctionNames::Definitions => { + Token::new(TokenType::spaces(0)) + } + } +} + /// Creates a new Token containing new line whitespace, used for trivia pub fn create_newline_trivia(ctx: &Context) -> Token { Token::new(TokenType::Whitespace { diff --git a/src/editorconfig.rs b/src/editorconfig.rs index 32ea6cfd..6cc12968 100644 --- a/src/editorconfig.rs +++ b/src/editorconfig.rs @@ -1,6 +1,6 @@ use crate::{ CallParenType, CollapseSimpleStatement, Config, IndentType, LineEndings, QuoteStyle, - SortRequiresConfig, + SortRequiresConfig, SpaceAfterFunctionNames, }; use ec4rs::{ properties_of, @@ -65,6 +65,14 @@ property_choice! { (None, "none") } +property_choice! { + SpaceAfterFunctionNamesChoice, "space_after_function_names"; + (Always, "always"), + (Definitions, "definitions"), + (Calls, "calls"), + (Never, "never") +} + property_choice! { CollapseSimpleStatementChoice, "collapse_simple_statement"; (Never, "never"), @@ -128,6 +136,22 @@ fn load(mut config: Config, properties: &Properties) -> Config { CallParenthesesChoice::None => config.call_parentheses = CallParenType::None, } } + if let Ok(space_after_function_names) = properties.get::() { + match space_after_function_names { + SpaceAfterFunctionNamesChoice::Always => { + config.space_after_function_names = SpaceAfterFunctionNames::Always + } + SpaceAfterFunctionNamesChoice::Definitions => { + config.space_after_function_names = SpaceAfterFunctionNames::Definitions + } + SpaceAfterFunctionNamesChoice::Calls => { + config.space_after_function_names = SpaceAfterFunctionNames::Calls + } + SpaceAfterFunctionNamesChoice::Never => { + config.space_after_function_names = SpaceAfterFunctionNames::Never + } + } + } if let Ok(collapse_simple_statement) = properties.get::() { match collapse_simple_statement { CollapseSimpleStatementChoice::Never => { @@ -309,6 +333,50 @@ mod tests { assert_eq!(config.call_parentheses, CallParenType::None); } + #[test] + fn test_space_after_function_names_always() { + let mut properties = Properties::new(); + properties.insert_raw_for_key("space_after_function_names", "Always"); + let config = Config::from(&properties); + assert_eq!( + config.space_after_function_names, + SpaceAfterFunctionNames::Always + ); + } + + #[test] + fn test_space_after_function_names_definitions() { + let mut properties = Properties::new(); + properties.insert_raw_for_key("space_after_function_names", "Definitions"); + let config = Config::from(&properties); + assert_eq!( + config.space_after_function_names, + SpaceAfterFunctionNames::Definitions + ); + } + + #[test] + fn test_space_after_function_names_calls() { + let mut properties = Properties::new(); + properties.insert_raw_for_key("space_after_function_names", "Calls"); + let config = Config::from(&properties); + assert_eq!( + config.space_after_function_names, + SpaceAfterFunctionNames::Calls + ); + } + + #[test] + fn test_space_after_function_names_never() { + let mut properties = Properties::new(); + properties.insert_raw_for_key("space_after_function_names", "Never"); + let config = Config::from(&properties); + assert_eq!( + config.space_after_function_names, + SpaceAfterFunctionNames::Never + ); + } + #[test] fn test_collapse_simple_statement_never() { let mut properties = Properties::new(); diff --git a/src/formatters/functions.rs b/src/formatters/functions.rs index 0ceb814d..f3d7fb7c 100644 --- a/src/formatters/functions.rs +++ b/src/formatters/functions.rs @@ -10,7 +10,10 @@ use full_moon::tokenizer::{Token, TokenKind, TokenReference, TokenType}; #[cfg(feature = "luau")] use crate::formatters::luau::{format_generic_declaration, format_type_specifier}; use crate::{ - context::{create_indent_trivia, create_newline_trivia, Context}, + context::{ + create_function_call_trivia, create_function_definition_trivia, create_indent_trivia, + create_newline_trivia, Context, + }, fmt_symbol, formatters::{ block::{format_block, format_last_stmt_no_trivia}, @@ -42,7 +45,9 @@ pub fn format_anonymous_function( shape: Shape, ) -> (TokenReference, FunctionBody) { const FUNCTION_LEN: usize = "function".len(); - let function_token = fmt_symbol!(ctx, function_token, "function", shape); + let function_definition_trivia = vec![create_function_definition_trivia(ctx)]; + let function_token = fmt_symbol!(ctx, function_token, "function", shape) + .update_trailing_trivia(FormatTriviaType::Append(function_definition_trivia)); let function_body = format_function_body(ctx, function_body, shape.add_width(FUNCTION_LEN)); (function_token, function_body) @@ -73,13 +78,14 @@ pub fn format_call( shape: Shape, call_next_node: FunctionCallNextNode, ) -> Call { + let function_call_trivia = vec![create_function_call_trivia(ctx)]; match call { - Call::AnonymousCall(function_args) => Call::AnonymousCall(format_function_args( - ctx, - function_args, - shape, - call_next_node, - )), + Call::AnonymousCall(function_args) => { + let formatted_function_args = + format_function_args(ctx, function_args, shape, call_next_node) + .update_leading_trivia(FormatTriviaType::Append(function_call_trivia)); + Call::AnonymousCall(formatted_function_args) + } Call::MethodCall(method_call) => { Call::MethodCall(format_method_call(ctx, method_call, shape, call_next_node)) } @@ -1150,6 +1156,7 @@ pub fn format_function_declaration( // Calculate trivia let leading_trivia = vec![create_indent_trivia(ctx, shape)]; let trailing_trivia = vec![create_newline_trivia(ctx)]; + let function_definition_trivia = vec![create_function_definition_trivia(ctx)]; let function_token = fmt_symbol!( ctx, @@ -1158,7 +1165,8 @@ pub fn format_function_declaration( shape ) .update_leading_trivia(FormatTriviaType::Append(leading_trivia)); - let formatted_function_name = format_function_name(ctx, function_declaration.name(), shape); + let formatted_function_name = format_function_name(ctx, function_declaration.name(), shape) + .update_trailing_trivia(FormatTriviaType::Append(function_definition_trivia)); let shape = shape + (9 + strip_trivia(&formatted_function_name).to_string().len()); // 9 = "function " let function_body = format_function_body(ctx, function_declaration.body(), shape) @@ -1178,11 +1186,13 @@ pub fn format_local_function( // Calculate trivia let leading_trivia = vec![create_indent_trivia(ctx, shape)]; let trailing_trivia = vec![create_newline_trivia(ctx)]; + let function_definition_trivia = vec![create_function_definition_trivia(ctx)]; let local_token = fmt_symbol!(ctx, local_function.local_token(), "local ", shape) .update_leading_trivia(FormatTriviaType::Append(leading_trivia)); let function_token = fmt_symbol!(ctx, local_function.function_token(), "function ", shape); - let formatted_name = format_token_reference(ctx, local_function.name(), shape); + let formatted_name = format_token_reference(ctx, local_function.name(), shape) + .update_trailing_trivia(FormatTriviaType::Append(function_definition_trivia)); let shape = shape + (6 + 9 + strip_trivia(&formatted_name).to_string().len()); // 6 = "local ", 9 = "function " let function_body = format_function_body(ctx, local_function.body(), shape) @@ -1201,12 +1211,14 @@ pub fn format_method_call( shape: Shape, call_next_node: FunctionCallNextNode, ) -> MethodCall { + let function_call_trivia = vec![create_function_call_trivia(ctx)]; let formatted_colon_token = format_token_reference(ctx, method_call.colon_token(), shape); let formatted_name = format_token_reference(ctx, method_call.name(), shape); let shape = shape + (formatted_colon_token.to_string().len() + formatted_name.to_string().len()); let formatted_function_args = - format_function_args(ctx, method_call.args(), shape, call_next_node); + format_function_args(ctx, method_call.args(), shape, call_next_node) + .update_leading_trivia(FormatTriviaType::Append(function_call_trivia)); MethodCall::new(formatted_name, formatted_function_args).with_colon_token(formatted_colon_token) } diff --git a/src/lib.rs b/src/lib.rs index 75b12487..bfb97bb0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -139,6 +139,23 @@ impl SortRequiresConfig { } } +/// When to use spaces after function names +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize)] +#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize))] +#[cfg_attr(feature = "fromstr", derive(strum::EnumString))] +pub enum SpaceAfterFunctionNames { + /// Never use spaces after function names. + #[default] + Never, + /// Use spaces after function names only for function definitions. + Definitions, + /// Use spaces after function names only for function calls. + Calls, + /// Use spaces after function names in definitions and calls. + Always, +} + /// The configuration to use when formatting. #[derive(Copy, Clone, Debug, Deserialize)] #[serde(default, deny_unknown_fields)] @@ -178,6 +195,12 @@ pub struct Config { pub collapse_simple_statement: CollapseSimpleStatement, /// Configuration for the sort requires codemod pub sort_requires: SortRequiresConfig, + /// Whether we should include a space between the function name and arguments. + /// * if space_after_function_names is set to [`SpaceAfterFunctions::Never`] a space is never used. + /// * if space_after_function_names is set to [`SpaceAfterFunctions::Definitions`] a space is used only for definitions. + /// * if space_after_function_names is set to [`SpaceAfterFunctions::Calls`] a space is used only for calls. + /// * if space_after_function_names is set to [`SpaceAfterFunctions::Always`] a space is used for both definitions and calls. + pub space_after_function_names: SpaceAfterFunctionNames, } #[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)] @@ -346,6 +369,7 @@ impl Default for Config { call_parentheses: CallParenType::default(), collapse_simple_statement: CollapseSimpleStatement::default(), sort_requires: SortRequiresConfig::default(), + space_after_function_names: SpaceAfterFunctionNames::default(), } } } diff --git a/tests/test_spaces_after_function_names.rs b/tests/test_spaces_after_function_names.rs new file mode 100644 index 00000000..5bd97269 --- /dev/null +++ b/tests/test_spaces_after_function_names.rs @@ -0,0 +1,134 @@ +use stylua_lib::{format_code, Config, OutputVerification, SpaceAfterFunctionNames}; + +fn format(input: &str, space_after_function_names: SpaceAfterFunctionNames) -> String { + format_code( + input, + Config { + space_after_function_names, + ..Config::default() + }, + None, + OutputVerification::None, + ) + .unwrap() +} + +const STARTINGCODE: &str = r###" +local foo = function() end +local function bar () end +function baz() end +a = {} +function a:b () end +function a.c () end +function qiz () return function () end end +foo() +bar () +baz() +a:b () +a.c () +qiz()() +"###; + +#[test] +fn test_never_space_after_function_names() { + insta::assert_snapshot!( + format(STARTINGCODE, + SpaceAfterFunctionNames::Never + ), + @r###" +local foo = function() end +local function bar() end +function baz() end +a = {} +function a:b() end +function a.c() end +function qiz() + return function() end +end +foo() +bar() +baz() +a:b() +a.c() +qiz()() + "### + ); +} + +#[test] +fn test_space_after_function_definitions() { + insta::assert_snapshot!( + format(STARTINGCODE, + SpaceAfterFunctionNames::Definitions + ), + @r###" +local foo = function () end +local function bar () end +function baz () end +a = {} +function a:b () end +function a.c () end +function qiz () + return function () end +end +foo() +bar() +baz() +a:b() +a.c() +qiz()() + "### + ); +} + +#[test] +fn test_space_after_function_calls() { + insta::assert_snapshot!( + format(STARTINGCODE, + SpaceAfterFunctionNames::Calls + ), + @r###" +local foo = function() end +local function bar() end +function baz() end +a = {} +function a:b() end +function a.c() end +function qiz() + return function() end +end +foo () +bar () +baz () +a:b () +a.c () +qiz () () + "### + ); +} + +#[test] +fn test_always_space_after_function_names() { + insta::assert_snapshot!( + format(STARTINGCODE, + SpaceAfterFunctionNames::Always + ), + @r###" +local foo = function () end +local function bar () end +function baz () end +a = {} +function a:b () end +function a.c () end +function qiz () + return function () end +end +foo () +bar () +baz () +a:b () +a.c () +qiz () () + "### + ); +}