lib/modules: add suggestions to invalid option name errors (#442263)

This commit is contained in:
Silvan Mosberger 2026-02-03 19:10:35 +00:00 committed by GitHub
commit 58b187378d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 235 additions and 1 deletions

View file

@ -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" ]

View file

@ -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

View file

@ -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";
};
};
}

View file

@ -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;
};
}

View file

@ -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;
};
}

View file

@ -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;
};
}

View file

@ -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;
};
}

View file

@ -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;
};
};
}