From 570a1a3ad773d93aa2128a8fba49f98e1e115d5d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Na=C3=AFm=20Favier?= <n@monade.li>
Date: Sat, 8 Jul 2023 12:28:13 +0200
Subject: [PATCH] parser: merge nested dynamic attributes

Fixes https://github.com/NixOS/nix/issues/7115
---
 doc/manual/src/release-notes/rl-next.md       | 19 +++++++++++++++++++
 src/libexpr/parser.y                          |  1 +
 .../lang/eval-fail-dup-dynamic-attrs.err.exp  |  8 ++++++++
 tests/lang/eval-fail-dup-dynamic-attrs.nix    |  4 ++++
 tests/lang/eval-okay-merge-dynamic-attrs.exp  |  1 +
 tests/lang/eval-okay-merge-dynamic-attrs.nix  | 13 +++++++++++++
 6 files changed, 46 insertions(+)
 create mode 100644 tests/lang/eval-fail-dup-dynamic-attrs.err.exp
 create mode 100644 tests/lang/eval-fail-dup-dynamic-attrs.nix
 create mode 100644 tests/lang/eval-okay-merge-dynamic-attrs.exp
 create mode 100644 tests/lang/eval-okay-merge-dynamic-attrs.nix

diff --git a/doc/manual/src/release-notes/rl-next.md b/doc/manual/src/release-notes/rl-next.md
index 139d07188..160245a31 100644
--- a/doc/manual/src/release-notes/rl-next.md
+++ b/doc/manual/src/release-notes/rl-next.md
@@ -6,3 +6,22 @@
 
 - Nix now allows unprivileged/[`allowed-users`](../command-ref/conf-file.md#conf-allowed-users) to sign paths.
   Previously, only [`trusted-users`](../command-ref/conf-file.md#conf-trusted-users) users could sign paths.
+
+- Nested dynamic attributes are now merged correctly by the parser. For example:
+
+  ```nix
+  {
+    nested = { foo = 1; };
+    nested = { ${"ba" + "r"} = 2; };
+  }
+  ```
+
+  This used to silently discard `nested.bar`, but now behaves as one would expect and evaluates to:
+
+  ```nix
+  { nested = { bar = 2; foo = 1; }; }
+  ```
+
+  Note that the feature of merging multiple attribute set declarations is of questionable value.
+  It allows writing expressions that are very hard to read, for instance when there are many lines of code between two declarations of the same attribute.
+  This has been around for a long time and is therefore supported for backwards compatibility, but should not be relied upon.
diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y
index 0a1ad9967..217c17382 100644
--- a/src/libexpr/parser.y
+++ b/src/libexpr/parser.y
@@ -137,6 +137,7 @@ static void addAttr(ExprAttrs * attrs, AttrPath && attrPath,
                         dupAttr(state, ad.first, j2->second.pos, ad.second.pos);
                     jAttrs->attrs.emplace(ad.first, ad.second);
                 }
+                jAttrs->dynamicAttrs.insert(jAttrs->dynamicAttrs.end(), ae->dynamicAttrs.begin(), ae->dynamicAttrs.end());
             } else {
                 dupAttr(state, attrPath, pos, j->second.pos);
             }
diff --git a/tests/lang/eval-fail-dup-dynamic-attrs.err.exp b/tests/lang/eval-fail-dup-dynamic-attrs.err.exp
new file mode 100644
index 000000000..e01f8e6d0
--- /dev/null
+++ b/tests/lang/eval-fail-dup-dynamic-attrs.err.exp
@@ -0,0 +1,8 @@
+error: dynamic attribute 'b' already defined at /pwd/lang/eval-fail-dup-dynamic-attrs.nix:2:11
+
+       at /pwd/lang/eval-fail-dup-dynamic-attrs.nix:3:11:
+
+            2|   set = { "${"" + "b"}" = 1; };
+            3|   set = { "${"b" + ""}" = 2; };
+             |           ^
+            4| }
diff --git a/tests/lang/eval-fail-dup-dynamic-attrs.nix b/tests/lang/eval-fail-dup-dynamic-attrs.nix
new file mode 100644
index 000000000..7ea17f6c8
--- /dev/null
+++ b/tests/lang/eval-fail-dup-dynamic-attrs.nix
@@ -0,0 +1,4 @@
+{
+  set = { "${"" + "b"}" = 1; };
+  set = { "${"b" + ""}" = 2; };
+}
diff --git a/tests/lang/eval-okay-merge-dynamic-attrs.exp b/tests/lang/eval-okay-merge-dynamic-attrs.exp
new file mode 100644
index 000000000..157d677ce
--- /dev/null
+++ b/tests/lang/eval-okay-merge-dynamic-attrs.exp
@@ -0,0 +1 @@
+{ set1 = { a = 1; b = 2; }; set2 = { a = 1; b = 2; }; set3 = { a = 1; b = 2; }; set4 = { a = 1; b = 2; }; }
diff --git a/tests/lang/eval-okay-merge-dynamic-attrs.nix b/tests/lang/eval-okay-merge-dynamic-attrs.nix
new file mode 100644
index 000000000..f459a554f
--- /dev/null
+++ b/tests/lang/eval-okay-merge-dynamic-attrs.nix
@@ -0,0 +1,13 @@
+{
+  set1 = { a = 1; };
+  set1 = { "${"b" + ""}" = 2; };
+
+  set2 = { "${"b" + ""}" = 2; };
+  set2 = { a = 1; };
+
+  set3.a = 1;
+  set3."${"b" + ""}" = 2;
+
+  set4."${"b" + ""}" = 2;
+  set4.a = 1;
+}