lib.modules: default to emptyValue (#500104)

This commit is contained in:
Johannes Kirschbauer
2026-03-25 11:00:39 +00:00
committed by GitHub
5 changed files with 94 additions and 5 deletions

View File

@@ -1246,6 +1246,8 @@ let
allInvalid = filter (def: !type.check def.value) defsFinal;
in
throw "A definition for option `${showOption loc}' is not of type `${type.description}'. Definition values:${showDefs allInvalid}"
else if type.emptyValue ? value then
type.emptyValue.value
else
# (nixos-option detects this specific error message and gives it special
# handling. If changed here, please change it there too.)

View File

@@ -589,11 +589,16 @@ rec {
renderOptionValue opt.example
);
}
// optionalAttrs (opt ? defaultText || opt ? default) {
default = builtins.addErrorContext "while evaluating the ${
if opt ? defaultText then "defaultText" else "default value"
} of option `${name}`" (renderOptionValue (opt.defaultText or opt.default));
}
//
optionalAttrs (opt ? defaultText || opt ? default || ((opt.type or { }).emptyValue or { }) ? value)
{
default =
builtins.addErrorContext
"while evaluating the ${
if opt ? defaultText then "defaultText" else "default value"
} of option `${name}`"
(renderOptionValue (opt.defaultText or opt.default or opt.type.emptyValue.value));
}
// optionalAttrs (opt ? relatedPackages && opt.relatedPackages != null) {
inherit (opt) relatedPackages;
};

View File

@@ -3393,6 +3393,44 @@ runTests {
];
};
testEmptyValueOption = {
expr =
let
module =
{ lib, ... }:
{
options = {
"empty-value" = lib.mkOption {
type = lib.mkOptionType {
name = "propagate-empty-value-to-default";
emptyValue.value = 2;
};
};
};
};
eval = evalModules {
modules = [ module ];
};
in
filter (o: o.name == "empty-value") (optionAttrSetToDocList eval.options);
expected = [
{
declarations = [ ];
default = {
_type = "literalExpression";
text = "2";
};
description = null;
internal = false;
loc = [ "empty-value" ];
name = "empty-value";
readOnly = false;
type = "propagate-empty-value-to-default";
visible = true;
}
];
};
testDocOptionVisiblity = {
expr =
let

View File

@@ -674,6 +674,17 @@ checkConfigOutput "{}" config.submodule.a ./emptyValues.nix
checkConfigError 'The option .int.a. was accessed but has no value defined. Try setting the option.' config.int.a ./emptyValues.nix
checkConfigError 'The option .nonEmptyList.a. was accessed but has no value defined. Try setting the option.' config.nonEmptyList.a ./emptyValues.nix
## defaults
checkConfigOutput "\[\]" config.list ./defaults.nix
checkConfigOutput "{}" config.attrs ./defaults.nix
checkConfigOutput "{}" config.attrsOf ./defaults.nix
checkConfigOutput "null" config.null ./defaults.nix
checkConfigOutput "{}" config.submodule ./defaults.nix
checkConfigOutput "\[\]" config.unique ./defaults.nix
checkConfigOutput "\[\]" config.coercedTo ./defaults.nix
# These types don't have empty values
checkConfigError 'The option .int. was accessed but has no value defined. Try setting the option.' config.int ./defaults.nix
# types.unique
# requires a single definition
checkConfigError 'The option .examples\.merged. is defined multiple times while it.s expected to be unique' config.examples.merged.a ./types-unique.nix

View File

@@ -0,0 +1,33 @@
{ lib, ... }:
let
inherit (lib) types;
in
{
options = {
list = lib.mkOption {
type = types.listOf types.int;
};
attrs = lib.mkOption {
type = types.attrs;
};
attrsOf = lib.mkOption {
type = types.attrsOf types.int;
};
null = lib.mkOption {
type = types.nullOr types.int;
};
submodule = lib.mkOption {
type = types.submodule { };
};
unique = lib.mkOption {
type = types.unique { message = "hi"; } (types.listOf types.int);
};
coercedTo = lib.mkOption {
type = types.coercedTo (types.attrsOf types.int) builtins.attrNames (types.listOf types.str);
};
# no empty value
int = lib.mkOption {
type = types.int;
};
};
}