nix-community.nixvim/lib/deprecation.nix
Matt Sturgeon d94956e5da
lib/deprecation: implement aliases for mkRemovedPackageOptionModule
Allows most existing configs to continue building, now with warnings
instead of assertions when the old `plugins.*.*Package` options are
used.

An assertion will still occur if there is a merge conflict, e.g:
`plugins.a.fooPackage = null` and `plugins.b.fooPackage = pkgs.foo`.
2025-04-15 22:15:18 +01:00

190 lines
6 KiB
Nix
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{ lib }:
rec {
# Get a (sub)option by walking the path,
# checking for submodules along the way
getOptionRecursive =
opt: prefix: optionPath:
if optionPath == [ ] then
opt
else if lib.isOption opt then
getOptionRecursive (opt.type.getSubOptions prefix) prefix optionPath
else
let
name = lib.head optionPath;
opt' = lib.getAttr name opt;
prefix' = prefix ++ [ name ];
optionPath' = lib.drop 1 optionPath;
in
getOptionRecursive opt' prefix' optionPath';
# Like mkRemovedOptionModule, but has support for nested sub-options
# and uses warnings instead of assertions.
mkDeprecatedSubOptionModule =
optionPath: replacementInstructions:
{ options, ... }:
{
options = lib.setAttrByPath optionPath (
lib.mkOption {
# When (e.g.) `mkAttrs` is used on a submodule, this option will be evaluated.
# Therefore we have to apply _something_ (null) when there's no definition.
apply =
v:
let
# Avoid "option used but not defined" errors
res = builtins.tryEval v;
in
if res.success then res.value else null;
visible = false;
}
);
config.warnings =
let
opt = getOptionRecursive options [ ] optionPath;
in
lib.optional opt.isDefined ''
The option definition `${lib.showOption optionPath}' in ${lib.showFiles opt.files} is deprecated.
${replacementInstructions}
'';
};
/*
Returns a function that maps
[
"someOption"
["fooBar" "someSubOption"]
{ old = "someOtherOption"; new = ["foo_bar" "some_other_option"]}
]
to
[
(lib.mkRenamedOptionModule
(oldPath ++ ["someOption"])
(newPath ++ ["some_option"])
)
(lib.mkRenamedOptionModule
(oldPath ++ ["fooBar" "someSubOption"])
(newPath ++ ["foo_bar" "some_sub_option"])
)
(lib.mkRenamedOptionModule
(oldPath ++ ["someOtherOption"])
(newPath ++ ["foo_bar" "some_other_option"])
)
]
*/
mkSettingsRenamedOptionModules =
oldPrefix: newPrefix:
map (
spec:
let
finalSpec =
if lib.isAttrs spec then
lib.mapAttrs (_: lib.toList) spec
else
{
old = lib.toList spec;
new = map lib.nixvim.toSnakeCase finalSpec.old;
};
in
lib.mkRenamedOptionModule (oldPrefix ++ finalSpec.old) (newPrefix ++ finalSpec.new)
);
# A clone of types.coercedTo, but it prints a warning when oldType is used.
transitionType =
oldType: coerceFn: newType:
assert lib.assertMsg (
oldType.getSubModules == null
) "transitionType: oldType must not have submodules (its a ${oldType.description})";
lib.mkOptionType rec {
name = "transitionType";
inherit (newType) description;
check = x: (oldType.check x && newType.check (coerceFn x)) || newType.check x;
merge =
opt: defs:
let
coerceVal =
val:
if oldType.check val then
lib.warn ''
Passing a ${oldType.description} for `${lib.showOption opt}' is deprecated, use ${newType.description} instead. Definitions: ${lib.options.showDefs defs}
'' (coerceFn val)
else
val;
in
newType.merge opt (map (def: def // { value = coerceVal def.value; }) defs);
inherit (newType) emptyValue;
inherit (newType) getSubOptions;
inherit (newType) getSubModules;
substSubModules = m: transitionType oldType coerceFn (newType.substSubModules m);
typeMerge = t1: t2: null;
functor = (lib.types.defaultFunctor name) // {
wrapped = newType;
};
nestedTypes.coercedType = oldType;
nestedTypes.finalType = newType;
};
mkRemovedPackageOptionModule =
{
plugin,
packageName,
oldPackageName ? packageName,
}:
{ config, options, ... }:
let
cfg = config.dependencies.${packageName};
opt = options.dependencies.${packageName};
oldOptPath = builtins.concatMap lib.toList [
"plugins"
plugin
"${oldPackageName}Package"
];
oldOpt = lib.getAttrFromPath oldOptPath options;
# We can't use `oldOpt.value` because that will use our `apply`, so we merge the definitions ourselves:
oldDef = lib.modules.mergeDefinitions oldOpt.loc oldOpt.type oldOpt.definitionsWithLocations;
# Conceptually similar to `mkDerivedConfig`, but uses our manually merged definition
configFromOldDef =
{
predicate ? _: true,
apply ? lib.id,
}:
let
inherit (oldDef) mergedValue isDefined;
inherit (oldDef.defsFinal') highestPrio;
condition = isDefined && predicate mergedValue;
value = apply mergedValue;
in
lib.mkIf condition (lib.mkOverride highestPrio value);
in
{
options = lib.setAttrByPath oldOptPath (
lib.mkOption {
type = with lib.types; nullOr package;
description = "Alias of `${opt.enable}` and `${opt.package}`.";
visible = false;
apply =
let
value = if cfg.enable then cfg.package else null;
use = builtins.trace "Obsolete option `${oldOpt}' is used. It was replaced by `${opt.enable}' and `${opt.package}'.";
in
_: use value;
}
);
config = lib.mkIf oldOpt.isDefined {
warnings = [
"The option `${oldOpt}' defined in ${lib.showFiles oldOpt.files} has been replaced by `${opt.enable}' and `${opt.package}'."
];
dependencies.${packageName} = {
enable = configFromOldDef {
apply = pkg: pkg != null;
};
package = configFromOldDef {
predicate = pkg: pkg != null;
};
};
};
};
}