@@ -421,11 +421,18 @@ fn cli_key_matches(key: &str, meta: &meta::SettingMeta) -> bool {
421421}
422422
423423/// Lower-case kebab form of a setting / flag identifier. Splits on
424- /// `-`, `_`, camelCase boundaries, and dotted-path segments so callers
425- /// can compare `strict-dep-builds`, `strictDepBuilds`, `STRICT_DEP_BUILDS`,
426- /// and `strict_dep_builds` interchangeably. Dots are preserved so
427- /// nested settings like `peerDependencyRules.ignoreMissing` keep their
428- /// path structure.
424+ /// `-`, `_`, dotted-path segments, and lowercase→UPPER transitions so
425+ /// callers can compare `strict-dep-builds`, `strictDepBuilds`,
426+ /// `STRICT_DEP_BUILDS`, and `strict_dep_builds` interchangeably. Dots
427+ /// are preserved so nested settings like
428+ /// `peerDependencyRules.ignoreMissing` keep their path structure.
429+ ///
430+ /// Consecutive uppercase runs (e.g. `XMLConfig`) are collapsed to a
431+ /// single lowercase token (`xmlconfig`), matching the auto-alias
432+ /// generator in `aube-settings/build.rs`. No pnpm setting today contains
433+ /// an internal acronym, so the imperfection is invisible in practice; if
434+ /// one is ever added, the synthesized npmrc alias and this matcher have
435+ /// to evolve together.
429436fn to_kebab_case ( s : & str ) -> String {
430437 let mut out = String :: with_capacity ( s. len ( ) + 4 ) ;
431438 let mut prev_lower = false ;
@@ -452,20 +459,25 @@ fn to_kebab_case(s: &str) -> String {
452459 out
453460}
454461
455- /// Walk the per-callsite `cli` slice (newest entry first) and then the
456- /// process-global `--config.<key>` overrides. First match wins, mirroring
457- /// the existing reverse-scan semantics for `cli`. Returning the borrowed
458- /// raw value lets each caller pick its own parsing path; the global
459- /// overrides have `'static` storage so the merged lifetime is whichever
460- /// `cli` borrow the caller passed in.
461- fn cli_raw_for < ' a > ( meta : & meta:: SettingMeta , cli : & ' a [ ( String , String ) ] ) -> Option < & ' a str > {
462+ /// Walk the per-callsite `cli` slice (newest entry first), then the
463+ /// process-global `--config.<key>` overrides. The `accept` predicate
464+ /// lets typed callers (`bool`, `int`) keep scanning past an unparseable
465+ /// value so a later valid duplicate still wins — matching the original
466+ /// per-source loops. String / string-list callers pass `|_| true`; the
467+ /// global overrides have `'static` storage so the merged lifetime is
468+ /// whichever `cli` borrow the caller passed in.
469+ fn cli_raw_for < ' a > (
470+ meta : & meta:: SettingMeta ,
471+ cli : & ' a [ ( String , String ) ] ,
472+ accept : impl Fn ( & str ) -> bool ,
473+ ) -> Option < & ' a str > {
462474 for ( key, raw) in cli. iter ( ) . rev ( ) {
463- if cli_key_matches ( key, meta) {
475+ if cli_key_matches ( key, meta) && accept ( raw . as_str ( ) ) {
464476 return Some ( raw. as_str ( ) ) ;
465477 }
466478 }
467479 for ( key, raw) in global_cli_overrides ( ) . iter ( ) . rev ( ) {
468- if cli_key_matches ( key, meta) {
480+ if cli_key_matches ( key, meta) && accept ( raw . as_str ( ) ) {
469481 return Some ( raw. as_str ( ) ) ;
470482 }
471483 }
@@ -477,13 +489,16 @@ fn cli_raw_for<'a>(meta: &meta::SettingMeta, cli: &'a [(String, String)]) -> Opt
477489/// before building the `ResolveCtx`. Keys may be either an alias
478490/// declared in `sources.cli` or the canonical setting name (in any
479491/// reasonable case form), so generic `--config.<key>` overrides reach
480- /// every setting without per-flag wiring.
492+ /// every setting without per-flag wiring. An unparseable value (e.g.
493+ /// `--config.strictDepBuilds=notabool`) is skipped rather than masking
494+ /// an earlier valid entry — caller still gets `None` if every match is
495+ /// invalid, matching how `bool_from_npmrc` handles the same case.
481496pub ( crate ) fn bool_from_cli ( setting : & str , cli : & [ ( String , String ) ] ) -> Option < bool > {
482497 let meta = meta:: find ( setting) ?;
483498 if meta. type_ != "bool" {
484499 return None ;
485500 }
486- cli_raw_for ( meta, cli) . and_then ( parse_bool)
501+ cli_raw_for ( meta, cli, |raw| parse_bool ( raw ) . is_some ( ) ) . and_then ( parse_bool)
487502}
488503
489504/// Resolve a `string` setting from a parsed CLI flag bag.
@@ -492,7 +507,7 @@ pub fn string_from_cli(setting: &str, cli: &[(String, String)]) -> Option<String
492507 if !is_stringish ( meta. type_ ) {
493508 return None ;
494509 }
495- cli_raw_for ( meta, cli) . map ( ToOwned :: to_owned)
510+ cli_raw_for ( meta, cli, |_| true ) . map ( ToOwned :: to_owned)
496511}
497512
498513/// Resolve an `int` setting from a parsed CLI flag bag.
@@ -501,7 +516,8 @@ pub(crate) fn u64_from_cli(setting: &str, cli: &[(String, String)]) -> Option<u6
501516 if meta. type_ != "int" {
502517 return None ;
503518 }
504- cli_raw_for ( meta, cli) . and_then ( |raw| raw. trim ( ) . parse :: < u64 > ( ) . ok ( ) )
519+ cli_raw_for ( meta, cli, |raw| raw. trim ( ) . parse :: < u64 > ( ) . is_ok ( ) )
520+ . and_then ( |raw| raw. trim ( ) . parse :: < u64 > ( ) . ok ( ) )
505521}
506522
507523/// Resolve a `list<string>` setting from a parsed CLI flag bag.
@@ -510,7 +526,7 @@ pub(crate) fn string_list_from_cli(setting: &str, cli: &[(String, String)]) -> O
510526 if meta. type_ != "list<string>" {
511527 return None ;
512528 }
513- cli_raw_for ( meta, cli) . map ( parse_string_list)
529+ cli_raw_for ( meta, cli, |_| true ) . map ( parse_string_list)
514530}
515531
516532/// Parse a pnpm/npm-style stringified list. Accepts a JSON-ish array
@@ -833,6 +849,25 @@ mod tests {
833849 assert_eq ! ( bool_from_cli( "verifyStoreIntegrity" , & cli) , Some ( true ) ) ;
834850 }
835851
852+ #[ test]
853+ fn cli_bag_falls_through_unparseable_values_to_earlier_valid_entry ( ) {
854+ // Regression: an unparseable `--config.<key>=garbage` must not
855+ // mask an earlier valid entry for the same setting. Iteration
856+ // is reverse, so the later (garbage) entry is visited first;
857+ // the helper has to keep scanning rather than commit to it.
858+ let cli = vec ! [
859+ ( "strictDepBuilds" . to_string( ) , "true" . to_string( ) ) ,
860+ ( "strictDepBuilds" . to_string( ) , "notabool" . to_string( ) ) ,
861+ ] ;
862+ assert_eq ! ( bool_from_cli( "strictDepBuilds" , & cli) , Some ( true ) ) ;
863+
864+ let cli = vec ! [
865+ ( "network-concurrency" . to_string( ) , "8" . to_string( ) ) ,
866+ ( "network-concurrency" . to_string( ) , "garbage" . to_string( ) ) ,
867+ ] ;
868+ assert_eq ! ( u64_from_cli( "networkConcurrency" , & cli) , Some ( 8 ) ) ;
869+ }
870+
836871 #[ test]
837872 fn cli_beats_env_beats_npmrc_beats_workspace_yaml ( ) {
838873 // Precedence order is cli > env > npmrc > workspaceYaml. This
0 commit comments