Skip to content

Commit cded477

Browse files
committed
This creates a multi source reader that can track which "source part" text comes from and map back to those places relative to each part.
The idea is that multiple files could feed a parsing document and errors reported in terms of those files not the logical place Of course it works for a single string and presents 1 source and nothing will be relative
1 parent 5f8c5bd commit cded477

15 files changed

Lines changed: 552 additions & 60 deletions
Lines changed: 16 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,20 @@
11
package graphql.parser;
22

33
import graphql.language.SourceLocation;
4+
import graphql.parser.MultiSourceReader.SourceAndLine;
45
import org.antlr.v4.runtime.BailErrorStrategy;
56
import org.antlr.v4.runtime.Parser;
67
import org.antlr.v4.runtime.RecognitionException;
78
import org.antlr.v4.runtime.Token;
89
import org.antlr.v4.runtime.misc.ParseCancellationException;
910

10-
import java.io.BufferedReader;
11-
import java.io.IOException;
12-
import java.io.StringReader;
13-
import java.util.ArrayList;
1411
import java.util.List;
1512

1613
public class ExtendedBailStrategy extends BailErrorStrategy {
17-
private final String input;
18-
private final String sourceName;
14+
private final MultiSourceReader multiSourceReader;
1915

20-
public ExtendedBailStrategy(String input, String sourceName) {
21-
this.input = input;
22-
this.sourceName = sourceName;
16+
public ExtendedBailStrategy(MultiSourceReader multiSourceReader) {
17+
this.multiSourceReader = multiSourceReader;
2318
}
2419

2520
@Override
@@ -41,7 +36,10 @@ public Token recoverInline(Parser recognizer) throws RecognitionException {
4136
}
4237

4338
InvalidSyntaxException mkMoreTokensException(Token token) {
44-
SourceLocation sourceLocation = new SourceLocation(token.getLine(), token.getCharPositionInLine());
39+
SourceAndLine sourceAndLine = multiSourceReader.getSourceAndLineFromOverallLine(token.getLine());
40+
int column = token.getCharPositionInLine();
41+
42+
SourceLocation sourceLocation = new SourceLocation(sourceAndLine.getLine(), column, sourceAndLine.getSourceName());
4543
String sourcePreview = mkPreview(token.getLine());
4644
return new InvalidSyntaxException(sourceLocation,
4745
"There are more tokens in the query that have not been consumed",
@@ -55,41 +53,28 @@ private InvalidSyntaxException mkException(Parser recognizer, RecognitionExcepti
5553
SourceLocation sourceLocation = null;
5654
Token currentToken = recognizer.getCurrentToken();
5755
if (currentToken != null) {
58-
int line = currentToken.getLine();
56+
int tokenLine = currentToken.getLine();
5957
int column = currentToken.getCharPositionInLine();
58+
SourceAndLine sourceAndLine = multiSourceReader.getSourceAndLineFromOverallLine(tokenLine);
6059
offendingToken = currentToken.getText();
61-
sourcePreview = mkPreview(line);
62-
sourceLocation = new SourceLocation(line, column, sourceName);
60+
sourcePreview = mkPreview(tokenLine);
61+
sourceLocation = new SourceLocation(sourceAndLine.getLine(), column, sourceAndLine.getSourceName());
6362
}
6463
return new InvalidSyntaxException(sourceLocation, null, sourcePreview, offendingToken, cause);
6564
}
6665

6766
/* grabs 3 lines before and after the syntax error */
6867
private String mkPreview(int line) {
6968
StringBuilder sb = new StringBuilder();
70-
BufferedReader reader = new BufferedReader(new StringReader(input));
7169
int startLine = line - 3;
7270
int endLine = line + 3;
73-
try {
74-
List<String> lines = readAllLines(reader);
75-
for (int i = 0; i < lines.size(); i++) {
76-
if (i >= startLine && i <= endLine) {
77-
sb.append(lines.get(i)).append('\n');
78-
}
71+
List<String> lines = multiSourceReader.getData();
72+
for (int i = 0; i < lines.size(); i++) {
73+
if (i >= startLine && i <= endLine) {
74+
sb.append(lines.get(i)).append('\n');
7975
}
80-
} catch (IOException ignored) {
81-
// this cant happen - its in memory
8276
}
8377
return sb.toString();
8478
}
8579

86-
private List<String> readAllLines(BufferedReader reader) throws IOException {
87-
List<String> lines = new ArrayList<>();
88-
String ln;
89-
while ((ln = reader.readLine()) != null) {
90-
lines.add(ln);
91-
}
92-
reader.close();
93-
return lines;
94-
}
9580
}

‎src/main/java/graphql/parser/GraphqlAntlrToLanguage.java‎

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@
5959
import graphql.parser.antlr.GraphqlLexer;
6060
import graphql.parser.antlr.GraphqlParser;
6161
import org.antlr.v4.runtime.CommonTokenStream;
62-
import org.antlr.v4.runtime.IntStream;
6362
import org.antlr.v4.runtime.ParserRuleContext;
6463
import org.antlr.v4.runtime.Token;
6564

@@ -81,10 +80,12 @@ public class GraphqlAntlrToLanguage {
8180
private static final int CHANNEL_COMMENTS = 2;
8281
private static final int CHANNEL_IGNORED_CHARS = 3;
8382
private final CommonTokenStream tokens;
83+
private final MultiSourceReader multiSourceReader;
8484

8585

86-
public GraphqlAntlrToLanguage(CommonTokenStream tokens) {
86+
public GraphqlAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiSourceReader) {
8787
this.tokens = tokens;
88+
this.multiSourceReader = multiSourceReader;
8889
}
8990

9091
//MARKER START: Here GraphqlOperation.g4 specific methods begin
@@ -809,14 +810,9 @@ protected Description newDescription(GraphqlParser.DescriptionContext descriptio
809810
}
810811

811812
protected SourceLocation getSourceLocation(Token token) {
812-
String sourceName = token.getTokenSource().getSourceName();
813-
if (IntStream.UNKNOWN_SOURCE_NAME.equals(sourceName)) {
814-
// UNKNOWN_SOURCE_NAME is Antrl's way of indicating that no source name was given during parsing --
815-
// which is the case when queries and other operations are parsed. We don't want this hardcoded
816-
// '<unknown>' sourceName to leak to clients when the response is serialized as JSON, so we null it.
817-
sourceName = null;
818-
}
819-
return new SourceLocation(token.getLine(), token.getCharPositionInLine() + 1, sourceName);
813+
MultiSourceReader.SourceAndLine sourceAndLine = multiSourceReader.getSourceAndLineFromOverallLine(token.getLine());
814+
int column = token.getCharPositionInLine() + 1;
815+
return new SourceLocation(sourceAndLine.getLine(), column, sourceAndLine.getSourceName());
820816
}
821817

822818
protected SourceLocation getSourceLocation(ParserRuleContext parserRuleContext) {
@@ -847,7 +843,9 @@ protected List<Comment> getCommentOnChannel(List<Token> refChannel) {
847843
continue;
848844
}
849845
text = text.replaceFirst("^#", "");
850-
comments.add(new Comment(text, new SourceLocation(refTok.getLine(), refTok.getCharPositionInLine())));
846+
MultiSourceReader.SourceAndLine sourceAndLine = multiSourceReader.getSourceAndLineFromOverallLine(refTok.getLine());
847+
int column = refTok.getCharPositionInLine();
848+
comments.add(new Comment(text, new SourceLocation(sourceAndLine.getLine(), column, sourceAndLine.getSourceName())));
851849
}
852850
return comments;
853851
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
package graphql.parser;
2+
3+
import graphql.Assert;
4+
import graphql.Internal;
5+
6+
import java.io.IOException;
7+
import java.io.LineNumberReader;
8+
import java.io.Reader;
9+
import java.io.StringReader;
10+
import java.io.UncheckedIOException;
11+
import java.util.ArrayList;
12+
import java.util.List;
13+
14+
/**
15+
* This reader allows you to read N number readers and combine them as one logical reader
16+
* however you can then map back to the underlying readers in terms of their source name
17+
* and the relative lines numbers.
18+
*
19+
* It can also track all data in memory if you want to have all of the previous read data in
20+
* place at some point in time.
21+
*/
22+
@Internal
23+
public class MultiSourceReader extends Reader {
24+
25+
private final List<SourcePart> sourceParts;
26+
private final StringBuilder data = new StringBuilder();
27+
private int currentIndex = 0;
28+
private int overallLineNumber = 0;
29+
private final boolean trackData;
30+
31+
32+
private MultiSourceReader(Builder builder) {
33+
this.sourceParts = builder.sourceParts;
34+
this.trackData = builder.trackData;
35+
}
36+
37+
@Override
38+
public int read(char[] cbuf, int off, int len) throws IOException {
39+
while (true) {
40+
synchronized (this) {
41+
if (currentIndex >= sourceParts.size()) {
42+
return -1;
43+
}
44+
SourcePart sourcePart = sourceParts.get(currentIndex);
45+
int read = sourcePart.lineReader.read(cbuf, off, len);
46+
overallLineNumber = calcLineNumber();
47+
if (read == -1) {
48+
currentIndex++;
49+
} else {
50+
trackData(cbuf, off, read);
51+
return read;
52+
}
53+
}
54+
}
55+
}
56+
57+
private void trackData(char[] cbuf, int off, int len) {
58+
if (trackData) {
59+
data.append(cbuf, off, len);
60+
}
61+
}
62+
63+
private int calcLineNumber() {
64+
int linenumber = 0;
65+
for (SourcePart sourcePart : sourceParts) {
66+
linenumber += sourcePart.lineReader.getLineNumber();
67+
}
68+
return linenumber;
69+
}
70+
71+
public static class SourceAndLine {
72+
private String sourceName = null;
73+
private int line = 0;
74+
75+
public String getSourceName() {
76+
return sourceName;
77+
}
78+
79+
public int getLine() {
80+
return line;
81+
}
82+
}
83+
84+
public SourceAndLine getSourceAndLineFromOverallLine(int overallLineNumber) {
85+
SourceAndLine sourceAndLine = new SourceAndLine();
86+
if (sourceParts.isEmpty()) {
87+
return sourceAndLine;
88+
}
89+
SourcePart currentPart;
90+
if (currentIndex >= sourceParts.size()) {
91+
currentPart = sourceParts.get(sourceParts.size() - 1);
92+
} else {
93+
currentPart = sourceParts.get(currentIndex);
94+
}
95+
int page = 0;
96+
int previousPage;
97+
for (SourcePart sourcePart : sourceParts) {
98+
sourceAndLine.sourceName = sourcePart.sourceName;
99+
if (sourcePart == currentPart) {
100+
// we cant go any further
101+
int offset = currentPart.lineReader.getLineNumber();
102+
previousPage = page;
103+
page += offset;
104+
if (page > overallLineNumber) {
105+
sourceAndLine.line = overallLineNumber - previousPage;
106+
} else {
107+
sourceAndLine.line = page;
108+
}
109+
return sourceAndLine;
110+
} else {
111+
previousPage = page;
112+
page += sourcePart.lineReader.getLineNumber();
113+
if (page > overallLineNumber) {
114+
sourceAndLine.line = overallLineNumber - previousPage;
115+
return sourceAndLine;
116+
}
117+
}
118+
}
119+
sourceAndLine.line = overallLineNumber - page;
120+
return sourceAndLine;
121+
}
122+
123+
public int getLineNumber() {
124+
synchronized (this) {
125+
if (sourceParts.isEmpty()) {
126+
return 0;
127+
}
128+
if (currentIndex >= sourceParts.size()) {
129+
return sourceParts.get(sourceParts.size() - 1).lineReader.getLineNumber();
130+
}
131+
return sourceParts.get(currentIndex).lineReader.getLineNumber();
132+
}
133+
}
134+
135+
public String getSourceName() {
136+
synchronized (this) {
137+
if (sourceParts.isEmpty()) {
138+
return null;
139+
}
140+
if (currentIndex >= sourceParts.size()) {
141+
return sourceParts.get(sourceParts.size() - 1).sourceName;
142+
}
143+
return sourceParts.get(currentIndex).sourceName;
144+
}
145+
}
146+
147+
public int getOverallLineNumber() {
148+
return overallLineNumber;
149+
}
150+
151+
public List<String> getData() {
152+
LineNumberReader reader = new LineNumberReader(new StringReader(data.toString()));
153+
List<String> lines = new ArrayList<>();
154+
while (true) {
155+
try {
156+
String line = reader.readLine();
157+
if (line == null) {
158+
return lines;
159+
}
160+
lines.add(line);
161+
} catch (IOException e) {
162+
throw new UncheckedIOException(e);
163+
}
164+
}
165+
}
166+
167+
@Override
168+
public void close() throws IOException {
169+
synchronized (this) {
170+
for (SourcePart sourcePart : sourceParts) {
171+
if (!sourcePart.closed) {
172+
sourcePart.lineReader.close();
173+
sourcePart.closed = true;
174+
}
175+
}
176+
}
177+
}
178+
179+
private static class SourcePart {
180+
String sourceName;
181+
LineNumberReader lineReader;
182+
boolean closed;
183+
}
184+
185+
186+
public static Builder newMultiSourceLineReader() {
187+
return new Builder();
188+
}
189+
190+
public static class Builder {
191+
List<SourcePart> sourceParts = new ArrayList<>();
192+
boolean trackData = true;
193+
194+
public Builder reader(Reader reader, String sourceName) {
195+
SourcePart sourcePart = new SourcePart();
196+
sourcePart.lineReader = new LineNumberReader(Assert.assertNotNull(reader));
197+
sourcePart.sourceName = sourceName;
198+
sourcePart.closed = false;
199+
sourceParts.add(sourcePart);
200+
return this;
201+
}
202+
203+
public Builder string(String input, String sourceName) {
204+
SourcePart sourcePart = new SourcePart();
205+
sourcePart.lineReader = new LineNumberReader(new StringReader(input));
206+
sourcePart.sourceName = sourceName;
207+
sourcePart.closed = false;
208+
sourceParts.add(sourcePart);
209+
return this;
210+
}
211+
212+
public Builder trackData(boolean trackData) {
213+
this.trackData = trackData;
214+
return this;
215+
216+
}
217+
218+
public MultiSourceReader build() {
219+
return new MultiSourceReader(this);
220+
}
221+
}
222+
223+
}

0 commit comments

Comments
 (0)