1use std::collections::HashMap;
4
5use super::ResolvedCrateConfig;
6use crate::config::extras::Language;
7use crate::config::output::{BuildCommandConfig, CleanConfig, LintConfig, SetupConfig, TestConfig, UpdateConfig};
8use crate::config::tools::LangContext;
9use crate::config::{build_defaults, clean_defaults, lint_defaults, setup_defaults, test_defaults, update_defaults};
10
11impl ResolvedCrateConfig {
12 pub fn package_dir(&self, lang: Language) -> String {
15 let override_path = match lang {
16 Language::Python => self.python.as_ref().and_then(|c| c.scaffold_output.as_ref()),
17 Language::Node => self.node.as_ref().and_then(|c| c.scaffold_output.as_ref()),
18 Language::Ruby => self.ruby.as_ref().and_then(|c| c.scaffold_output.as_ref()),
19 Language::Php => self.php.as_ref().and_then(|c| c.scaffold_output.as_ref()),
20 Language::Elixir => self.elixir.as_ref().and_then(|c| c.scaffold_output.as_ref()),
21 _ => None,
22 };
23 if let Some(p) = override_path {
24 p.to_string_lossy().to_string()
25 } else {
26 match lang {
27 Language::Python => "packages/python".to_string(),
28 Language::Node => "packages/node".to_string(),
29 Language::Ruby => "packages/ruby".to_string(),
30 Language::Php => "packages/php".to_string(),
31 Language::Elixir => "packages/elixir".to_string(),
32 _ => format!("packages/{lang}"),
33 }
34 }
35 }
36
37 pub fn run_wrapper_for_language(&self, lang: Language) -> Option<&str> {
39 match lang {
40 Language::Python => self.python.as_ref().and_then(|c| c.run_wrapper.as_deref()),
41 Language::Node => self.node.as_ref().and_then(|c| c.run_wrapper.as_deref()),
42 Language::Ruby => self.ruby.as_ref().and_then(|c| c.run_wrapper.as_deref()),
43 Language::Php => self.php.as_ref().and_then(|c| c.run_wrapper.as_deref()),
44 Language::Elixir => self.elixir.as_ref().and_then(|c| c.run_wrapper.as_deref()),
45 Language::Wasm => self.wasm.as_ref().and_then(|c| c.run_wrapper.as_deref()),
46 Language::Go => self.go.as_ref().and_then(|c| c.run_wrapper.as_deref()),
47 Language::Java => self.java.as_ref().and_then(|c| c.run_wrapper.as_deref()),
48 Language::Csharp => self.csharp.as_ref().and_then(|c| c.run_wrapper.as_deref()),
49 Language::R => self.r.as_ref().and_then(|c| c.run_wrapper.as_deref()),
50 Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.run_wrapper.as_deref()),
51 Language::KotlinAndroid => self.kotlin_android.as_ref().and_then(|c| c.run_wrapper.as_deref()),
52 Language::Dart => self.dart.as_ref().and_then(|c| c.run_wrapper.as_deref()),
53 Language::Swift => self.swift.as_ref().and_then(|c| c.run_wrapper.as_deref()),
54 Language::Gleam => self.gleam.as_ref().and_then(|c| c.run_wrapper.as_deref()),
55 Language::Zig => self.zig.as_ref().and_then(|c| c.run_wrapper.as_deref()),
56 Language::Ffi | Language::Rust | Language::C => None,
57 }
58 }
59
60 pub fn extra_lint_paths_for_language(&self, lang: Language) -> &[String] {
62 match lang {
63 Language::Python => self
64 .python
65 .as_ref()
66 .map(|c| c.extra_lint_paths.as_slice())
67 .unwrap_or(&[]),
68 Language::Node => self.node.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
69 Language::Ruby => self.ruby.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
70 Language::Php => self.php.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
71 Language::Elixir => self
72 .elixir
73 .as_ref()
74 .map(|c| c.extra_lint_paths.as_slice())
75 .unwrap_or(&[]),
76 Language::Wasm => self.wasm.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
77 Language::Go => self.go.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
78 Language::Java => self.java.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
79 Language::Csharp => self
80 .csharp
81 .as_ref()
82 .map(|c| c.extra_lint_paths.as_slice())
83 .unwrap_or(&[]),
84 Language::R => self.r.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
85 Language::Kotlin => self
86 .kotlin
87 .as_ref()
88 .map(|c| c.extra_lint_paths.as_slice())
89 .unwrap_or(&[]),
90 Language::KotlinAndroid => self
91 .kotlin_android
92 .as_ref()
93 .map(|c| c.extra_lint_paths.as_slice())
94 .unwrap_or(&[]),
95 Language::Dart => self.dart.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
96 Language::Swift => self
97 .swift
98 .as_ref()
99 .map(|c| c.extra_lint_paths.as_slice())
100 .unwrap_or(&[]),
101 Language::Gleam => self
102 .gleam
103 .as_ref()
104 .map(|c| c.extra_lint_paths.as_slice())
105 .unwrap_or(&[]),
106 Language::Zig => self.zig.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
107 Language::Ffi | Language::Rust | Language::C => &[],
108 }
109 }
110
111 pub fn project_file_for_language(&self, lang: Language) -> Option<&str> {
113 match lang {
114 Language::Java => self.java.as_ref().and_then(|c| c.project_file.as_deref()),
115 Language::Csharp => self.csharp.as_ref().and_then(|c| c.project_file.as_deref()),
116 _ => None,
117 }
118 }
119
120 pub fn lint_config_for_language(&self, lang: Language) -> LintConfig {
122 let lang_str = lang.to_string();
123 if let Some(explicit) = self.lint.get(&lang_str) {
124 return explicit.clone();
125 }
126 let output_dir = self.package_dir(lang);
127 let run_wrapper = self.run_wrapper_for_language(lang);
128 let extra_lint_paths = self.extra_lint_paths_for_language(lang);
129 let project_file = self.project_file_for_language(lang);
130 let ctx = LangContext {
131 tools: &self.tools,
132 run_wrapper,
133 extra_lint_paths,
134 project_file,
135 };
136 lint_defaults::default_lint_config(lang, &output_dir, &ctx)
137 }
138
139 pub fn update_config_for_language(&self, lang: Language) -> UpdateConfig {
141 let lang_str = lang.to_string();
142 if let Some(explicit) = self.update.get(&lang_str) {
143 return explicit.clone();
144 }
145 let output_dir = self.package_dir(lang);
146 let ctx = LangContext {
147 tools: &self.tools,
148 run_wrapper: None,
149 extra_lint_paths: &[],
150 project_file: None,
151 };
152 update_defaults::default_update_config(lang, &output_dir, &ctx)
153 }
154
155 pub fn test_config_for_language(&self, lang: Language) -> TestConfig {
157 let lang_str = lang.to_string();
158 if let Some(explicit) = self.test.get(&lang_str) {
159 return explicit.clone();
160 }
161 let output_dir = self.package_dir(lang);
162 let run_wrapper = self.run_wrapper_for_language(lang);
163 let project_file = self.project_file_for_language(lang);
164 let ctx = LangContext {
165 tools: &self.tools,
166 run_wrapper,
167 extra_lint_paths: &[],
168 project_file,
169 };
170 test_defaults::default_test_config(lang, &output_dir, &ctx)
171 }
172
173 pub fn setup_config_for_language(&self, lang: Language) -> SetupConfig {
175 let lang_str = lang.to_string();
176 if let Some(explicit) = self.setup.get(&lang_str) {
177 return explicit.clone();
178 }
179 let output_dir = self.package_dir(lang);
180 let ctx = LangContext {
181 tools: &self.tools,
182 run_wrapper: None,
183 extra_lint_paths: &[],
184 project_file: None,
185 };
186 setup_defaults::default_setup_config(lang, &output_dir, &ctx)
187 }
188
189 pub fn clean_config_for_language(&self, lang: Language) -> CleanConfig {
191 let lang_str = lang.to_string();
192 if let Some(explicit) = self.clean.get(&lang_str) {
193 return explicit.clone();
194 }
195 let output_dir = self.package_dir(lang);
196 let ctx = LangContext {
197 tools: &self.tools,
198 run_wrapper: None,
199 extra_lint_paths: &[],
200 project_file: None,
201 };
202 clean_defaults::default_clean_config(lang, &output_dir, &ctx)
203 }
204
205 pub fn build_command_config_for_language(&self, lang: Language) -> BuildCommandConfig {
207 let lang_str = lang.to_string();
208 let output_dir = self.package_dir(lang);
209 let run_wrapper = self.run_wrapper_for_language(lang);
210 let project_file = self.project_file_for_language(lang);
211 let ctx = LangContext {
212 tools: &self.tools,
213 run_wrapper,
214 extra_lint_paths: &[],
215 project_file,
216 };
217 let default = build_defaults::default_build_config(lang, &output_dir, &self.name, &ctx);
218 if let Some(explicit) = self.build_commands.get(&lang_str) {
219 default.merge_overlay(explicit)
220 } else {
221 default
222 }
223 }
224
225 pub fn features_for_language(&self, lang: Language) -> &[String] {
227 let override_features = match lang {
228 Language::Python => self.python.as_ref().and_then(|c| c.features.as_deref()),
229 Language::Node => self.node.as_ref().and_then(|c| c.features.as_deref()),
230 Language::Ruby => self.ruby.as_ref().and_then(|c| c.features.as_deref()),
231 Language::Php => self.php.as_ref().and_then(|c| c.features.as_deref()),
232 Language::Elixir => self.elixir.as_ref().and_then(|c| c.features.as_deref()),
233 Language::Wasm => self.wasm.as_ref().and_then(|c| c.features.as_deref()),
234 Language::Ffi => self.ffi.as_ref().and_then(|c| c.features.as_deref()),
235 Language::Go => self.go.as_ref().and_then(|c| c.features.as_deref()),
236 Language::Java => self.java.as_ref().and_then(|c| c.features.as_deref()),
237 Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.features.as_deref()),
238 Language::KotlinAndroid => self.kotlin_android.as_ref().and_then(|c| c.features.as_deref()),
239 Language::Csharp => self.csharp.as_ref().and_then(|c| c.features.as_deref()),
240 Language::R => self.r.as_ref().and_then(|c| c.features.as_deref()),
241 Language::Zig => self.zig.as_ref().and_then(|c| c.features.as_deref()),
242 Language::Dart => self.dart.as_ref().and_then(|c| c.features.as_deref()),
243 Language::Swift => self.swift.as_ref().and_then(|c| c.features.as_deref()),
244 Language::Gleam => self.gleam.as_ref().and_then(|c| c.features.as_deref()),
245 Language::Rust | Language::C => None,
246 };
247 override_features.unwrap_or(&self.features)
248 }
249
250 pub fn extra_deps_for_language(&self, lang: Language) -> HashMap<String, toml::Value> {
252 let mut deps = self.extra_dependencies.clone();
253 let lang_deps = match lang {
254 Language::Python => self.python.as_ref().map(|c| &c.extra_dependencies),
255 Language::Node => self.node.as_ref().map(|c| &c.extra_dependencies),
256 Language::Ruby => self.ruby.as_ref().map(|c| &c.extra_dependencies),
257 Language::Php => self.php.as_ref().map(|c| &c.extra_dependencies),
258 Language::Elixir => self.elixir.as_ref().map(|c| &c.extra_dependencies),
259 Language::Wasm => self.wasm.as_ref().map(|c| &c.extra_dependencies),
260 Language::Dart => self.dart.as_ref().map(|c| &c.extra_dependencies),
261 _ => None,
262 };
263 if let Some(lang_deps) = lang_deps {
264 deps.extend(lang_deps.iter().map(|(k, v)| (k.clone(), v.clone())));
265 }
266 let exclude: &[String] = match lang {
267 Language::Wasm => self
268 .wasm
269 .as_ref()
270 .map(|c| c.exclude_extra_dependencies.as_slice())
271 .unwrap_or(&[]),
272 Language::Dart => self
273 .dart
274 .as_ref()
275 .map(|c| c.exclude_extra_dependencies.as_slice())
276 .unwrap_or(&[]),
277 Language::Swift => self
278 .swift
279 .as_ref()
280 .map(|c| c.exclude_extra_dependencies.as_slice())
281 .unwrap_or(&[]),
282 _ => &[],
283 };
284 for key in exclude {
285 deps.remove(key);
286 }
287 deps
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use crate::config::extras::Language;
294 use crate::config::new_config::NewAlefConfig;
295
296 fn resolved_one(toml: &str) -> super::super::ResolvedCrateConfig {
297 let cfg: NewAlefConfig = toml::from_str(toml).unwrap();
298 cfg.resolve().unwrap().remove(0)
299 }
300
301 fn minimal() -> super::super::ResolvedCrateConfig {
302 resolved_one(
303 r#"
304[workspace]
305languages = ["python", "node"]
306
307[[crates]]
308name = "test-lib"
309sources = ["src/lib.rs"]
310"#,
311 )
312 }
313
314 #[test]
315 fn resolved_lint_config_inherits_workspace_when_crate_unset() {
316 let r = resolved_one(
317 r#"
318[workspace]
319languages = ["python"]
320
321[workspace.lint.python]
322check = "ruff check ."
323
324[[crates]]
325name = "test-lib"
326sources = ["src/lib.rs"]
327"#,
328 );
329 let lint = r.lint_config_for_language(Language::Python);
330 assert_eq!(lint.check.unwrap().commands(), vec!["ruff check ."]);
331 }
332
333 #[test]
334 fn resolved_lint_config_crate_overrides_workspace_field_wholesale() {
335 let r = resolved_one(
336 r#"
337[workspace]
338languages = ["python"]
339
340[workspace.lint.python]
341check = "ruff check ."
342
343[[crates]]
344name = "test-lib"
345sources = ["src/lib.rs"]
346
347[crates.lint.python]
348check = "ruff check crates/test-lib-py/"
349"#,
350 );
351 let lint = r.lint_config_for_language(Language::Python);
352 assert_eq!(lint.check.unwrap().commands(), vec!["ruff check crates/test-lib-py/"]);
353 }
354
355 #[test]
356 fn resolved_features_per_language_overrides_crate_default() {
357 let r = resolved_one(
358 r#"
359[workspace]
360languages = ["python"]
361
362[[crates]]
363name = "test-lib"
364sources = ["src/lib.rs"]
365features = ["base"]
366
367[crates.python]
368features = ["python-extra"]
369"#,
370 );
371 assert_eq!(r.features_for_language(Language::Python), &["python-extra"]);
372 assert_eq!(r.features_for_language(Language::Node), &["base"]);
373 }
374
375 #[test]
376 fn resolved_extra_deps_crate_value_wins_on_key_collision() {
377 let r = resolved_one(
378 r#"
379[workspace]
380languages = ["python"]
381
382[[crates]]
383name = "test-lib"
384sources = ["src/lib.rs"]
385
386[crates.extra_dependencies]
387tokio = "1"
388
389[crates.python]
390extra_dependencies = { tokio = "2" }
391"#,
392 );
393 let deps = r.extra_deps_for_language(Language::Python);
394 let tokio = deps.get("tokio").unwrap().as_str().unwrap();
395 assert_eq!(tokio, "2", "per-language extra_dep should win on collision");
396 }
397
398 #[test]
399 fn resolved_extra_deps_excludes_apply_after_merge() {
400 let r = resolved_one(
401 r#"
402[workspace]
403languages = ["wasm"]
404
405[[crates]]
406name = "test-lib"
407sources = ["src/lib.rs"]
408
409[crates.extra_dependencies]
410tokio = "1"
411serde = "1"
412
413[crates.wasm]
414exclude_extra_dependencies = ["tokio"]
415"#,
416 );
417 let deps = r.extra_deps_for_language(Language::Wasm);
418 assert!(!deps.contains_key("tokio"), "excluded dep should be absent");
419 assert!(deps.contains_key("serde"), "non-excluded dep should be present");
420 }
421
422 #[test]
423 fn package_dir_defaults_are_correct() {
424 let r = minimal();
425 assert_eq!(r.package_dir(Language::Python), "packages/python");
426 assert_eq!(r.package_dir(Language::Node), "packages/node");
427 assert_eq!(r.package_dir(Language::Ruby), "packages/ruby");
428 assert_eq!(r.package_dir(Language::Go), "packages/go");
429 assert_eq!(r.package_dir(Language::Java), "packages/java");
430 }
431}