Add the tool "nixos-typecheck" that can check an option declaration to:
- Enforce that an option declaration has a "defaultText" if and only if the type of the option derives from "package", "packageSet" or "nixpkgsConfig" and if a "default" attribute is defined. - Enforce that the value of the "example" attribute is wrapped with "literalExample" if the type of the option derives from "package", "packageSet" or "nixpkgsConfig". - Warn if a "defaultText" is defined in an option declaration if the type of the option does not derive from "package", "packageSet" or "nixpkgsConfig". - Warn if no "type" is defined in an option declaration.
This commit is contained in:
+82
-17
@@ -23,8 +23,6 @@ rec {
|
||||
specialArgs ? {}
|
||||
, # This would be remove in the future, Prefer _module.args option instead.
|
||||
args ? {}
|
||||
, # This would be remove in the future, Prefer _module.check option instead.
|
||||
check ? true
|
||||
}:
|
||||
let
|
||||
# This internal module declare internal options under the `_module'
|
||||
@@ -45,9 +43,22 @@ rec {
|
||||
_module.check = mkOption {
|
||||
type = types.bool;
|
||||
internal = true;
|
||||
default = check;
|
||||
default = true;
|
||||
description = "Whether to check whether all option definitions have matching declarations.";
|
||||
};
|
||||
|
||||
_module.typeInference = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
internal = true;
|
||||
default = null; # TODO: Move away from 'null' after enough testing.
|
||||
description = ''
|
||||
Mode of type inferencing. Possible values are:
|
||||
null = Disable type inferencing completely. Use 'types.unspecified' for every option without type definition.
|
||||
"silent" = Try to infer type of option without type definition, but do not print anything.
|
||||
"printUnspecified" = Try to infer type of option without type definition and print options for which no full type could be inferred.
|
||||
"printAll" = Try to infer type of option without type definition and print all options without type definition.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
@@ -60,7 +71,7 @@ rec {
|
||||
# Note: the list of modules is reversed to maintain backward
|
||||
# compatibility with the old module system. Not sure if this is
|
||||
# the most sensible policy.
|
||||
options = mergeModules prefix (reverseList closed);
|
||||
options = mergeModules (config._module) prefix (reverseList closed);
|
||||
|
||||
# Traverse options and extract the option values into the final
|
||||
# config set. At the same time, check whether all option
|
||||
@@ -170,11 +181,11 @@ rec {
|
||||
At the same time, for each option declaration, it will merge the
|
||||
corresponding option definitions in all machines, returning them
|
||||
in the ‘value’ attribute of each option. */
|
||||
mergeModules = prefix: modules:
|
||||
mergeModules' prefix modules
|
||||
mergeModules = _module: prefix: modules:
|
||||
mergeModules' _module prefix modules
|
||||
(concatMap (m: map (config: { inherit (m) file; inherit config; }) (pushDownProperties m.config)) modules);
|
||||
|
||||
mergeModules' = prefix: options: configs:
|
||||
mergeModules' = _module: prefix: options: configs:
|
||||
listToAttrs (map (name: {
|
||||
# We're descending into attribute ‘name’.
|
||||
inherit name;
|
||||
@@ -200,8 +211,8 @@ rec {
|
||||
(filter (m: m.config ? ${name}) configs);
|
||||
in
|
||||
if nrOptions == length decls then
|
||||
let opt = fixupOptionType loc (mergeOptionDecls loc decls);
|
||||
in evalOptionValue loc opt defns'
|
||||
let opt = fixupOptionType _module.typeInference loc (mergeOptionDecls loc decls);
|
||||
in evalOptionValue _module loc opt defns'
|
||||
else if nrOptions != 0 then
|
||||
let
|
||||
firstOption = findFirst (m: isOption m.options) "" decls;
|
||||
@@ -209,7 +220,7 @@ rec {
|
||||
in
|
||||
throw "The option `${showOption loc}' in `${firstOption.file}' is a prefix of options in `${firstNonOption.file}'."
|
||||
else
|
||||
mergeModules' loc decls defns;
|
||||
mergeModules' _module loc decls defns;
|
||||
}) (concatMap (m: attrNames m.options) options))
|
||||
// { _definedNames = map (m: { inherit (m) file; names = attrNames m.config; }) configs; };
|
||||
|
||||
@@ -258,7 +269,7 @@ rec {
|
||||
|
||||
/* Merge all the definitions of an option to produce the final
|
||||
config value. */
|
||||
evalOptionValue = loc: opt: defs:
|
||||
evalOptionValue = _module: loc: opt: defs:
|
||||
let
|
||||
# Add in the default value for this option, if any.
|
||||
defs' =
|
||||
@@ -270,7 +281,7 @@ rec {
|
||||
if opt.readOnly or false && length defs' > 1 then
|
||||
throw "The option `${showOption loc}' is read-only, but it's set multiple times."
|
||||
else
|
||||
mergeDefinitions loc opt.type defs';
|
||||
mergeDefinitions _module loc opt.type defs';
|
||||
|
||||
# Check whether the option is defined, and apply the ‘apply’
|
||||
# function to the merged value. This allows options to yield a
|
||||
@@ -291,7 +302,7 @@ rec {
|
||||
};
|
||||
|
||||
# Merge definitions of a value of a given type.
|
||||
mergeDefinitions = loc: type: defs: rec {
|
||||
mergeDefinitions = _module: loc: type: defs: rec {
|
||||
defsFinal =
|
||||
let
|
||||
# Process mkMerge and mkIf properties.
|
||||
@@ -314,7 +325,7 @@ rec {
|
||||
mergedValue = foldl' (res: def:
|
||||
if type.check def.value then res
|
||||
else throw "The option value `${showOption loc}' in `${def.file}' is not a ${type.name}.")
|
||||
(type.merge loc defsFinal) defsFinal;
|
||||
(type.merge _module loc defsFinal) defsFinal;
|
||||
|
||||
isDefined = defsFinal != [];
|
||||
|
||||
@@ -412,7 +423,7 @@ rec {
|
||||
/* Hack for backward compatibility: convert options of type
|
||||
optionSet to options of type submodule. FIXME: remove
|
||||
eventually. */
|
||||
fixupOptionType = loc: opt:
|
||||
fixupOptionType = typeInference: loc: opt:
|
||||
let
|
||||
options = opt.options or
|
||||
(throw "Option `${showOption loc'}' has type optionSet but has no option attribute, in ${showFiles opt.declarations}.");
|
||||
@@ -424,12 +435,66 @@ rec {
|
||||
else if tp.name == "list of option sets" then types.listOf (types.submodule options)
|
||||
else if tp.name == "null or option set" then types.nullOr (types.submodule options)
|
||||
else tp;
|
||||
|
||||
in
|
||||
if opt.type.getSubModules or null == null
|
||||
then opt // { type = f (opt.type or types.unspecified); }
|
||||
then opt // { type = f (opt.type or (inferType typeInference loc opt)); }
|
||||
else opt // { type = opt.type.substSubModules opt.options; options = []; };
|
||||
|
||||
|
||||
/* Function that tries to infer the type of an option from the default value of the option. */
|
||||
inferType = mode: loc: opt:
|
||||
let
|
||||
doc = x: elemAt x 0;
|
||||
type = x: elemAt x 1;
|
||||
containsUnspecified = x: elemAt x 2;
|
||||
inferType' = def:
|
||||
if isDerivation def then [ "package" types.package false ]
|
||||
else if isBool def then [ "bool" types.bool false ]
|
||||
else if builtins.isString def then [ "str" types.str false ]
|
||||
else if isInt def then [ "int" types.int false ]
|
||||
else if isFunction def then [ "functionTo unspecified" (types.functionTo types.unspecified) true ]
|
||||
else if isList def then
|
||||
let nestedType = if (length def > 0) && (all (x: (type (inferType' x)) == (type (inferType' (head def)))) def)
|
||||
then inferType' (head def)
|
||||
else [ "unspecified" types.unspecified true ];
|
||||
in [ "listOf ${doc nestedType}" (types.listOf (type nestedType)) (containsUnspecified nestedType) ]
|
||||
else if isAttrs def then
|
||||
let list = mapAttrsToList (_: v: v) (removeAttrs def ["_args"]);
|
||||
nestedType = if (length list > 0) && (all (x: (type (inferType' x)) == (type (inferType' (head list)))) list)
|
||||
then inferType' (head list)
|
||||
else [ "unspecified" types.unspecified true ];
|
||||
in [ "attrsOf ${doc nestedType}" (types.attrsOf (type nestedType)) (containsUnspecified nestedType) ]
|
||||
else [ "unspecified" types.unspecified true ];
|
||||
|
||||
inferDoc = x: ''
|
||||
Inferring the type of "${showOption loc}" to "${doc x}".
|
||||
Please verify the inferred type and define the type explicitely in ${showFiles opt.declarations}!
|
||||
'';
|
||||
|
||||
inferredType = printMode:
|
||||
let inferred = inferType' opt.default;
|
||||
in if printMode == "silent" then type inferred
|
||||
else if printMode == "printAll" then builtins.trace (inferDoc inferred) (type inferred)
|
||||
else if printMode == "printUnspecified" && (containsUnspecified inferred) then builtins.trace (inferDoc inferred) (type inferred)
|
||||
else type inferred;
|
||||
|
||||
noInferDoc = ''
|
||||
Could not infer a type for "${showOption loc}", using "unspecified" instead.
|
||||
Please define the type explicitely in ${showFiles opt.declarations}!
|
||||
'';
|
||||
|
||||
hasDefault = (opt ? default) && !(opt ? defaultText);
|
||||
isExternalVisible = (opt.visible or true) && !(opt.internal or false);
|
||||
in
|
||||
|
||||
if isNull mode || !isExternalVisible
|
||||
then types.unspecified
|
||||
else if hasDefault
|
||||
then inferredType mode /* Set to 'true' to see every type that is being inferred, not just those types that result in 'unspecified'. */
|
||||
else if mode != "silent" then builtins.trace noInferDoc types.unspecified else types.unspecified;
|
||||
|
||||
|
||||
/* Properties. */
|
||||
|
||||
mkIf = condition: content:
|
||||
@@ -497,7 +562,7 @@ rec {
|
||||
|
||||
|
||||
/* Compatibility. */
|
||||
fixMergeModules = modules: args: evalModules { inherit modules args; check = false; };
|
||||
fixMergeModules = modules: args: evalModules { inherit args; modules = (modules ++ [{ _module.check = false; }]); };
|
||||
|
||||
|
||||
/* Return a module that causes a warning to be shown if the
|
||||
|
||||
Reference in New Issue
Block a user