diff options
author | Thomas Strobel <ts468@cam.ac.uk> | 2016-02-20 01:47:01 +0100 |
---|---|---|
committer | Thomas Strobel <ts468@cam.ac.uk> | 2016-02-29 01:09:00 +0100 |
commit | cad8957eabcbf73062226d28366fd446c15c8737 (patch) | |
tree | 0299e96391c14f612e7bd1cf3b2274198425fd61 /lib/types.nix | |
parent | c483224c82c8e94324c03576e64c5dfbf16bd2f8 (diff) |
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.
Diffstat (limited to 'lib/types.nix')
-rw-r--r-- | lib/types.nix | 217 |
1 files changed, 169 insertions, 48 deletions
diff --git a/lib/types.nix b/lib/types.nix index b4d29ac84d28..385103d42149 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -1,6 +1,8 @@ # Definitions related to run-time type checking. Used in particular # to type-check NixOS configurations. +let lib = import ./default.nix; in + with import ./lists.nix; with import ./attrsets.nix; with import ./options.nix; @@ -21,6 +23,8 @@ rec { mkOptionType = { # Human-readable representation of the type. name + , # Parseable representation of the type. + typerep , # Function applied to each definition that should return true if # its type-correct, false otherwise. check ? (x: true) @@ -31,40 +35,59 @@ rec { # definition values and locations (e.g. [ { file = "/foo.nix"; # value = 1; } { file = "/bar.nix"; value = 2 } ]). merge ? mergeDefaultOption - , # Return a flat list of sub-options. Used to generate - # documentation. - getSubOptions ? prefix: {} + , # Return list of sub-options. + getSubOptions ? {} + , # Same as 'getSubOptions', but with extra information about the + # location of the option which is used to generate documentation. + getSubOptionsPrefixed ? null , # List of modules if any, or null if none. getSubModules ? null , # Function for building the same option type with a different list of # modules. substSubModules ? m: null + , # List of type representations (typerep) of all the elementary types + # that are nested within the type. For an elementary type the list is + # a singleton of the typerep of itself. + # NOTE: Must be specified for every container type! + nestedTypes ? null + , # List of all default values, and an empty list if no default value exists. + defaultValues ? [] }: { _type = "option-type"; - inherit name check merge getSubOptions getSubModules substSubModules; + inherit name typerep check merge getSubOptions getSubModules substSubModules defaultValues; + nestedTypes = if (isNull nestedTypes) then (singleton typerep) else nestedTypes; + getSubOptionsPrefixed = if (isNull getSubOptionsPrefixed) then (prefix: getSubOptions) else getSubOptionsPrefixed; }; types = rec { + # + # Elementary types + # + unspecified = mkOptionType { name = "unspecified"; + typerep = "(unspecified)"; }; bool = mkOptionType { name = "boolean"; + typerep = "(boolean)"; check = isBool; merge = mergeEqualOption; }; int = mkOptionType { name = "integer"; + typerep = "(integer)"; check = isInt; merge = mergeOneOption; }; str = mkOptionType { name = "string"; + typerep = "(string)"; check = isString; merge = mergeOneOption; }; @@ -72,73 +95,111 @@ rec { # Merge multiple definitions by concatenating them (with the given # separator between the values). separatedString = sep: mkOptionType { - name = "string"; + name = "string" + (optionalString (sep != "") " separated by ${sep}"); + typerep = "(separatedString(${escape ["(" ")"] sep}))"; check = isString; - merge = loc: defs: concatStringsSep sep (getValues defs); + merge = _module: loc: defs: concatStringsSep sep (getValues defs); }; - lines = separatedString "\n"; - commas = separatedString ","; - envVar = separatedString ":"; + lines = separatedString "\n" // { typerep = "(lines)"; }; + commas = separatedString "," // { typerep = "(commas)"; }; + envVar = separatedString ":" // { typerep = "(envVar)"; }; # Deprecated; should not be used because it quietly concatenates # strings, which is usually not what you want. - string = separatedString ""; + string = separatedString "" // { typerep = "(string)"; }; attrs = mkOptionType { name = "attribute set"; + typerep = "(attrs)"; check = isAttrs; - merge = loc: foldl' (res: def: mergeAttrs res def.value) {}; + merge = _module: loc: foldl' (res: def: mergeAttrs res def.value) {}; }; # derivation is a reserved keyword. package = mkOptionType { name = "package"; + typerep = "(package)"; check = x: isDerivation x || isStorePath x; - merge = loc: defs: - let res = mergeOneOption loc defs; + merge = _module: loc: defs: + let res = mergeOneOption _module loc defs; in if isDerivation res then res else toDerivation res; }; + # The correct type of packageSet would be: + # packageSet = attrsOf (either package packageSet) + # (Not sure if nix would allow to define a recursive type.) + # However, currently it is not possible to check that a packageSet actually + # contains packages (that is derivations). The check 'isDerivation' is too + # eager for the current implementation of the assertion mechanism and of the + # licenses control mechanism. That means it is not generally possible to go + # into the attribute set of packages to check that every attribute would + # evaluate to a derivation if the package would actually be evaluated. Maybe + # that restriction can be lifted in the future, but for now the content of + # the packageSet is not checked. + # TODO: The 'merge' function is copied from 'mergeDefaultOption' to keep + # backwards compatibility with the 'unspecified' type that was used for + # package sets previously. Maybe review if the merge function has to change. + packageSet = mkOptionType { + name = "derivation set"; + typerep = "(packageSet)"; + check = isAttrs; + merge = _module: loc: defs: foldl' mergeAttrs {} (map (x: x.value) defs); + }; + path = mkOptionType { name = "path"; + typerep = "(path)"; # Hacky: there is no ‘isPath’ primop. check = x: builtins.substring 0 1 (toString x) == "/"; merge = mergeOneOption; }; + + # + # Container types + # + # drop this in the future: list = builtins.trace "`types.list' is deprecated; use `types.listOf' instead" types.listOf; listOf = elemType: mkOptionType { name = "list of ${elemType.name}s"; + typerep = "(listOf${elemType.typerep})"; check = isList; - merge = loc: defs: + merge = _module: loc: defs: map (x: x.value) (filter (x: x ? value) (concatLists (imap (n: def: imap (m: def': - (mergeDefinitions + (mergeDefinitions _module (loc ++ ["[definition ${toString n}-entry ${toString m}]"]) elemType [{ inherit (def) file; value = def'; }] ).optionalValue ) def.value) defs))); - getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]); + getSubOptions = elemType.getSubOptions; + getSubOptionsPrefixed = prefix: elemType.getSubOptionsPrefixed (prefix ++ ["*"]); getSubModules = elemType.getSubModules; substSubModules = m: listOf (elemType.substSubModules m); + nestedTypes = elemType.nestedTypes; + defaultValues = [[]]; }; attrsOf = elemType: mkOptionType { name = "attribute set of ${elemType.name}s"; + typerep = "(attrsOf${elemType.typerep})"; check = isAttrs; - merge = loc: defs: + merge = _module: loc: defs: mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs: - (mergeDefinitions (loc ++ [name]) elemType defs).optionalValue + (mergeDefinitions _module (loc ++ [name]) elemType defs).optionalValue ) # Push down position info. (map (def: listToAttrs (mapAttrsToList (n: def': { name = n; value = { inherit (def) file; value = def'; }; }) def.value)) defs))); - getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]); + getSubOptions = elemType.getSubOptions; + getSubOptionsPrefixed = prefix: elemType.getSubOptionsPrefixed (prefix ++ ["<name>"]); getSubModules = elemType.getSubModules; substSubModules = m: attrsOf (elemType.substSubModules m); + nestedTypes = elemType.nestedTypes; + defaultValues = [{}]; }; # List or attribute set of ... @@ -159,18 +220,23 @@ rec { attrOnly = attrsOf elemType; in mkOptionType { name = "list or attribute set of ${elemType.name}s"; + typerep = "(loaOf${elemType.typerep})"; check = x: isList x || isAttrs x; - merge = loc: defs: attrOnly.merge loc (imap convertIfList defs); - getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name?>"]); + merge = _module: loc: defs: attrOnly.merge _module loc (imap convertIfList defs); + getSubOptions = elemType.getSubOptions; + getSubOptionsPrefixed = prefix: elemType.getSubOptionsPrefixed (prefix ++ ["<name?>"]); getSubModules = elemType.getSubModules; substSubModules = m: loaOf (elemType.substSubModules m); + nestedTypes = elemType.nestedTypes; + defaultValues = [{} []]; }; # List or element of ... loeOf = elemType: mkOptionType { name = "element or list of ${elemType.name}s"; + typerep = "(loeOf${elemType.typerep})"; check = x: isList x || elemType.check x; - merge = loc: defs: + merge = _module: loc: defs: let defs' = filterOverrides defs; res = (head defs').value; @@ -181,81 +247,136 @@ rec { else if !isString res then throw "The option `${showOption loc}' does not have a string value, in ${showFiles (getFiles defs)}." else res; + nestedTypes = elemType.nestedTypes; + defaultValues = [[]] ++ elemType.defaultValues; }; uniq = elemType: mkOptionType { - inherit (elemType) name check; + inherit (elemType) check; + name = "unique ${elemType.name}"; + typerep = "(uniq${elemType.typerep})"; merge = mergeOneOption; getSubOptions = elemType.getSubOptions; + getSubOptionsPrefixed = prefix: elemType.getSubOptionsPrefixed prefix; getSubModules = elemType.getSubModules; substSubModules = m: uniq (elemType.substSubModules m); + nestedTypes = elemType.nestedTypes; + defaultValues = elemType.defaultValues; }; nullOr = elemType: mkOptionType { name = "null or ${elemType.name}"; + typerep = "(nullOr${elemType.typerep})"; check = x: x == null || elemType.check x; - merge = loc: defs: + merge = _module: loc: defs: let nrNulls = count (def: def.value == null) defs; in if nrNulls == length defs then null else if nrNulls != 0 then throw "The option `${showOption loc}' is defined both null and not null, in ${showFiles (getFiles defs)}." - else elemType.merge loc defs; + else elemType.merge _module loc defs; getSubOptions = elemType.getSubOptions; + getSubOptionsPrefixed = prefix: elemType.getSubOptionsPrefixed prefix; getSubModules = elemType.getSubModules; substSubModules = m: nullOr (elemType.substSubModules m); + nestedTypes = elemType.nestedTypes; + defaultValues = [null] ++ elemType.defaultValues; + }; + + enum = values: + let + show = v: + if builtins.isString v then ''"${v}"'' + else if builtins.isInt v then builtins.toString v + else ''<${builtins.typeOf v}>''; + in + mkOptionType { + name = "one of ${concatMapStringsSep ", " show values}"; + typerep = "(enum${concatMapStrings (x: "(${escape ["(" ")"] (builtins.toString x)})") values})"; + check = flip elem values; + merge = mergeOneOption; + nestedTypes = []; + }; + + either = t1: t2: mkOptionType { + name = "${t1.name} or ${t2.name}"; + typerep = "(either${t1.typerep}${t2.typerep})"; + check = x: t1.check x || t2.check x; + merge = mergeOneOption; + nestedTypes = t1.nestedTypes ++ t2.nestedTypes; + defaultValues = t1.defaultValues ++ t2.defaultValues; }; + + # + # Complex types + # + submodule = opts: let opts' = toList opts; inherit (import ./modules.nix) evalModules; + filterVisible = filter (opt: (if opt ? visible then opt.visible else true) && (if opt ? internal then !opt.internal else true)); in mkOptionType rec { name = "submodule"; + typerep = "(submodule)"; check = x: isAttrs x || isFunction x; - merge = loc: defs: + merge = _module: loc: defs: let + internalModule = [ { inherit _module; } { _module.args.name = lib.mkForce (last loc); } ]; coerce = def: if isFunction def then def else { config = def; }; - modules = opts' ++ map (def: { _file = def.file; imports = [(coerce def.value)]; }) defs; + modules = opts' ++ internalModule ++ map (def: { _file = def.file; imports = [(coerce def.value)]; }) defs; in (evalModules { inherit modules; - args.name = last loc; prefix = loc; }).config; - getSubOptions = prefix: (evalModules - { modules = opts'; inherit prefix; + getSubOptions = getSubOptionsPrefixed []; + getSubOptionsPrefixed = prefix: _module: + let # FIXME: hack to get shit to evaluate. - args = { name = ""; }; }).options; + internalModule = [ { inherit _module; } { _module.args.name = lib.mkForce ""; } ]; + in (evalModules { + modules = opts' ++ internalModule; + inherit prefix; + }).options; getSubModules = opts'; substSubModules = m: submodule m; + nestedTypes = concatMap (opt: opt.type.nestedTypes) (collect (lib.isType "option") (getSubOptions {})); + defaultValues = [{}]; }; - enum = values: - let - show = v: - if builtins.isString v then ''"${v}"'' - else if builtins.isInt v then builtins.toString v - else ''<${builtins.typeOf v}>''; - in - mkOptionType { - name = "one of ${concatMapStringsSep ", " show values}"; - check = flip elem values; - merge = mergeOneOption; - }; - either = t1: t2: mkOptionType { - name = "${t1.name} or ${t2.name}"; - check = x: t1.check x || t2.check x; - merge = mergeOneOption; - }; + + # + # Legacy types + # # Obsolete alternative to configOf. It takes its option # declarations from the ‘options’ attribute of containing option # declaration. optionSet = mkOptionType { name = /* builtins.trace "types.optionSet is deprecated; use types.submodule instead" */ "option set"; + typerep = "(optionSet)"; }; + + # Try to remove module options of that type wherever possible. + # A module option taking a function can not be introspected and documented properly. + functionTo = resultType: + mkOptionType { + name = "function to ${resultType.name}"; + typerep = "(function${resultType.typerep})"; + check = builtins.isFunction; + merge = mergeOneOption; + nestedTypes = resultType.nestedTypes; + defaultValues = map (x: {...}:x) resultType.nestedTypes; # INFO: It seems as nix can't compare functions, yet. + }; + + + # + # misc + # + # Augment the given type with an additional type check function. addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; }; |