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