stylua_lib/
lib.rs

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