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/// If blocks should be allowed to have leading and trailing newline gaps.
157#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize)]
158#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
159#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
160#[cfg_attr(feature = "fromstr", derive(strum::EnumString))]
161pub enum BlockNewlineGaps {
162    /// Never allow leading or trailing newline gaps
163    #[default]
164    Never,
165    /// Preserve both leading and trailing newline gaps if present in input
166    Preserve,
167}
168
169/// An optional formatting range.
170/// If provided, only content within these boundaries (inclusive) will be formatted.
171/// Both boundaries are optional, and are given as byte offsets from the beginning of the file.
172#[derive(Debug, Copy, Clone, Deserialize)]
173#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
174pub struct Range {
175    pub start: Option<usize>,
176    pub end: Option<usize>,
177}
178
179#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
180impl Range {
181    /// Creates a new formatting range from the given start and end point.
182    /// All content within these boundaries (inclusive) will be formatted.
183    pub fn from_values(start: Option<usize>, end: Option<usize>) -> Self {
184        Self { start, end }
185    }
186}
187
188/// Configuration for the Sort Requires codemod
189#[derive(Copy, Clone, Debug, Default, Deserialize)]
190#[serde(default, deny_unknown_fields)]
191#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
192#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
193pub struct SortRequiresConfig {
194    /// Whether the sort requires codemod is enabled
195    pub enabled: bool,
196}
197
198#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
199impl SortRequiresConfig {
200    pub fn new() -> Self {
201        SortRequiresConfig::default()
202    }
203    #[deprecated(since = "0.19.0", note = "access `.enabled` directly instead")]
204    #[cfg(not(all(target_arch = "wasm32", feature = "wasm-bindgen")))]
205    pub fn enabled(&self) -> bool {
206        self.enabled
207    }
208    #[deprecated(since = "0.19.0", note = "modify `.enabled` directly instead")]
209    pub fn set_enabled(&self, enabled: bool) -> Self {
210        Self { enabled }
211    }
212}
213
214/// When to use spaces after function names
215#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize)]
216#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
217#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
218#[cfg_attr(feature = "fromstr", derive(strum::EnumString))]
219pub enum SpaceAfterFunctionNames {
220    /// Never use spaces after function names.
221    #[default]
222    Never,
223    /// Use spaces after function names only for function definitions.
224    Definitions,
225    /// Use spaces after function names only for function calls.
226    Calls,
227    /// Use spaces after function names in definitions and calls.
228    Always,
229}
230
231/// The configuration to use when formatting.
232#[derive(Copy, Clone, Debug, Deserialize)]
233#[serde(default, deny_unknown_fields)]
234#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
235#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
236pub struct Config {
237    /// The type of Lua syntax to parse.
238    pub syntax: LuaVersion,
239    /// The approximate line length to use when printing the code.
240    /// This is used as a guide to determine when to wrap lines, but note
241    /// that this is not a hard upper bound.
242    pub column_width: usize,
243    /// The type of line endings to use.
244    pub line_endings: LineEndings,
245    /// The type of indents to use.
246    pub indent_type: IndentType,
247    /// The width of a single indentation level.
248    /// If `indent_type` is set to [`IndentType::Spaces`], then this is the number of spaces to use.
249    /// If `indent_type` is set to [`IndentType::Tabs`], then this is used as a heuristic to guide when to wrap lines.
250    pub indent_width: usize,
251    /// The style of quotes to use in string literals.
252    pub quote_style: QuoteStyle,
253    /// Whether to omit parentheses around function calls which take a single string literal or table.
254    /// This is added for adoption reasons only, and is not recommended for new work.
255    #[deprecated(note = "use `call_parentheses` instead")]
256    pub no_call_parentheses: bool,
257    /// When to use call parentheses.
258    /// if call_parentheses is set to [`CallParenType::Always`] call parentheses is always applied.
259    /// if call_parentheses is set to [`CallParenType::NoSingleTable`] call parentheses is omitted when
260    /// function is called with only one string argument.
261    /// if call_parentheses is set to [`CallParenType::NoSingleTable`] call parentheses is omitted when
262    /// function is called with only one table argument.
263    /// if call_parentheses is set to [`CallParenType::None`] call parentheses is omitted when
264    /// function is called with only one table or string argument (same as no_call_parentheses).
265    pub call_parentheses: CallParenType,
266    /// Whether we should collapse simple structures like functions or guard statements
267    /// if set to [`CollapseSimpleStatement::None`] structures are never collapsed.
268    /// if set to [`CollapseSimpleStatement::FunctionOnly`] then simple functions (i.e., functions with a single laststmt) can be collapsed
269    pub collapse_simple_statement: CollapseSimpleStatement,
270    /// Whether we should allow blocks to preserve leading and trailing newline gaps.
271    /// if set to [`BlockNewlineGaps::Never`] then newline gaps are never allowed at the start or end of blocks.
272    /// if set to [`BlockNewlineGaps::Preserve`] then newline gaps are preserved at the start and end of blocks.
273    pub block_newline_gaps: BlockNewlineGaps,
274    /// Configuration for the sort requires codemod
275    pub sort_requires: SortRequiresConfig,
276    /// Whether we should include a space between the function name and arguments.
277    /// * if space_after_function_names is set to [`SpaceAfterFunctionNames::Never`] a space is never used.
278    /// * if space_after_function_names is set to [`SpaceAfterFunctionNames::Definitions`] a space is used only for definitions.
279    /// * if space_after_function_names is set to [`SpaceAfterFunctionNames::Calls`] a space is used only for calls.
280    /// * if space_after_function_names is set to [`SpaceAfterFunctionNames::Always`] a space is used for both definitions and calls.
281    pub space_after_function_names: SpaceAfterFunctionNames,
282}
283
284#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
285impl Config {
286    /// Creates a new Config with the default values
287    pub fn new() -> Self {
288        Config::default()
289    }
290}
291
292impl Default for Config {
293    fn default() -> Self {
294        #[allow(deprecated)]
295        Self {
296            syntax: LuaVersion::default(),
297            column_width: 120,
298            line_endings: LineEndings::default(),
299            indent_type: IndentType::default(),
300            indent_width: 4,
301            quote_style: QuoteStyle::default(),
302            no_call_parentheses: false,
303            call_parentheses: CallParenType::default(),
304            collapse_simple_statement: CollapseSimpleStatement::default(),
305            sort_requires: SortRequiresConfig::default(),
306            space_after_function_names: SpaceAfterFunctionNames::default(),
307            block_newline_gaps: BlockNewlineGaps::default(),
308        }
309    }
310}
311
312/// The type of verification to perform to validate that the output AST is still correct.
313#[derive(Debug, Copy, Clone, Deserialize)]
314#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
315pub enum OutputVerification {
316    /// Reparse the generated output to detect any changes to code correctness.
317    Full,
318    /// Perform no verification of the output.
319    None,
320}
321
322#[cfg(all(feature = "luau", feature = "lua53"))]
323fn is_luau_and_lua53_conflict(error: &full_moon::Error) -> bool {
324    match error {
325        full_moon::Error::AstError(ast_error) => match ast_error.token().token_type() {
326            TokenType::Symbol {
327                symbol: Symbol::DoubleGreaterThan,
328            } => ast_error.error_message() == "expected '>' to close generic type parameter list",
329            _ => false,
330        },
331        _ => false,
332    }
333}
334
335#[cfg(all(feature = "luau", feature = "lua52"))]
336fn is_luau_and_lua52_conflict(error: &full_moon::Error) -> bool {
337    match error {
338        full_moon::Error::AstError(ast_error) => {
339            ast_error.error_message() == "expected label name after `::`"
340        }
341        _ => false,
342    }
343}
344
345fn print_full_moon_error(error: &full_moon::Error) -> String {
346    match error {
347        full_moon::Error::AstError(ast_error) => format!(
348            "unexpected token `{}` ({}:{} to {}:{}), {}",
349            ast_error.token(),
350            ast_error.range().0.line(),
351            ast_error.range().0.character(),
352            ast_error.range().1.line(),
353            ast_error.range().1.character(),
354            ast_error.error_message()
355        ),
356        full_moon::Error::TokenizerError(tokenizer_error) => tokenizer_error.to_string(),
357    }
358}
359
360fn print_full_moon_errors(errors: &[full_moon::Error]) -> String {
361    #[allow(unused_mut)]
362    let mut error_message = if errors.len() == 1 {
363        print_full_moon_error(errors.first().unwrap())
364    } else {
365        errors
366            .iter()
367            .map(|err| "\n - ".to_string() + &print_full_moon_error(err))
368            .collect::<String>()
369    };
370
371    #[cfg(all(feature = "luau", feature = "lua53"))]
372    {
373        let recommend_luau_syntax = errors.iter().any(is_luau_and_lua53_conflict);
374        if recommend_luau_syntax {
375            error_message += "\nhint: this looks like a conflict with Lua 5.3 and Luau generics syntax, add `syntax = \"Luau\"` to stylua.toml to resolve";
376        }
377    }
378
379    #[cfg(all(feature = "luau", feature = "lua52"))]
380    {
381        let recommend_lua52_syntax = errors.iter().any(is_luau_and_lua52_conflict);
382        if recommend_lua52_syntax {
383            error_message += "\nhint: this looks like a conflict with Luau and Lua 5.2 label syntax, add `syntax = \"Lua52\"` to stylua.toml to resolve";
384        }
385    }
386
387    error_message
388}
389
390/// A formatting error
391#[derive(Clone, Debug, Error)]
392pub enum Error {
393    /// The input AST has a parsing error.
394    #[error("error parsing: {}", print_full_moon_errors(.0))]
395    ParseError(Vec<full_moon::Error>),
396    /// The output AST after formatting generated a parse error. This is a definite error.
397    #[error("INTERNAL ERROR: Output AST generated a syntax error. Please report this at https://github.com/johnnymorganz/stylua/issues: {}", print_full_moon_errors(.0))]
398    VerificationAstError(Vec<full_moon::Error>),
399    /// The output AST after formatting differs from the input AST.
400    #[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")]
401    VerificationAstDifference,
402}
403
404/// Formats given [`Ast`]
405#[allow(clippy::result_large_err)]
406pub fn format_ast(
407    input_ast: Ast,
408    config: Config,
409    range: Option<Range>,
410    verify_output: OutputVerification,
411) -> Result<Ast, Error> {
412    // Clone the input AST only if we are verifying, to later use for checking
413    let input_ast_for_verification = if let OutputVerification::Full = verify_output {
414        Some(input_ast.to_owned())
415    } else {
416        None
417    };
418
419    let ctx = Context::new(config, range);
420
421    // Perform require sorting beforehand if necessary
422    let input_ast = match config.sort_requires.enabled {
423        true => sort_requires::sort_requires(&ctx, input_ast),
424        false => input_ast,
425    };
426
427    let code_formatter = formatters::CodeFormatter::new(ctx);
428    let ast = code_formatter.format(input_ast);
429
430    // If we are verifying, reparse the output then check it matches the original input
431    if let Some(input_ast) = input_ast_for_verification {
432        let output = ast.to_string();
433        let reparsed_output =
434            match full_moon::parse_fallible(&output, config.syntax.into()).into_result() {
435                Ok(ast) => ast,
436                Err(error) => {
437                    return Err(Error::VerificationAstError(error));
438                }
439            };
440
441        let mut ast_verifier = verify_ast::AstVerifier::new();
442        if !ast_verifier.compare(input_ast, reparsed_output) {
443            return Err(Error::VerificationAstDifference);
444        }
445    }
446
447    Ok(ast)
448}
449
450/// Formats given Lua code
451#[allow(clippy::result_large_err)]
452pub fn format_code(
453    code: &str,
454    config: Config,
455    range: Option<Range>,
456    verify_output: OutputVerification,
457) -> Result<String, Error> {
458    let input_ast = match full_moon::parse_fallible(code, config.syntax.into()).into_result() {
459        Ok(ast) => ast,
460        Err(error) => {
461            return Err(Error::ParseError(error));
462        }
463    };
464
465    let ast = format_ast(input_ast, config, range, verify_output)?;
466    let output = ast.to_string();
467
468    Ok(output)
469}
470
471#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
472#[wasm_bindgen(js_name = formatCode)]
473pub fn format_code_wasm(
474    code: &str,
475    config: Config,
476    range: Option<Range>,
477    verify_output: OutputVerification,
478) -> Result<String, String> {
479    format_code(code, config, range, verify_output).map_err(|err| err.to_string())
480}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485
486    #[test]
487    fn test_entry_point() {
488        let output = format_code(
489            "local   x   =    1",
490            Config::default(),
491            None,
492            OutputVerification::None,
493        )
494        .unwrap();
495        assert_eq!(output, "local x = 1\n");
496    }
497
498    #[test]
499    fn test_invalid_input() {
500        let output = format_code(
501            "local   x   = ",
502            Config::default(),
503            None,
504            OutputVerification::None,
505        );
506        assert!(matches!(output, Err(Error::ParseError(_))))
507    }
508
509    #[test]
510    fn test_with_ast_verification() {
511        let output = format_code(
512            "local   x   =    1",
513            Config::default(),
514            None,
515            OutputVerification::Full,
516        )
517        .unwrap();
518        assert_eq!(output, "local x = 1\n");
519    }
520}