From 88705f848be030ed17cd54ebc974d073c5b11e35 Mon Sep 17 00:00:00 2001 From: "Shahar \"Dawn\" Or" Date: Mon, 12 May 2025 18:44:07 +0700 Subject: [PATCH] New CLI flag `--replace-eval-errors` Co-authored-by: Farid Zakaria --- .../source/command-ref/nix-instantiate.md | 12 ++++++++ src/libexpr-tests/json.cc | 2 +- src/libexpr/include/nix/expr/value-to-json.hh | 4 +-- src/libexpr/primops.cc | 4 +-- src/libexpr/primops/fetchTree.cc | 2 +- src/libexpr/value-to-json.cc | 28 +++++++++++++++---- src/libflake/flake.cc | 2 +- src/libutil/posix-source-accessor.cc | 2 +- src/nix-env/nix-env.cc | 2 +- src/nix-instantiate/nix-instantiate.cc | 11 +++++--- src/nix/eval.cc | 9 +++++- src/nix/eval.md | 7 +++++ tests/functional/eval.sh | 17 +++++++++++ tests/functional/replace-eval-errors.nix | 11 ++++++++ 14 files changed, 93 insertions(+), 20 deletions(-) create mode 100644 tests/functional/replace-eval-errors.nix diff --git a/doc/manual/source/command-ref/nix-instantiate.md b/doc/manual/source/command-ref/nix-instantiate.md index 38454515d57..587a837809c 100644 --- a/doc/manual/source/command-ref/nix-instantiate.md +++ b/doc/manual/source/command-ref/nix-instantiate.md @@ -112,6 +112,11 @@ standard input. When used with `--eval`, print the resulting value as an JSON representation of the abstract syntax tree rather than as a Nix expression. +- `--replace-eval-errors` + + When used with `--eval` and `--json`, replace any evaluation errors with the string + `"«evaluation error»"`. + - `--xml` When used with `--eval`, print the resulting value as an XML @@ -205,3 +210,10 @@ $ nix-instantiate --eval --xml --strict --expr '{ x = {}; }' ``` + +Replacing evaluation errors: + +```console +$ nix-instantiate --eval --json --replace-eval-errors --expr '{ a = throw "fail"; }' +{"a":"«evaluation error»"} +``` diff --git a/src/libexpr-tests/json.cc b/src/libexpr-tests/json.cc index 11f31d05851..fb5b3b18ab6 100644 --- a/src/libexpr-tests/json.cc +++ b/src/libexpr-tests/json.cc @@ -9,7 +9,7 @@ namespace nix { std::string getJSONValue(Value& value) { std::stringstream ss; NixStringContext ps; - printValueAsJSON(state, true, value, noPos, ss, ps); + printValueAsJSON(state, true, false, value, noPos, ss, ps); return ss.str(); } }; diff --git a/src/libexpr/include/nix/expr/value-to-json.hh b/src/libexpr/include/nix/expr/value-to-json.hh index 1a691134705..45091dbcbac 100644 --- a/src/libexpr/include/nix/expr/value-to-json.hh +++ b/src/libexpr/include/nix/expr/value-to-json.hh @@ -10,10 +10,10 @@ namespace nix { -nlohmann::json printValueAsJSON(EvalState & state, bool strict, +nlohmann::json printValueAsJSON(EvalState & state, bool strict, bool replaceEvalErrors, Value & v, const PosIdx pos, NixStringContext & context, bool copyToStore = true); -void printValueAsJSON(EvalState & state, bool strict, +void printValueAsJSON(EvalState & state, bool strict, bool replaceEvalErrors, Value & v, const PosIdx pos, std::ostream & str, NixStringContext & context, bool copyToStore = true); diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 535a9a50117..34b292bd199 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -1388,7 +1388,7 @@ static void derivationStrictInternal( if (i->name == state.sStructuredAttrs) continue; - jsonObject->emplace(key, printValueAsJSON(state, true, *i->value, pos, context)); + jsonObject->emplace(key, printValueAsJSON(state, true, false, *i->value, pos, context)); if (i->name == state.sBuilder) drv.builder = state.forceString(*i->value, context, pos, context_below); @@ -2328,7 +2328,7 @@ static void prim_toJSON(EvalState & state, const PosIdx pos, Value * * args, Val { std::ostringstream out; NixStringContext context; - printValueAsJSON(state, true, *args[0], pos, out, context); + printValueAsJSON(state, true, false, *args[0], pos, out, context); v.mkString(toView(out), context); } diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc index 745705e04c1..ebc63c5a36c 100644 --- a/src/libexpr/primops/fetchTree.cc +++ b/src/libexpr/primops/fetchTree.cc @@ -135,7 +135,7 @@ static void fetchTree( attrs.emplace(state.symbols[attr.name], uint64_t(intValue)); } else if (state.symbols[attr.name] == "publicKeys") { experimentalFeatureSettings.require(Xp::VerifiedFetches); - attrs.emplace(state.symbols[attr.name], printValueAsJSON(state, true, *attr.value, pos, context).dump()); + attrs.emplace(state.symbols[attr.name], printValueAsJSON(state, true, false, *attr.value, pos, context).dump()); } else state.error("argument '%s' to '%s' is %s while a string, Boolean or integer is expected", diff --git a/src/libexpr/value-to-json.cc b/src/libexpr/value-to-json.cc index 4c0667d9e50..a406c95db65 100644 --- a/src/libexpr/value-to-json.cc +++ b/src/libexpr/value-to-json.cc @@ -6,12 +6,13 @@ #include #include #include +#include namespace nix { using json = nlohmann::json; // TODO: rename. It doesn't print. -json printValueAsJSON(EvalState & state, bool strict, +json printValueAsJSON(EvalState & state, bool strict, bool replaceEvalErrors, Value & v, const PosIdx pos, NixStringContext & context, bool copyToStore) { checkInterrupt(); @@ -54,13 +55,27 @@ json printValueAsJSON(EvalState & state, bool strict, break; } if (auto i = v.attrs()->get(state.sOutPath)) - return printValueAsJSON(state, strict, *i->value, i->pos, context, copyToStore); + return printValueAsJSON(state, strict, replaceEvalErrors, *i->value, i->pos, context, copyToStore); else { out = json::object(); for (auto & a : v.attrs()->lexicographicOrder(state.symbols)) { try { - out.emplace(state.symbols[a->name], printValueAsJSON(state, strict, *a->value, a->pos, context, copyToStore)); + out.emplace(state.symbols[a->name], printValueAsJSON(state, strict, replaceEvalErrors, *a->value, a->pos, context, copyToStore)); } catch (Error & e) { + std::cerr << "Caught an Error of type: " << typeid(e).name() << std::endl; + // std::cerr << "Caught an Error of type: " << e.message() << std::endl; + // std::cerr << "Caught an Error of type: " << e.what() << std::endl; + + // TODO: Figure out what Error is here? + // We seem to be not catching FileNotFoundError. + bool isEvalError = dynamic_cast(&e); + bool isFileNotFoundError = dynamic_cast(&e); + // Restrict replaceEvalErrors only only evaluation errors + if (replaceEvalErrors && (isEvalError || isFileNotFoundError)) { + out.emplace(state.symbols[a->name], "«evaluation error»"); + continue; + } + e.addTrace(state.positions[a->pos], HintFmt("while evaluating attribute '%1%'", state.symbols[a->name])); throw; @@ -75,8 +90,9 @@ json printValueAsJSON(EvalState & state, bool strict, int i = 0; for (auto elem : v.listItems()) { try { - out.push_back(printValueAsJSON(state, strict, *elem, pos, context, copyToStore)); + out.push_back(printValueAsJSON(state, strict, replaceEvalErrors, *elem, pos, context, copyToStore)); } catch (Error & e) { + // TODO: Missing catch e.addTrace(state.positions[pos], HintFmt("while evaluating list element at index %1%", i)); throw; @@ -106,11 +122,11 @@ json printValueAsJSON(EvalState & state, bool strict, return out; } -void printValueAsJSON(EvalState & state, bool strict, +void printValueAsJSON(EvalState & state, bool strict, bool replaceEvalErrors, Value & v, const PosIdx pos, std::ostream & str, NixStringContext & context, bool copyToStore) { try { - str << printValueAsJSON(state, strict, v, pos, context, copyToStore); + str << printValueAsJSON(state, strict, replaceEvalErrors, v, pos, context, copyToStore); } catch (nlohmann::json::exception & e) { throw JSONSerializationError("JSON serialization error: %s", e.what()); } diff --git a/src/libflake/flake.cc b/src/libflake/flake.cc index 516b38131f8..63ceedbd351 100644 --- a/src/libflake/flake.cc +++ b/src/libflake/flake.cc @@ -91,7 +91,7 @@ static void parseFlakeInputAttr( if (attr.name == state.symbols.create("publicKeys")) { experimentalFeatureSettings.require(Xp::VerifiedFetches); NixStringContext emptyContext = {}; - attrs.emplace(state.symbols[attr.name], printValueAsJSON(state, true, *attr.value, attr.pos, emptyContext).dump()); + attrs.emplace(state.symbols[attr.name], printValueAsJSON(state, true, false, *attr.value, attr.pos, emptyContext).dump()); } else state.error("flake input attribute '%s' is %s while a string, Boolean, or integer is expected", state.symbols[attr.name], showType(*attr.value)).debugThrow(); diff --git a/src/libutil/posix-source-accessor.cc b/src/libutil/posix-source-accessor.cc index 773540e6a09..12c0d0844d9 100644 --- a/src/libutil/posix-source-accessor.cc +++ b/src/libutil/posix-source-accessor.cc @@ -54,7 +54,7 @@ void PosixSourceAccessor::readFile( #endif )); if (!fd) - throw SysError("opening file '%1%'", ap.string()); + throw SysError("opening file9 '%1%'", ap.string()); struct stat st; if (fstat(fromDescriptorReadOnly(fd.get()), &st) == -1) diff --git a/src/nix-env/nix-env.cc b/src/nix-env/nix-env.cc index ff629d43020..f38c0579484 100644 --- a/src/nix-env/nix-env.cc +++ b/src/nix-env/nix-env.cc @@ -984,7 +984,7 @@ static void queryJSON(Globals & globals, std::vector & elems, bool metaObj[j] = nullptr; } else { NixStringContext context; - metaObj[j] = printValueAsJSON(*globals.state, true, *v, noPos, context); + metaObj[j] = printValueAsJSON(*globals.state, true, false, *v, noPos, context); } } } diff --git a/src/nix-instantiate/nix-instantiate.cc b/src/nix-instantiate/nix-instantiate.cc index c1b6cc66a4b..9abfaec4555 100644 --- a/src/nix-instantiate/nix-instantiate.cc +++ b/src/nix-instantiate/nix-instantiate.cc @@ -28,7 +28,7 @@ static int rootNr = 0; enum OutputKind { okPlain, okRaw, okXML, okJSON }; void processExpr(EvalState & state, const Strings & attrPaths, - bool parseOnly, bool strict, Bindings & autoArgs, + bool parseOnly, bool strict, bool replaceEvalErrors, Bindings & autoArgs, bool evalOnly, OutputKind output, bool location, Expr * e) { if (parseOnly) { @@ -58,7 +58,7 @@ void processExpr(EvalState & state, const Strings & attrPaths, else if (output == okXML) printValueAsXML(state, strict, location, vRes, std::cout, context, noPos); else if (output == okJSON) { - printValueAsJSON(state, strict, vRes, v.determinePos(noPos), std::cout, context); + printValueAsJSON(state, strict, replaceEvalErrors, vRes, v.determinePos(noPos), std::cout, context); std::cout << std::endl; } else { if (strict) state.forceValueDeep(vRes); @@ -106,6 +106,7 @@ static int main_nix_instantiate(int argc, char * * argv) OutputKind outputKind = okPlain; bool xmlOutputSourceLocation = true; bool strict = false; + bool replaceEvalErrors = false; Strings attrPaths; bool wantsReadWrite = false; @@ -147,6 +148,8 @@ static int main_nix_instantiate(int argc, char * * argv) xmlOutputSourceLocation = false; else if (*arg == "--strict") strict = true; + else if (*arg == "--replace-eval-errors") + replaceEvalErrors = true; else if (*arg == "--dry-run") settings.readOnlyMode = true; else if (*arg != "" && arg->at(0) == '-') @@ -184,7 +187,7 @@ static int main_nix_instantiate(int argc, char * * argv) if (readStdin) { Expr * e = state->parseStdin(); - processExpr(*state, attrPaths, parseOnly, strict, autoArgs, + processExpr(*state, attrPaths, parseOnly, strict, replaceEvalErrors, autoArgs, evalOnly, outputKind, xmlOutputSourceLocation, e); } else if (files.empty() && !fromArgs) files.push_back("./default.nix"); @@ -193,7 +196,7 @@ static int main_nix_instantiate(int argc, char * * argv) Expr * e = fromArgs ? state->parseExprFromString(i, state->rootPath(".")) : state->parseExprFromFile(resolveExprPath(lookupFileArg(*state, i))); - processExpr(*state, attrPaths, parseOnly, strict, autoArgs, + processExpr(*state, attrPaths, parseOnly, strict, replaceEvalErrors, autoArgs, evalOnly, outputKind, xmlOutputSourceLocation, e); } diff --git a/src/nix/eval.cc b/src/nix/eval.cc index be064e5527a..e9369eb3989 100644 --- a/src/nix/eval.cc +++ b/src/nix/eval.cc @@ -15,6 +15,7 @@ namespace nix::fs { using namespace std::filesystem; } struct CmdEval : MixJSON, InstallableValueCommand, MixReadOnlyOption { bool raw = false; + bool replaceEvalErrors = false; std::optional apply; std::optional writeTo; @@ -39,6 +40,12 @@ struct CmdEval : MixJSON, InstallableValueCommand, MixReadOnlyOption .labels = {"path"}, .handler = {&writeTo}, }); + + addFlag({ + .longName = "replace-eval-errors", + .description = "When used with `--json` the Nix evaluator will replace evaluation errors with a fixed value.", + .handler = {&replaceEvalErrors, true}, + }); } std::string description() override @@ -118,7 +125,7 @@ struct CmdEval : MixJSON, InstallableValueCommand, MixReadOnlyOption } else if (json) { - printJSON(printValueAsJSON(*state, true, *v, pos, context, false)); + printJSON(printValueAsJSON(*state, true, replaceEvalErrors, *v, pos, context, false)); } else { diff --git a/src/nix/eval.md b/src/nix/eval.md index bd5b035e18a..9b5e1aea6d2 100644 --- a/src/nix/eval.md +++ b/src/nix/eval.md @@ -48,6 +48,13 @@ R""( # cat ./out/subdir/bla 123 +* Replace evaluation errors: + + ```console + $ nix eval --json --replace-eval-errors --expr '{ a = throw "fail"; }' + {"a":"«evaluation error»"} + ``` + # Description This command evaluates the given Nix expression, and prints the result on standard output. diff --git a/tests/functional/eval.sh b/tests/functional/eval.sh index f876f5ac483..5bd14ed0347 100755 --- a/tests/functional/eval.sh +++ b/tests/functional/eval.sh @@ -35,6 +35,23 @@ nix-instantiate --eval -E 'assert 1 + 2 == 3; true' [[ $(nix-instantiate -A int --eval - < "./eval.nix") == 123 ]] [[ "$(nix-instantiate --eval -E '{"assert"=1;bar=2;}')" == '{ "assert" = 1; bar = 2; }' ]] +expected="$(echo '{ +"missingAttr":"«evaluation error»", +"insideAList":["«evaluation error»"], +"deeper":{v:"«evaluation error»"}, +"failedAssertion":"«evaluation error»", +"missingFile":"«evaluation error»", +"missingImport":"«evaluation error»", +"outOfBounds":"«evaluation error»", +"failedCoersion":"«evaluation error»", +"failedAddition":"«evaluation error»" +}' | tr -d '\n')" +actual="$(nix-instantiate --eval --json --strict --replace-eval-errors "./replace-eval-errors.nix")" +[[ $actual == "$expected" ]] || diff --unified <(echo "$actual") <(echo "$expected") >&2 + +actual="$(nix eval --json --replace-eval-errors -f "./replace-eval-errors.nix")" +[[ $actual == "$expected" ]] || diff --unified <(echo "$actual") <(echo "$expected") >&2 + # Check that symlink cycles don't cause a hang. ln -sfn cycle.nix "$TEST_ROOT/cycle.nix" (! nix eval --file "$TEST_ROOT/cycle.nix") diff --git a/tests/functional/replace-eval-errors.nix b/tests/functional/replace-eval-errors.nix new file mode 100644 index 00000000000..5aabc4a0d2b --- /dev/null +++ b/tests/functional/replace-eval-errors.nix @@ -0,0 +1,11 @@ +{ + missingAttr = let bar = { }; in bar.notExist; + insideAList = [ (throw "a throw") ]; + deeper = { v = throw "v"; }; + failedAssertion = assert true; assert false; null; + missingFile = builtins.readFile ./missing-file.txt; + missingImport = import ./missing-import.nix; + outOfBounds = builtins.elemAt [ 1 2 3 ] 100; + failedCoersion = "${1}"; + failedAddition = 1.0 + "a string"; +}