From e198743aa1dfd690cad1724a9a6762db5ef8297f Mon Sep 17 00:00:00 2001 From: Victor Borja Date: Thu, 5 Mar 2026 16:00:24 -0600 Subject: [PATCH] fix: avoid merging results of functor. dont use types.functionTo merge --- .../modules/tests/aspect_functor_merge.nix | 65 +++++++++++++++++++ nix/types.nix | 31 ++++++++- 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 checkmate/modules/tests/aspect_functor_merge.nix diff --git a/checkmate/modules/tests/aspect_functor_merge.nix b/checkmate/modules/tests/aspect_functor_merge.nix new file mode 100644 index 0000000..d15411f --- /dev/null +++ b/checkmate/modules/tests/aspect_functor_merge.nix @@ -0,0 +1,65 @@ +# Test that when multiple modules define the same aspect with a custom +# __functor twice, the functor is not called once per definition (which would +# duplicate the includes). Using lib.types.functionTo merges both functors. +# +# This was a bug where `functionTo` merge would +# invoke all definitions and merge their results, causing duplication +# when the functor uses `self` (the merged aspect) to produce includes. +# +# See https://github.com/vic/den/issues/216 +{ lib, new-scope, ... }: +{ + + flake.tests."test functor merge does not duplicate includes" = + let + first = lib.evalModules { + modules = [ + (new-scope "kit") + # Two separate modules defining the same aspect with same __functor. + { + kit.aspects.groups = { + myclass.names = [ "alice" ]; + __functor = self: { + inherit (self) myclass; + }; + }; + } + { + kit.aspects.groups = { + myclass.names = [ "bob" ]; + __functor = self: { + inherit (self) myclass; + }; + }; + } + ( + { config, ... }: + { + kit.aspects.main = { + myclass.names = [ "main" ]; + includes = [ config.kit.aspects.groups ]; + }; + } + ) + ]; + }; + + second = lib.evalModules { + modules = [ + { options.names = lib.mkOption { type = lib.types.listOf lib.types.str; }; } + first.config.kit.modules.myclass.main + ]; + }; + + expr = lib.sort (a: b: a < b) second.config.names; + expected = [ + "alice" + "bob" + "main" + ]; + in + { + inherit expr expected; + }; + +} diff --git a/nix/types.nix b/nix/types.nix index 7687391..454b998 100644 --- a/nix/types.nix +++ b/nix/types.nix @@ -24,6 +24,35 @@ let apply = fn; }; + # Like lib.types.functionTo, but it does not merges all definitions, and keeps + # just the last one. + functorType = lib.mkOptionType { + name = "aspectFunctor"; + description = "aspect functor function"; + check = lib.isFunction; + merge = + loc: defs: + let + # Use only the last definition to avoid duplication from + # functionTo merging all definitions with the same args. + # All definitions receive the same merged `self`, so they + # produce equivalent results - picking one is correct. + lastDef = lib.last defs; + innerType = providerType; + in + { + __functionArgs = lib.functionArgs lastDef.value; + __functor = + _: callerArgs: + (lib.modules.mergeDefinitions (loc ++ [ "" ]) innerType [ + { + inherit (lastDef) file; + value = lastDef.value callerArgs; + } + ]).mergedValue; + }; + }; + # Check if function has submodule-style arguments isSubmoduleFn = m: @@ -101,7 +130,7 @@ let internal = true; visible = false; description = "Functor to default provider"; - type = lib.types.functionTo providerType; + type = functorType; default = aspect: { class, aspect-chain }: if true || (class aspect-chain) then aspect else aspect; };