new: let an explicit --lang win over build-system detection

The wizard overwrote cli.lang with the detected ecosystem even when the
user passed --lang, and re-asked the language question in the ambiguous
and skeleton cases despite the documented 'flag > detected > default'
merge order. The flag now short-circuits the language step entirely
(detection stays informational); behavior without the flag is
unchanged.
This commit is contained in:
2026-09-17 16:44:35 +02:00
parent 8ad50aaf83
commit 70e375a34d
+123 -20
View File
@@ -122,10 +122,30 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
cli.name = Some(answer); cli.name = Some(answer);
} }
// 2. Language / build system. // 2. Language / build system. An explicit `--lang` wins over any
let mut preselected: Option<TemplateId> = None; // detection (flag > detected/probe > default): it is never
match &detection { // overwritten and the question is never re-asked — the detection is
Detection::Single(id) if detection_decides => { // only logged for information.
match language_choice(cli.lang.as_deref(), &detection, detection_decides) {
LanguageChoice::Flag => match &detection {
Detection::Single(id) => log::info!(
"Detected: {} project in {}; --lang takes precedence",
id.display_name(),
detect_dir.display()
),
Detection::Ambiguous(candidates) => log::info!(
"Several build systems found in {} ({}); --lang takes \
precedence",
detect_dir.display(),
candidates
.iter()
.map(|id| id.as_str())
.collect::<Vec<_>>()
.join(", ")
),
Detection::Empty => {}
},
LanguageChoice::Detected(id) => {
log::info!( log::info!(
"Detected: {} project in {}", "Detected: {} project in {}",
id.display_name(), id.display_name(),
@@ -133,12 +153,16 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
); );
cli.lang = Some(id.as_str().to_string()); cli.lang = Some(id.as_str().to_string());
} }
Detection::Single(id) => { LanguageChoice::Ask(preselected) => {
// A skeleton was asked for: still ask, preselecting the let menu = language_menu(&[]);
// detected ecosystem. let default = preselected
preselected = Some(*id); .unwrap_or(TemplateId::Empty)
.display_name()
.to_string();
let id = select_template(&menu, &default)?;
cli.lang = Some(id.as_str().to_string());
} }
Detection::Ambiguous(candidates) => { LanguageChoice::Ambiguous(candidates) => {
log::info!( log::info!(
"Several build systems found in {} ({}): candidates listed \ "Several build systems found in {} ({}): candidates listed \
first, the highest-precedence one preselected", first, the highest-precedence one preselected",
@@ -149,20 +173,10 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", ") .join(", ")
); );
let menu = language_menu(candidates); let menu = language_menu(&candidates);
let id = select_template(&menu, &menu[0])?; let id = select_template(&menu, &menu[0])?;
cli.lang = Some(id.as_str().to_string()); cli.lang = Some(id.as_str().to_string());
} }
Detection::Empty => {}
}
if cli.lang.is_none() {
let menu = language_menu(&[]);
let default = preselected
.unwrap_or(TemplateId::Empty)
.display_name()
.to_string();
let id = select_template(&menu, &default)?;
cli.lang = Some(id.as_str().to_string());
} }
let template = TemplateId::parse(cli.lang.as_deref().unwrap_or_default())?; let template = TemplateId::parse(cli.lang.as_deref().unwrap_or_default())?;
@@ -690,6 +704,42 @@ fn language_menu(candidates: &[TemplateId]) -> Vec<String> {
.collect() .collect()
} }
/// What the language step does with the detection result: how an explicit
/// `--lang` flag combines with it (flag > detected/probe > default).
#[derive(Debug, PartialEq, Eq)]
enum LanguageChoice {
/// An explicit `--lang` wins: it is kept as-is, the question is never
/// re-asked and the detection stays informational.
Flag,
/// No flag, confident detection, packaging the detected directory: the
/// detection decides.
Detected(TemplateId),
/// Ask the question, preselecting the detected ecosystem (a skeleton
/// was asked for); `None` when nothing was detected.
Ask(Option<TemplateId>),
/// Ask the question with the ambiguous candidates listed first (the
/// highest-precedence one preselected).
Ambiguous(Vec<TemplateId>),
}
/// The language step's decision for one wizard run. With the flag absent
/// this mirrors the historical behavior exactly; the flag always wins.
fn language_choice(
flag: Option<&str>,
detection: &Detection,
detection_decides: bool,
) -> LanguageChoice {
if flag.is_some() {
return LanguageChoice::Flag;
}
match detection {
Detection::Single(id) if detection_decides => LanguageChoice::Detected(*id),
Detection::Single(id) => LanguageChoice::Ask(Some(*id)),
Detection::Ambiguous(candidates) => LanguageChoice::Ambiguous(candidates.clone()),
Detection::Empty => LanguageChoice::Ask(None),
}
}
/// The license menu: the curated SPDX list plus the free-text entry. /// The license menu: the curated SPDX list plus the free-text entry.
fn license_menu() -> Vec<String> { fn license_menu() -> Vec<String> {
KNOWN_LICENSES KNOWN_LICENSES
@@ -1043,6 +1093,59 @@ mod tests {
assert_eq!(unique.len(), menu.len()); assert_eq!(unique.len(), menu.len());
} }
/// An explicit `--lang` wins over any detection (the flag outranks the
/// detection and the defaults): the flag is kept and the question is
/// never re-asked, whatever the detection found (regression: a
/// confident detection used to overwrite the flag and an ambiguous one
/// re-asked the question).
#[test]
fn language_choice_flag_wins_over_detection() {
let detections = [
Detection::Single(Tid::Rust),
Detection::Ambiguous(vec![Tid::Rust, Tid::Python]),
Detection::Empty,
];
for detection in &detections {
for decides in [false, true] {
assert_eq!(
language_choice(Some("python"), detection, decides),
LanguageChoice::Flag,
"detection {detection:?}, decides {decides}"
);
}
}
}
/// Without the flag the detection behaves exactly as before: it decides
/// for a confident detection (packaging the detected directory), asks
/// with a preselection for a skeleton run, lists the candidates first
/// when ambiguous, and falls back to the plain menu otherwise.
#[test]
fn language_choice_without_flag_follows_detection() {
assert_eq!(
language_choice(None, &Detection::Single(Tid::Rust), true),
LanguageChoice::Detected(Tid::Rust)
);
// Skeleton run: ask, preselecting the detected ecosystem.
assert_eq!(
language_choice(None, &Detection::Single(Tid::Rust), false),
LanguageChoice::Ask(Some(Tid::Rust))
);
assert_eq!(
language_choice(
None,
&Detection::Ambiguous(vec![Tid::Go, Tid::Python]),
true
),
LanguageChoice::Ambiguous(vec![Tid::Go, Tid::Python])
);
// Nothing detected: plain menu, the empty template preselected.
assert_eq!(
language_choice(None, &Detection::Empty, false),
LanguageChoice::Ask(None)
);
}
#[test] #[test]
fn license_menu_and_defaults() { fn license_menu_and_defaults() {
let menu = license_menu(); let menu = license_menu();