diff --git a/lib/tests/misc.nix b/lib/tests/misc.nix index f5f1fb5e7c2d..d17231061d69 100644 --- a/lib/tests/misc.nix +++ b/lib/tests/misc.nix @@ -2830,6 +2830,42 @@ runTests { expected = "unknown"; }; + # https://github.com/NixOS/nixpkgs/issues/396849 + "test: submodule definitions aren't unchecked when evaluating submodule documentation" = { + expr = + let + module = + { lib, ... }: + { + options.foo = lib.mkOption { type = lib.types.submodule submodule; }; + }; + + submodule = { + options.bar = lib.mkOption { type = lib.types.int; }; + config.submoduleWrong = throw "yikes"; + }; + + options = (evalModules { modules = [ module ]; }).options; + + renderableOpts = filter (o: !o.internal) (optionAttrSetToDocList options); + # Evaluate the whole docs + in + builtins.deepSeq renderableOpts + # Return the locations + (map (o: o.loc) renderableOpts); + expected = [ + [ + "_module" + "args" + ] + [ "foo" ] + [ + "foo" + "bar" + ] + ]; + }; + testFreeformOptions = { expr = let diff --git a/lib/types.nix b/lib/types.nix index 11b1b5463bc7..92cdb1491c7c 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -290,6 +290,15 @@ let unparenthesize: t: if unparenthesize (t.descriptionClass or null) then t.description else "(${t.description})"; + noCheckForDocsModule = { + # When generating documentation, our goal isn't to check anything. + # Quite the opposite in fact. Generating docs is somewhat of a + # challenge, evaluating modules in a *lacking* context. Anything + # that makes the docs avoid an error is a win. + config._module.check = lib.mkForce false; + _file = ""; + }; + # When adding new types don't forget to document them in # nixos/doc/manual/development/option-types.section.md! types = rec { @@ -1209,7 +1218,14 @@ let in mkOptionType { inherit name; - description = if description != null then description else freeformType.description or name; + description = + if description != null then + description + else + let + docsEval = base.extendModules { modules = [ noCheckForDocsModule ]; }; + in + docsEval._module.freeformType.description or name; check = x: isAttrs x || isFunction x || path.check x; merge = loc: defs: @@ -1222,7 +1238,18 @@ let }; getSubOptions = prefix: - (base.extendModules { inherit prefix; }).options + let + docsEval = ( + base.extendModules { + inherit prefix; + modules = [ noCheckForDocsModule ]; + } + ); + # Intentionally shadow the freeformType from the possibly *checked* + # configuration. See `noCheckForDocsModule` comment. + inherit (docsEval._module) freeformType; + in + docsEval.options // optionalAttrs (freeformType != null) { # Expose the sub options of the freeform type. Note that the option # discovery doesn't care about the attribute name used here, so this