summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorSilvan Mosberger <github@infinisil.com>2026-02-03 19:10:35 +0000
committerGitHub <noreply@github.com>2026-02-03 19:10:35 +0000
commit58b187378d0e5ce84df8633e014fa8b961de4bab (patch)
treefa980c84091fa4265a7a4f25a215f1df12c6786c /lib
parentkdePackages.kdenlive: add workaround for broken shaderc pc file (#486634) (diff)
parentlib/modules: Clarify variable names in suggestions logic (diff)
downloadnixpkgs-58b187378d0e5ce84df8633e014fa8b961de4bab.tar.gz
lib/modules: add suggestions to invalid option name errors (#442263)
Diffstat (limited to 'lib')
-rw-r--r--lib/modules.nix44
-rwxr-xr-xlib/tests/modules.sh8
-rw-r--r--lib/tests/modules/error-typo-deeply-nested.nix42
-rw-r--r--lib/tests/modules/error-typo-large-attrset.nix56
-rw-r--r--lib/tests/modules/error-typo-multiple-suggestions.nix22
-rw-r--r--lib/tests/modules/error-typo-nested.nix18
-rw-r--r--lib/tests/modules/error-typo-outside-with-nested.nix18
-rw-r--r--lib/tests/modules/error-typo-submodule.nix28
8 files changed, 235 insertions, 1 deletions
diff --git a/lib/modules.nix b/lib/modules.nix
index e545f9c9707d..6d430effcc89 100644
--- a/lib/modules.nix
+++ b/lib/modules.nix
@@ -12,6 +12,7 @@ let
concatMap
concatStringsSep
elem
+ elemAt
filter
foldl'
functionArgs
@@ -20,12 +21,14 @@ let
head
id
imap1
+ init
isAttrs
isBool
isFunction
oldestSupportedReleaseIsAtLeast
isList
isString
+ last
length
mapAttrs
mapAttrsToList
@@ -34,12 +37,16 @@ let
optional
optionalAttrs
optionalString
+ pipe
recursiveUpdate
+ remove
reverseList
sort
+ sortOn
seq
setAttrByPath
substring
+ take
throwIfNot
trace
typeOf
@@ -60,6 +67,8 @@ let
;
inherit (lib.strings)
isConvertibleWithToString
+ levenshtein
+ levenshteinAtMost
;
showDeclPrefix =
@@ -304,8 +313,41 @@ let
addErrorContext
"while evaluating the error message for definitions for `${optText}', which is an option that does not exist"
(addErrorContext "while evaluating a definition from `${firstDef.file}'" (showDefs [ firstDef ]));
+
+ # absInvalidOptionParent is absolute; other variables are relative to the submodule prefix
+ absInvalidOptionParent = init (prefix ++ firstDef.prefix);
+ invalidOptionParent = init firstDef.prefix;
+ siblingOptionNames = attrNames (attrByPath invalidOptionParent { } options);
+ candidateNames =
+ if invalidOptionParent == [ ] then remove "_module" siblingOptionNames else siblingOptionNames;
+ invalidOptionName = last firstDef.prefix;
+ # For small option sets, check all; for large sets, only check distance ≤ 2
+ suggestions =
+ if length candidateNames < 100 then
+ pipe candidateNames [
+ (sortOn (levenshtein invalidOptionName))
+ (take 3)
+ ]
+ else
+ pipe candidateNames [
+ # levenshteinAtMost is only fast for distance ≤ 2
+ (filter (levenshteinAtMost 2 invalidOptionName))
+ (sortOn (levenshtein invalidOptionName))
+ (take 3)
+ ];
+ suggestion =
+ if suggestions == [ ] then
+ ""
+ else if length suggestions == 1 then
+ "\n\nDid you mean `${showOption (absInvalidOptionParent ++ [ (head suggestions) ])}'?"
+ else
+ "\n\nDid you mean ${
+ concatStringsSep ", " (
+ map (s: "`${showOption (absInvalidOptionParent ++ [ s ])}'") (init suggestions)
+ )
+ } or `${showOption (absInvalidOptionParent ++ [ (last suggestions) ])}'?";
in
- "The option `${optText}' does not exist. Definition values:${defText}";
+ "The option `${optText}' does not exist. Definition values:${defText}${suggestion}";
in
if
attrNames options == [ "_module" ]
diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh
index d148cd1839da..aa9d3443fb6a 100755
--- a/lib/tests/modules.sh
+++ b/lib/tests/modules.sh
@@ -870,6 +870,14 @@ checkConfigError 'A definition for option .* is not of type .*' config.addCheckF
checkConfigOutput '^true$' config.result ./v2-check-coherence.nix
+# Option name suggestions
+checkConfigError 'Did you mean .set\.enable.\?' config.set ./error-typo-nested.nix
+checkConfigError 'Did you mean .set.\?' config ./error-typo-outside-with-nested.nix
+checkConfigError 'Did you mean .bar., .baz. or .foo.\?' config ./error-typo-multiple-suggestions.nix
+checkConfigError 'Did you mean .enable., .ebe. or .enabled.\?' config ./error-typo-large-attrset.nix
+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
+
cat <<EOF
====== module tests ======
$pass Pass
diff --git a/lib/tests/modules/error-typo-deeply-nested.nix b/lib/tests/modules/error-typo-deeply-nested.nix
new file mode 100644
index 000000000000..978bd088494d
--- /dev/null
+++ b/lib/tests/modules/error-typo-deeply-nested.nix
@@ -0,0 +1,42 @@
+{ lib, ... }:
+
+{
+ options.services = {
+ nginx = {
+ enable = lib.mkOption {
+ default = false;
+ type = lib.types.bool;
+ };
+ virtualHosts = lib.mkOption {
+ type = lib.types.attrsOf (
+ lib.types.submodule {
+ options = {
+ enableSSL = lib.mkOption {
+ default = false;
+ type = lib.types.bool;
+ };
+ ssl = {
+ certificate = lib.mkOption {
+ default = "";
+ type = lib.types.str;
+ };
+ certificateKey = lib.mkOption {
+ default = "";
+ type = lib.types.str;
+ };
+ };
+ };
+ }
+ );
+ default = { };
+ };
+ };
+ };
+
+ config = {
+ services.nginx.virtualHosts."example.com" = {
+ # Typo: "certficate" instead of "certificate" (nested within submodule)
+ ssl.certficate = "/path/to/cert";
+ };
+ };
+}
diff --git a/lib/tests/modules/error-typo-large-attrset.nix b/lib/tests/modules/error-typo-large-attrset.nix
new file mode 100644
index 000000000000..0991f9a2c9e2
--- /dev/null
+++ b/lib/tests/modules/error-typo-large-attrset.nix
@@ -0,0 +1,56 @@
+{ lib, ... }:
+
+let
+ inherit (lib) mkOption concatMapAttrs;
+
+ ten = {
+ a = null;
+ b = null;
+ c = null;
+ d = null;
+ e = null;
+ f = null;
+ g = null;
+ h = null;
+ i = null;
+ j = null;
+ };
+
+ # Generate 1000 options (10 * 10 * 10)
+ generatedOptions = concatMapAttrs (
+ k1: _:
+ concatMapAttrs (
+ k2: _:
+ concatMapAttrs (k3: _: {
+ "${k1}${k2}${k3}" = mkOption {
+ type = lib.types.bool;
+ default = false;
+ };
+ }) ten
+ ) ten
+ ) ten;
+
+ # Add some sensible options that are close to our typo
+ sensibleOptions = {
+ enable = mkOption {
+ type = lib.types.bool;
+ default = false;
+ };
+ enabled = mkOption {
+ type = lib.types.bool;
+ default = false;
+ };
+ disable = mkOption {
+ type = lib.types.bool;
+ default = false;
+ };
+ };
+in
+{
+ options = generatedOptions // sensibleOptions;
+
+ config = {
+ # Typo: "enble" is distance 1 from "enable"
+ enble = true;
+ };
+}
diff --git a/lib/tests/modules/error-typo-multiple-suggestions.nix b/lib/tests/modules/error-typo-multiple-suggestions.nix
new file mode 100644
index 000000000000..d01d649c3a05
--- /dev/null
+++ b/lib/tests/modules/error-typo-multiple-suggestions.nix
@@ -0,0 +1,22 @@
+{ lib, ... }:
+
+{
+ options.foo = lib.mkOption {
+ default = false;
+ type = lib.types.bool;
+ };
+
+ options.bar = lib.mkOption {
+ default = false;
+ type = lib.types.bool;
+ };
+
+ options.baz = lib.mkOption {
+ default = false;
+ type = lib.types.bool;
+ };
+
+ config = {
+ far = true;
+ };
+}
diff --git a/lib/tests/modules/error-typo-nested.nix b/lib/tests/modules/error-typo-nested.nix
new file mode 100644
index 000000000000..f29cd03f803d
--- /dev/null
+++ b/lib/tests/modules/error-typo-nested.nix
@@ -0,0 +1,18 @@
+{ lib, ... }:
+
+{
+ options.set = {
+ enable = lib.mkOption {
+ default = false;
+ example = true;
+ type = lib.types.bool;
+ description = ''
+ Some descriptive text
+ '';
+ };
+ };
+
+ config = {
+ set.ena = true;
+ };
+}
diff --git a/lib/tests/modules/error-typo-outside-with-nested.nix b/lib/tests/modules/error-typo-outside-with-nested.nix
new file mode 100644
index 000000000000..d4f181673c50
--- /dev/null
+++ b/lib/tests/modules/error-typo-outside-with-nested.nix
@@ -0,0 +1,18 @@
+{ lib, ... }:
+
+{
+ options.set = {
+ enable = lib.mkOption {
+ default = false;
+ example = true;
+ type = lib.types.bool;
+ description = ''
+ Some descriptive text
+ '';
+ };
+ };
+
+ config = {
+ sea.enable = true;
+ };
+}
diff --git a/lib/tests/modules/error-typo-submodule.nix b/lib/tests/modules/error-typo-submodule.nix
new file mode 100644
index 000000000000..5a9db862e584
--- /dev/null
+++ b/lib/tests/modules/error-typo-submodule.nix
@@ -0,0 +1,28 @@
+{ lib, ... }:
+
+{
+ options.services = lib.mkOption {
+ type = lib.types.attrsOf (
+ lib.types.submodule {
+ options = {
+ enable = lib.mkOption {
+ default = false;
+ type = lib.types.bool;
+ };
+ port = lib.mkOption {
+ default = 8080;
+ type = lib.types.int;
+ };
+ };
+ }
+ );
+ default = { };
+ };
+
+ config = {
+ services.myservice = {
+ # Typo: "prot" instead of "port"
+ prot = 9000;
+ };
+ };
+}