Skip to main content

lux_cli/
format.rs

1use std::path::PathBuf;
2
3use clap::Args;
4use emmylua_formatter as luafmt;
5use eyre::{Context, Result};
6use lux_lib::{
7    config::Config, lua_version::LuaVersion, package::PackageName, project::Project,
8    workspace::Workspace,
9};
10use path_slash::PathExt;
11use walkdir::WalkDir;
12
13#[derive(Args)]
14pub struct Fmt {
15    /// Optional path to a workspace or Lua file to format.
16    workspace_or_file: Option<PathBuf>,
17
18    #[clap(default_value = "stylua")]
19    #[arg(long)]
20    backend: FmtBackend,
21
22    /// Package to format.
23    #[arg(short, long, visible_short_alias = 'p')]
24    package: Option<PackageName>,
25}
26
27#[derive(clap::ValueEnum, Clone, Debug)]
28enum FmtBackend {
29    /// Mainly follows the [Roblox Lua style guide](https://roblox.github.io/lua-style-guide/).
30    Stylua,
31    /// The default formatter used by [emmylua-analyzer-rust](https://github.com/EmmyLuaLs/emmylua-analyzer-rust).
32    /// If invoked with `lx --lua-version=<version> fmt`, Lux will configure the luafmt syntax level
33    /// to match the specified Lua version.
34    Luafmt,
35    /// The default formatter used by [lua-language-server](https://luals.github.io/).
36    EmmyluaCodestyle,
37}
38
39pub fn format(args: Fmt, config: Config) -> Result<()> {
40    let workspace = Workspace::current_or_err()?;
41    if let Some(package) = &args.package {
42        let project = workspace.select_member(package)?;
43        format_project(&args, &workspace, project, &config)?;
44    } else {
45        for project in workspace.members() {
46            format_project(&args, &workspace, project, &config)?;
47        }
48    }
49    Ok(())
50}
51
52fn format_project(
53    args: &Fmt,
54    workspace: &Workspace,
55    project: &Project,
56    config: &Config,
57) -> Result<()> {
58    let root = workspace.root();
59
60    let stylua_config: stylua_lib::Config = std::fs::read_to_string(root.join("stylua.toml"))
61        .or_else(|_| std::fs::read_to_string(root.join(".stylua.toml")))
62        .map(|config: String| toml::from_str(&config).unwrap_or_default())
63        .or_else(|_| {
64            stylua_lib::editorconfig::parse(stylua_lib::Config::new(), &root.join("*.lua"))
65        })
66        .unwrap_or_default();
67
68    let luafmt_config = luafmt::resolve_config_for_path(Some(root.as_ref()), None)
69        .map(|resolved| resolved.config)
70        .unwrap_or_default();
71    let luafmt_syntax_level = workspace
72        .lua_version(config)
73        .map(lua_version_to_luafmt_syntax_level)
74        .unwrap_or(luafmt_config.syntax.level);
75
76    let emmylua_config = root.join(".editorconfig");
77
78    let workspace_or_file = args
79        .workspace_or_file
80        .as_ref()
81        .map(std::path::absolute)
82        .transpose()?;
83
84    WalkDir::new(project.root().join("src"))
85        .into_iter()
86        .chain(WalkDir::new(project.root().join("lua")))
87        .chain(WalkDir::new(project.root().join("lib")))
88        .chain(WalkDir::new(project.root().join("spec")))
89        .chain(WalkDir::new(project.root().join("test")))
90        .chain(WalkDir::new(project.root().join("tests")))
91        .filter_map(Result::ok)
92        .filter(|file| {
93            workspace_or_file
94                .as_ref()
95                .is_none_or(|workspace_or_file| file.path().starts_with(workspace_or_file))
96        })
97        .try_for_each(|file| {
98            if PathBuf::from(file.file_name())
99                .extension()
100                .is_some_and(|ext| ext == "lua")
101            {
102                let file = file.path();
103                let unformatted_code = std::fs::read_to_string(file)?;
104                let formatted_code = match args.backend {
105                    FmtBackend::Stylua => stylua_lib::format_code(
106                        &unformatted_code,
107                        stylua_config,
108                        None,
109                        stylua_lib::OutputVerification::Full,
110                    )
111                    .context(format!("error formatting {} with stylua.", file.display()))?,
112                    FmtBackend::Luafmt => {
113                        luafmt::check_text(
114                            &unformatted_code,
115                            luafmt_syntax_level.into(),
116                            &luafmt_config,
117                        )
118                        .formatted
119                    }
120                    FmtBackend::EmmyluaCodestyle => {
121                        let uri = file.to_slash_lossy().to_string();
122                        if emmylua_config.is_file() {
123                            emmylua_codestyle::update_code_style(
124                                &uri,
125                                &emmylua_config.to_slash_lossy(),
126                            );
127                        }
128                        emmylua_codestyle::reformat_code(
129                            &unformatted_code,
130                            &uri,
131                            emmylua_codestyle::FormattingOptions::default(),
132                        )
133                    }
134                };
135
136                std::fs::write(file, formatted_code)
137                    .context(format!("error writing formatted file {}.", file.display()))?
138            };
139            Ok::<_, eyre::Report>(())
140        })?;
141
142    // Format the rockspec
143
144    let rockspec = project.root().join("extra.rockspec");
145
146    if rockspec.exists() {
147        let unformatted_code = std::fs::read_to_string(&rockspec)?;
148        let formatted_code = match args.backend {
149            FmtBackend::Stylua => stylua_lib::format_code(
150                &unformatted_code,
151                stylua_config,
152                None,
153                stylua_lib::OutputVerification::Full,
154            )?,
155            FmtBackend::Luafmt => {
156                luafmt::check_text(
157                    &unformatted_code,
158                    luafmt_syntax_level.into(),
159                    &luafmt_config,
160                )
161                .formatted
162            }
163            FmtBackend::EmmyluaCodestyle => {
164                let uri = rockspec.to_slash_lossy().to_string();
165                if emmylua_config.is_file() {
166                    emmylua_codestyle::update_code_style(&uri, &emmylua_config.to_slash_lossy());
167                }
168                emmylua_codestyle::reformat_code(
169                    &unformatted_code,
170                    &uri,
171                    emmylua_codestyle::FormattingOptions::default(),
172                )
173            }
174        };
175
176        std::fs::write(rockspec, formatted_code)?;
177    }
178    Ok(())
179}
180
181fn lua_version_to_luafmt_syntax_level(lua_version: LuaVersion) -> luafmt::LuaSyntaxLevel {
182    match lua_version {
183        LuaVersion::Lua51 => luafmt::LuaSyntaxLevel::Lua51,
184        LuaVersion::Lua52 => luafmt::LuaSyntaxLevel::Lua52,
185        LuaVersion::Lua53 => luafmt::LuaSyntaxLevel::Lua53,
186        LuaVersion::Lua54 => luafmt::LuaSyntaxLevel::Lua54,
187        LuaVersion::Lua55 => luafmt::LuaSyntaxLevel::Lua55,
188        LuaVersion::LuaJIT | LuaVersion::LuaJIT52 => luafmt::LuaSyntaxLevel::LuaJIT,
189    }
190}