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#[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 #[default]
29 All,
30 Lua51,
32 #[cfg(feature = "lua52")]
34 Lua52,
35 #[cfg(feature = "lua53")]
37 Lua53,
38 #[cfg(feature = "lua54")]
40 Lua54,
41 #[cfg(feature = "luau")]
43 Luau,
44 #[cfg(feature = "luajit")]
46 LuaJIT,
47 #[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#[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 #[default]
84 Tabs,
85 Spaces,
87}
88
89#[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 #[default]
98 Unix,
99 Windows,
101}
102
103#[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 #[default]
111 AutoPreferDouble,
112 AutoPreferSingle,
114 ForceDouble,
116 ForceSingle,
118}
119
120#[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 #[default]
128 Always,
129 NoSingleString,
131 NoSingleTable,
133 None,
135 Input,
137}
138
139#[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 #[default]
147 Never,
148 FunctionOnly,
150 ConditionalOnly,
152 Always,
154}
155
156#[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 pub fn from_values(start: Option<usize>, end: Option<usize>) -> Self {
171 Self { start, end }
172 }
173}
174
175#[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 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#[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 #[default]
209 Never,
210 Definitions,
212 Calls,
214 Always,
216}
217
218#[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 pub syntax: LuaVersion,
226 pub column_width: usize,
230 pub line_endings: LineEndings,
232 pub indent_type: IndentType,
234 pub indent_width: usize,
238 pub quote_style: QuoteStyle,
240 #[deprecated(note = "use `call_parentheses` instead")]
243 pub no_call_parentheses: bool,
244 pub call_parentheses: CallParenType,
253 pub collapse_simple_statement: CollapseSimpleStatement,
257 pub sort_requires: SortRequiresConfig,
259 pub space_after_function_names: SpaceAfterFunctionNames,
265}
266
267#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
268impl Config {
269 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#[derive(Debug, Copy, Clone, Deserialize)]
296#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
297pub enum OutputVerification {
298 Full,
300 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#[derive(Clone, Debug, Error)]
374pub enum Error {
375 #[error("error parsing: {}", print_full_moon_errors(.0))]
377 ParseError(Vec<full_moon::Error>),
378 #[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 #[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#[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 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 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 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#[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}