From 0f54833b4a093521fe640a7bd8a67766d21e2db5 Mon Sep 17 00:00:00 2001 From: Winter Date: Thu, 11 Sep 2025 22:48:47 -0400 Subject: [PATCH 1/4] lib/modules: add suggestions to invalid option name errors This change introduces the suggesting of possible valid option names for a given invalid option, based on the keys of said invalid option's parent attrset. That is, `foo.bar.baz` will only be suggested keys from `foo.bar`, while `(config).baz` will only be suggested keys from the toplevel. --- lib/modules.nix | 15 ++++++++++++++- lib/tests/modules.sh | 4 ++++ lib/tests/modules/error-typo-nested.nix | 18 ++++++++++++++++++ .../modules/error-typo-outside-with-nested.nix | 18 ++++++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 lib/tests/modules/error-typo-nested.nix create mode 100644 lib/tests/modules/error-typo-outside-with-nested.nix diff --git a/lib/modules.nix b/lib/modules.nix index c55d276d4970..fd5cf76eda03 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -20,12 +20,14 @@ let head id imap1 + init isAttrs isBool isFunction oldestSupportedReleaseIsAtLeast isList isString + last length mapAttrs mapAttrsToList @@ -35,6 +37,7 @@ let optionalAttrs optionalString recursiveUpdate + remove reverseList sort seq @@ -60,6 +63,7 @@ let ; inherit (lib.strings) isConvertibleWithToString + levenshtein ; showDeclPrefix = @@ -303,8 +307,17 @@ 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 ])); + + prefix' = init (prefix ++ firstDef.prefix); + adj = attrNames (attrByPath prefix' { } options); + adj' = if prefix' == [ ] then remove "_module" adj else adj; + lev = levenshtein (last firstDef.prefix); + closest = if adj' == [ ] then null else head (sort (p: q: lev p < lev q) adj'); + suggestion = + optionalString (closest != null) + "\n\nDid you mean `${showOption (prefix' ++ [ closest ])}'?"; 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 a4aa5201715c..69fb2aa9c7ae 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -741,6 +741,10 @@ checkConfigError 'attribute .*bar.* not found' config.sub.conditionalImportAsNix checkConfigError 'attribute .*foo.* not found' config.sub.conditionalImportAsDarwin.foo ./specialArgs-class.nix checkConfigOutput '"foo"' config.sub.conditionalImportAsDarwin.bar ./specialArgs-class.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 + cat < Date: Tue, 28 Oct 2025 15:16:50 +0100 Subject: [PATCH 2/4] lib/modules: Support multiple suggestions, optimize --- lib/modules.nix | 32 +++++++++-- lib/tests/modules.sh | 2 + .../modules/error-typo-large-attrset.nix | 56 +++++++++++++++++++ .../error-typo-multiple-suggestions.nix | 22 ++++++++ 4 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 lib/tests/modules/error-typo-large-attrset.nix create mode 100644 lib/tests/modules/error-typo-multiple-suggestions.nix diff --git a/lib/modules.nix b/lib/modules.nix index fd5cf76eda03..eb4afd03d03b 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -12,6 +12,7 @@ let concatMap concatStringsSep elem + elemAt filter foldl' functionArgs @@ -36,13 +37,16 @@ let optional optionalAttrs optionalString + pipe recursiveUpdate remove reverseList sort + sortOn seq setAttrByPath substring + take throwIfNot trace typeOf @@ -64,6 +68,7 @@ let inherit (lib.strings) isConvertibleWithToString levenshtein + levenshteinAtMost ; showDeclPrefix = @@ -311,11 +316,30 @@ let prefix' = init (prefix ++ firstDef.prefix); adj = attrNames (attrByPath prefix' { } options); adj' = if prefix' == [ ] then remove "_module" adj else adj; - lev = levenshtein (last firstDef.prefix); - closest = if adj' == [ ] then null else head (sort (p: q: lev p < lev q) adj'); + invalidOptName = last firstDef.prefix; + # For small option sets, check all; for large sets, only check distance ≤ 2 + suggestions = + if length adj' < 100 then + pipe adj' [ + (sortOn (levenshtein invalidOptName)) + (take 3) + ] + else + pipe adj' [ + # levenshteinAtMost is only fast for distance ≤ 2 + (filter (levenshteinAtMost 2 invalidOptName)) + (sortOn (levenshtein invalidOptName)) + (take 3) + ]; suggestion = - optionalString (closest != null) - "\n\nDid you mean `${showOption (prefix' ++ [ closest ])}'?"; + if suggestions == [ ] then + "" + else if length suggestions == 1 then + "\n\nDid you mean `${showOption (prefix' ++ [ (head suggestions) ])}'?" + else + "\n\nDid you mean ${ + concatStringsSep ", " (map (s: "`${showOption (prefix' ++ [ s ])}'") (init suggestions)) + } or `${showOption (prefix' ++ [ (last suggestions) ])}'?"; in "The option `${optText}' does not exist. Definition values:${defText}${suggestion}"; in diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 69fb2aa9c7ae..af40d521635e 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -744,6 +744,8 @@ checkConfigOutput '"foo"' config.sub.conditionalImportAsDarwin.bar ./specialArgs # 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 cat < Date: Tue, 28 Oct 2025 15:22:17 +0100 Subject: [PATCH 3/4] lib/modules: Fix suggestions in submodules --- lib/modules.nix | 6 ++- lib/tests/modules.sh | 2 + .../modules/error-typo-deeply-nested.nix | 42 +++++++++++++++++++ lib/tests/modules/error-typo-submodule.nix | 28 +++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 lib/tests/modules/error-typo-deeply-nested.nix create mode 100644 lib/tests/modules/error-typo-submodule.nix diff --git a/lib/modules.nix b/lib/modules.nix index eb4afd03d03b..6ae8f90ee8f3 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -314,8 +314,10 @@ let (addErrorContext "while evaluating a definition from `${firstDef.file}'" (showDefs [ firstDef ])); prefix' = init (prefix ++ firstDef.prefix); - adj = attrNames (attrByPath prefix' { } options); - adj' = if prefix' == [ ] then remove "_module" adj else adj; + # In submodules, prefix is absolute, but options and most variables are relative to the submodule prefix. + lookupPath = init firstDef.prefix; + adj = attrNames (attrByPath lookupPath { } options); + adj' = if lookupPath == [ ] then remove "_module" adj else adj; invalidOptName = last firstDef.prefix; # For small option sets, check all; for large sets, only check distance ≤ 2 suggestions = diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index af40d521635e..fcf8420116c0 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -746,6 +746,8 @@ checkConfigError 'Did you mean .set\.enable.\?' config.set ./error-typo-nested.n 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 < Date: Tue, 28 Oct 2025 15:26:12 +0100 Subject: [PATCH 4/4] lib/modules: Clarify variable names in suggestions logic --- lib/modules.nix | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/lib/modules.nix b/lib/modules.nix index 6ae8f90ee8f3..912d609ba62c 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -313,35 +313,38 @@ let "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 ])); - prefix' = init (prefix ++ firstDef.prefix); - # In submodules, prefix is absolute, but options and most variables are relative to the submodule prefix. - lookupPath = init firstDef.prefix; - adj = attrNames (attrByPath lookupPath { } options); - adj' = if lookupPath == [ ] then remove "_module" adj else adj; - invalidOptName = last firstDef.prefix; + # 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 adj' < 100 then - pipe adj' [ - (sortOn (levenshtein invalidOptName)) + if length candidateNames < 100 then + pipe candidateNames [ + (sortOn (levenshtein invalidOptionName)) (take 3) ] else - pipe adj' [ + pipe candidateNames [ # levenshteinAtMost is only fast for distance ≤ 2 - (filter (levenshteinAtMost 2 invalidOptName)) - (sortOn (levenshtein invalidOptName)) + (filter (levenshteinAtMost 2 invalidOptionName)) + (sortOn (levenshtein invalidOptionName)) (take 3) ]; suggestion = if suggestions == [ ] then "" else if length suggestions == 1 then - "\n\nDid you mean `${showOption (prefix' ++ [ (head suggestions) ])}'?" + "\n\nDid you mean `${showOption (absInvalidOptionParent ++ [ (head suggestions) ])}'?" else "\n\nDid you mean ${ - concatStringsSep ", " (map (s: "`${showOption (prefix' ++ [ s ])}'") (init suggestions)) - } or `${showOption (prefix' ++ [ (last suggestions) ])}'?"; + concatStringsSep ", " ( + map (s: "`${showOption (absInvalidOptionParent ++ [ s ])}'") (init suggestions) + ) + } or `${showOption (absInvalidOptionParent ++ [ (last suggestions) ])}'?"; in "The option `${optText}' does not exist. Definition values:${defText}${suggestion}"; in