diff --git a/lib/options.nix b/lib/options.nix index 4f75da5ad51a8..b22e53da86c9c 100644 --- a/lib/options.nix +++ b/lib/options.nix @@ -420,20 +420,24 @@ rec { Placeholders will not be quoted as they are not actual values: (showOption ["foo" "*" "bar"]) == "foo.*.bar" (showOption ["foo" "" "bar"]) == "foo..bar" + (showOption ["foo" "" "bar"]) == "foo..bar" */ showOption = parts: let + # If the part is a named placeholder of the form "<...>" don't escape it. + # Required for compatibility with: namedAttrsOf + # Can lead to misleading escaping if somebody uses literally "<...>" in their option names. + # This is the trade-off to allow for named placeholders in option names. + isNamedPlaceholder = builtins.match "\<(.*)\>"; + # "" # functionTo + # "" # attrsOf submoule + # "" # attrsWith { name = "customName"; elemType = submoule; } + # We assume that these are "special values" and not real configuration data. + # If it is real configuration data, it is rendered incorrectly. + # "*" # listOf (submodule {}) escapeOptionPart = part: - let - # We assume that these are "special values" and not real configuration data. - # If it is real configuration data, it is rendered incorrectly. - specialIdentifiers = [ - "" # attrsOf (submodule {}) - "*" # listOf (submodule {}) - "" # functionTo - ]; - in if builtins.elem part specialIdentifiers - then part - else lib.strings.escapeNixIdentifier part; + if part == "*" || isNamedPlaceholder part != null + then part + else lib.strings.escapeNixIdentifier part; in (concatStringsSep ".") (map escapeOptionPart parts); showFiles = files: concatStringsSep " and " (map (f: "`${f}'") files); diff --git a/lib/tests/misc.nix b/lib/tests/misc.nix index 72b3d630b8da8..508291c4306c3 100644 --- a/lib/tests/misc.nix +++ b/lib/tests/misc.nix @@ -1871,6 +1871,44 @@ runTests { expected = [ [ "_module" "args" ] [ "foo" ] [ "foo" "" "bar" ] [ "foo" "bar" ] ]; }; + testAttrsWithName = { + expr = let + eval = evalModules { + modules = [ + { + options = { + foo = lib.mkOption { + type = lib.types.attrsWith { + name = "MyCustomPlaceholder"; + elemType = lib.types.submodule { + options.bar = lib.mkOption { + type = lib.types.int; + default = 42; + }; + }; + }; + }; + }; + } + ]; + }; + opt = eval.options.foo; + in + (opt.type.getSubOptions opt.loc).bar.loc; + expected = [ + "foo" + "" + "bar" + ]; + }; + + testShowOptionWithPlaceholder = { + # , *, should now be escaped. It is used as a placeholder by convention. + # Other symbols should be escaped. `{}` + expr = lib.showOption ["" "" "*" "{foo}"]; + expected = "..*.\"{foo}\""; + }; + testCartesianProductOfEmptySet = { expr = cartesianProduct {}; expected = [ {} ]; diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 2884c00d0768c..8b213f0a6449c 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -575,6 +575,11 @@ checkConfigOutput '^38|27$' options.submoduleLine38.declarationPositions.1.line # nested options work checkConfigOutput '^34$' options.nested.nestedLine34.declarationPositions.0.line ./declaration-positions.nix +# AttrsWith tests +checkConfigOutput '^11$' config.result ./lazy-attrsWith.nix +checkConfigOutput '^"mergedName..nested"$' config.result ./name-merge-attrsWith-1.nix +checkConfigError 'The option .mergedName. in .*\.nix. is already declared in .*\.nix' config.mergedName ./name-merge-attrsWith-2.nix + cat < = "id" + name = "id"; + elemType = types.submodule { + options.nested = mkOption { + type = types.int; + default = 1; + }; + }; + }; + }; + } + ) + # Module B + ( + { ... }: + { + options.mergedName = mkOption { + # default: "" + type = types.attrsOf (types.submodule { }); + # default = {}; + }; + } + ) + + # Output + ( + { + options, + ... + }: + { + options.result = mkOption { + default = lib.concatStringsSep "." (options.mergedName.type.getSubOptions options.mergedName.loc) + .nested.loc; + }; + } + ) + ]; +} diff --git a/lib/tests/modules/name-merge-attrsWith-2.nix b/lib/tests/modules/name-merge-attrsWith-2.nix new file mode 100644 index 0000000000000..ab7e8e3193e6d --- /dev/null +++ b/lib/tests/modules/name-merge-attrsWith-2.nix @@ -0,0 +1,39 @@ +# Non mergable attrsWith +{ lib, ... }: +let + inherit (lib) types mkOption; +in +{ + imports = [ + # Module A + ( + { ... }: + { + options.mergedName = mkOption { + default = { }; + type = types.attrsWith { + name = "id"; + elemType = types.submodule { + options.nested = mkOption { + type = types.int; + default = 1; + }; + }; + }; + }; + } + ) + # Module B + ( + { ... }: + { + options.mergedName = mkOption { + type = types.attrsWith { + name = "other"; + elemType = types.submodule { }; + }; + }; + } + ) + ]; +} diff --git a/lib/types.nix b/lib/types.nix index 82d7425ca6439..fd2d146c0fda5 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -582,48 +582,85 @@ rec { substSubModules = m: nonEmptyListOf (elemType.substSubModules m); }; - attrsOf = elemType: mkOptionType rec { - name = "attrsOf"; - description = "attribute set of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}"; - descriptionClass = "composite"; - check = isAttrs; - merge = loc: defs: - mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs: - (mergeDefinitions (loc ++ [name]) elemType defs).optionalValue - ) - # Push down position info. - (map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs))); - emptyValue = { value = {}; }; - getSubOptions = prefix: elemType.getSubOptions (prefix ++ [""]); - getSubModules = elemType.getSubModules; - substSubModules = m: attrsOf (elemType.substSubModules m); - functor = (defaultFunctor name) // { wrapped = elemType; }; - nestedTypes.elemType = elemType; - }; + attrsOf = elemType: attrsWith { inherit elemType; }; # A version of attrsOf that's lazy in its values at the expense of # conditional definitions not working properly. E.g. defining a value with # `foo.attr = mkIf false 10`, then `foo ? attr == true`, whereas with # attrsOf it would correctly be `false`. Accessing `foo.attr` would throw an # error that it's not defined. Use only if conditional definitions don't make sense. - lazyAttrsOf = elemType: mkOptionType rec { - name = "lazyAttrsOf"; - description = "lazy attribute set of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}"; + lazyAttrsOf = elemType: attrsWith { inherit elemType; lazy = true; }; + + # base type for lazyAttrsOf and attrsOf + attrsWith = { + elemType, + placeholder ? "name", + lazy ? false, + }: + let + typeName = if lazy then "lazyAttrsOf" else "attrsOf"; + # Push down position info. + pushPositions = map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value); + in + mkOptionType { + name = typeName; + description = (if lazy then "lazy attribute set" else "attribute set") + " of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}"; descriptionClass = "composite"; check = isAttrs; - merge = loc: defs: - zipAttrsWith (name: defs: - let merged = mergeDefinitions (loc ++ [name]) elemType defs; - # mergedValue will trigger an appropriate error when accessed - in merged.optionalValue.value or elemType.emptyValue.value or merged.mergedValue - ) - # Push down position info. - (map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs); + merge = if lazy then ( + # Lazy merge Function + loc: defs: + zipAttrsWith (name: defs: + let merged = mergeDefinitions (loc ++ [name]) elemType defs; + # mergedValue will trigger an appropriate error when accessed + in merged.optionalValue.value or elemType.emptyValue.value or merged.mergedValue + ) + # Push down position info. + (pushPositions defs) + ) else ( + # Non-lazy merge Function + loc: defs: + mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs: + (mergeDefinitions (loc ++ [name]) elemType (defs)).optionalValue + ) + # Push down position info. + (pushPositions defs))) + ); emptyValue = { value = {}; }; - getSubOptions = prefix: elemType.getSubOptions (prefix ++ [""]); + getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<${placeholder}>"]); getSubModules = elemType.getSubModules; - substSubModules = m: lazyAttrsOf (elemType.substSubModules m); - functor = (defaultFunctor name) // { wrapped = elemType; }; + substSubModules = m: attrsWith { elemType = elemType.substSubModules m; inherit lazy placeholder; }; + functor = defaultFunctor "attrsWith" // { + wrapped = elemType; + payload = { + # Important!: Add new function attributes here in case of future changes + inherit elemType lazy placeholder; + }; + binOp = lhs: rhs: + let + elemType = lhs.elemType.typeMerge rhs.elemType.functor; + placeholder = + if lhs.placeholder == rhs.placeholder then + lhs.placeholder + else if lhs.placeholder == "name" then + rhs.placeholder + else if rhs.placeholder == "name" then + lhs.placeholder + else + null; + lazy = + if lhs.lazy == rhs.lazy then + lhs.lazy + else + null; + in + if elemType == null || lazy == null || placeholder == null then + null + else + { + inherit elemType placeholder lazy; + }; + }; nestedTypes.elemType = elemType; };