···537537 mergeModules' prefix modules
538538 (concatMap (m: map (config: { file = m._file; inherit config; }) (pushDownProperties m.config)) modules);
539539540540- mergeModules' = prefix: options: configs:
540540+ mergeModules' = prefix: modules: configs:
541541 let
542542 # an attrset 'name' => list of submodules that declare ‘name’.
543543 declsByName =
···554554 else
555555 mapAttrs
556556 (n: option:
557557- [{ inherit (module) _file; options = option; }]
557557+ [{ inherit (module) _file; pos = builtins.unsafeGetAttrPos n subtree; options = option; }]
558558 )
559559 subtree
560560 )
561561- options);
561561+ modules);
562562563563 # The root of any module definition must be an attrset.
564564 checkedConfigs =
···762762 else res.options;
763763 in opt.options // res //
764764 { declarations = res.declarations ++ [opt._file];
765765+ # In the case of modules that are generated dynamically, we won't
766766+ # have exact declaration lines; fall back to just the file being
767767+ # evaluated.
768768+ declarationPositions = res.declarationPositions
769769+ ++ (if opt.pos != null
770770+ then [opt.pos]
771771+ else [{ file = opt._file; line = null; column = null; }]);
765772 options = submodules;
766773 } // typeSet
767767- ) { inherit loc; declarations = []; options = []; } opts;
774774+ ) { inherit loc; declarations = []; declarationPositions = []; options = []; } opts;
768775769776 /* Merge all the definitions of an option to produce the final
770777 config value. */
+19-1
lib/tests/modules.sh
···3939checkConfigOutput() {
4040 local outputContains=$1
4141 shift
4242- if evalConfig "$@" 2>/dev/null | grep --silent "$outputContains" ; then
4242+ if evalConfig "$@" 2>/dev/null | grep -E --silent "$outputContains" ; then
4343 ((++pass))
4444 else
4545 echo 2>&1 "error: Expected result matching '$outputContains', while evaluating"
···443443# Anonymous modules get deduplicated by key
444444checkConfigOutput '^"pear"$' config.once.raw ./merge-module-with-key.nix
445445checkConfigOutput '^"pear\\npear"$' config.twice.raw ./merge-module-with-key.nix
446446+447447+# Declaration positions
448448+# Line should be present for direct options
449449+checkConfigOutput '^10$' options.imported.line10.declarationPositions.0.line ./declaration-positions.nix
450450+checkConfigOutput '/declaration-positions.nix"$' options.imported.line10.declarationPositions.0.file ./declaration-positions.nix
451451+# Generated options may not have line numbers but they will at least get the
452452+# right file
453453+checkConfigOutput '/declaration-positions.nix"$' options.generated.line18.declarationPositions.0.file ./declaration-positions.nix
454454+checkConfigOutput '^null$' options.generated.line18.declarationPositions.0.line ./declaration-positions.nix
455455+# Submodules don't break it
456456+checkConfigOutput '^39$' config.submoduleLine34.submodDeclLine39.0.line ./declaration-positions.nix
457457+checkConfigOutput '/declaration-positions.nix"$' config.submoduleLine34.submodDeclLine39.0.file ./declaration-positions.nix
458458+# New options under freeform submodules get collected into the parent submodule
459459+# (consistent with .declarations behaviour, but weird; notably appears in system.build)
460460+checkConfigOutput '^34|23$' options.submoduleLine34.declarationPositions.0.line ./declaration-positions.nix
461461+checkConfigOutput '^34|23$' options.submoduleLine34.declarationPositions.1.line ./declaration-positions.nix
462462+# nested options work
463463+checkConfigOutput '^30$' options.nested.nestedLine30.declarationPositions.0.line ./declaration-positions.nix
446464447465cat <<EOF
448466====== module tests ======
+49
lib/tests/modules/declaration-positions.nix
···11+{ lib, options, ... }:
22+let discardPositions = lib.mapAttrs (k: v: v);
33+in
44+# unsafeGetAttrPos is unspecified best-effort behavior, so we only want to consider this test on an evaluator that satisfies some basic assumptions about this function.
55+assert builtins.unsafeGetAttrPos "a" { a = true; } != null;
66+assert builtins.unsafeGetAttrPos "a" (discardPositions { a = true; }) == null;
77+{
88+ imports = [
99+ {
1010+ options.imported.line10 = lib.mkOption {
1111+ type = lib.types.int;
1212+ };
1313+1414+ # Simulates various patterns of generating modules such as
1515+ # programs.firefox.nativeMessagingHosts.ff2mpv. We don't expect to get
1616+ # line numbers for these, but we can fall back on knowing the file.
1717+ options.generated = discardPositions {
1818+ line18 = lib.mkOption {
1919+ type = lib.types.int;
2020+ };
2121+ };
2222+2323+ options.submoduleLine34.extraOptLine23 = lib.mkOption {
2424+ default = 1;
2525+ type = lib.types.int;
2626+ };
2727+ }
2828+ ];
2929+3030+ options.nested.nestedLine30 = lib.mkOption {
3131+ type = lib.types.int;
3232+ };
3333+3434+ options.submoduleLine34 = lib.mkOption {
3535+ default = { };
3636+ type = lib.types.submoduleWith {
3737+ modules = [
3838+ ({ options, ... }: {
3939+ options.submodDeclLine39 = lib.mkOption { };
4040+ })
4141+ { freeformType = with lib.types; lazyAttrsOf (uniq unspecified); }
4242+ ];
4343+ };
4444+ };
4545+4646+ config = {
4747+ submoduleLine34.submodDeclLine39 = (options.submoduleLine34.type.getSubOptions [ ]).submodDeclLine39.declarationPositions;
4848+ };
4949+}