Merge pull request #98155 from Infinisil/improved-module-errors

Improved module errors
This commit is contained in:
Eelco Dolstra 2020-09-23 15:28:13 +02:00 committed by GitHub
commit ecc00bd8f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 55 additions and 33 deletions

View file

@ -117,7 +117,7 @@ rec {
if config._module.check && config._module.freeformType == null && merged.unmatchedDefns != [] then if config._module.check && config._module.freeformType == null && merged.unmatchedDefns != [] then
let let
firstDef = head merged.unmatchedDefns; firstDef = head merged.unmatchedDefns;
baseMsg = "The option `${showOption (prefix ++ firstDef.prefix)}' defined in `${firstDef.file}' does not exist."; baseMsg = "The option `${showOption (prefix ++ firstDef.prefix)}' does not exist. Definition values:${showDefs [ firstDef ]}";
in in
if attrNames options == [ "_module" ] if attrNames options == [ "_module" ]
then throw '' then throw ''
@ -449,7 +449,13 @@ rec {
# Handle properties, check types, and merge everything together. # Handle properties, check types, and merge everything together.
res = res =
if opt.readOnly or false && length defs' > 1 then if opt.readOnly or false && length defs' > 1 then
throw "The option `${showOption loc}' is read-only, but it's set multiple times." let
# For a better error message, evaluate all readOnly definitions as
# if they were the only definition.
separateDefs = map (def: def // {
value = (mergeDefinitions loc opt.type [ def ]).mergedValue;
}) defs';
in throw "The option `${showOption loc}' is read-only, but it's set multiple times. Definition values:${showDefs separateDefs}"
else else
mergeDefinitions loc opt.type defs'; mergeDefinitions loc opt.type defs';
@ -497,8 +503,8 @@ rec {
mergedValue = mergedValue =
if isDefined then if isDefined then
if all (def: type.check def.value) defsFinal then type.merge loc defsFinal if all (def: type.check def.value) defsFinal then type.merge loc defsFinal
else let firstInvalid = findFirst (def: ! type.check def.value) null defsFinal; else let allInvalid = filter (def: ! type.check def.value) defsFinal;
in throw "The option value `${showOption loc}' in `${firstInvalid.file}' is not of type `${type.description}'." in throw "A definition for option `${showOption loc}' is not of type `${type.description}'. Definition values:${showDefs allInvalid}"
else else
# (nixos-option detects this specific error message and gives it special # (nixos-option detects this specific error message and gives it special
# handling. If changed here, please change it there too.) # handling. If changed here, please change it there too.)

View file

@ -96,12 +96,12 @@ rec {
else if all isBool list then foldl' lib.or false list else if all isBool list then foldl' lib.or false list
else if all isString list then lib.concatStrings list else if all isString list then lib.concatStrings list
else if all isInt list && all (x: x == head list) list then head list else if all isInt list && all (x: x == head list) list then head list
else throw "Cannot merge definitions of `${showOption loc}' given in ${showFiles (getFiles defs)}."; else throw "Cannot merge definitions of `${showOption loc}'. Definition values:${showDefs defs}";
mergeOneOption = loc: defs: mergeOneOption = loc: defs:
if defs == [] then abort "This case should never happen." if defs == [] then abort "This case should never happen."
else if length defs != 1 then else if length defs != 1 then
throw "The unique option `${showOption loc}' is defined multiple times, in:\n - ${concatStringsSep "\n - " (getFiles defs)}." throw "The unique option `${showOption loc}' is defined multiple times. Definition values:${showDefs defs}"
else (head defs).value; else (head defs).value;
/* "Merge" option definitions by checking that they all have the same value. */ /* "Merge" option definitions by checking that they all have the same value. */
@ -111,11 +111,11 @@ rec {
# This also makes it work for functions, because the foldl' below would try # This also makes it work for functions, because the foldl' below would try
# to compare the first element with itself, which is false for functions # to compare the first element with itself, which is false for functions
else if length defs == 1 then (elemAt defs 0).value else if length defs == 1 then (elemAt defs 0).value
else foldl' (val: def: else (foldl' (first: def:
if def.value != val then if def.value != first.value then
throw "The option `${showOption loc}' has conflicting definitions, in ${showFiles (getFiles defs)}." throw "The option `${showOption loc}' has conflicting definition values:${showDefs [ first def ]}"
else else
val) (head defs).value defs; first) (head defs) defs).value;
/* Extracts values of all "value" keys of the given list. /* Extracts values of all "value" keys of the given list.
@ -213,6 +213,24 @@ rec {
else escaped; else escaped;
in (concatStringsSep ".") (map escapeOptionPart parts); in (concatStringsSep ".") (map escapeOptionPart parts);
showFiles = files: concatStringsSep " and " (map (f: "`${f}'") files); showFiles = files: concatStringsSep " and " (map (f: "`${f}'") files);
showDefs = defs: concatMapStrings (def:
let
# Pretty print the value for display, if successful
prettyEval = builtins.tryEval (lib.generators.toPretty {} def.value);
# Split it into its lines
lines = filter (v: ! isList v) (builtins.split "\n" prettyEval.value);
# Only display the first 5 lines, and indent them for better visibility
value = concatStringsSep "\n " (take 5 lines ++ optional (length lines > 5) "...");
result =
# Don't print any value if evaluating the value strictly fails
if ! prettyEval.success then ""
# Put it on a new line if it consists of multiple
else if length lines > 1 then ":\n " + value
else ": " + value;
in "\n- In `${def.file}'${result}"
) defs;
unknownModule = "<unknown-file>"; unknownModule = "<unknown-file>";
} }

View file

@ -49,7 +49,7 @@ checkConfigError() {
reportFailure "$@" reportFailure "$@"
return 1 return 1
else else
if echo "$err" | grep --silent "$errorContains" ; then if echo "$err" | grep -zP --silent "$errorContains" ; then
pass=$((pass + 1)) pass=$((pass + 1))
return 0; return 0;
else else
@ -62,17 +62,17 @@ checkConfigError() {
# Check boolean option. # Check boolean option.
checkConfigOutput "false" config.enable ./declare-enable.nix checkConfigOutput "false" config.enable ./declare-enable.nix
checkConfigError 'The option .* defined in .* does not exist.' config.enable ./define-enable.nix checkConfigError 'The option .* does not exist. Definition values:\n- In .*: true' config.enable ./define-enable.nix
# Check integer types. # Check integer types.
# unsigned # unsigned
checkConfigOutput "42" config.value ./declare-int-unsigned-value.nix ./define-value-int-positive.nix checkConfigOutput "42" config.value ./declare-int-unsigned-value.nix ./define-value-int-positive.nix
checkConfigError 'The option value .* in .* is not of type.*unsigned integer.*' config.value ./declare-int-unsigned-value.nix ./define-value-int-negative.nix checkConfigError 'A definition for option .* is not of type.*unsigned integer.*. Definition values:\n- In .*: -23' config.value ./declare-int-unsigned-value.nix ./define-value-int-negative.nix
# positive # positive
checkConfigError 'The option value .* in .* is not of type.*positive integer.*' config.value ./declare-int-positive-value.nix ./define-value-int-zero.nix checkConfigError 'A definition for option .* is not of type.*positive integer.*. Definition values:\n- In .*: 0' config.value ./declare-int-positive-value.nix ./define-value-int-zero.nix
# between # between
checkConfigOutput "42" config.value ./declare-int-between-value.nix ./define-value-int-positive.nix checkConfigOutput "42" config.value ./declare-int-between-value.nix ./define-value-int-positive.nix
checkConfigError 'The option value .* in .* is not of type.*between.*-21 and 43.*inclusive.*' config.value ./declare-int-between-value.nix ./define-value-int-negative.nix checkConfigError 'A definition for option .* is not of type.*between.*-21 and 43.*inclusive.*. Definition values:\n- In .*: -23' config.value ./declare-int-between-value.nix ./define-value-int-negative.nix
# Check either types # Check either types
# types.either # types.either
@ -125,7 +125,7 @@ checkConfigOutput 'true' "$@" ./define-enable.nix ./define-attrsOfSub-foo-enable
set -- config.enable ./define-enable.nix ./declare-enable.nix set -- config.enable ./define-enable.nix ./declare-enable.nix
checkConfigOutput "true" "$@" checkConfigOutput "true" "$@"
checkConfigOutput "false" "$@" ./disable-define-enable.nix checkConfigOutput "false" "$@" ./disable-define-enable.nix
checkConfigError "The option .*enable.* defined in .* does not exist" "$@" ./disable-declare-enable.nix checkConfigError "The option .*enable.* does not exist. Definition values:\n- In .*: true" "$@" ./disable-declare-enable.nix
checkConfigError "attribute .*enable.* in selection path .*config.enable.* not found" "$@" ./disable-define-enable.nix ./disable-declare-enable.nix checkConfigError "attribute .*enable.* in selection path .*config.enable.* not found" "$@" ./disable-define-enable.nix ./disable-declare-enable.nix
checkConfigError "attribute .*enable.* in selection path .*config.enable.* not found" "$@" ./disable-enable-modules.nix checkConfigError "attribute .*enable.* in selection path .*config.enable.* not found" "$@" ./disable-enable-modules.nix
@ -142,17 +142,17 @@ checkConfigError 'infinite recursion encountered' "$@"
# Check _module.check. # Check _module.check.
set -- config.enable ./declare-enable.nix ./define-enable.nix ./define-attrsOfSub-foo.nix set -- config.enable ./declare-enable.nix ./define-enable.nix ./define-attrsOfSub-foo.nix
checkConfigError 'The option .* defined in .* does not exist.' "$@" checkConfigError 'The option .* does not exist. Definition values:\n- In .*' "$@"
checkConfigOutput "true" "$@" ./define-module-check.nix checkConfigOutput "true" "$@" ./define-module-check.nix
# Check coerced value. # Check coerced value.
checkConfigOutput "\"42\"" config.value ./declare-coerced-value.nix checkConfigOutput "\"42\"" config.value ./declare-coerced-value.nix
checkConfigOutput "\"24\"" config.value ./declare-coerced-value.nix ./define-value-string.nix checkConfigOutput "\"24\"" config.value ./declare-coerced-value.nix ./define-value-string.nix
checkConfigError 'The option value .* in .* is not.*string or signed integer convertible to it' config.value ./declare-coerced-value.nix ./define-value-list.nix checkConfigError 'A definition for option .* is not.*string or signed integer convertible to it.*. Definition values:\n- In .*: \[ \]' config.value ./declare-coerced-value.nix ./define-value-list.nix
# Check coerced value with unsound coercion # Check coerced value with unsound coercion
checkConfigOutput "12" config.value ./declare-coerced-value-unsound.nix checkConfigOutput "12" config.value ./declare-coerced-value-unsound.nix
checkConfigError 'The option value .* in .* is not.*8 bit signed integer.* or string convertible to it' config.value ./declare-coerced-value-unsound.nix ./define-value-string-bigint.nix checkConfigError 'A definition for option .* is not of type .*. Definition values:\n- In .*: "1000"' config.value ./declare-coerced-value-unsound.nix ./define-value-string-bigint.nix
checkConfigError 'unrecognised JSON value' config.value ./declare-coerced-value-unsound.nix ./define-value-string-arbitrary.nix checkConfigError 'unrecognised JSON value' config.value ./declare-coerced-value-unsound.nix ./define-value-string-arbitrary.nix
# Check mkAliasOptionModule. # Check mkAliasOptionModule.
@ -183,7 +183,7 @@ checkConfigOutput "true" config.submodule.enable ./declare-submoduleWith-path.ni
checkConfigOutput "true" config.enable ./disable-recursive/main.nix checkConfigOutput "true" config.enable ./disable-recursive/main.nix
checkConfigOutput "true" config.enable ./disable-recursive/{main.nix,disable-foo.nix} checkConfigOutput "true" config.enable ./disable-recursive/{main.nix,disable-foo.nix}
checkConfigOutput "true" config.enable ./disable-recursive/{main.nix,disable-bar.nix} checkConfigOutput "true" config.enable ./disable-recursive/{main.nix,disable-bar.nix}
checkConfigError 'The option .* defined in .* does not exist' config.enable ./disable-recursive/{main.nix,disable-foo.nix,disable-bar.nix} checkConfigError 'The option .* does not exist. Definition values:\n- In .*: true' config.enable ./disable-recursive/{main.nix,disable-foo.nix,disable-bar.nix}
# Check that imports can depend on derivations # Check that imports can depend on derivations
checkConfigOutput "true" config.enable ./import-from-store.nix checkConfigOutput "true" config.enable ./import-from-store.nix
@ -207,7 +207,7 @@ checkConfigOutput "empty" config.value.foo ./declare-lazyAttrsOf.nix ./attrsOf-c
# Even with multiple assignments, a type error should be thrown if any of them aren't valid # Even with multiple assignments, a type error should be thrown if any of them aren't valid
checkConfigError 'The option value .* in .* is not of type .*' \ checkConfigError 'A definition for option .* is not of type .*' \
config.value ./declare-int-unsigned-value.nix ./define-value-list.nix ./define-value-int-positive.nix config.value ./declare-int-unsigned-value.nix ./define-value-list.nix ./define-value-int-positive.nix
## Freeform modules ## Freeform modules
@ -216,7 +216,7 @@ checkConfigOutput 24 config.value ./freeform-attrsOf.nix ./define-value-string.n
# No freeform assigments shouldn't make it error # No freeform assigments shouldn't make it error
checkConfigOutput '{ }' config ./freeform-attrsOf.nix checkConfigOutput '{ }' config ./freeform-attrsOf.nix
# but only if the type matches # but only if the type matches
checkConfigError 'The option value .* in .* is not of type .*' config.value ./freeform-attrsOf.nix ./define-value-list.nix checkConfigError 'A definition for option .* is not of type .*' config.value ./freeform-attrsOf.nix ./define-value-list.nix
# and properties should be applied # and properties should be applied
checkConfigOutput yes config.value ./freeform-attrsOf.nix ./define-value-string-properties.nix checkConfigOutput yes config.value ./freeform-attrsOf.nix ./define-value-string-properties.nix
# Options should still be declarable, and be able to have a type that doesn't match the freeform type # Options should still be declarable, and be able to have a type that doesn't match the freeform type
@ -251,7 +251,7 @@ checkConfigOutput / config.value.path ./types-anything/equal-atoms.nix
checkConfigOutput null config.value.null ./types-anything/equal-atoms.nix checkConfigOutput null config.value.null ./types-anything/equal-atoms.nix
checkConfigOutput 0.1 config.value.float ./types-anything/equal-atoms.nix checkConfigOutput 0.1 config.value.float ./types-anything/equal-atoms.nix
# Functions can't be merged together # Functions can't be merged together
checkConfigError "The option .* has conflicting definitions" config.value.multiple-lambdas ./types-anything/functions.nix checkConfigError "The option .* has conflicting definition values" config.value.multiple-lambdas ./types-anything/functions.nix
checkConfigOutput '<LAMBDA>' config.value.single-lambda ./types-anything/functions.nix checkConfigOutput '<LAMBDA>' config.value.single-lambda ./types-anything/functions.nix
# Check that all mk* modifiers are applied # Check that all mk* modifiers are applied
checkConfigError 'attribute .* not found' config.value.mkiffalse ./types-anything/mk-mods.nix checkConfigError 'attribute .* not found' config.value.mkiffalse ./types-anything/mk-mods.nix

View file

@ -299,16 +299,14 @@ rec {
check = isList; check = isList;
merge = loc: defs: merge = loc: defs:
map (x: x.value) (filter (x: x ? value) (concatLists (imap1 (n: def: map (x: x.value) (filter (x: x ? value) (concatLists (imap1 (n: def:
if isList def.value then imap1 (m: def':
imap1 (m: def': (mergeDefinitions
(mergeDefinitions (loc ++ ["[definition ${toString n}-entry ${toString m}]"])
(loc ++ ["[definition ${toString n}-entry ${toString m}]"]) elemType
elemType [{ inherit (def) file; value = def'; }]
[{ inherit (def) file; value = def'; }] ).optionalValue
).optionalValue ) def.value
) def.value ) defs)));
else
throw "The option value `${showOption loc}` in `${def.file}` is not a list.") defs)));
emptyValue = { value = {}; }; emptyValue = { value = {}; };
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]); getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]);
getSubModules = elemType.getSubModules; getSubModules = elemType.getSubModules;