use context::Context;
use full_moon::ast::Ast;
use serde::Deserialize;
use thiserror::Error;
#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
use wasm_bindgen::prelude::*;
#[macro_use]
mod context;
#[cfg(feature = "editorconfig")]
pub mod editorconfig;
mod formatters;
mod shape;
mod sort_requires;
mod verify_ast;
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize)]
#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "fromstr", derive(strum::EnumString))]
pub enum LuaVersion {
#[default]
All,
Lua51,
#[cfg(feature = "lua52")]
Lua52,
#[cfg(feature = "lua53")]
Lua53,
#[cfg(feature = "lua54")]
Lua54,
#[cfg(feature = "luau")]
Luau,
#[cfg(feature = "luajit")]
LuaJIT,
}
impl From<LuaVersion> for full_moon::LuaVersion {
fn from(val: LuaVersion) -> Self {
match val {
LuaVersion::All => full_moon::LuaVersion::new(),
LuaVersion::Lua51 => full_moon::LuaVersion::lua51(),
#[cfg(feature = "lua52")]
LuaVersion::Lua52 => full_moon::LuaVersion::lua52(),
#[cfg(feature = "lua53")]
LuaVersion::Lua53 => full_moon::LuaVersion::lua53(),
#[cfg(feature = "lua54")]
LuaVersion::Lua54 => full_moon::LuaVersion::lua54(),
#[cfg(feature = "luau")]
LuaVersion::Luau => full_moon::LuaVersion::luau(),
#[cfg(feature = "luajit")]
LuaVersion::LuaJIT => full_moon::LuaVersion::luajit(),
}
}
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize)]
#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "fromstr", derive(strum::EnumString))]
pub enum IndentType {
#[default]
Tabs,
Spaces,
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize)]
#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "fromstr", derive(strum::EnumString))]
pub enum LineEndings {
#[default]
Unix,
Windows,
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize)]
#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "fromstr", derive(strum::EnumString))]
pub enum QuoteStyle {
#[default]
AutoPreferDouble,
AutoPreferSingle,
ForceDouble,
ForceSingle,
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize)]
#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "fromstr", derive(strum::EnumString))]
pub enum CallParenType {
#[default]
Always,
NoSingleString,
NoSingleTable,
None,
Input,
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize)]
#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "fromstr", derive(strum::EnumString))]
pub enum CollapseSimpleStatement {
#[default]
Never,
FunctionOnly,
ConditionalOnly,
Always,
}
#[derive(Debug, Copy, Clone, Deserialize)]
#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
pub struct Range {
pub start: Option<usize>,
pub end: Option<usize>,
}
#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
impl Range {
pub fn from_values(start: Option<usize>, end: Option<usize>) -> Self {
Self { start, end }
}
}
#[derive(Copy, Clone, Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct SortRequiresConfig {
pub enabled: bool,
}
#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
impl SortRequiresConfig {
pub fn new() -> Self {
SortRequiresConfig::default()
}
#[deprecated(since = "0.19.0", note = "access `.enabled` directly instead")]
#[cfg(not(all(target_arch = "wasm32", feature = "wasm-bindgen")))]
pub fn enabled(&self) -> bool {
self.enabled
}
#[deprecated(since = "0.19.0", note = "modify `.enabled` directly instead")]
pub fn set_enabled(&self, enabled: bool) -> Self {
Self { enabled }
}
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize)]
#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "fromstr", derive(strum::EnumString))]
pub enum SpaceAfterFunctionNames {
#[default]
Never,
Definitions,
Calls,
Always,
}
#[derive(Copy, Clone, Debug, Deserialize)]
#[serde(default, deny_unknown_fields)]
#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
pub struct Config {
pub syntax: LuaVersion,
pub column_width: usize,
pub line_endings: LineEndings,
pub indent_type: IndentType,
pub indent_width: usize,
pub quote_style: QuoteStyle,
#[deprecated(note = "use `call_parentheses` instead")]
pub no_call_parentheses: bool,
pub call_parentheses: CallParenType,
pub collapse_simple_statement: CollapseSimpleStatement,
pub sort_requires: SortRequiresConfig,
pub space_after_function_names: SpaceAfterFunctionNames,
}
#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
impl Config {
pub fn new() -> Self {
Config::default()
}
}
impl Default for Config {
fn default() -> Self {
#[allow(deprecated)]
Self {
syntax: LuaVersion::default(),
column_width: 120,
line_endings: LineEndings::default(),
indent_type: IndentType::default(),
indent_width: 4,
quote_style: QuoteStyle::default(),
no_call_parentheses: false,
call_parentheses: CallParenType::default(),
collapse_simple_statement: CollapseSimpleStatement::default(),
sort_requires: SortRequiresConfig::default(),
space_after_function_names: SpaceAfterFunctionNames::default(),
}
}
}
#[derive(Debug, Copy, Clone, Deserialize)]
#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
pub enum OutputVerification {
Full,
None,
}
fn print_full_moon_error(error: &full_moon::Error) -> String {
match error {
full_moon::Error::AstError(ast_error) => format!(
"unexpected token `{}` ({}:{} to {}:{}), {}",
ast_error.token(),
ast_error.range().0.line(),
ast_error.range().0.character(),
ast_error.range().1.line(),
ast_error.range().1.character(),
ast_error.error_message()
),
full_moon::Error::TokenizerError(tokenizer_error) => tokenizer_error.to_string(),
}
}
fn print_full_moon_errors(errors: &[full_moon::Error]) -> String {
if errors.len() == 1 {
print_full_moon_error(errors.first().unwrap())
} else {
errors
.iter()
.map(|err| "\n - ".to_string() + &print_full_moon_error(err))
.collect::<String>()
}
}
#[derive(Clone, Debug, Error)]
pub enum Error {
#[error("error parsing: {}", print_full_moon_errors(.0))]
ParseError(Vec<full_moon::Error>),
#[error("INTERNAL ERROR: Output AST generated a syntax error. Please report this at https://github.com/johnnymorganz/stylua/issues: {}", print_full_moon_errors(.0))]
VerificationAstError(Vec<full_moon::Error>),
#[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")]
VerificationAstDifference,
}
#[allow(clippy::result_large_err)]
pub fn format_ast(
input_ast: Ast,
config: Config,
range: Option<Range>,
verify_output: OutputVerification,
) -> Result<Ast, Error> {
let input_ast_for_verification = if let OutputVerification::Full = verify_output {
Some(input_ast.to_owned())
} else {
None
};
let ctx = Context::new(config, range);
let input_ast = match config.sort_requires.enabled {
true => sort_requires::sort_requires(&ctx, input_ast),
false => input_ast,
};
let code_formatter = formatters::CodeFormatter::new(ctx);
let ast = code_formatter.format(input_ast);
if let Some(input_ast) = input_ast_for_verification {
let output = ast.to_string();
let reparsed_output =
match full_moon::parse_fallible(&output, config.syntax.into()).into_result() {
Ok(ast) => ast,
Err(error) => {
return Err(Error::VerificationAstError(error));
}
};
let mut ast_verifier = verify_ast::AstVerifier::new();
if !ast_verifier.compare(input_ast, reparsed_output) {
return Err(Error::VerificationAstDifference);
}
}
Ok(ast)
}
#[allow(clippy::result_large_err)]
pub fn format_code(
code: &str,
config: Config,
range: Option<Range>,
verify_output: OutputVerification,
) -> Result<String, Error> {
let input_ast = match full_moon::parse_fallible(code, config.syntax.into()).into_result() {
Ok(ast) => ast,
Err(error) => {
return Err(Error::ParseError(error));
}
};
let ast = format_ast(input_ast, config, range, verify_output)?;
let output = ast.to_string();
Ok(output)
}
#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
#[wasm_bindgen(js_name = formatCode)]
pub fn format_code_wasm(
code: &str,
config: Config,
range: Option<Range>,
verify_output: OutputVerification,
) -> Result<String, String> {
format_code(code, config, range, verify_output).map_err(|err| err.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_entry_point() {
let output = format_code(
"local x = 1",
Config::default(),
None,
OutputVerification::None,
)
.unwrap();
assert_eq!(output, "local x = 1\n");
}
#[test]
fn test_invalid_input() {
let output = format_code(
"local x = ",
Config::default(),
None,
OutputVerification::None,
);
assert!(matches!(output, Err(Error::ParseError(_))))
}
#[test]
fn test_with_ast_verification() {
let output = format_code(
"local x = 1",
Config::default(),
None,
OutputVerification::Full,
)
.unwrap();
assert_eq!(output, "local x = 1\n");
}
}