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, 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 #[default]
164 Never,
165 Preserve,
167}
168
169#[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 pub fn from_values(start: Option<usize>, end: Option<usize>) -> Self {
184 Self { start, end }
185 }
186}
187
188#[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 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#[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 #[default]
222 Never,
223 Definitions,
225 Calls,
227 Always,
229}
230
231#[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 pub syntax: LuaVersion,
239 pub column_width: usize,
243 pub line_endings: LineEndings,
245 pub indent_type: IndentType,
247 pub indent_width: usize,
251 pub quote_style: QuoteStyle,
253 #[deprecated(note = "use `call_parentheses` instead")]
256 pub no_call_parentheses: bool,
257 pub call_parentheses: CallParenType,
266 pub collapse_simple_statement: CollapseSimpleStatement,
270 pub block_newline_gaps: BlockNewlineGaps,
274 pub sort_requires: SortRequiresConfig,
276 pub space_after_function_names: SpaceAfterFunctionNames,
282}
283
284#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
285impl Config {
286 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#[derive(Debug, Copy, Clone, Deserialize)]
314#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
315pub enum OutputVerification {
316 Full,
318 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#[derive(Clone, Debug, Error)]
392pub enum Error {
393 #[error("error parsing: {}", print_full_moon_errors(.0))]
395 ParseError(Vec<full_moon::Error>),
396 #[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 #[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#[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 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 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 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#[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}