docs: eval modules without access to pkgs

Replace the `package-options` test with a stricter implementation.

When evaluating modules for use in the docs, provide them with a stubbed
`pkgs` instance that throws an error whenever a package is evaluated.

This ensures we don't accidentally use any packages in defaults or
examples.
This commit is contained in:
Matt Sturgeon 2025-01-24 21:22:01 +00:00
parent 0b4a4e8327
commit d7df583211
No known key found for this signature in database
GPG key ID: 4F91844CED1A8299
3 changed files with 51 additions and 96 deletions

View file

@ -9,6 +9,56 @@ let
pkgs = import ./pkgs.nix { inherit system nixpkgs; };
inherit (pkgs) lib;
# A stub pkgs instance used while evaluating the nixvim modules for the docs
# If any non-meta attr is accessed, the eval will throw
noPkgs =
let
# Known suffixes for package sets
suffixes = [
"Plugins"
"Packages"
];
# Predicate for whether an attr name looks like a package set
# Determines whether stubPackage should recurse
isPackageSet = name: builtins.any (lib.flip lib.strings.hasSuffix name) suffixes;
# Need to retain `meta.homepage` if present
stubPackage =
prefix: name: package:
let
loc = prefix ++ [ name ];
in
if isPackageSet name then
lib.mapAttrs (stubPackage loc) package
else
lib.mapAttrs (_: throwAccessError loc) package
// lib.optionalAttrs (package ? meta) { inherit (package) meta; };
throwAccessError =
loc:
throw "Attempted to access `${
lib.concatStringsSep "." ([ "pkgs" ] ++ loc)
}` while rendering the docs.";
in
lib.fix (
self:
lib.mapAttrs (stubPackage [ ]) pkgs
// {
pkgs = self;
# The following pkgs attrs are required to eval nixvim, even for the docs:
inherit (pkgs)
_type
stdenv
stdenvNoCC
symlinkJoin
runCommand
runCommandLocal
writeShellApplication
;
}
);
nixvimPath = toString ./..;
gitHubDeclaration = user: repo: branch: subpath: {
@ -37,7 +87,7 @@ let
modules = [
{
isDocs = true;
nixpkgs.pkgs = pkgs;
_module.args.pkgs = lib.mkForce noPkgs;
}
];
};

View file

@ -44,7 +44,6 @@ in
nixpkgs-module = callTest ./nixpkgs-module.nix { };
plugins-by-name = callTest ./plugins-by-name.nix { };
generated = callTest ./generated.nix { };
package-options = callTest ./package-options.nix { };
lsp-all-servers = callTest ./lsp-servers.nix { };
}
# Expose some tests from the docs as flake-checks too

View file

@ -1,94 +0,0 @@
# This test ensures "package" options use a "literalExpression" in their defaultText
# I.e. it validates `lib.mkPackageOption` was used to build the package options.
{
nixvimConfiguration,
lib,
runCommandLocal,
}:
let
inherit (builtins)
filter
head
map
match
;
inherit (lib.strings) concatMapStringsSep removePrefix removeSuffix;
inherit (lib.attrsets) isDerivation;
inherit (lib.options)
isOption
renderOptionValue
showOption
;
# This doesn't collect any submodule sub-options,
# but that's fine since most of our "package" options are in the top level module eval
options = lib.collect isOption nixvimConfiguration.options;
# All visible non-sub options that default to a derivation
drvOptions = filter (
{
default ? null,
visible ? true,
internal ? false,
...
}:
let
# Some options have defaults that throw when evaluated
default' = (builtins.tryEval default).value;
in
visible && !internal && isDerivation default'
) options;
# Bad options do not use `literalExpression` in their `defaultText`,
# or have a `defaultText` that doesn't start with "pkgs."
badOptions = filter (
opt:
opt.defaultText._type or null != "literalExpression"
|| match ''pkgs[.].*'' (opt.defaultText.text or "") == null
) drvOptions;
in
runCommandLocal "validate-package-options"
{
# Use structuredAttrs to avoid "Argument List Too Long" errors
# and get proper bash array support.
__structuredAttrs = true;
# Passthroughs for debugging purposes
passthru = {
inherit nixvimConfiguration drvOptions badOptions;
};
# Error strings to print
errors =
let
# A little hack to get the flake's source in the nix store
# We will use this to make the option declaration sources more readable
src = removeSuffix "modules/top-level/output.nix" (
head nixvimConfiguration.options.package.declarations
);
in
map (
opt:
let
# The default, as rendered in documentation. Will always be a literalExpression.
default = builtins.addErrorContext "while evaluating the default text for `${showOption opt.loc}`" (
renderOptionValue (opt.defaultText or opt.default)
);
in
''
- ${showOption opt.loc} (${default.text}), declared in:
${concatMapStringsSep "\n" (file: " - ${removePrefix src file}") opt.declarations}
''
) badOptions;
}
''
if [ -n "$errors" ]; then
echo "Found ''${#errors[@]} errors:"
for err in "''${errors[@]}"; do
echo "$err"
done
echo "(''${#errors[@]} options with errors)"
exit 1
fi
touch $out
''