diff --git a/llvm/include/llvm/Support/JSON.h b/llvm/include/llvm/Support/JSON.h --- a/llvm/include/llvm/Support/JSON.h +++ b/llvm/include/llvm/Support/JSON.h @@ -624,6 +624,14 @@ /// Returns the last error reported, or else a generic error. Error getError() const; + /// Print the root value with the error shown inline as a comment. + /// Unrelated parts of the value are elided for brevity, e.g. + /// { + /// "id": 42, + /// "name": /* expected string */ null, + /// "properties": { ... } + /// } + void printErrorContext(const Value &, llvm::raw_ostream &) const; }; // Standard deserializers are provided for primitive types. diff --git a/llvm/lib/Support/JSON.cpp b/llvm/lib/Support/JSON.cpp --- a/llvm/lib/Support/JSON.cpp +++ b/llvm/lib/Support/JSON.cpp @@ -235,6 +235,133 @@ return createStringError(llvm::inconvertibleErrorCode(), OS.str()); } +namespace { + +std::vector sortedElements(const Object &O) { + std::vector Elements; + for (const auto &E : O) + Elements.push_back(&E); + llvm::sort(Elements, + [](const Object::value_type *L, const Object::value_type *R) { + return L->first < R->first; + }); + return Elements; +} + +// Prints a one-line version of a value that isn't our main focus. +// We interleave writes to OS and JOS, exploiting the lack of extra buffering. +// This is OK as we own the implementation. +// FIXME: once we have a "write custom serialized value" API, use it here. +void abbreviate(const Value &V, OStream &JOS, raw_ostream &OS) { + switch (V.kind()) { + case Value::Array: + JOS.array([&] { + if (!V.getAsArray()->empty()) + OS << " ... "; + }); + break; + case Value::Object: + JOS.object([&] { + if (!V.getAsObject()->empty()) + OS << " ... "; + }); + break; + case Value::String: { + llvm::StringRef S = *V.getAsString(); + if (S.size() < 40) { + JOS.value(V); + } else { + std::string Truncated = fixUTF8(S.take_front(37)); + Truncated.append("..."); + JOS.value(Truncated); + } + break; + } + default: + JOS.value(V); + } +} + +// Prints a semi-expanded version of a value that is our main focus. +// Array/Object entries are printed, but not recursively as they may be huge. +void abbreviateChildren(const Value &V, OStream &JOS, raw_ostream &OS) { + switch (V.kind()) { + case Value::Array: + JOS.array([&] { + for (const auto &V : *V.getAsArray()) + abbreviate(V, JOS, OS); + }); + break; + case Value::Object: + JOS.object([&] { + for (const auto *KV : sortedElements(*V.getAsObject())) { + JOS.attributeBegin(KV->first); + abbreviate(KV->second, JOS, OS); + JOS.attributeEnd(); + } + }); + break; + default: + JOS.value(V); + } +} + +} // namespace + +void Path::Root::printErrorContext(const Value &R, raw_ostream &OS) const { + OStream JOS(OS, /*IndentSize=*/2); + // PrintValue recurses down the path, printing the ancestors of our target. + // Siblings of nodes along the path are printed with abbreviate(), and the + // target itself is printed with the somewhat richer abbreviateChildren(). + // 'Recurse' is the lambda itself, to allow recursive calls. + auto PrintValue = [&](const Value &V, ArrayRef Path, auto &Recurse) { + // Print the target node itself, with the error as a comment. + // Also used if we can't follow our path, e.g. it names a field that + // *should* exist but doesn't. + auto HighlightCurrent = [&] { + std::string Comment = "error: "; + Comment.append(ErrorMessage.data(), ErrorMessage.size()); + JOS.comment(Comment); + abbreviateChildren(V, JOS, OS); + }; + if (Path.empty()) // We reached our target. + return HighlightCurrent(); + const Segment &S = Path.back(); // Path is in reverse order. + if (S.isField()) { + // Current node is an object, path names a field. + llvm::StringRef FieldName = S.field(); + const Object *O = V.getAsObject(); + if (!O || !O->get(FieldName)) + return HighlightCurrent(); + JOS.object([&] { + for (const auto *KV : sortedElements(*O)) { + JOS.attributeBegin(KV->first); + if (FieldName.equals(KV->first)) + Recurse(KV->second, Path.drop_back(), Recurse); + else + abbreviate(KV->second, JOS, OS); + JOS.attributeEnd(); + } + }); + } else { + // Current node is an array, path names an element. + const Array *A = V.getAsArray(); + if (!A || S.index() >= A->size()) + return HighlightCurrent(); + JOS.array([&] { + unsigned Current = 0; + for (const auto &V : *A) { + if (Current++ == S.index()) + Recurse(V, Path.drop_back(), Recurse); + else + abbreviate(V, JOS, OS); + } + }); + } + }; + PrintValue(R, ErrorPath, PrintValue); +} + namespace { // Simple recursive-descent JSON parser. class Parser { @@ -555,17 +682,6 @@ } char ParseError::ID = 0; -static std::vector sortedElements(const Object &O) { - std::vector Elements; - for (const auto &E : O) - Elements.push_back(&E); - llvm::sort(Elements, - [](const Object::value_type *L, const Object::value_type *R) { - return L->first < R->first; - }); - return Elements; -} - bool isUTF8(llvm::StringRef S, size_t *ErrOffset) { // Fast-path for ASCII, which is valid UTF-8. if (LLVM_LIKELY(isASCII(S))) diff --git a/llvm/unittests/Support/JSONTest.cpp b/llvm/unittests/Support/JSONTest.cpp --- a/llvm/unittests/Support/JSONTest.cpp +++ b/llvm/unittests/Support/JSONTest.cpp @@ -465,12 +465,38 @@ TEST(JSONTest, Path) { Path::Root R("foo"); Path P = R, A = P.field("a"), B = P.field("b"); + P.report("oh no"); + EXPECT_THAT_ERROR(R.getError(), FailedWithMessage("oh no when parsing foo")); A.index(1).field("c").index(2).report("boom"); EXPECT_THAT_ERROR(R.getError(), FailedWithMessage("boom at foo.a[1].c[2]")); B.field("d").field("e").report("bam"); EXPECT_THAT_ERROR(R.getError(), FailedWithMessage("bam at foo.b.d.e")); - P.report("oh no"); - EXPECT_THAT_ERROR(R.getError(), FailedWithMessage("oh no when parsing foo")); + + Value V = Object{ + {"a", Array{42}}, + {"b", + Object{{"d", + Object{ + {"e", Array{1, Object{{"x", "y"}}}}, + {"f", "a moderately long string: 48 characters in total"}, + }}}}, + }; + std::string Err; + raw_string_ostream OS(Err); + R.printErrorContext(V, OS); + const char *Expected = R"({ + "a": [ ... ], + "b": { + "d": { + "e": /* error: bam */ [ + 1, + { ... } + ], + "f": "a moderately long string: 48 characte..." + } + } +})"; + EXPECT_EQ(Expected, OS.str()); } } // namespace