diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index c07ab327c92a..c6d10e2a3fdb 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -1450,7 +1450,10 @@ static std::string showAttrSelectionPath(EvalState & state, Env & env, std::span void ExprSelect::eval(EvalState & state, Env & env, Value & v) { Value vTmp; - PosIdx pos2; + // current or last *definition site* (the attr, not the select) + PosIdx attrPos; + const AttrName * unresolvedOrEnd = attrPathStart; + // cursor, result if successful Value * vAttrs = &vTmp; e->eval(state, env, vTmp); @@ -1483,28 +1486,34 @@ void ExprSelect::eval(EvalState & state, Env & env, Value & v) allAttrNames.insert(std::string(state.symbols[attr.name])); auto suggestions = Suggestions::bestMatches(allAttrNames, state.symbols[name]); state.error("attribute '%1%' missing", state.symbols[name]) - .atPos(pos) .withSuggestions(suggestions) .withFrame(env, *this) .debugThrow(); } } vAttrs = j->value; - pos2 = j->pos; + attrPos = j->pos; + unresolvedOrEnd = &i + 1; if (state.countCalls) - state.attrSelects->try_emplace_or_visit(pos2, 1, [](auto & i) { i.second++; }); + state.attrSelects->try_emplace_or_visit(attrPos, 1, [](auto & i) { i.second++; }); } - state.forceValue(*vAttrs, (pos2 ? pos2 : this->pos)); + state.forceValue(*vAttrs, (attrPos ? attrPos : this->pos)); } catch (Error & e) { - if (pos2) { - auto pos2r = state.positions[pos2]; - auto origin = std::get_if(&pos2r.origin); - if (!(origin && *origin == state.derivationInternal)) - state.addErrorTrace( - e, pos2, "while evaluating the attribute '%1%'", showAttrSelectionPath(state, env, getAttrPath())); + // Traces are printed in reverse, so we add context before the main item. + if (attrPos) { + auto attrPosR = state.positions[attrPos]; + auto origin = std::get_if(&attrPosR.origin); + if (!(origin && *origin == state.derivationInternal)) { + auto successPath = + showAttrSelectionPath(state, env, std::span(attrPathStart, unresolvedOrEnd)); + state.addErrorTrace(e, attrPos, "from the definition of '%1%'", successPath); + } } + // Add main item: the selection site itself (`a.b`), ie the actual access + state.addErrorTrace( + e, getPos(), "while evaluating the attribute '%1%'", showAttrSelectionPath(state, env, getAttrPath())); throw; } diff --git a/src/libexpr/include/nix/expr/nixexpr.hh b/src/libexpr/include/nix/expr/nixexpr.hh index b13c00ad541e..aaf09e9dae69 100644 --- a/src/libexpr/include/nix/expr/nixexpr.hh +++ b/src/libexpr/include/nix/expr/nixexpr.hh @@ -316,6 +316,9 @@ struct ExprSelect : Expr return pos; } + /** + * @return `std::span` starting at `attrPathStart` + */ std::span getAttrPath() const { return {attrPathStart, nAttrPath}; diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y index 77e63ed6299f..d8e7ba0b8fa0 100644 --- a/src/libexpr/parser.y +++ b/src/libexpr/parser.y @@ -384,7 +384,7 @@ expr_simple /* Let expressions `let {..., body = ...}' are just desugared into `(rec {..., body = ...}).body'. */ | LET '{' binds '}' - { $3->recursive = true; $3->pos = CUR_POS; $$ = state->exprs.add(state->exprs.alloc, noPos, $3, state->s.body); } + { $3->recursive = true; $3->pos = CUR_POS; $$ = state->exprs.add(state->exprs.alloc, CUR_POS, $3, state->s.body); } | REC '{' binds '}' { $3->recursive = true; $3->pos = CUR_POS; $$ = $3; } | '{' binds1 '}' diff --git a/tests/functional/lang/eval-fail-assert.err.exp b/tests/functional/lang/eval-fail-assert.err.exp index 5fffe79bf0d6..884c405b5a03 100644 --- a/tests/functional/lang/eval-fail-assert.err.exp +++ b/tests/functional/lang/eval-fail-assert.err.exp @@ -1,5 +1,11 @@ error: … while evaluating the attribute 'body' + at /pwd/lang/eval-fail-assert.nix:1:1: + 1| let { + | ^ + 2| x = + + … from the definition of 'body' at /pwd/lang/eval-fail-assert.nix:7:3: 6| 7| body = x "x"; diff --git a/tests/functional/lang/eval-fail-attr-name-type.err.exp b/tests/functional/lang/eval-fail-attr-name-type.err.exp index 4ea209b130fd..809504dd7afd 100644 --- a/tests/functional/lang/eval-fail-attr-name-type.err.exp +++ b/tests/functional/lang/eval-fail-attr-name-type.err.exp @@ -1,5 +1,12 @@ error: … while evaluating the attribute 'puppy."${key}"' + at /pwd/lang/eval-fail-attr-name-type.nix:7:1: + 6| in + 7| attrs.puppy.${key} + | ^ + 8| + + … from the definition of 'puppy' at /pwd/lang/eval-fail-attr-name-type.nix:3:5: 2| attrs = { 3| puppy.doggy = { }; diff --git a/tests/functional/lang/eval-fail-blackhole.err.exp b/tests/functional/lang/eval-fail-blackhole.err.exp index 95e33a5fe456..7555039e8285 100644 --- a/tests/functional/lang/eval-fail-blackhole.err.exp +++ b/tests/functional/lang/eval-fail-blackhole.err.exp @@ -1,5 +1,11 @@ error: … while evaluating the attribute 'body' + at /pwd/lang/eval-fail-blackhole.nix:1:1: + 1| let { + | ^ + 2| body = x; + + … from the definition of 'body' at /pwd/lang/eval-fail-blackhole.nix:2:3: 1| let { 2| body = x; diff --git a/tests/functional/lang/eval-fail-remove.err.exp b/tests/functional/lang/eval-fail-remove.err.exp index 0e087688a255..8a7e052708a6 100644 --- a/tests/functional/lang/eval-fail-remove.err.exp +++ b/tests/functional/lang/eval-fail-remove.err.exp @@ -1,15 +1,23 @@ error: … while evaluating the attribute 'body' + at /pwd/lang/eval-fail-remove.nix:1:1: + 1| let { + | ^ + 2| attrs = { + + … from the definition of 'body' at /pwd/lang/eval-fail-remove.nix:7:3: 6| 7| body = (removeAttrs attrs [ "x" ]).x; | ^ 8| } - error: attribute 'x' missing - at /pwd/lang/eval-fail-remove.nix:7:10: + … while evaluating the attribute 'x' + at /pwd/lang/eval-fail-remove.nix:7:10: 6| 7| body = (removeAttrs attrs [ "x" ]).x; | ^ 8| } + + error: attribute 'x' missing Did you mean y? diff --git a/tests/functional/lang/eval-fail-scope-5.err.exp b/tests/functional/lang/eval-fail-scope-5.err.exp index 6edc85f4f161..74905c1d91fe 100644 --- a/tests/functional/lang/eval-fail-scope-5.err.exp +++ b/tests/functional/lang/eval-fail-scope-5.err.exp @@ -1,5 +1,11 @@ error: … while evaluating the attribute 'body' + at /pwd/lang/eval-fail-scope-5.nix:1:1: + 1| let { + | ^ + 2| + + … from the definition of 'body' at /pwd/lang/eval-fail-scope-5.nix:13:3: 12| 13| body = f { }; diff --git a/tests/functional/lang/eval-fail-using-set-as-attr-name.err.exp b/tests/functional/lang/eval-fail-using-set-as-attr-name.err.exp index 9a59f37f35e4..3cf87491ece9 100644 --- a/tests/functional/lang/eval-fail-using-set-as-attr-name.err.exp +++ b/tests/functional/lang/eval-fail-using-set-as-attr-name.err.exp @@ -1,4 +1,11 @@ error: + … while evaluating the attribute '"${key}"' + at /pwd/lang/eval-fail-using-set-as-attr-name.nix:7:1: + 6| in + 7| attr.${key} + | ^ + 8| + … while evaluating an attribute name at /pwd/lang/eval-fail-using-set-as-attr-name.nix:7:8: 6| in diff --git a/tests/functional/repl/doc-constant.expected b/tests/functional/repl/doc-constant.expected index bda53bbfa660..e2ccd0945080 100644 --- a/tests/functional/repl/doc-constant.expected +++ b/tests/functional/repl/doc-constant.expected @@ -27,6 +27,11 @@ Unchangeably constant. nix-repl> :doc lib.attr.undocument error: … while evaluating the attribute 'attr.undocument' + at «string»:1:1: + 1| lib.attr.undocument + | ^ + + … from the definition of 'attr' at /path/to/tests/functional/repl/doc-comments.nix:52:3: 51| */ 52| lib.attr.empty = { }; @@ -34,9 +39,6 @@ error: 53| error: attribute 'undocument' missing - at «string»:1:1: - 1| lib.attr.undocument - | ^ Did you mean undocumented? nix-repl> :doc (import ./doc-comments.nix).constant @@ -89,20 +91,31 @@ error: undefined variable 'missing' | ^ nix-repl> :doc lib.missing -error: attribute 'missing' missing - at «string»:1:1: +error: + … while evaluating the attribute 'missing' + at «string»:1:1: 1| lib.missing | ^ + error: attribute 'missing' missing + nix-repl> :doc lib.missing.attr -error: attribute 'missing' missing - at «string»:1:1: +error: + … while evaluating the attribute 'missing' + at «string»:1:1: 1| lib.missing.attr | ^ + error: attribute 'missing' missing + nix-repl> :doc lib.attr.undocumental error: … while evaluating the attribute 'attr.undocumental' + at «string»:1:1: + 1| lib.attr.undocumental + | ^ + + … from the definition of 'attr' at /path/to/tests/functional/repl/doc-comments.nix:52:3: 51| */ 52| lib.attr.empty = { }; @@ -110,7 +123,4 @@ error: 53| error: attribute 'undocumental' missing - at «string»:1:1: - 1| lib.attr.undocumental - | ^ Did you mean undocumented? diff --git a/tests/functional/repl/inherit-missing-shows-pos.expected b/tests/functional/repl/inherit-missing-shows-pos.expected index dc02de2070f1..0d313ee7fe3c 100644 --- a/tests/functional/repl/inherit-missing-shows-pos.expected +++ b/tests/functional/repl/inherit-missing-shows-pos.expected @@ -6,8 +6,11 @@ nix-repl> a = { x = 1; } nix-repl> inherit (a) y nix-repl> y -error: attribute 'y' missing - at «string»:1:13: +error: + … while evaluating the attribute 'y' + at «string»:1:13: 1| inherit (a) y | ^ + + error: attribute 'y' missing Did you mean x? diff --git a/tests/functional/repl/missing-attr-shows-def.expected b/tests/functional/repl/missing-attr-shows-def.expected new file mode 100644 index 000000000000..790421113e13 --- /dev/null +++ b/tests/functional/repl/missing-attr-shows-def.expected @@ -0,0 +1,18 @@ +Nix +Type :? for help. + +nix-repl> s = { a = { }; } + +nix-repl> s.a.b +error: + … while evaluating the attribute 'a.b' + at «string»:1:1: + 1| s.a.b + | ^ + + … from the definition of 'a' + at «string»:1:7: + 1| s = { a = { }; } + | ^ + + error: attribute 'b' missing diff --git a/tests/functional/repl/missing-attr-shows-def.in b/tests/functional/repl/missing-attr-shows-def.in new file mode 100644 index 000000000000..87bac0ec64ba --- /dev/null +++ b/tests/functional/repl/missing-attr-shows-def.in @@ -0,0 +1,3 @@ +# COM: a missing nested attribute reports where the containing set is defined +s = { a = { }; } +s.a.b