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 workspace_or_file: Option<PathBuf>,
17
18 #[clap(default_value = "stylua")]
19 #[arg(long)]
20 backend: FmtBackend,
21
22 #[arg(short, long, visible_short_alias = 'p')]
24 package: Option<PackageName>,
25}
26
27#[derive(clap::ValueEnum, Clone, Debug)]
28enum FmtBackend {
29 Stylua,
31 Luafmt,
35 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 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}