[JSON] Allow emitting comments in json::OStream

This isn't standard JSON, but is a popular extension.
It will be used to show errors in context, rendering pseudo-json for humans.

Differential Revision: https://reviews.llvm.org/D88103
This commit is contained in:
Sam McCall 2020-09-23 23:33:19 +02:00
parent 3f739f736b
commit 140b7b6f09
3 changed files with 54 additions and 3 deletions

View File

@ -706,6 +706,7 @@ public:
/// json::OStream allows writing well-formed JSON without materializing /// json::OStream allows writing well-formed JSON without materializing
/// all structures as json::Value ahead of time. /// all structures as json::Value ahead of time.
/// It's faster, lower-level, and less safe than OS << json::Value. /// It's faster, lower-level, and less safe than OS << json::Value.
/// It also allows emitting more constructs, such as comments.
/// ///
/// Only one "top-level" object can be written to a stream. /// Only one "top-level" object can be written to a stream.
/// Simplest usage involves passing lambdas (Blocks) to fill in containers: /// Simplest usage involves passing lambdas (Blocks) to fill in containers:
@ -791,6 +792,10 @@ class OStream {
Contents(); Contents();
objectEnd(); objectEnd();
} }
/// Emit a JavaScript comment associated with the next printed value.
/// The string must be valid until the next attribute or value is emitted.
/// Comments are not part of standard JSON, and many parsers reject them!
void comment(llvm::StringRef);
// High level functions to output object attributes. // High level functions to output object attributes.
// Valid only within an object (any number of times). // Valid only within an object (any number of times).
@ -826,6 +831,7 @@ class OStream {
} }
void valueBegin(); void valueBegin();
void flushComment();
void newline(); void newline();
enum Context { enum Context {
@ -838,6 +844,7 @@ class OStream {
bool HasValue = false; bool HasValue = false;
}; };
llvm::SmallVector<State, 16> Stack; // Never empty. llvm::SmallVector<State, 16> Stack; // Never empty.
llvm::StringRef PendingComment;
llvm::raw_ostream &OS; llvm::raw_ostream &OS;
unsigned IndentSize; unsigned IndentSize;
unsigned Indent = 0; unsigned Indent = 0;

View File

@ -9,6 +9,7 @@
#include "llvm/Support/JSON.h" #include "llvm/Support/JSON.h"
#include "llvm/Support/ConvertUTF.h" #include "llvm/Support/ConvertUTF.h"
#include "llvm/Support/Format.h" #include "llvm/Support/Format.h"
#include "llvm/Support/raw_ostream.h"
#include <cctype> #include <cctype>
namespace llvm { namespace llvm {
@ -633,9 +634,40 @@ void llvm::json::OStream::valueBegin() {
} }
if (Stack.back().Ctx == Array) if (Stack.back().Ctx == Array)
newline(); newline();
flushComment();
Stack.back().HasValue = true; Stack.back().HasValue = true;
} }
void OStream::comment(llvm::StringRef Comment) {
assert(PendingComment.empty() && "Only one comment per value!");
PendingComment = Comment;
}
void OStream::flushComment() {
if (PendingComment.empty())
return;
OS << (IndentSize ? "/* " : "/*");
// Be sure not to accidentally emit "*/". Transform to "* /".
while (!PendingComment.empty()) {
auto Pos = PendingComment.find("*/");
if (Pos == StringRef::npos) {
OS << PendingComment;
PendingComment = "";
} else {
OS << PendingComment.take_front(Pos) << "* /";
PendingComment = PendingComment.drop_front(Pos + 2);
}
}
OS << (IndentSize ? " */" : "*/");
// Comments are on their own line unless attached to an attribute value.
if (Stack.size() > 1 && Stack.back().Ctx == Singleton) {
if (IndentSize)
OS << ' ';
} else {
newline();
}
}
void llvm::json::OStream::newline() { void llvm::json::OStream::newline() {
if (IndentSize) { if (IndentSize) {
OS.write('\n'); OS.write('\n');
@ -657,6 +689,7 @@ void llvm::json::OStream::arrayEnd() {
if (Stack.back().HasValue) if (Stack.back().HasValue)
newline(); newline();
OS << ']'; OS << ']';
assert(PendingComment.empty());
Stack.pop_back(); Stack.pop_back();
assert(!Stack.empty()); assert(!Stack.empty());
} }
@ -675,6 +708,7 @@ void llvm::json::OStream::objectEnd() {
if (Stack.back().HasValue) if (Stack.back().HasValue)
newline(); newline();
OS << '}'; OS << '}';
assert(PendingComment.empty());
Stack.pop_back(); Stack.pop_back();
assert(!Stack.empty()); assert(!Stack.empty());
} }
@ -684,6 +718,7 @@ void llvm::json::OStream::attributeBegin(llvm::StringRef Key) {
if (Stack.back().HasValue) if (Stack.back().HasValue)
OS << ','; OS << ',';
newline(); newline();
flushComment();
Stack.back().HasValue = true; Stack.back().HasValue = true;
Stack.emplace_back(); Stack.emplace_back();
Stack.back().Ctx = Singleton; Stack.back().Ctx = Singleton;
@ -701,6 +736,7 @@ void llvm::json::OStream::attributeBegin(llvm::StringRef Key) {
void llvm::json::OStream::attributeEnd() { void llvm::json::OStream::attributeEnd() {
assert(Stack.back().Ctx == Singleton); assert(Stack.back().Ctx == Singleton);
assert(Stack.back().HasValue && "Attribute must have a value"); assert(Stack.back().HasValue && "Attribute must have a value");
assert(PendingComment.empty());
Stack.pop_back(); Stack.pop_back();
assert(Stack.back().Ctx == Object); assert(Stack.back().Ctx == Object);
} }

View File

@ -420,15 +420,19 @@ TEST(JSONTest, Stream) {
std::string S; std::string S;
llvm::raw_string_ostream OS(S); llvm::raw_string_ostream OS(S);
OStream J(OS, Indent); OStream J(OS, Indent);
J.comment("top*/level");
J.object([&] { J.object([&] {
J.attributeArray("foo", [&] { J.attributeArray("foo", [&] {
J.value(nullptr); J.value(nullptr);
J.comment("element");
J.value(42.5); J.value(42.5);
J.arrayBegin(); J.arrayBegin();
J.value(43); J.value(43);
J.arrayEnd(); J.arrayEnd();
}); });
J.comment("attribute");
J.attributeBegin("bar"); J.attributeBegin("bar");
J.comment("attribute value");
J.objectBegin(); J.objectBegin();
J.objectEnd(); J.objectEnd();
J.attributeEnd(); J.attributeEnd();
@ -437,17 +441,21 @@ TEST(JSONTest, Stream) {
return OS.str(); return OS.str();
}; };
const char *Plain = R"({"foo":[null,42.5,[43]],"bar":{},"baz":"xyz"})"; const char *Plain =
R"(/*top* /level*/{"foo":[null,/*element*/42.5,[43]],/*attribute*/"bar":/*attribute value*/{},"baz":"xyz"})";
EXPECT_EQ(Plain, StreamStuff(0)); EXPECT_EQ(Plain, StreamStuff(0));
const char *Pretty = R"({ const char *Pretty = R"(/* top* /level */
{
"foo": [ "foo": [
null, null,
/* element */
42.5, 42.5,
[ [
43 43
] ]
], ],
"bar": {}, /* attribute */
"bar": /* attribute value */ {},
"baz": "xyz" "baz": "xyz"
})"; })";
EXPECT_EQ(Pretty, StreamStuff(2)); EXPECT_EQ(Pretty, StreamStuff(2));