diff --git a/runtime-testsuite/pom.xml b/runtime-testsuite/pom.xml
index 189714b33b..a7e0819c37 100644
--- a/runtime-testsuite/pom.xml
+++ b/runtime-testsuite/pom.xml
@@ -117,8 +117,8 @@
maven-compiler-plugin
8
- 9
- 9
+ 11
+ 11
diff --git a/runtime-testsuite/resources/org/antlr/v4/test/runtime/descriptors/ParseTrees/JsonEmpty.txt b/runtime-testsuite/resources/org/antlr/v4/test/runtime/descriptors/ParseTrees/JsonEmpty.txt
new file mode 100644
index 0000000000..a89efbb5ac
--- /dev/null
+++ b/runtime-testsuite/resources/org/antlr/v4/test/runtime/descriptors/ParseTrees/JsonEmpty.txt
@@ -0,0 +1,53 @@
+[type]
+Parser
+
+[grammar]
+grammar VisitorCalc;
+
+s
+@init {
+
+}
+@after {
+
+}
+ : expr EOF
+ |
+ ;
+
+expr
+ : INT # number
+ | ID # var
+ | ID '(' ')' # func
+ | expr (MUL | DIV) expr # multiply
+ | expr (ADD | SUB) expr # add
+ ;
+
+INT : [0-9]+;
+ID : [a-zA-Z_]+ ;
+MUL : '*';
+DIV : '/';
+ADD : '+';
+SUB : '-';
+WS : [ \t]+ -> channel(HIDDEN);
+
+[start]
+s
+
+[input]
+
+
+[output]
+"""{"rules":["s","expr"],"input":"","tokens":[{"type":-1,"line":1,"pos":0,"channel":0,"start":0,"stop":-1}],"tree":"0"}
+"""
+
+[skip]
+Cpp
+CSharp
+Dart
+Go
+JavaScript
+PHP
+Swift
+Python2
+Python3
diff --git a/runtime-testsuite/resources/org/antlr/v4/test/runtime/descriptors/ParseTrees/JsonExpr.txt b/runtime-testsuite/resources/org/antlr/v4/test/runtime/descriptors/ParseTrees/JsonExpr.txt
new file mode 100644
index 0000000000..1aa4864b49
--- /dev/null
+++ b/runtime-testsuite/resources/org/antlr/v4/test/runtime/descriptors/ParseTrees/JsonExpr.txt
@@ -0,0 +1,53 @@
+[type]
+Parser
+
+[grammar]
+grammar VisitorCalc;
+
+s
+@init {
+
+}
+@after {
+
+}
+ : expr EOF
+ |
+ ;
+
+expr
+ : INT # number
+ | ID # var
+ | ID '(' ')' # func
+ | expr (MUL | DIV) expr # multiply
+ | expr (ADD | SUB) expr # add
+ ;
+
+INT : [0-9]+;
+ID : [a-zA-Z_]+ ;
+MUL : '*';
+DIV : '/';
+ADD : '+';
+SUB : '-';
+WS : [ \t]+ -> channel(HIDDEN);
+
+[start]
+s
+
+[input]
+1+2
+
+[output]
+"""{"rules":["s","expr"],"input":"1+2","tokens":[{"type":3,"line":1,"pos":0,"channel":0,"start":0,"stop":0},{"type":7,"line":1,"pos":1,"channel":0,"start":1,"stop":1},{"type":3,"line":1,"pos":2,"channel":0,"start":2,"stop":2},{"type":-1,"line":1,"pos":3,"channel":0,"start":3,"stop":2}],"tree":{"0":[{"1":[{"1":[0]},1,{"1":[2]}]},3]}}
+"""
+
+[skip]
+Cpp
+CSharp
+Dart
+Go
+JavaScript
+PHP
+Swift
+Python2
+Python3
diff --git a/runtime-testsuite/resources/org/antlr/v4/test/runtime/descriptors/ParseTrees/JsonMismatchedToken.txt b/runtime-testsuite/resources/org/antlr/v4/test/runtime/descriptors/ParseTrees/JsonMismatchedToken.txt
new file mode 100644
index 0000000000..ddbd7554d1
--- /dev/null
+++ b/runtime-testsuite/resources/org/antlr/v4/test/runtime/descriptors/ParseTrees/JsonMismatchedToken.txt
@@ -0,0 +1,57 @@
+[type]
+Parser
+
+[grammar]
+grammar VisitorCalc;
+
+s
+ : expr EOF
+ |
+ ;
+
+expr
+@init {
+
+}
+@after {
+
+}
+ : INT # number
+ | ID # var
+ | ID '(' ')' # func
+ | expr (MUL | DIV) expr # multiply
+ | expr (ADD | SUB) expr # add
+ ;
+
+INT : [0-9]+;
+ID : [a-zA-Z_]+ ;
+MUL : '*';
+DIV : '/';
+ADD : '+';
+SUB : '-';
+WS : [ \t]+ -> channel(HIDDEN);
+
+[start]
+expr
+
+[input]
+f(
+
+[output]
+"""{"rules":["s","expr"],"input":"f(","tokens":[{"type":4,"line":1,"pos":0,"channel":0,"start":0,"stop":0},{"type":1,"line":1,"pos":1,"channel":0,"start":1,"stop":1},{"type":-1,"line":1,"pos":2,"channel":0,"start":2,"stop":1}],"tree":{"1":[0,1,{"error":""}]}}
+"""
+
+[errors]
+"""line 1:2 missing ')' at ''
+"""
+
+[skip]
+Cpp
+CSharp
+Dart
+Go
+JavaScript
+PHP
+Swift
+Python2
+Python3
diff --git a/runtime-testsuite/resources/org/antlr/v4/test/runtime/descriptors/ParseTrees/JsonNotTiny.txt b/runtime-testsuite/resources/org/antlr/v4/test/runtime/descriptors/ParseTrees/JsonNotTiny.txt
new file mode 100644
index 0000000000..469af44eb0
--- /dev/null
+++ b/runtime-testsuite/resources/org/antlr/v4/test/runtime/descriptors/ParseTrees/JsonNotTiny.txt
@@ -0,0 +1,59 @@
+[type]
+Parser
+
+[grammar]
+grammar Expr;
+
+program
+@init {
+
+}
+@after {
+
+
+}
+ : stat EOF
+ | def EOF
+ ;
+
+stat: ID '=' expr ';'
+ | expr ';'
+ ;
+
+def : ID '(' ID (',' ID)* ')' '{' stat* '}' ;
+
+expr: ID
+ | INT
+ | func
+ | 'not' expr
+ | expr 'and' expr
+ | expr 'or' expr
+ ;
+
+func : ID '(' expr (',' expr)* ')' ;
+
+INT : [0-9]+ ;
+ID: [a-zA-Z_][a-zA-Z_0-9]* ;
+WS: [ \t\n\r\f]+ -> skip ;
+
+[start]
+program
+
+[input]
+f(x, y, z) { x = x and y or z; g(30); }
+
+[output]
+"""(program (def f ( x , y , z ) { (stat x = (expr (expr (expr x) and (expr y)) or (expr z)) ;) (stat (expr (func g ( (expr 30) ))) ;) }) )
+{"rules":["program","stat","def","expr","func"],"input":"f(x, y, z) { x = x and y or z; g(30); }","tokens":[{"type":12,"line":1,"pos":0,"channel":0,"start":0,"stop":0},{"type":3,"line":1,"pos":1,"channel":0,"start":1,"stop":1},{"type":12,"line":1,"pos":2,"channel":0,"start":2,"stop":2},{"type":4,"line":1,"pos":3,"channel":0,"start":3,"stop":3},{"type":12,"line":1,"pos":5,"channel":0,"start":5,"stop":5},{"type":4,"line":1,"pos":6,"channel":0,"start":6,"stop":6},{"type":12,"line":1,"pos":8,"channel":0,"start":8,"stop":8},{"type":5,"line":1,"pos":9,"channel":0,"start":9,"stop":9},{"type":6,"line":1,"pos":11,"channel":0,"start":11,"stop":11},{"type":12,"line":1,"pos":13,"channel":0,"start":13,"stop":13},{"type":1,"line":1,"pos":15,"channel":0,"start":15,"stop":15},{"type":12,"line":1,"pos":17,"channel":0,"start":17,"stop":17},{"type":9,"line":1,"pos":19,"channel":0,"start":19,"stop":21},{"type":12,"line":1,"pos":23,"channel":0,"start":23,"stop":23},{"type":10,"line":1,"pos":25,"channel":0,"start":25,"stop":26},{"type":12,"line":1,"pos":28,"channel":0,"start":28,"stop":28},{"type":2,"line":1,"pos":29,"channel":0,"start":29,"stop":29},{"type":12,"line":1,"pos":31,"channel":0,"start":31,"stop":31},{"type":3,"line":1,"pos":32,"channel":0,"start":32,"stop":32},{"type":11,"line":1,"pos":33,"channel":0,"start":33,"stop":34},{"type":5,"line":1,"pos":35,"channel":0,"start":35,"stop":35},{"type":2,"line":1,"pos":36,"channel":0,"start":36,"stop":36},{"type":7,"line":1,"pos":38,"channel":0,"start":38,"stop":38},{"type":-1,"line":1,"pos":39,"channel":0,"start":39,"stop":38}],"tree":{"0":[{"2":[0,1,2,3,4,5,6,7,8,{"1":[9,10,{"3":[{"3":[{"3":[11]},12,{"3":[13]}]},14,{"3":[15]}]},16]},{"1":[{"3":[{"4":[17,18,{"3":[19]},20]}]},21]},22]},23]}}
+"""
+
+[skip]
+Cpp
+CSharp
+Dart
+Go
+JavaScript
+PHP
+Swift
+Python2
+Python3
diff --git a/runtime-testsuite/resources/org/antlr/v4/test/runtime/descriptors/ParseTrees/JsonOneToken.txt b/runtime-testsuite/resources/org/antlr/v4/test/runtime/descriptors/ParseTrees/JsonOneToken.txt
new file mode 100644
index 0000000000..0cbf762df8
--- /dev/null
+++ b/runtime-testsuite/resources/org/antlr/v4/test/runtime/descriptors/ParseTrees/JsonOneToken.txt
@@ -0,0 +1,53 @@
+[type]
+Parser
+
+[grammar]
+grammar VisitorCalc;
+
+s
+ : expr EOF
+ |
+ ;
+
+expr
+@init {
+
+}
+@after {
+
+}
+ : INT # number
+ | ID # var
+ | ID '(' ')' # func
+ | expr (MUL | DIV) expr # multiply
+ | expr (ADD | SUB) expr # add
+ ;
+
+INT : [0-9]+;
+ID : [a-zA-Z_]+ ;
+MUL : '*';
+DIV : '/';
+ADD : '+';
+SUB : '-';
+WS : [ \t]+ -> channel(HIDDEN);
+
+[start]
+expr
+
+[input]
+8
+
+[output]
+"""{"rules":["s","expr"],"input":"8","tokens":[{"type":3,"line":1,"pos":0,"channel":0,"start":0,"stop":0},{"type":-1,"line":1,"pos":1,"channel":0,"start":1,"stop":0}],"tree":{"1":[0]}}
+"""
+
+[skip]
+Cpp
+CSharp
+Dart
+Go
+JavaScript
+PHP
+Swift
+Python2
+Python3
diff --git a/runtime-testsuite/resources/org/antlr/v4/test/runtime/descriptors/ParseTrees/JsonOneTokenOneRule.txt b/runtime-testsuite/resources/org/antlr/v4/test/runtime/descriptors/ParseTrees/JsonOneTokenOneRule.txt
new file mode 100644
index 0000000000..843037e079
--- /dev/null
+++ b/runtime-testsuite/resources/org/antlr/v4/test/runtime/descriptors/ParseTrees/JsonOneTokenOneRule.txt
@@ -0,0 +1,53 @@
+[type]
+Parser
+
+[grammar]
+grammar VisitorCalc;
+
+s
+@init {
+
+}
+@after {
+
+}
+ : expr EOF
+ |
+ ;
+
+expr
+ : INT # number
+ | ID # var
+ | ID '(' ')' # func
+ | expr (MUL | DIV) expr # multiply
+ | expr (ADD | SUB) expr # add
+ ;
+
+INT : [0-9]+;
+ID : [a-zA-Z_]+ ;
+MUL : '*';
+DIV : '/';
+ADD : '+';
+SUB : '-';
+WS : [ \t]+ -> channel(HIDDEN);
+
+[start]
+s
+
+[input]
+99
+
+[output]
+"""{"rules":["s","expr"],"input":"99","tokens":[{"type":3,"line":1,"pos":0,"channel":0,"start":0,"stop":1},{"type":-1,"line":1,"pos":2,"channel":0,"start":2,"stop":1}],"tree":{"0":[{"1":[0]},1]}}
+"""
+
+[skip]
+Cpp
+CSharp
+Dart
+Go
+JavaScript
+PHP
+Swift
+Python2
+Python3
diff --git a/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/CSharp.test.stg b/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/CSharp.test.stg
index 14def7e03d..a0ce031103 100644
--- a/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/CSharp.test.stg
+++ b/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/CSharp.test.stg
@@ -56,6 +56,8 @@ BailErrorStrategy() ::= <%ErrorHandler = new BailErrorStrategy();%>
ToStringTree(s) ::= <%.ToStringTree(this)%>
+ToJSON(s) ::= ""
+
Column() ::= "this.Column"
Text() ::= "this.Text"
diff --git a/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Cpp.test.stg b/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Cpp.test.stg
index 61efce7a28..9d84c058b6 100644
--- a/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Cpp.test.stg
+++ b/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Cpp.test.stg
@@ -35,6 +35,8 @@ BuildParseTrees() ::= "setBuildParseTree(true);"
BailErrorStrategy() ::= "_errHandler = std::make_shared\();"
ToStringTree(s) ::= "->toStringTree(this)"
+ToJSON(s) ::= ""
+
Column() ::= "getCharPositionInLine()"
Text() ::= "getText()"
ValEquals(a,b) ::= " == "
diff --git a/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Dart.test.stg b/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Dart.test.stg
index a9c224fe47..94e5bbe2c7 100644
--- a/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Dart.test.stg
+++ b/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Dart.test.stg
@@ -55,6 +55,7 @@ BuildParseTrees() ::= "buildParseTree = true;"
BailErrorStrategy() ::= <%errorHandler = new BailErrorStrategy();%>
ToStringTree(s) ::= <%.toStringTree(parser: this)%>
+ToJSON(s) ::= ""
Column() ::= "this.charPositionInLine"
diff --git a/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Go.test.stg b/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Go.test.stg
index c1f62e6f9e..cb5b4856ac 100644
--- a/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Go.test.stg
+++ b/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Go.test.stg
@@ -55,6 +55,7 @@ BuildParseTrees() ::= "p.BuildParseTrees = true"
BailErrorStrategy() ::= <%p.SetErrorHandler(antlr.NewBailErrorStrategy())%>
ToStringTree(s) ::= <%.ToStringTree(nil, p)%>
+ToJSON(s) ::= ""
Column() ::= "p.GetCharPositionInLine()"
diff --git a/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Java.test.stg b/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Java.test.stg
index b1aa46db10..20a1ba7e2d 100644
--- a/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Java.test.stg
+++ b/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Java.test.stg
@@ -56,6 +56,8 @@ BailErrorStrategy() ::= <%setErrorHandler(new BailErrorStrategy());%>
ToStringTree(s) ::= <%.toStringTree(this)%>
+ToJSON(s) ::= <%JsonSerializer.toJSON(,this)%>
+
Column() ::= "this.getCharPositionInLine()"
Text() ::= "this.getText()"
diff --git a/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/JavaScript.test.stg b/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/JavaScript.test.stg
index 709f13e39a..43b9352a48 100644
--- a/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/JavaScript.test.stg
+++ b/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/JavaScript.test.stg
@@ -55,6 +55,7 @@ BuildParseTrees() ::= "this.buildParseTrees = true;"
BailErrorStrategy() ::= <%this._errHandler = new antlr4.error.BailErrorStrategy();%>
ToStringTree(s) ::= <%.toStringTree(null, this)%>
+ToJSON(s) ::= ""
Column() ::= "this.column"
diff --git a/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/PHP.test.stg b/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/PHP.test.stg
index a2ea874663..6f66e14d51 100644
--- a/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/PHP.test.stg
+++ b/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/PHP.test.stg
@@ -38,6 +38,7 @@ BuildParseTrees() ::= "\$this->setBuildParseTree(true);"
BailErrorStrategy() ::= <%\$this->setErrorHandler(new Antlr\\Antlr4\\Runtime\\Error\\BailErrorStrategy());%>
ToStringTree(s) ::= <%->toStringTree(\$this->getRuleNames())%>
+ToJSON(s) ::= ""
Column() ::= "\$this->getCharPositionInLine()"
Text() ::= "\$this->getText()"
ValEquals(a,b) ::= <%===%>
diff --git a/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Python2.test.stg b/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Python2.test.stg
index 2292cafe7b..214269625f 100644
--- a/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Python2.test.stg
+++ b/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Python2.test.stg
@@ -55,6 +55,7 @@ BuildParseTrees() ::= "self._buildParseTrees = True"
BailErrorStrategy() ::= <%self._errHandler = BailErrorStrategy()%>
ToStringTree(s) ::= <%.toStringTree(recog=self)%>
+ToJSON(s) ::= ""
Column() ::= "self.column"
diff --git a/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Python3.test.stg b/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Python3.test.stg
index 65dcdcd83a..f0059f9534 100644
--- a/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Python3.test.stg
+++ b/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Python3.test.stg
@@ -55,6 +55,7 @@ BuildParseTrees() ::= "self._buildParseTrees = True"
BailErrorStrategy() ::= <%self._errHandler = BailErrorStrategy()%>
ToStringTree(s) ::= <%.toStringTree(recog=self)%>
+ToJSON(s) ::= ""
Column() ::= "self.column"
diff --git a/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Swift.test.stg b/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Swift.test.stg
index c2c12b63c2..0a8e03c46c 100755
--- a/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Swift.test.stg
+++ b/runtime-testsuite/resources/org/antlr/v4/test/runtime/templates/Swift.test.stg
@@ -55,6 +55,7 @@ BuildParseTrees() ::= "setBuildParseTree(true)"
BailErrorStrategy() ::= <%setErrorHandler(BailErrorStrategy())%>
ToStringTree(s) ::= <%.toStringTree(self)%>
+ToJSON(s) ::= ""
Column() ::= "self.getCharPositionInLine()"
diff --git a/runtime-testsuite/test/org/antlr/v4/test/runtime/java/api/VisitorCalc.g4 b/runtime-testsuite/test/org/antlr/v4/test/runtime/java/api/VisitorCalc.g4
index 6ca1c2e835..899287828e 100644
--- a/runtime-testsuite/test/org/antlr/v4/test/runtime/java/api/VisitorCalc.g4
+++ b/runtime-testsuite/test/org/antlr/v4/test/runtime/java/api/VisitorCalc.g4
@@ -2,15 +2,19 @@ grammar VisitorCalc;
s
: expr EOF
+ |
;
expr
: INT # number
+ | ID # var
+ | ID '(' ')' # func
| expr (MUL | DIV) expr # multiply
| expr (ADD | SUB) expr # add
;
INT : [0-9]+;
+ID : [a-zA-Z_]+ ;
MUL : '*';
DIV : '/';
ADD : '+';
diff --git a/runtime/Java/src/org/antlr/v4/runtime/misc/Utils.java b/runtime/Java/src/org/antlr/v4/runtime/misc/Utils.java
index e4d91a1910..56817d6a6b 100644
--- a/runtime/Java/src/org/antlr/v4/runtime/misc/Utils.java
+++ b/runtime/Java/src/org/antlr/v4/runtime/misc/Utils.java
@@ -70,6 +70,16 @@ public static String escapeWhitespace(String s, boolean escapeSpaces) {
return buf.toString();
}
+ /** @since 4.10.2 */
+ public static String escapeJSONString(String s) {
+ s = s.replace("\\", "\\\\");
+ s = s.replace("\"", "\\\"");
+ s = s.replace("\n", "\\n");
+ s = s.replace("\r", "\\r");
+ s = s.replace("\t", "\\t");
+ return s;
+ }
+
public static void writeFile(String fileName, String content) throws IOException {
writeFile(fileName, content, null);
}
diff --git a/runtime/Java/src/org/antlr/v4/runtime/tree/JsonSerializer.java b/runtime/Java/src/org/antlr/v4/runtime/tree/JsonSerializer.java
new file mode 100644
index 0000000000..de5a8abe74
--- /dev/null
+++ b/runtime/Java/src/org/antlr/v4/runtime/tree/JsonSerializer.java
@@ -0,0 +1,198 @@
+package org.antlr.v4.runtime.tree;
+
+import org.antlr.v4.runtime.*;
+import org.antlr.v4.runtime.atn.ATN;
+import org.antlr.v4.runtime.misc.Interval;
+import org.antlr.v4.runtime.misc.Utils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** This "class" wraps support functions that generate JSON for parse trees.
+ * The JSON includes everything needed to reconstruct a parse tree:
+ *
+ * Rule names (field: "rules")
+ * Input chars (field: "input")
+ * Tokens (field: "tokens")
+ * Parse tree (field: "tree"; refs rule indexes and token indexes)
+ *
+ * For example, given input "99" and a simple expression grammar giving parse tree
+ * "(s (expr 99) EOF)", the full JSON (formatted by jq) looks like:
+ *
+ * {
+ * "rules": [
+ * "s",
+ * "expr"
+ * ],
+ * "input": "99",
+ * "tokens": [
+ * {
+ * "type": 3,
+ * "line": 1,
+ * "pos": 0,
+ * "channel": 0,
+ * "start": 0,
+ * "stop": 1
+ * },
+ * {
+ * "type": -1,
+ * "line": 1,
+ * "pos": 2,
+ * "channel": 0,
+ * "start": 2,
+ * "stop": 1
+ * }
+ * ],
+ * "tree": {
+ * "0": [
+ * {
+ * "1": [
+ * 0
+ * ]
+ * },
+ * 1
+ * ]
+ * }
+ * }
+ *
+ * Notice that the tree is just a series of nested references to integers, which refer to rules
+ * and tokens.
+ *
+ * One potential use case: Create an ANTLR server that accepts a grammar and input as parameters then
+ * returns JSON for the parse tree and the tokens. This can be deserialized by JavaScript in a web browser
+ * to display the parse result.
+ *
+ * To load and dump elements from Python 3:
+ *
+ * import json
+ *
+ * with open("/tmp/t.json") as f:
+ * data = f.read()
+ *
+ * data = json.loads(data)
+ * print(data['rules'])
+ * print(data['input'])
+ * for t in data['tokens']:
+ * print(t)
+ * print(data['tree'])
+ *
+ * @since 4.10.2
+ */
+public class JsonSerializer {
+ /** Create a JSON representation of a parse tree and include all other information necessary to reconstruct
+ * a printable parse tree: the rules, input, tokens, and the tree structure that refers to the rule
+ * and token indexes. Extract all information from the parser, which is assumed to be in a state
+ * post-parse and the object that created tree t.
+ *
+ * @param t The parse tree to serialize as JSON
+ * @param recog The parser that created the parse tree and is in the post-recognition state
+ * @return JSON representing the parse tree
+ */
+ public static String toJSON(Tree t, Parser recog) {
+ String[] ruleNames = recog != null ? recog.getRuleNames() : null;
+ if ( t==null || ruleNames==null ) {
+ return null;
+ }
+ TokenStream tokenStream = recog.getInputStream();
+ CharStream inputStream = tokenStream.getTokenSource().getInputStream();
+ return toJSON(t, Arrays.asList(ruleNames), tokenStream, inputStream);
+ }
+
+ /** Create a JSON representation of a parse tree and include all other information necessary to reconstruct
+ * a printable parse tree: the rules, input, tokens, and the tree structure that refers to the rule
+ * and token indexes. The tree and rule names are required but the token stream and input stream are optional.
+ */
+ public static String toJSON(Tree t,
+ final List ruleNames,
+ final TokenStream tokenStream,
+ final CharStream inputStream)
+ {
+ if ( t==null || ruleNames==null ) {
+ return null;
+ }
+
+ StringBuilder buf = new StringBuilder();
+ buf.append("{");
+ buf.append("\"rules\":[\"");
+ buf.append(String.join("\",\"", ruleNames));
+ buf.append("\"],");
+
+ if ( inputStream!=null ) {
+ Interval allchar = Interval.of(0, inputStream.size() - 1);
+ String input = inputStream.getText(allchar);
+ input = Utils.escapeJSONString(input);
+ buf.append("\"input\":\"");
+ buf.append(input);
+ buf.append("\",");
+ }
+
+ if ( tokenStream!=null ) {
+ List tokenStrings = new ArrayList<>();
+ for (int i = 0; i < tokenStream.size(); i++) {
+ Token tok = tokenStream.get(i);
+ String s = String.format("{\"type\":%d,\"line\":%d,\"pos\":%d,\"channel\":%d,\"start\":%d,\"stop\":%d}",
+ tok.getType(), tok.getLine(), tok.getCharPositionInLine(), tok.getChannel(),
+ tok.getStartIndex(), tok.getStopIndex());
+ tokenStrings.add(s);
+ }
+ buf.append("\"tokens\":[");
+ buf.append(String.join(",", tokenStrings));
+ buf.append("],");
+ }
+
+ String tree = toJSONTree(t);
+ buf.append("\"tree\":");
+ buf.append(tree);
+ buf.append("}");
+
+ return buf.toString();
+ }
+
+ /** Create a JSON representation of a parse tree. The tree is just a series of nested references
+ * to integers, which refer to rules and tokens.
+ */
+ public static String toJSONTree(final Tree t) {
+ StringBuilder buf = new StringBuilder();
+ if ( t.getChildCount()==0 ) {
+ return getJSONNodeText(t);
+ }
+ buf.append("{");
+ buf.append(getJSONNodeText(t));
+ buf.append(":[");
+ for (int i = 0; i0 ) buf.append(',');
+ buf.append(toJSONTree(t.getChild(i)));
+ }
+ buf.append("]");
+ buf.append("}");
+ return buf.toString();
+ }
+
+ /** Create appropriate JSON text for a tree node */
+ public static String getJSONNodeText(Tree t) {
+ if ( t instanceof RuleContext) {
+ int ruleIndex = ((RuleContext)t).getRuleContext().getRuleIndex();
+ int altNumber = ((RuleContext) t).getAltNumber();
+ if ( altNumber!= ATN.INVALID_ALT_NUMBER ) {
+ return String.format("\"%d:%d\"",ruleIndex,altNumber);
+ }
+ return String.format("\"%d\"",ruleIndex);
+ }
+ else if ( t instanceof ErrorNode) {
+ Token symbol = ((TerminalNode)t).getSymbol();
+ if (symbol != null) {
+ return "{\"error\":\"" + symbol.getText() + "\"}";
+ }
+ return "{\"error\":\""+t.getPayload().toString()+"\"}";
+ }
+ else if ( t instanceof TerminalNode) {
+ Token symbol = ((TerminalNode)t).getSymbol();
+ if (symbol != null) {
+ return String.valueOf(symbol.getTokenIndex());
+ }
+ return "-1";
+ }
+ return "";
+ }
+}
diff --git a/runtime/Java/src/org/antlr/v4/runtime/tree/Trees.java b/runtime/Java/src/org/antlr/v4/runtime/tree/Trees.java
index bf6531b7aa..b114a6c485 100644
--- a/runtime/Java/src/org/antlr/v4/runtime/tree/Trees.java
+++ b/runtime/Java/src/org/antlr/v4/runtime/tree/Trees.java
@@ -6,11 +6,7 @@
package org.antlr.v4.runtime.tree;
-import org.antlr.v4.runtime.CommonToken;
-import org.antlr.v4.runtime.Parser;
-import org.antlr.v4.runtime.ParserRuleContext;
-import org.antlr.v4.runtime.RuleContext;
-import org.antlr.v4.runtime.Token;
+import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.atn.ATN;
import org.antlr.v4.runtime.misc.Interval;
import org.antlr.v4.runtime.misc.Predicate;