diff --git a/maintainers/flake-module.nix b/maintainers/flake-module.nix index 8dcff9c63..414e6c570 100644 --- a/maintainers/flake-module.nix +++ b/maintainers/flake-module.nix @@ -79,6 +79,8 @@ # Not supported by nixfmt ''^tests/functional/lang/eval-okay-deprecate-cursed-or\.nix$'' ''^tests/functional/lang/eval-okay-attrs5\.nix$'' + ''^tests/functional/lang/eval-fail-dynamic-attrs-inherit\.nix$'' + ''^tests/functional/lang/eval-fail-dynamic-attrs-inherit-2\.nix$'' # More syntax tests # These tests, or parts of them, should have been parse-* test cases. diff --git a/src/libexpr/include/nix/expr/parser-state.hh b/src/libexpr/include/nix/expr/parser-state.hh index c2a49a3d3..5a94f62e8 100644 --- a/src/libexpr/include/nix/expr/parser-state.hh +++ b/src/libexpr/include/nix/expr/parser-state.hh @@ -47,6 +47,79 @@ struct ParserLocation } }; +/** + * This represents a string-like parse that possibly has yet to be constructed. + * + * Examples: + * "foo" + * ${"foo" + "bar"} + * "foo.bar" + * "foo-${a}" + * + * Using this type allows us to avoid construction altogether in cases where what we actually need is the string + * contents. For example in foo."bar.baz", there is no need to construct an AST node for "bar.baz", but we don't know + * that until we bubble the value up during parsing and see that it's a node in an AttrPath. + */ +class ToBeStringyExpr +{ +private: + using Raw = std::variant; + Raw raw; + +public: + ToBeStringyExpr() = default; + + ToBeStringyExpr(std::string_view v) + : raw(v) + { + } + + ToBeStringyExpr(Expr * expr) + : raw(expr) + { + assert(expr); + } + + /** + * Visits the expression and invokes an overloaded functor object \ref f. + * If the underlying Expr has a dynamic type of ExprString the overload taking std::string_view + * is invoked. + * + * Used to consistently handle simple StringExpr ${"string"} as non-dynamic attributes. + * @see https://github.com/NixOS/nix/issues/14642 + */ + template + void visit(F && f) + { + std::visit( + overloaded{ + [&](std::string_view str) { f(str); }, + [&](Expr * expr) { + ExprString * str = dynamic_cast(expr); + if (str) + f(str->v.string_view()); + else + f(expr); + }, + [](std::monostate) { unreachable(); }}, + raw); + } + + /** + * Get or create an Expr from either an existing Expr or from a string. + * Delays the allocation or an AST node in case the parser only cares about string contents. + */ + Expr * toExpr(Exprs & exprs) + { + return std::visit( + overloaded{ + [&](std::string_view str) -> Expr * { return exprs.add(exprs.alloc, str); }, + [&](Expr * expr) { return expr; }, + [](std::monostate) -> Expr * { unreachable(); }}, + raw); + } +}; + struct LexerState { /** diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y index a9166c5b5..520086e28 100644 --- a/src/libexpr/parser.y +++ b/src/libexpr/parser.y @@ -138,7 +138,7 @@ static Expr * makeCall(Exprs & exprs, PosIdx pos, Expr * fn, Expr * arg) { %type >> string_parts_interpolated %type >>> ind_string_parts %type path_start -%type > string_parts string_attr +%type string_parts string_attr %type attr %token ID %token STR IND_STR @@ -297,12 +297,7 @@ expr_simple } | INT_LIT { $$ = state->exprs.add($1); } | FLOAT_LIT { $$ = state->exprs.add($1); } - | '"' string_parts '"' { - std::visit(overloaded{ - [&](std::string_view str) { $$ = state->exprs.add(state->exprs.alloc, str); }, - [&](Expr * expr) { $$ = expr; }}, - $2); - } + | '"' string_parts '"' { $$ = $2.toExpr(state->exprs); } | IND_STRING_OPEN ind_string_parts IND_STRING_CLOSE { $$ = state->stripIndentation(CUR_POS, $2); } @@ -342,9 +337,9 @@ expr_simple ; string_parts - : STR { $$ = $1; } - | string_parts_interpolated { $$ = state->exprs.add(state->exprs.alloc, CUR_POS, true, $1); } - | { $$ = std::string_view(); } + : STR { $$ = {$1}; } + | string_parts_interpolated { $$ = {state->exprs.add(state->exprs.alloc, CUR_POS, true, $1)}; } + | { $$ = {std::string_view()}; } ; string_parts_interpolated @@ -447,15 +442,15 @@ attrs : attrs attr { $$ = std::move($1); $$.emplace_back(state->symbols.create($2), state->at(@2)); } | attrs string_attr { $$ = std::move($1); - std::visit(overloaded { + $2.visit(overloaded{ [&](std::string_view str) { $$.emplace_back(state->symbols.create(str), state->at(@2)); }, [&](Expr * expr) { - throw ParseError({ - .msg = HintFmt("dynamic attributes not allowed in inherit"), - .pos = state->positions[state->at(@2)] - }); - } - }, $2); + throw ParseError({ + .msg = HintFmt("dynamic attributes not allowed in inherit"), + .pos = state->positions[state->at(@2)] + }); + }} + ); } | { } ; @@ -464,17 +459,17 @@ attrpath : attrpath '.' attr { $$ = std::move($1); $$.emplace_back(state->symbols.create($3)); } | attrpath '.' string_attr { $$ = std::move($1); - std::visit(overloaded { + $3.visit(overloaded{ [&](std::string_view str) { $$.emplace_back(state->symbols.create(str)); }, - [&](Expr * expr) { $$.emplace_back(expr); } - }, std::move($3)); + [&](Expr * expr) { $$.emplace_back(expr); }} + ); } | attr { $$.emplace_back(state->symbols.create($1)); } | string_attr - { std::visit(overloaded { + { $1.visit(overloaded{ [&](std::string_view str) { $$.emplace_back(state->symbols.create(str)); }, - [&](Expr * expr) { $$.emplace_back(expr); } - }, std::move($1)); + [&](Expr * expr) { $$.emplace_back(expr); }} + ); } ; @@ -485,7 +480,7 @@ attr string_attr : '"' string_parts '"' { $$ = std::move($2); } - | DOLLAR_CURLY expr '}' { $$ = $2; } + | DOLLAR_CURLY expr '}' { $$ = {$2}; } ; list diff --git a/tests/functional/lang/eval-fail-dynamic-attrs-inherit-2.err.exp b/tests/functional/lang/eval-fail-dynamic-attrs-inherit-2.err.exp new file mode 100644 index 000000000..e71fc23b5 --- /dev/null +++ b/tests/functional/lang/eval-fail-dynamic-attrs-inherit-2.err.exp @@ -0,0 +1,6 @@ +error: dynamic attributes not allowed in inherit + at /pwd/lang/eval-fail-dynamic-attrs-inherit-2.nix:5:15: + 4| { + 5| inherit (a) ${"b" + ""}; + | ^ + 6| } diff --git a/tests/functional/lang/eval-fail-dynamic-attrs-inherit-2.nix b/tests/functional/lang/eval-fail-dynamic-attrs-inherit-2.nix new file mode 100644 index 000000000..7af9685fe --- /dev/null +++ b/tests/functional/lang/eval-fail-dynamic-attrs-inherit-2.nix @@ -0,0 +1,6 @@ +let + a.b = 1; +in +{ + inherit (a) ${"b" + ""}; +} diff --git a/tests/functional/lang/eval-fail-dynamic-attrs-inherit.err.exp b/tests/functional/lang/eval-fail-dynamic-attrs-inherit.err.exp new file mode 100644 index 000000000..b08b0e201 --- /dev/null +++ b/tests/functional/lang/eval-fail-dynamic-attrs-inherit.err.exp @@ -0,0 +1,6 @@ +error: dynamic attributes not allowed in inherit + at /pwd/lang/eval-fail-dynamic-attrs-inherit.nix:5:11: + 4| { + 5| inherit ${"a" + ""}; + | ^ + 6| } diff --git a/tests/functional/lang/eval-fail-dynamic-attrs-inherit.nix b/tests/functional/lang/eval-fail-dynamic-attrs-inherit.nix new file mode 100644 index 000000000..3a9b68410 --- /dev/null +++ b/tests/functional/lang/eval-fail-dynamic-attrs-inherit.nix @@ -0,0 +1,6 @@ +let + a = 1; +in +{ + inherit ${"a" + ""}; +} diff --git a/tests/functional/lang/eval-fail-dynamic-attrs-let-2.err.exp b/tests/functional/lang/eval-fail-dynamic-attrs-let-2.err.exp new file mode 100644 index 000000000..2eb7f04a7 --- /dev/null +++ b/tests/functional/lang/eval-fail-dynamic-attrs-let-2.err.exp @@ -0,0 +1,5 @@ +error: dynamic attributes not allowed in let + at /pwd/lang/eval-fail-dynamic-attrs-let-2.nix:1:1: + 1| let + | ^ + 2| ${"${"a"}"} = 1; diff --git a/tests/functional/lang/eval-fail-dynamic-attrs-let-2.nix b/tests/functional/lang/eval-fail-dynamic-attrs-let-2.nix new file mode 100644 index 000000000..bcec33ddf --- /dev/null +++ b/tests/functional/lang/eval-fail-dynamic-attrs-let-2.nix @@ -0,0 +1,4 @@ +let + ${"${"a"}"} = 1; +in +a diff --git a/tests/functional/lang/eval-fail-dynamic-attrs-let-3.err.exp b/tests/functional/lang/eval-fail-dynamic-attrs-let-3.err.exp new file mode 100644 index 000000000..0f44e25dd --- /dev/null +++ b/tests/functional/lang/eval-fail-dynamic-attrs-let-3.err.exp @@ -0,0 +1,5 @@ +error: dynamic attributes not allowed in let + at /pwd/lang/eval-fail-dynamic-attrs-let-3.nix:1:1: + 1| let + | ^ + 2| "${"a"}" = 1; diff --git a/tests/functional/lang/eval-fail-dynamic-attrs-let-3.nix b/tests/functional/lang/eval-fail-dynamic-attrs-let-3.nix new file mode 100644 index 000000000..37453c530 --- /dev/null +++ b/tests/functional/lang/eval-fail-dynamic-attrs-let-3.nix @@ -0,0 +1,4 @@ +let + "${"a"}" = 1; +in +a diff --git a/tests/functional/lang/eval-fail-dynamic-attrs-let.err.exp b/tests/functional/lang/eval-fail-dynamic-attrs-let.err.exp new file mode 100644 index 000000000..ca3192133 --- /dev/null +++ b/tests/functional/lang/eval-fail-dynamic-attrs-let.err.exp @@ -0,0 +1,5 @@ +error: dynamic attributes not allowed in let + at /pwd/lang/eval-fail-dynamic-attrs-let.nix:1:1: + 1| let + | ^ + 2| ${"a" + ""} = 1; diff --git a/tests/functional/lang/eval-fail-dynamic-attrs-let.nix b/tests/functional/lang/eval-fail-dynamic-attrs-let.nix new file mode 100644 index 000000000..fca32ae4f --- /dev/null +++ b/tests/functional/lang/eval-fail-dynamic-attrs-let.nix @@ -0,0 +1,4 @@ +let + ${"a" + ""} = 1; +in +a diff --git a/tests/functional/lang/eval-okay-dynamic-attrs-3.exp b/tests/functional/lang/eval-okay-dynamic-attrs-3.exp new file mode 100644 index 000000000..9d27f872c --- /dev/null +++ b/tests/functional/lang/eval-okay-dynamic-attrs-3.exp @@ -0,0 +1 @@ +{ a = 1; attrs = { b = 1; c = 1; d = 1; }; b = 1; c = 1; d = 1; } diff --git a/tests/functional/lang/eval-okay-dynamic-attrs-3.nix b/tests/functional/lang/eval-okay-dynamic-attrs-3.nix new file mode 100644 index 000000000..d55ed82f8 --- /dev/null +++ b/tests/functional/lang/eval-okay-dynamic-attrs-3.nix @@ -0,0 +1,14 @@ +# dynamic attrs are not generally allowed in `let`, and inherit, but they are if they only contain a string +let + ${"a"} = 1; + attrs = rec { + b = c; + ${"c"} = d; + d = a; + }; +in +{ + inherit ${"a"}; + inherit attrs; + inherit (attrs) ${"b"} ${"c"} d; +}