[llvm] 38de1c3 - [JSON] Display errors associated with Paths in context

Sam McCall via llvm-commits llvm-commits at lists.llvm.org
Wed Sep 23 15:34:28 PDT 2020


Author: Sam McCall
Date: 2020-09-24T00:34:11+02:00
New Revision: 38de1c33a8374bb16abfb024a973d851c170bafc

URL: https://github.com/llvm/llvm-project/commit/38de1c33a8374bb16abfb024a973d851c170bafc
DIFF: https://github.com/llvm/llvm-project/commit/38de1c33a8374bb16abfb024a973d851c170bafc.diff

LOG: [JSON] Display errors associated with Paths in context

When an error occurs processing a JSON object, seeing the actual
surrounding data helps. Dumping just the node where the problem
was identified can be too much or too little information.

printErrorContext() shows the error message in its context, as a comment.
JSON values along the path to the broken place are shown in some detail,
the rest of the document is elided. For example:

```
{
  "credentials": [
    {
      "username": /* error: expected string */ 42,
      "password": "secret"
    },
    { ... }
  ]
  "backups": { ... }
}
```

Differential Revision: https://reviews.llvm.org/D88103

Added: 
    

Modified: 
    llvm/include/llvm/Support/JSON.h
    llvm/lib/Support/JSON.cpp
    llvm/unittests/Support/JSONTest.cpp

Removed: 
    


################################################################################
diff  --git a/llvm/include/llvm/Support/JSON.h b/llvm/include/llvm/Support/JSON.h
index e98941d5390f..49e3c0f581c3 100644
--- a/llvm/include/llvm/Support/JSON.h
+++ b/llvm/include/llvm/Support/JSON.h
@@ -624,6 +624,14 @@ class Path::Root {
 
   /// 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
index f7c51a4ce3aa..0672dddd4ac0 100644
--- a/llvm/lib/Support/JSON.cpp
+++ b/llvm/lib/Support/JSON.cpp
@@ -235,6 +235,133 @@ Error Path::Root::getError() const {
   return createStringError(llvm::inconvertibleErrorCode(), OS.str());
 }
 
+namespace {
+
+std::vector<const Object::value_type *> sortedElements(const Object &O) {
+  std::vector<const Object::value_type *> 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<Segment> 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 @@ Expected<Value> parse(StringRef JSON) {
 }
 char ParseError::ID = 0;
 
-static std::vector<const Object::value_type *> sortedElements(const Object &O) {
-  std::vector<const Object::value_type *> 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
index 8c7b470756e3..51323280598e 100644
--- a/llvm/unittests/Support/JSONTest.cpp
+++ b/llvm/unittests/Support/JSONTest.cpp
@@ -465,12 +465,38 @@ TEST(JSONTest, Stream) {
 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


        


More information about the llvm-commits mailing list