diff --git a/doc/release-notes/rl-2511.section.md b/doc/release-notes/rl-2511.section.md index a62e6bc0c001d..6267735ddb2a7 100644 --- a/doc/release-notes/rl-2511.section.md +++ b/doc/release-notes/rl-2511.section.md @@ -177,7 +177,13 @@ ### Deprecations {#sec-nixpkgs-release-25.11-lib-deprecations} - Create the first release note entry in this section! +- `types.either` silently accepted mismatching types when used in `freeformType`. Module maintainers should fix the used type + In most cases wrapping `either` with `attrsOf` should be sufficient. + Since types.either was used to bootstrap other types. This also affects the following types: + - `oneOf` + - `number` + - `numbers.*` ### Additions and Improvements {#sec-nixpkgs-release-25.11-lib-additions-improvements} diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index fbbccd8172f67..5c715bc7f8c90 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -257,6 +257,10 @@ checkConfigError 'A definition for option .* is not of type.*between.*-21 and 43 # types.either checkConfigOutput '^42$' config.value ./declare-either.nix ./define-value-int-positive.nix checkConfigOutput '^"24"$' config.value ./declare-either.nix ./define-value-string.nix + +# Check regression detected in https://github.com/NixOS/nixpkgs/issues/438459 +# Can be removed after 26.05 release. +checkConfigOutput '^"bar"$' config.foo ./freeform-either.nix # types.oneOf checkConfigOutput '^42$' config.value ./declare-oneOf.nix ./define-value-int-positive.nix checkConfigOutput '^\[\]$' config.value ./declare-oneOf.nix ./define-value-list.nix diff --git a/lib/tests/modules/freeform-either.nix b/lib/tests/modules/freeform-either.nix new file mode 100644 index 0000000000000..edbd359315434 --- /dev/null +++ b/lib/tests/modules/freeform-either.nix @@ -0,0 +1,7 @@ +{ lib, ... }: +{ + # Proper type would be attrsOf (either int str) + # This checks backwards compatibility for the incorrect usage of either + freeformType = lib.types.either lib.types.int lib.types.str; + foo = "bar"; +} diff --git a/lib/types.nix b/lib/types.nix index 6b51f9254a00c..33acd09f1304f 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -106,6 +106,14 @@ let in if invalidDefs != [ ] then { message = "Definition values: ${showDefs invalidDefs}"; } else null; + # Compat helper for merge to safely access the .value of merge.v2 + checkHeadError = + loc: descr: checkedAndMerged: + if checkedAndMerged.headError or null != null then + throw "A definition for option `${showOption loc}' is not of type `${descr}'. TypeError: ${checkedAndMerged.headError.message}" + else + checkedAndMerged.value; + outer_types = rec { isType = type: x: (x._type or "") == type; @@ -715,7 +723,7 @@ let merge = { __functor = self: loc: defs: - (self.v2 { inherit loc defs; }).value; + checkHeadError loc description (self.v2 { inherit loc defs; }); v2 = { loc, defs }: let @@ -828,7 +836,7 @@ let merge = { __functor = self: loc: defs: - (self.v2 { inherit loc defs; }).value; + checkHeadError loc description (self.v2 { inherit loc defs; }); v2 = { loc, defs }: let @@ -1252,7 +1260,7 @@ let merge = { __functor = self: loc: defs: - (self.v2 { inherit loc defs; }).value; + checkHeadError loc description (self.v2 { inherit loc defs; }); v2 = { loc, defs }: let @@ -1417,7 +1425,16 @@ let merge = { __functor = self: loc: defs: - (self.v2 { inherit loc defs; }).value; + let + cm = (self.v2 { inherit loc defs; }); + in + if cm.headError ? fallback then + lib.warn ( + cm.headError.message + + "\nIf you are the module maintainer, please fix the type. This will be turned into an error after 26.05" + ) cm.headError.fallback + else + checkHeadError loc description cm; v2 = { loc, defs }: let @@ -1452,6 +1469,9 @@ let }; headError = { message = "The option `${showOption loc}` is neither a value of type `${t1.description}` nor `${t2.description}`, Definition values: ${showDefs defs}"; + # Specific for either, to ease migrations + # until 26.05 this is a warning, afterwards it will be removed + fallback = mergeOneOption loc defs; }; value = abort "(t.merge.v2 defs).value must only be accessed when `.headError == null`. This is a bug in code that consumes a module system type."; }; @@ -1501,7 +1521,7 @@ let merge = { __functor = self: loc: defs: - (self.v2 { inherit loc defs; }).value; + checkHeadError loc description (self.v2 { inherit loc defs; }); v2 = { loc, defs }: let @@ -1568,7 +1588,7 @@ let merge = { __functor = self: loc: defs: - (self.v2 { inherit loc defs; }).value; + checkHeadError loc elemType.description (self.v2 { inherit loc defs; }); v2 = { loc, defs }: let