stylua_lib/
lib.rs

1use context::Context;
2use full_moon::ast::Ast;
3#[cfg(all(feature = "luau", any(feature = "lua52", feature = "lua53")))]
4use full_moon::tokenizer::{Symbol, TokenType};
5use serde::Deserialize;
6use thiserror::Error;
7#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
8use wasm_bindgen::prelude::*;
9
10#[macro_use]
11mod context;
12#[cfg(feature = "editorconfig")]
13pub mod editorconfig;
14mod formatters;
15mod shape;
16mod sort_requires;
17mod verify_ast;
18
19/// The Lua syntax version to use
20#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize)]
21#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
22#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
23#[cfg_attr(feature = "fromstr", derive(strum::EnumString))]
24pub enum LuaVersion {
25    /// Parse all syntax versions at the same time. This allows most general usage.
26    /// For overlapping syntaxes (e.g., Lua5.2 label syntax and Luau type assertions), select a
27    /// specific syntax version
28    #[default]
29    All,
30    /// Parse Lua 5.1 code
31    Lua51,
32    /// Parse Lua 5.2 code
33    #[cfg(feature = "lua52")]
34    Lua52,
35    /// Parse Lua 5.3 code
36    #[cfg(feature = "lua53")]
37    Lua53,
38    /// Parse Lua 5.4 code
39    #[cfg(feature = "lua54")]
40    Lua54,
41    /// Parse Luau code
42    #[cfg(feature = "luau")]
43    Luau,
44    /// Parse LuaJIT code
45    #[cfg(feature = "luajit")]
46    LuaJIT,
47    /// Parse Cfx Lua code
48    #[cfg(feature = "cfxlua")]
49    CfxLua,
50}
51
52impl From<LuaVersion> for full_moon::LuaVersion {
53    fn from(val: LuaVersion) -> Self {
54        match val {
55            #[cfg(feature = "cfxlua")]
56            LuaVersion::All => full_moon::LuaVersion::new().with_cfxlua(),
57            #[cfg(not(feature = "cfxlua"))]
58            LuaVersion::All => full_moon::LuaVersion::new(),
59            LuaVersion::Lua51 => full_moon::LuaVersion::lua51(),
60            #[cfg(feature = "lua52")]
61            LuaVersion::Lua52 => full_moon::LuaVersion::lua52(),
62            #[cfg(feature = "lua53")]
63            LuaVersion::Lua53 => full_moon::LuaVersion::lua53(),
64            #[cfg(feature = "lua54")]
65            LuaVersion::Lua54 => full_moon::LuaVersion::lua54(),
66            #[cfg(feature = "luau")]
67            LuaVersion::Luau => full_moon::LuaVersion::luau(),
68            #[cfg(feature = "luajit")]
69            LuaVersion::LuaJIT => full_moon::LuaVersion::luajit(),
70            #[cfg(feature = "cfxlua")]
71            LuaVersion::CfxLua => full_moon::LuaVersion::cfxlua(),
72        }
73    }
74}
75
76/// The type of indents to use when indenting
77#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize)]
78#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
79#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
80#[cfg_attr(feature = "fromstr", derive(strum::EnumString))]
81pub enum IndentType {
82    /// Indent using tabs (`\t`)
83    #[default]
84    Tabs,
85    /// Indent using spaces (` `)
86    Spaces,
87}
88
89/// The type of line endings to use at the end of a line
90#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize)]
91#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
92#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
93#[cfg_attr(feature = "fromstr", derive(strum::EnumString))]
94pub enum LineEndings {
95    // Auto,
96    /// Unix Line Endings (LF) - `\n`
97    #[default]
98    Unix,
99    /// Windows Line Endings (CRLF) - `\r\n`
100    Windows,
101}
102
103/// The style of quotes to use within string literals
104#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize)]
105#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
106#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
107#[cfg_attr(feature = "fromstr", derive(strum::EnumString))]
108pub enum QuoteStyle {
109    /// Use double quotes where possible, but change to single quotes if it produces less escapes
110    #[default]
111    AutoPreferDouble,
112    /// Use single quotes where possible, but change to double quotes if it produces less escapes
113    AutoPreferSingle,
114    /// Always use double quotes in all strings
115    ForceDouble,
116    /// Always use single quotes in all strings
117    ForceSingle,
118}
119
120/// When to use call parentheses
121#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize)]
122#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
123#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
124#[cfg_attr(feature = "fromstr", derive(strum::EnumString))]
125pub enum CallParenType {
126    /// Use call parentheses all the time
127    #[default]
128    Always,
129    /// Skip call parentheses when only a string argument is used.
130    NoSingleString,
131    /// Skip call parentheses when only a table argument is used.
132    NoSingleTable,
133    /// Skip call parentheses when only a table or string argument is used.
134    None,
135    /// Keep call parentheses based on its presence in input code.
136    Input,
137}
138
139/// What mode to use if we want to collapse simple functions / guard statements
140#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize)]
141#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
142#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
143#[cfg_attr(feature = "fromstr", derive(strum::EnumString))]
144pub enum CollapseSimpleStatement {
145    /// Never collapse
146    #[default]
147    Never,
148    /// Collapse simple functions onto a single line
149    FunctionOnly,
150    /// Collapse simple if guards onto a single line
151    ConditionalOnly,
152    /// Collapse all simple statements onto a single line
153    Always,
154}
155
156/// An optional formatting range.
157/// If provided, only content within these boundaries (inclusive) will be formatted.
158/// Both boundaries are optional, and are given as byte offsets from the beginning of the file.
159#[derive(Debug, Copy, Clone, Deserialize)]
160#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
161pub struct Range {
162    pub start: Option<usize>,
163    pub end: Option<usize>,
164}
165
166#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
167impl Range {
168    /// Creates a new formatting range from the given start and end point.
169    /// All content within these boundaries (inclusive) will be formatted.
170    pub fn from_values(start: Option<usize>, end: Option<usize>) -> Self {
171        Self { start, end }
172    }
173}
174
175/// Configuration for the Sort Requires codemod
176#[derive(Copy, Clone, Debug, Default, Deserialize)]
177#[serde(default, deny_unknown_fields)]
178#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
179#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
180pub struct SortRequiresConfig {
181    /// Whether the sort requires codemod is enabled
182    pub enabled: bool,
183}
184
185#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
186impl SortRequiresConfig {
187    pub fn new() -> Self {
188        SortRequiresConfig::default()
189    }
190    #[deprecated(since = "0.19.0", note = "access `.enabled` directly instead")]
191    #[cfg(not(all(target_arch = "wasm32", feature = "wasm-bindgen")))]
192    pub fn enabled(&self) -> bool {
193        self.enabled
194    }
195    #[deprecated(since = "0.19.0", note = "modify `.enabled` directly instead")]
196    pub fn set_enabled(&self, enabled: bool) -> Self {
197        Self { enabled }
198    }
199}
200
201/// When to use spaces after function names
202#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize)]
203#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
204#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
205#[cfg_attr(feature = "fromstr", derive(strum::EnumString))]
206pub enum SpaceAfterFunctionNames {
207    /// Never use spaces after function names.
208    #[default]
209    Never,
210    /// Use spaces after function names only for function definitions.
211    Definitions,
212    /// Use spaces after function names only for function calls.
213    Calls,
214    /// Use spaces after function names in definitions and calls.
215    Always,
216}
217
218/// The configuration to use when formatting.
219#[derive(Copy, Clone, Debug, Deserialize)]
220#[serde(default, deny_unknown_fields)]
221#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
222#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
223pub struct Config {
224    /// The type of Lua syntax to parse.
225    pub syntax: LuaVersion,
226    /// The approximate line length to use when printing the code.
227    /// This is used as a guide to determine when to wrap lines, but note
228    /// that this is not a hard upper bound.
229    pub column_width: usize,
230    /// The type of line endings to use.
231    pub line_endings: LineEndings,
232    /// The type of indents to use.
233    pub indent_type: IndentType,
234    /// The width of a single indentation level.
235    /// If `indent_type` is set to [`IndentType::Spaces`], then this is the number of spaces to use.
236    /// If `indent_type` is set to [`IndentType::Tabs`], then this is used as a heuristic to guide when to wrap lines.
237    pub indent_width: usize,
238    /// The style of quotes to use in string literals.
239    pub quote_style: QuoteStyle,
240    /// Whether to omit parentheses around function calls which take a single string literal or table.
241    /// This is added for adoption reasons only, and is not recommended for new work.
242    #[deprecated(note = "use `call_parentheses` instead")]
243    pub no_call_parentheses: bool,
244    /// When to use call parentheses.
245    /// if call_parentheses is set to [`CallParenType::Always`] call parentheses is always applied.
246    /// if call_parentheses is set to [`CallParenType::NoSingleTable`] call parentheses is omitted when
247    /// function is called with only one string argument.
248    /// if call_parentheses is set to [`CallParenType::NoSingleTable`] call parentheses is omitted when
249    /// function is called with only one table argument.
250    /// if call_parentheses is set to [`CallParenType::None`] call parentheses is omitted when
251    /// function is called with only one table or string argument (same as no_call_parentheses).
252    pub call_parentheses: CallParenType,
253    /// Whether we should collapse simple structures like functions or guard statements
254    /// if set to [`CollapseSimpleStatement::None`] structures are never collapsed.
255    /// if set to [`CollapseSimpleStatement::FunctionOnly`] then simple functions (i.e., functions with a single laststmt) can be collapsed
256    pub collapse_simple_statement: CollapseSimpleStatement,
257    /// Configuration for the sort requires codemod
258    pub sort_requires: SortRequiresConfig,
259    /// Whether we should include a space between the function name and arguments.
260    /// * if space_after_function_names is set to [`SpaceAfterFunctionNames::Never`] a space is never used.
261    /// * if space_after_function_names is set to [`SpaceAfterFunctionNames::Definitions`] a space is used only for definitions.
262    /// * if space_after_function_names is set to [`SpaceAfterFunctionNames::Calls`] a space is used only for calls.
263    /// * if space_after_function_names is set to [`SpaceAfterFunctionNames::Always`] a space is used for both definitions and calls.
264    pub space_after_function_names: SpaceAfterFunctionNames,
265}
266
267#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
268impl Config {
269    /// Creates a new Config with the default values
270    pub fn new() -> Self {
271        Config::default()
272    }
273}
274
275impl Default for Config {
276    fn default() -> Self {
277        #[allow(deprecated)]
278        Self {
279            syntax: LuaVersion::default(),
280            column_width: 120,
281            line_endings: LineEndings::default(),
282            indent_type: IndentType::default(),
283            indent_width: 4,
284            quote_style: QuoteStyle::default(),
285            no_call_parentheses: false,
286            call_parentheses: CallParenType::default(),
287            collapse_simple_statement: CollapseSimpleStatement::default(),
288            sort_requires: SortRequiresConfig::default(),
289            space_after_function_names: SpaceAfterFunctionNames::default(),
290        }
291    }
292}
293
294/// The type of verification to perform to validate that the output AST is still correct.
295#[derive(Debug, Copy, Clone, Deserialize)]
296#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
297pub enum OutputVerification {
298    /// Reparse the generated output to detect any changes to code correctness.
299    Full,
300    /// Perform no verification of the output.
301    None,
302}
303
304#[cfg(all(feature = "luau", feature = "lua53"))]
305fn is_luau_and_lua53_conflict(error: &full_moon::Error) -> bool {
306    match error {
307        full_moon::Error::AstError(ast_error) => match ast_error.token().token_type() {
308            TokenType::Symbol {
309                symbol: Symbol::DoubleGreaterThan,
310            } => ast_error.error_message() == "expected '>' to close generic type parameter list",
311            _ => false,
312        },
313        _ => false,
314    }
315}
316
317#[cfg(all(feature = "luau", feature = "lua52"))]
318fn is_luau_and_lua52_conflict(error: &full_moon::Error) -> bool {
319    match error {
320        full_moon::Error::AstError(ast_error) => {
321            ast_error.error_message() == "expected label name after `::`"
322        }
323        _ => false,
324    }
325}
326
327fn print_full_moon_error(error: &full_moon::Error) -> String {
328    match error {
329        full_moon::Error::AstError(ast_error) => format!(
330            "unexpected token `{}` ({}:{} to {}:{}), {}",
331            ast_error.token(),
332            ast_error.range().0.line(),
333            ast_error.range().0.character(),
334            ast_error.range().1.line(),
335            ast_error.range().1.character(),
336            ast_error.error_message()
337        ),
338        full_moon::Error::TokenizerError(tokenizer_error) => tokenizer_error.to_string(),
339    }
340}
341
342fn print_full_moon_errors(errors: &[full_moon::Error]) -> String {
343    #[allow(unused_mut)]
344    let mut error_message = if errors.len() == 1 {
345        print_full_moon_error(errors.first().unwrap())
346    } else {
347        errors
348            .iter()
349            .map(|err| "\n - ".to_string() + &print_full_moon_error(err))
350            .collect::<String>()
351    };
352
353    #[cfg(all(feature = "luau", feature = "lua53"))]
354    {
355        let recommend_luau_syntax = errors.iter().any(is_luau_and_lua53_conflict);
356        if recommend_luau_syntax {
357            error_message += "\nhint: this looks like a conflict with Lua 5.3 and Luau generics syntax, add `syntax = \"Luau\"` to stylua.toml to resolve";
358        }
359    }
360
361    #[cfg(all(feature = "luau", feature = "lua52"))]
362    {
363        let recommend_lua52_syntax = errors.iter().any(is_luau_and_lua52_conflict);
364        if recommend_lua52_syntax {
365            error_message += "\nhint: this looks like a conflict with Luau and Lua 5.2 label syntax, add `syntax = \"Lua52\"` to stylua.toml to resolve";
366        }
367    }
368
369    error_message
370}
371
372/// A formatting error
373#[derive(Clone, Debug, Error)]
374pub enum Error {
375    /// The input AST has a parsing error.
376    #[error("error parsing: {}", print_full_moon_errors(.0))]
377    ParseError(Vec<full_moon::Error>),
378    /// The output AST after formatting generated a parse error. This is a definite error.
379    #[error("INTERNAL ERROR: Output AST generated a syntax error. Please report this at https://github.com/johnnymorganz/stylua/issues: {}", print_full_moon_errors(.0))]
380    VerificationAstError(Vec<full_moon::Error>),
381    /// The output AST after formatting differs from the input AST.
382    #[error("INTERNAL WARNING: Output AST may be different to input AST. Code correctness may have changed. Please examine the formatting diff and report any issues at https://github.com/johnnymorganz/stylua/issues")]
383    VerificationAstDifference,
384}
385
386/// Formats given [`Ast`]
387#[allow(clippy::result_large_err)]
388pub fn format_ast(
389    input_ast: Ast,
390    config: Config,
391    range: Option<Range>,
392    verify_output: OutputVerification,
393) -> Result<Ast, Error> {
394    // Clone the input AST only if we are verifying, to later use for checking
395    let input_ast_for_verification = if let OutputVerification::Full = verify_output {
396        Some(input_ast.to_owned())
397    } else {
398        None
399    };
400
401    let ctx = Context::new(config, range);
402
403    // Perform require sorting beforehand if necessary
404    let input_ast = match config.sort_requires.enabled {
405        true => sort_requires::sort_requires(&ctx, input_ast),
406        false => input_ast,
407    };
408
409    let code_formatter = formatters::CodeFormatter::new(ctx);
410    let ast = code_formatter.format(input_ast);
411
412    // If we are verifying, reparse the output then check it matches the original input
413    if let Some(input_ast) = input_ast_for_verification {
414        let output = ast.to_string();
415        let reparsed_output =
416            match full_moon::parse_fallible(&output, config.syntax.into()).into_result() {
417                Ok(ast) => ast,
418                Err(error) => {
419                    return Err(Error::VerificationAstError(error));
420                }
421            };
422
423        let mut ast_verifier = verify_ast::AstVerifier::new();
424        if !ast_verifier.compare(input_ast, reparsed_output) {
425            return Err(Error::VerificationAstDifference);
426        }
427    }
428
429    Ok(ast)
430}
431
432/// Formats given Lua code
433#[allow(clippy::result_large_err)]
434pub fn format_code(
435    code: &str,
436    config: Config,
437    range: Option<Range>,
438    verify_output: OutputVerification,
439) -> Result<String, Error> {
440    let input_ast = match full_moon::parse_fallible(code, config.syntax.into()).into_result() {
441        Ok(ast) => ast,
442        Err(error) => {
443            return Err(Error::ParseError(error));
444        }
445    };
446
447    let ast = format_ast(input_ast, config, range, verify_output)?;
448    let output = ast.to_string();
449
450    Ok(output)
451}
452
453#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
454#[wasm_bindgen(js_name = formatCode)]
455pub fn format_code_wasm(
456    code: &str,
457    config: Config,
458    range: Option<Range>,
459    verify_output: OutputVerification,
460) -> Result<String, String> {
461    format_code(code, config, range, verify_output).map_err(|err| err.to_string())
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467
468    #[test]
469    fn test_entry_point() {
470        let output = format_code(
471            "local   x   =    1",
472            Config::default(),
473            None,
474            OutputVerification::None,
475        )
476        .unwrap();
477        assert_eq!(output, "local x = 1\n");
478    }
479
480    #[test]
481    fn test_invalid_input() {
482        let output = format_code(
483            "local   x   = ",
484            Config::default(),
485            None,
486            OutputVerification::None,
487        );
488        assert!(matches!(output, Err(Error::ParseError(_))))
489    }
490
491    #[test]
492    fn test_with_ast_verification() {
493        let output = format_code(
494            "local   x   =    1",
495            Config::default(),
496            None,
497            OutputVerification::Full,
498        )
499        .unwrap();
500        assert_eq!(output, "local x = 1\n");
501    }
502}