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 <