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 | Language::Jni => 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 | Language::Jni => &[],
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 | Language::Jni => 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 Language::Swift => self.swift.as_ref().map(|c| &c.extra_dependencies),
262 _ => None,
263 };
264 if let Some(lang_deps) = lang_deps {
265 deps.extend(lang_deps.iter().map(|(k, v)| (k.clone(), v.clone())));
266 }
267 let exclude: &[String] = match lang {
268 Language::Wasm => self
269 .wasm
270 .as_ref()
271 .map(|c| c.exclude_extra_dependencies.as_slice())
272 .unwrap_or(&[]),
273 Language::Dart => self
274 .dart
275 .as_ref()
276 .map(|c| c.exclude_extra_dependencies.as_slice())
277 .unwrap_or(&[]),
278 Language::Swift => self
279 .swift
280 .as_ref()
281 .map(|c| c.exclude_extra_dependencies.as_slice())
282 .unwrap_or(&[]),
283 _ => &[],
284 };
285 for key in exclude {
286 deps.remove(key);
287 }
288 deps
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use crate::config::extras::Language;
295 use crate::config::new_config::NewAlefConfig;
296
297 fn resolved_one(toml: &str) -> super::super::ResolvedCrateConfig {
298 let cfg: NewAlefConfig = toml::from_str(toml).unwrap();
299 cfg.resolve().unwrap().remove(0)
300 }
301
302 fn minimal() -> super::super::ResolvedCrateConfig {
303 resolved_one(
304 r#"
305[workspace]
306languages = ["python", "node"]
307
308[[crates]]
309name = "test-lib"
310sources = ["src/lib.rs"]
311"#,
312 )
313 }
314
315 #[test]
316 fn resolved_lint_config_inherits_workspace_when_crate_unset() {
317 let r = resolved_one(
318 r#"
319[workspace]
320languages = ["python"]
321
322[workspace.lint.python]
323check = "ruff check ."
324
325[[crates]]
326name = "test-lib"
327sources = ["src/lib.rs"]
328"#,
329 );
330 let lint = r.lint_config_for_language(Language::Python);
331 assert_eq!(lint.check.unwrap().commands(), vec!["ruff check ."]);
332 }
333
334 #[test]
335 fn resolved_lint_config_crate_overrides_workspace_field_wholesale() {
336 let r = resolved_one(
337 r#"
338[workspace]
339languages = ["python"]
340
341[workspace.lint.python]
342check = "ruff check ."
343
344[[crates]]
345name = "test-lib"
346sources = ["src/lib.rs"]
347
348[crates.lint.python]
349check = "ruff check crates/test-lib-py/"
350"#,
351 );
352 let lint = r.lint_config_for_language(Language::Python);
353 assert_eq!(lint.check.unwrap().commands(), vec!["ruff check crates/test-lib-py/"]);
354 }
355
356 #[test]
357 fn resolved_features_per_language_overrides_crate_default() {
358 let r = resolved_one(
359 r#"
360[workspace]
361languages = ["python"]
362
363[[crates]]
364name = "test-lib"
365sources = ["src/lib.rs"]
366features = ["base"]
367
368[crates.python]
369features = ["python-extra"]
370"#,
371 );
372 assert_eq!(r.features_for_language(Language::Python), &["python-extra"]);
373 assert_eq!(r.features_for_language(Language::Node), &["base"]);
374 }
375
376 #[test]
377 fn resolved_extra_deps_crate_value_wins_on_key_collision() {
378 let r = resolved_one(
379 r#"
380[workspace]
381languages = ["python"]
382
383[[crates]]
384name = "test-lib"
385sources = ["src/lib.rs"]
386
387[crates.extra_dependencies]
388tokio = "1"
389
390[crates.python]
391extra_dependencies = { tokio = "2" }
392"#,
393 );
394 let deps = r.extra_deps_for_language(Language::Python);
395 let tokio = deps.get("tokio").unwrap().as_str().unwrap();
396 assert_eq!(tokio, "2", "per-language extra_dep should win on collision");
397 }
398
399 #[test]
400 fn resolved_extra_deps_excludes_apply_after_merge() {
401 let r = resolved_one(
402 r#"
403[workspace]
404languages = ["wasm"]
405
406[[crates]]
407name = "test-lib"
408sources = ["src/lib.rs"]
409
410[crates.extra_dependencies]
411tokio = "1"
412serde = "1"
413
414[crates.wasm]
415exclude_extra_dependencies = ["tokio"]
416"#,
417 );
418 let deps = r.extra_deps_for_language(Language::Wasm);
419 assert!(!deps.contains_key("tokio"), "excluded dep should be absent");
420 assert!(deps.contains_key("serde"), "non-excluded dep should be present");
421 }
422
423 #[test]
424 fn resolved_extra_deps_includes_swift_overrides() {
425 let r = resolved_one(
426 r#"
427[workspace]
428languages = ["swift"]
429
430[[crates]]
431name = "test-lib"
432sources = ["src/lib.rs"]
433
434[crates.extra_dependencies]
435serde = "1"
436
437[crates.swift.extra_dependencies]
438tokio = "1"
439"#,
440 );
441 let deps = r.extra_deps_for_language(Language::Swift);
442 assert!(deps.contains_key("serde"), "crate-level dep should be present");
443 assert!(deps.contains_key("tokio"), "Swift dep should be present");
444 }
445
446 #[test]
447 fn package_dir_defaults_are_correct() {
448 let r = minimal();
449 assert_eq!(r.package_dir(Language::Python), "packages/python");
450 assert_eq!(r.package_dir(Language::Node), "packages/node");
451 assert_eq!(r.package_dir(Language::Ruby), "packages/ruby");
452 assert_eq!(r.package_dir(Language::Go), "packages/go");
453 assert_eq!(r.package_dir(Language::Java), "packages/java");
454 }
455}