From 43d998e6c09494aa66ebd677a2aa63eb7d1b3f3d Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Thu, 16 Apr 2026 20:43:32 +0100 Subject: [PATCH 1/6] types.attrListOf: init This adds a type for name-value mappings that preserve ordering. Motivating use case: command line flags for package modules / wrappers / modular services. The option value can be transformed into a command line in the correct order. Additionally, a convenience readOnly option could be provided to give easy introspection access to the values in an ad hoc manner. --- lib/tests/modules.sh | 7 + lib/tests/modules/declare-attrList.nix | 730 ++++++++++++++++++ lib/tests/modules/types.nix | 22 + lib/types.nix | 144 ++++ .../development/option-types.section.md | 17 + 5 files changed, 920 insertions(+) create mode 100644 lib/tests/modules/declare-attrList.nix diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 515ab169fe43..ac9cc83030d5 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -899,6 +899,13 @@ checkConfigError 'Did you mean .enable., .ebe. or .enabled.\?' config ./error-ty checkConfigError 'Did you mean .services\.myservice\.port. or .services\.myservice\.enable.\?' config.services.myservice ./error-typo-submodule.nix checkConfigError 'Did you mean .services\.nginx\.virtualHosts\."example\.com"\.ssl\.certificate. or .services\.nginx\.virtualHosts\."example\.com"\.ssl\.certificateKey.\?' config.services.nginx.virtualHosts.\"example.com\" ./error-typo-deeply-nested.nix +# types.attrListOf +checkConfigOutput '"ok"' config.assertions ./declare-attrList.nix +checkConfigError 'A definition for option .attrListInt.badValue.a. is not of type .signed integer.. Definition values:' config.attrListIntStrict.badValue ./declare-attrList.nix +checkConfigError 'A definition for option .attrList.badListElem. is not of type .attribute list of string.. Each list element must be a single-key attribute set.' config.attrListStrict.badListElem ./declare-attrList.nix +checkConfigError 'A definition for option .attrList.badString. is not of type .attribute list of string.. TypeError: Definition values:' config.attrListStrict.badString ./declare-attrList.nix +checkConfigError 'A definition for option .attrList.badListString. is not of type .attribute list of string.. Each list element must be a single-key attribute set.' config.attrListStrict.badListString ./declare-attrList.nix + cat <0)"; + assert + (attrListOf (enum [ + "a" + "b" + ])).description == "attribute list of (one of \"a\", \"b\")"; + assert + (attrListOf (strMatching "[0-9]+")).description + == "attribute list of string matching the pattern [0-9]+"; + assert + (attrListOf (nonEmptyListOf str)).description == "attribute list of non-empty (list of string)"; + assert (attrListOf (submodule { })).description == "attribute list of (submodule)"; + assert (coercedTo str abort int).description == "signed integer or string convertible to it"; assert (coercedTo int abort str).description == "string or signed integer convertible to it"; assert (coercedTo bool abort str).description == "string or boolean convertible to it"; diff --git a/lib/types.nix b/lib/types.nix index 5a42ada60d84..34d5b600322e 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -20,6 +20,7 @@ let isStorePath isString substring + sort throwIf toDerivation toList @@ -27,6 +28,7 @@ let ; inherit (lib.lists) concatLists + concatMap elemAt filter foldl' @@ -70,6 +72,9 @@ let mergeDefinitions fixupOptionType mergeOptionDecls + defaultOrderPriority + defaultOverridePriority + mkOverride ; inherit (lib.fileset) isFileset @@ -805,6 +810,145 @@ rec { substSubModules = m: nonEmptyListOf (elemType.substSubModules m); }; + attrListOf = + elemType: + mkOptionType rec { + name = "attrListOf"; + description = "attribute list of ${ + optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType + }"; + descriptionClass = "composite"; + check = { + __functor = _self: x: isList x || isAttrs x; + isV2MergeCoherent = true; + }; + merge = { + __functor = + self: loc: defs: + (self.v2 { inherit loc defs; }).value; + v2 = + { loc, defs }: + let + # Peel order and override properties from a value in any nesting order. + # Returns { value, prio, overridePrio }. + # mkOrder is stripped (we consume it for sorting). + # mkOverride is preserved in value (mergeDefinitions strips it). + peelProperties = + value: + let + type = value._type or null; + in + if type == "order" then + let + inner = peelProperties value.content; + in + { + inherit (inner) value overridePrio; + prio = value.priority; + } + else if type == "override" then + let + inner = peelProperties value.content; + in + { + inherit (inner) prio; + overridePrio = value.priority; + # Re-wrap mkOverride around the inner value (with mkOrder stripped) + value = mkOverride value.priority inner.value; + } + else + { + inherit value; + prio = defaultOrderPriority; + overridePrio = defaultOverridePriority; + }; + + # Extract { file, key, value, prio, overridePrio } from a single-key attrset, + # optionally wrapped in mkOrder at the element level (list format). + extractItem = + file: raw: + let + hasOrder = raw._type or null == "order"; + item = if hasOrder then raw.content else raw; + key = head (attrNames item); + peeled = peelProperties item.${key}; + in + if isAttrs item && length (attrNames item) == 1 then + peeled + // { + inherit file key; + prio = if hasOrder then raw.priority else peeled.prio; + } + else + throw "A definition for option `${showOption loc}' is not of type `${description}'. Each list element must be a single-key attribute set."; + + # Convert a definition to a flat list of { file, key, value, prio, overridePrio } + defToItems = + def: + if isList def.value then + map (extractItem def.file) def.value + else + # isAttrs: properties are on the values directly + map ( + key: + peelProperties def.value.${key} + // { + inherit (def) file; + inherit key; + } + ) (attrNames def.value); + + allItems = concatMap defToItems defs; + + # Per key, find the highest override priority (lowest number) + winningOverridePrio = foldl' ( + acc: item: + let + prev = acc.${item.key} or defaultOverridePriority; + in + if item.overridePrio < prev then + acc // { ${item.key} = item.overridePrio; } + else + # minimize `//` operations + acc + ) { } allItems; + + # Keep only items at the winning override priority for their key + items = sort (a: b: a.prio < b.prio) ( + filter ( + item: item.overridePrio == winningOverridePrio.${item.key} or defaultOverridePriority + ) allItems + ); + + evals = filter (e: e.eval.optionalValue ? value) ( + map (item: { + inherit (item) key; + eval = mergeDefinitions (loc ++ [ item.key ]) elemType [ + { + inherit (item) file value; + } + ]; + }) items + ); + in + { + headError = checkDefsForError check loc defs; + value = map (e: { ${e.key} = e.eval.optionalValue.value or e.eval.mergedValue; }) evals; + valueMeta.attrList = map (e: e.eval.checkedAndMerged.valueMeta) evals; + }; + }; + emptyValue = { + value = [ ]; + }; + getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "*" ]); + getSubModules = elemType.getSubModules; + substSubModules = m: attrListOf (elemType.substSubModules m); + functor = elemTypeFunctor name { inherit elemType; } // { + type = payload: types.attrListOf payload.elemType; + }; + nestedTypes.elemType = elemType; + }; + attrsOf = elemType: attrsWith { inherit elemType; }; # A version of attrsOf that's lazy in its values at the expense of diff --git a/nixos/doc/manual/development/option-types.section.md b/nixos/doc/manual/development/option-types.section.md index cc195abcc37b..24b966fbf3b3 100644 --- a/nixos/doc/manual/development/option-types.section.md +++ b/nixos/doc/manual/development/option-types.section.md @@ -494,6 +494,23 @@ Composed types are types that take a type as parameter. `listOf Displays the option as `foo.` in the manual. +`types.attrListOf` *`t`* + +: An ordered list of single-attribute attribute sets, where each value is of *`t`* type. + The output is always `[ { name1 = value1; } { name2 = value2; } ... ]`. + + Definitions can be provided in two formats, which may be mixed via `lib.mkMerge`, `imports`, etc: + + - **List format**: `[ { a = 1; } { b = 2; } ]` — each element must be a single-attribute attribute set. + Elements may be wrapped in `lib.mkOrder` (or `lib.mkBefore`/`lib.mkAfter`) to control ordering; + unwrapped elements use the default order priority. + + - **Attribute set format**: `{ a = lib.mkOrder 100 1; b = 2; }` — each name-value pair becomes a single-attribute attribute set in the output. + Values may be wrapped in `lib.mkOrder` (or `lib.mkBefore`/`lib.mkAfter`) to control ordering. + Values without `lib.mkOrder` use the default priority. + + Multiple definitions of the same option are concatenated and then sorted by priority. + Entries at the same priority level preserve their definition order. `types.uniq` *`t`* From 17fdb6f68af2e48cce69536e50b471391b22ad19 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sun, 19 Apr 2026 13:00:03 +0100 Subject: [PATCH 2/6] types.attrListWith: init Trivial extraction from attrListOf. --- lib/types.nix | 6 ++++-- .../doc/manual/development/option-types.section.md | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/types.nix b/lib/types.nix index 34d5b600322e..836efdf41831 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -810,8 +810,10 @@ rec { substSubModules = m: nonEmptyListOf (elemType.substSubModules m); }; - attrListOf = - elemType: + attrListOf = elemType: attrListWith { inherit elemType; }; + + attrListWith = + { elemType }: mkOptionType rec { name = "attrListOf"; description = "attribute list of ${ diff --git a/nixos/doc/manual/development/option-types.section.md b/nixos/doc/manual/development/option-types.section.md index 24b966fbf3b3..ff35193a746c 100644 --- a/nixos/doc/manual/development/option-types.section.md +++ b/nixos/doc/manual/development/option-types.section.md @@ -512,6 +512,19 @@ Composed types are types that take a type as parameter. `listOf Multiple definitions of the same option are concatenated and then sorted by priority. Entries at the same priority level preserve their definition order. +`types.attrListWith` { *`elemType`* } + +: An ordered list of single-attribute attribute sets, where each value is of *`elemType`* type. + + **Parameters** + + `elemType` (Required) + : Specifies the type of each value in the attribute list. + + **Behavior** + + - `attrListWith { elemType = t; }` is equivalent to `attrListOf t` + `types.uniq` *`t`* : Ensures that type *`t`* cannot be merged. It is used to ensure option From f1b62fdc4ebb63ffc48ca7e1add32979fa98055b Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Sun, 19 Apr 2026 13:00:32 +0100 Subject: [PATCH 3/6] types.attrListWith: add asAttrs This allows the type's return value to be accessed more easily. Motivating use case: - Built-in module provides CLI functionality by declaring an `attrListWith { asAttrs = true; }`, extracting the ordered list from `valueMeta` for the purpose of creating the `argv`. - User modules can read the command line's flags directly without having to parse the list of attrs. --- lib/tests/modules/declare-attrList.nix | 141 ++++++++++++++++++ lib/types.nix | 30 +++- .../development/option-types.section.md | 13 +- 3 files changed, 178 insertions(+), 6 deletions(-) diff --git a/lib/tests/modules/declare-attrList.nix b/lib/tests/modules/declare-attrList.nix index b83599de7a77..b6e664422af1 100644 --- a/lib/tests/modules/declare-attrList.nix +++ b/lib/tests/modules/declare-attrList.nix @@ -42,6 +42,27 @@ in ); }; + # asAttrs: value is a merged attrset, ordered list in valueMeta + asAttrs = mkOption { + type = types.lazyAttrsOf ( + types.attrListWith { + elemType = types.str; + asAttrs = true; + mergeAttrValues = _name: values: lib.last values; + } + ); + }; + + # asAttrs with default mergeAttrValues: duplicates collected into lists + asAttrsDefault = mkOption { + type = types.lazyAttrsOf ( + types.attrListWith { + elemType = types.int; + asAttrs = true; + } + ); + }; + # Strict wrappers that force deep evaluation, for testing error cases attrListStrict = mkOption { type = types.lazyAttrsOf types.raw; @@ -381,6 +402,53 @@ in } ]; + # asAttrs: unique keys — value is a plain attrset + asAttrs.unique = [ + { a = "alpha"; } + { b = "beta"; } + ]; + + # asAttrs: duplicate keys — last in order wins + asAttrs.duplicateKeys = mkMerge [ + { x = mkOrder 500 "first"; } + { x = mkOrder 1500 "last"; } + { y = "only"; } + ]; + + # asAttrs: with ordering — value is attrset, ordered list in valueMeta + asAttrs.ordered = { + z = mkOrder 200 "z-val"; + a = mkOrder 100 "a-val"; + }; + + # asAttrs: with mkForce — forced key overrides + asAttrs.withForce = mkMerge [ + { x = "unused: overridden by mkForce"; } + { + x = mkForce "forced"; + y = "kept"; + } + ]; + + # asAttrs: empty + asAttrs.empty = [ ]; + + # asAttrsDefault: unique keys + asAttrsDefault.unique = [ + { a = 1; } + { b = 2; } + ]; + + # asAttrsDefault: duplicate keys — default collects into lists + asAttrsDefault.duplicates = mkMerge [ + { x = mkOrder 500 10; } + { x = mkOrder 1500 30; } + { y = 99; } + [ + { x = 20; } + ] + ]; + # either: attrList branch matches for list input eitherAttrListOrInt = [ { a = "hello"; } @@ -723,6 +791,79 @@ in { a = "hello"; } ]; + # asAttrs: unique keys — value is a plain attrset + assert + cfg.asAttrs.unique == { + a = "alpha"; + b = "beta"; + }; + # ordered list preserved in valueMeta + assert + c.options.asAttrs.valueMeta.attrs.unique.attrListValue == [ + { a = "alpha"; } + { b = "beta"; } + ]; + + # asAttrs: duplicate keys — last in order wins + assert + cfg.asAttrs.duplicateKeys == { + x = "last"; + y = "only"; + }; + assert + c.options.asAttrs.valueMeta.attrs.duplicateKeys.attrListValue == [ + { x = "first"; } + { y = "only"; } + { x = "last"; } + ]; + + # asAttrs: ordered — value is attrset (unordered), list in valueMeta preserves order + assert + cfg.asAttrs.ordered == { + a = "a-val"; + z = "z-val"; + }; + assert + c.options.asAttrs.valueMeta.attrs.ordered.attrListValue == [ + { a = "a-val"; } + { z = "z-val"; } + ]; + + # asAttrs: mkForce — forced key overrides, value is attrset + assert + cfg.asAttrs.withForce == { + x = "forced"; + y = "kept"; + }; + + # asAttrs: empty — value is empty attrset + assert cfg.asAttrs.empty == { }; + + # asAttrsDefault: unique keys — each value wrapped in singleton list + assert + cfg.asAttrsDefault.unique == { + a = [ 1 ]; + b = [ 2 ]; + }; + + # asAttrsDefault: duplicate keys — values collected into list in order + assert + cfg.asAttrsDefault.duplicates == { + x = [ + 10 + 20 + 30 + ]; + y = [ 99 ]; + }; + assert + c.options.asAttrsDefault.valueMeta.attrs.duplicates.attrListValue == [ + { x = 10; } + { y = 99; } + { x = 20; } + { x = 30; } + ]; + # Error cases are tested via checkConfigError in modules.sh "ok"; diff --git a/lib/types.nix b/lib/types.nix index 836efdf41831..6079ea11b410 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -813,7 +813,11 @@ rec { attrListOf = elemType: attrListWith { inherit elemType; }; attrListWith = - { elemType }: + { + elemType, + asAttrs ? false, + mergeAttrValues ? _name: values: values, + }: mkOptionType rec { name = "attrListOf"; description = "attribute list of ${ @@ -932,21 +936,37 @@ rec { ]; }) items ); + + attrListValue = map (e: { ${e.key} = e.eval.optionalValue.value or e.eval.mergedValue; }) evals; in { headError = checkDefsForError check loc defs; - value = map (e: { ${e.key} = e.eval.optionalValue.value or e.eval.mergedValue; }) evals; + value = if asAttrs then zipAttrsWith mergeAttrValues attrListValue else attrListValue; valueMeta.attrList = map (e: e.eval.checkedAndMerged.valueMeta) evals; + /** + The ordered list representation, especially useful when asAttrs is set. + */ + valueMeta.attrListValue = attrListValue; }; }; emptyValue = { - value = [ ]; + value = if asAttrs then { } else [ ]; }; getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "*" ]); getSubModules = elemType.getSubModules; - substSubModules = m: attrListOf (elemType.substSubModules m); + substSubModules = + m: + attrListWith { + inherit asAttrs mergeAttrValues; + elemType = elemType.substSubModules m; + }; functor = elemTypeFunctor name { inherit elemType; } // { - type = payload: types.attrListOf payload.elemType; + type = + payload: + types.attrListWith { + inherit asAttrs mergeAttrValues; + inherit (payload) elemType; + }; }; nestedTypes.elemType = elemType; }; diff --git a/nixos/doc/manual/development/option-types.section.md b/nixos/doc/manual/development/option-types.section.md index ff35193a746c..720f5823d068 100644 --- a/nixos/doc/manual/development/option-types.section.md +++ b/nixos/doc/manual/development/option-types.section.md @@ -512,7 +512,7 @@ Composed types are types that take a type as parameter. `listOf Multiple definitions of the same option are concatenated and then sorted by priority. Entries at the same priority level preserve their definition order. -`types.attrListWith` { *`elemType`* } +`types.attrListWith` { *`elemType`*, *`asAttrs`* ? false, *`mergeAttrValues`* ? _name: values: values } : An ordered list of single-attribute attribute sets, where each value is of *`elemType`* type. @@ -521,6 +521,17 @@ Composed types are types that take a type as parameter. `listOf `elemType` (Required) : Specifies the type of each value in the attribute list. + `asAttrs` + : When `true`, the option value is an attribute set instead of a list. + Duplicate keys are merged using `mergeAttrValues`. + The ordered list is always available via `valueMeta.attrListValue`. + + `mergeAttrValues` + : A function `name: values: mergedValue` that controls how duplicate keys + are combined when `asAttrs = true`. This is passed as the callback to + `lib.zipAttrsWith`. The `values` list is in order of priority. + By default, all values are collected into a list. + **Behavior** - `attrListWith { elemType = t; }` is equivalent to `attrListOf t` From e29bf2412b0395f559b15fef91f1d7e095b5149f Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Mon, 4 May 2026 16:00:39 +0200 Subject: [PATCH 4/6] lib.modules.mapDefinitionValue: init --- lib/modules.nix | 23 ++++++++++++ lib/tests/misc.nix | 92 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/lib/modules.nix b/lib/modules.nix index 21ffbb9591c5..1adee44f7a33 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -1598,6 +1598,28 @@ let inherit priority content; }; + /** + Applies a function to the value inside a definition, + preserving all surrounding properties (`mkForce`, `mkOrder`, `mkIf`, etc.). + */ + mapDefinitionValue = + f: def: + if def ? _type then + if def._type == "merge" then + def // { contents = map (mapDefinitionValue f) def.contents; } + else if def._type == "if" then + def // { content = mapDefinitionValue f def.content; } + else if def._type == "override" then + def // { content = mapDefinitionValue f def.content; } + else if def._type == "order" then + def // { content = mapDefinitionValue f def.content; } + else if def._type == "definition" then + def // { value = mapDefinitionValue f def.value; } + else + f def + else + f def; + mkBefore = mkOrder 500; defaultOrderPriority = 1000; mkAfter = mkOrder 1500; @@ -2302,6 +2324,7 @@ private importApply importJSON importTOML + mapDefinitionValue mergeDefinitions mergeAttrDefinitionsWithPrio mergeOptionDecls # should be private? diff --git a/lib/tests/misc.nix b/lib/tests/misc.nix index e2872dcafce9..021a7eca25b8 100644 --- a/lib/tests/misc.nix +++ b/lib/tests/misc.nix @@ -5114,4 +5114,96 @@ runTests { ); expected = false; }; + + # mapDefinitionValue + + testMapDefinitionValuePlain = { + expr = lib.modules.mapDefinitionValue (x: x + 1) 5; + expected = 6; + }; + + testMapDefinitionValueMkForce = { + expr = lib.modules.mapDefinitionValue (x: x + 1) (lib.mkForce 5); + expected = lib.mkForce 6; + }; + + testMapDefinitionValueMkDefault = { + expr = lib.modules.mapDefinitionValue (x: x + 1) (lib.mkDefault 5); + expected = lib.mkDefault 6; + }; + + testMapDefinitionValueMkOrder = { + expr = lib.modules.mapDefinitionValue (x: x + 1) (lib.mkOrder 500 5); + expected = lib.mkOrder 500 6; + }; + + testMapDefinitionValueMkOverrideNested = { + expr = lib.modules.mapDefinitionValue (x: x + 1) (lib.mkForce (lib.mkOrder 500 5)); + expected = lib.mkForce (lib.mkOrder 500 6); + }; + + testMapDefinitionValueMkIf = { + expr = lib.modules.mapDefinitionValue (x: x + 1) (lib.mkIf true 5); + expected = lib.mkIf true 6; + }; + + testMapDefinitionValueMkMerge = { + expr = lib.modules.mapDefinitionValue (x: x + 1) ( + lib.mkMerge [ + 5 + 10 + ] + ); + expected = lib.mkMerge [ + 6 + 11 + ]; + }; + + testMapDefinitionValueMkDefinition = { + expr = lib.modules.mapDefinitionValue (x: x + 1) ( + lib.mkDefinition { + file = "test"; + value = 5; + } + ); + expected = lib.mkDefinition { + file = "test"; + value = 6; + }; + }; + + testMapDefinitionValueDeep = { + expr = lib.modules.mapDefinitionValue (x: x + 1) (lib.mkIf true (lib.mkForce (lib.mkOrder 500 5))); + expected = lib.mkIf true (lib.mkForce (lib.mkOrder 500 6)); + }; + + testMapDefinitionValueAllNested = { + expr = lib.modules.mapDefinitionValue (x: x + 1) ( + lib.mkMerge [ + (lib.mkIf true ( + lib.mkForce ( + lib.mkOrder 500 ( + lib.mkDefinition { + file = "test"; + value = lib.mkBefore 5; + } + ) + ) + )) + ] + ); + expected = lib.mkMerge [ + (lib.mkIf true ( + lib.mkForce ( + lib.mkOrder 500 ( + lib.mkDefinition { + file = "test"; + value = lib.mkBefore 6; + } + ) + ) + )) + ]; + }; } From 8aa6e3dbb9761e1a8f07c705aa76d7a0b81d8a50 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Mon, 4 May 2026 17:08:18 +0200 Subject: [PATCH 5/6] types.attrListWith: add valueMeta.definitions --- lib/tests/modules.sh | 3 ++ ...definitions-file-diagnostic-forwarding.nix | 25 +++++++++ lib/tests/modules/declare-attrList.nix | 54 +++++++++++++++++++ lib/types.nix | 11 +++- 4 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 lib/tests/modules/attrList-valueMeta-definitions-file-diagnostic-forwarding.nix diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index ac9cc83030d5..c7b661e2e2fe 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -906,6 +906,9 @@ checkConfigError 'A definition for option .attrList.badListElem. is not of type checkConfigError 'A definition for option .attrList.badString. is not of type .attribute list of string.. TypeError: Definition values:' config.attrListStrict.badString ./declare-attrList.nix checkConfigError 'A definition for option .attrList.badListString. is not of type .attribute list of string.. Each list element must be a single-key attribute set.' config.attrListStrict.badListString ./declare-attrList.nix +# attrListWith valueMeta.definitions: file propagation +checkConfigError 'the-defs-file\.nix' config.argv ./attrList-valueMeta-definitions-file-diagnostic-forwarding.nix + cat < Date: Sun, 24 May 2026 15:23:24 +0200 Subject: [PATCH 6/6] types.attrListWith: review fixes - Improve extractItem error messages: distinguish non-attrset elements from multi-key attrsets, and include the faulty definition via showDefs. - Use isType instead of raw _type access for order detection. - Disable type merging (typeMerge = t: null) instead of providing a functor-based merge. Add test confirming duplicate declarations fail. --- lib/tests/modules.sh | 7 ++++-- .../modules/declare-attrList-type-merge.nix | 12 +++++++++ lib/types.nix | 25 +++++++++++-------- 3 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 lib/tests/modules/declare-attrList-type-merge.nix diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index c7b661e2e2fe..4eb86ddd4962 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -902,13 +902,16 @@ checkConfigError 'Did you mean .services\.nginx\.virtualHosts\."example\.com"\.s # types.attrListOf checkConfigOutput '"ok"' config.assertions ./declare-attrList.nix checkConfigError 'A definition for option .attrListInt.badValue.a. is not of type .signed integer.. Definition values:' config.attrListIntStrict.badValue ./declare-attrList.nix -checkConfigError 'A definition for option .attrList.badListElem. is not of type .attribute list of string.. Each list element must be a single-key attribute set.' config.attrListStrict.badListElem ./declare-attrList.nix +checkConfigError 'A definition for option .attrList.badListElem. is not of type .attribute list of string.. Each list element must be a single-key attribute set, but got 2 keys' config.attrListStrict.badListElem ./declare-attrList.nix checkConfigError 'A definition for option .attrList.badString. is not of type .attribute list of string.. TypeError: Definition values:' config.attrListStrict.badString ./declare-attrList.nix -checkConfigError 'A definition for option .attrList.badListString. is not of type .attribute list of string.. Each list element must be a single-key attribute set.' config.attrListStrict.badListString ./declare-attrList.nix +checkConfigError 'A definition for option .attrList.badListString. is not of type .attribute list of string.. Each list element must be an attribute set, but got string' config.attrListStrict.badListString ./declare-attrList.nix # attrListWith valueMeta.definitions: file propagation checkConfigError 'the-defs-file\.nix' config.argv ./attrList-valueMeta-definitions-file-diagnostic-forwarding.nix +# attrListOf does not support type merging +checkConfigError 'The option .merged. in .*/declare-attrList-type-merge.nix. is already declared in .*/declare-attrList-type-merge.nix' config.merged ./declare-attrList-type-merge.nix + cat <