Skip to content

Commit d9dd0e5

Browse files
committed
[GR-9831] format the message for Syntax error to add more context akin to how cpython does it
PullRequest: graalpython/49
2 parents f4c8b6d + 7c11a13 commit d9dd0e5

File tree

3 files changed

+150
-44
lines changed

3 files changed

+150
-44
lines changed

graalpython/com.oracle.graal.python.test/src/tests/test_parser.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ def x(*, a=None, b):
6565
assert globs["retval"] == (None, 42)
6666

6767

68+
def test_syntax_error_simple():
69+
globs = {}
70+
was_exception = False
71+
try:
72+
exec("""c = a += 3""", globs)
73+
except SyntaxError:
74+
was_exception = True
75+
assert was_exception
76+
77+
6878
def test_lambda_no_args_with_nested_lambdas():
6979
no_err = True
7080
try:
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright (c) 2018, Oracle and/or its affiliates.
3+
*
4+
* The Universal Permissive License (UPL), Version 1.0
5+
*
6+
* Subject to the condition set forth below, permission is hereby granted to any
7+
* person obtaining a copy of this software, associated documentation and/or data
8+
* (collectively the "Software"), free of charge and under any and all copyright
9+
* rights in the Software, and any and all patent rights owned or freely
10+
* licensable by each licensor hereunder covering either (i) the unmodified
11+
* Software as contributed to or provided by such licensor, or (ii) the Larger
12+
* Works (as defined below), to deal in both
13+
*
14+
* (a) the Software, and
15+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
16+
* one is included with the Software (each a "Larger Work" to which the
17+
* Software is contributed by such licensors),
18+
*
19+
* without restriction, including without limitation the rights to copy, create
20+
* derivative works of, display, perform, and distribute the Software and make,
21+
* use, sell, offer for sale, import, export, have made, and have sold the
22+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
23+
* either these or other terms.
24+
*
25+
* This license is subject to the following condition:
26+
*
27+
* The above copyright notice and either this complete permission notice or at a
28+
* minimum a reference to the UPL must be included in all copies or substantial
29+
* portions of the Software.
30+
*
31+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
32+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
33+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
34+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
35+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
36+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
37+
* SOFTWARE.
38+
*/
39+
package com.oracle.graal.python.parser;
40+
41+
import org.antlr.v4.runtime.DefaultErrorStrategy;
42+
import org.antlr.v4.runtime.InputMismatchException;
43+
import org.antlr.v4.runtime.NoViableAltException;
44+
import org.antlr.v4.runtime.Parser;
45+
import org.antlr.v4.runtime.RecognitionException;
46+
import org.antlr.v4.runtime.Token;
47+
import org.antlr.v4.runtime.TokenStream;
48+
import org.antlr.v4.runtime.misc.Interval;
49+
50+
public class PythonErrorStrategy extends DefaultErrorStrategy {
51+
private static final String LINE_PADDING = " ";
52+
53+
@Override
54+
public void recover(Parser recognizer, RecognitionException e) {
55+
super.recover(recognizer, e);
56+
}
57+
58+
static int getLine(Throwable e) {
59+
if (e instanceof RecognitionException) {
60+
return ((RecognitionException) e).getOffendingToken().getLine();
61+
} else {
62+
Throwable cause = e.getCause();
63+
if (cause instanceof RecognitionException) {
64+
return ((RecognitionException) cause).getOffendingToken().getLine();
65+
}
66+
return -1;
67+
}
68+
}
69+
70+
private static String getTokeLineText(Parser recognizer, Token token) {
71+
TokenStream tokenStream = recognizer.getTokenStream();
72+
int index = token.getTokenIndex();
73+
// search for line start
74+
int tokenIndex = index;
75+
int start = -1;
76+
while (tokenIndex >= 0) {
77+
Token t = tokenStream.get(tokenIndex);
78+
if (t.getText().equals("\n")) {
79+
break;
80+
}
81+
start = t.getStartIndex();
82+
tokenIndex--;
83+
}
84+
85+
// search for line stop
86+
tokenIndex = index;
87+
int stop = -1;
88+
while (tokenIndex < tokenStream.size()) {
89+
Token t = tokenStream.get(tokenIndex);
90+
stop = t.getStopIndex();
91+
if (t.getText().equals("\n")) {
92+
break;
93+
}
94+
tokenIndex++;
95+
}
96+
return token.getInputStream().getText(Interval.of(start, stop));
97+
}
98+
99+
private static void handlePythonSyntaxError(Parser recognizer, RecognitionException e) {
100+
Token offendingToken = e.getOffendingToken();
101+
String lineText = getTokeLineText(recognizer, offendingToken);
102+
String errorMarker = new String(new char[offendingToken.getCharPositionInLine()]).replace('\0', ' ') + "^";
103+
String pythonSyntaxErrorMessage = "\n" + LINE_PADDING + lineText + "\n" + LINE_PADDING + errorMarker;
104+
recognizer.notifyErrorListeners(offendingToken, pythonSyntaxErrorMessage, e);
105+
}
106+
107+
@Override
108+
protected void reportInputMismatch(Parser recognizer, InputMismatchException e) {
109+
handlePythonSyntaxError(recognizer, e);
110+
}
111+
112+
@Override
113+
protected void reportNoViableAlternative(Parser recognizer, NoViableAltException e) {
114+
handlePythonSyntaxError(recognizer, e);
115+
}
116+
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/parser/PythonParserImpl.java

Lines changed: 24 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,11 @@
3333

3434
import org.antlr.v4.runtime.CharStreams;
3535
import org.antlr.v4.runtime.CodePointCharStream;
36-
import org.antlr.v4.runtime.NoViableAltException;
3736
import org.antlr.v4.runtime.ParserRuleContext;
38-
import org.antlr.v4.runtime.RecognitionException;
39-
import org.antlr.v4.runtime.Token;
4037

4138
import com.oracle.graal.python.PythonLanguage;
4239
import com.oracle.graal.python.nodes.PNode;
40+
import com.oracle.graal.python.parser.antlr.Builder;
4341
import com.oracle.graal.python.parser.antlr.Python3Parser;
4442
import com.oracle.graal.python.runtime.PythonCore;
4543
import com.oracle.graal.python.runtime.PythonParseResult;
@@ -56,13 +54,25 @@
5654
public final class PythonParserImpl implements PythonParser {
5755
private static final Map<String, ParserRuleContext> cachedParseTrees = new HashMap<>();
5856

57+
private static Python3Parser getPython3Parser(CodePointCharStream fromString) {
58+
Python3Parser parser = new Builder.Parser(fromString).build();
59+
parser.setErrorHandler(new PythonErrorStrategy());
60+
return parser;
61+
}
62+
63+
private static Python3Parser getPython3Parser(String string) {
64+
Python3Parser parser = new Builder.Parser(string).build();
65+
parser.setErrorHandler(new PythonErrorStrategy());
66+
return parser;
67+
}
68+
5969
@TruffleBoundary
6070
private static ParserRuleContext preParseWithAntlr(PythonCore core, Source source) {
6171
String path = source.getURI().toString();
6272
String[] pathParts = path.split(Pattern.quote(PythonCore.FILE_SEPARATOR));
6373
String fileDirAndName = pathParts[pathParts.length - 2] + PythonCore.FILE_SEPARATOR + pathParts[pathParts.length - 1];
6474
CodePointCharStream fromString = CharStreams.fromString(source.getCharacters().toString(), fileDirAndName);
65-
Python3Parser parser = new com.oracle.graal.python.parser.antlr.Builder.Parser(fromString).build();
75+
Python3Parser parser = getPython3Parser(fromString);
6676
ParserRuleContext input;
6777
if (!core.isInitialized()) {
6878
input = cachedParseTrees.get(fileDirAndName);
@@ -82,23 +92,12 @@ private static ParserRuleContext preParseWithAntlr(PythonCore core, Source sourc
8292
parser.reset();
8393
input = parser.eval_input();
8494
} catch (Throwable e2) {
85-
int line = -1;
86-
int column = -1;
8795
if (source.isInteractive() && e instanceof PIncompleteSourceException) {
8896
((PIncompleteSourceException) e).setSource(source);
8997
throw e;
90-
} else if (e instanceof RecognitionException) {
91-
Token token = ((RecognitionException) e).getOffendingToken();
92-
line = token.getLine();
93-
column = token.getCharPositionInLine();
94-
} else if (e.getCause() instanceof RecognitionException) {
95-
Token token = ((RecognitionException) e.getCause()).getOffendingToken();
96-
line = token.getLine();
97-
column = token.getCharPositionInLine();
98-
} else {
99-
throw core.raise(SyntaxError, e.getMessage());
10098
}
101-
throw core.raise(SyntaxError, getLocation(source, line), "invalid syntax: line %d, column %d. ", line, column);
99+
Node location = getLocation(source, PythonErrorStrategy.getLine(e));
100+
throw core.raise(SyntaxError, location, e.getMessage());
102101
}
103102
}
104103
}
@@ -107,7 +106,7 @@ private static ParserRuleContext preParseWithAntlr(PythonCore core, Source sourc
107106

108107
@TruffleBoundary
109108
private static ParserRuleContext preParseInlineWithAntlr(PythonCore core, Source source) {
110-
Python3Parser parser = new com.oracle.graal.python.parser.antlr.Builder.Parser(source.getCharacters().toString()).build();
109+
Python3Parser parser = getPython3Parser(source.getCharacters().toString());
111110
ParserRuleContext input;
112111
try {
113112
input = parser.single_input();
@@ -116,20 +115,8 @@ private static ParserRuleContext preParseInlineWithAntlr(PythonCore core, Source
116115
parser.reset();
117116
input = parser.eval_input();
118117
} catch (Throwable e2) {
119-
int line = -1;
120-
int column = -1;
121-
if (e instanceof RecognitionException) {
122-
Token token = ((RecognitionException) e).getOffendingToken();
123-
line = token.getLine();
124-
column = token.getCharPositionInLine();
125-
} else if (e.getCause() instanceof NoViableAltException) {
126-
Token token = ((NoViableAltException) e.getCause()).getOffendingToken();
127-
line = token.getLine();
128-
column = token.getCharPositionInLine();
129-
} else {
130-
throw core.raise(SyntaxError, e.getMessage());
131-
}
132-
throw core.raise(SyntaxError, getLocation(source, line), "invalid syntax: line %d, column %d. ", line, column);
118+
Node location = getLocation(source, PythonErrorStrategy.getLine(e));
119+
throw core.raise(SyntaxError, location, e.getMessage());
133120
}
134121
}
135122
return input;
@@ -164,7 +151,7 @@ public PNode parseInline(PythonCore core, Source source, Frame curFrame) {
164151
@Override
165152
@TruffleBoundary
166153
public PythonParseResult parseEval(PythonCore core, String expression, String filename) {
167-
Python3Parser parser = new com.oracle.graal.python.parser.antlr.Builder.Parser(expression).build();
154+
Python3Parser parser = getPython3Parser(expression);
168155
ParserRuleContext input;
169156
try {
170157
input = parser.eval_input();
@@ -179,7 +166,7 @@ public PythonParseResult parseEval(PythonCore core, String expression, String fi
179166
@Override
180167
@TruffleBoundary
181168
public PythonParseResult parseExec(PythonCore core, String expression, String filename) {
182-
Python3Parser parser = new com.oracle.graal.python.parser.antlr.Builder.Parser(expression).build();
169+
Python3Parser parser = getPython3Parser(expression);
183170
ParserRuleContext input;
184171
try {
185172
input = parser.file_input();
@@ -193,7 +180,7 @@ public PythonParseResult parseExec(PythonCore core, String expression, String fi
193180
@Override
194181
@TruffleBoundary
195182
public PythonParseResult parseSingle(PythonCore core, String expression, String filename) {
196-
Python3Parser parser = new com.oracle.graal.python.parser.antlr.Builder.Parser(expression).build();
183+
Python3Parser parser = getPython3Parser(expression);
197184
ParserRuleContext input;
198185
try {
199186
input = parser.single_input();
@@ -207,7 +194,7 @@ public PythonParseResult parseSingle(PythonCore core, String expression, String
207194
@Override
208195
@TruffleBoundary
209196
public boolean isIdentifier(PythonCore core, String snippet) {
210-
Python3Parser parser = new com.oracle.graal.python.parser.antlr.Builder.Parser(snippet).build();
197+
Python3Parser parser = getPython3Parser(snippet);
211198
Python3Parser.AtomContext input;
212199
try {
213200
input = parser.atom();
@@ -218,14 +205,7 @@ public boolean isIdentifier(PythonCore core, String snippet) {
218205
}
219206

220207
private static PException handleParserError(PythonCore core, Throwable e) {
221-
if (e instanceof RecognitionException) {
222-
Token token = ((RecognitionException) e).getOffendingToken();
223-
int line = token.getLine();
224-
int column = token.getCharPositionInLine();
225-
return core.raise(SyntaxError, "parser error at %d:%d\n%s", line, column, e.toString());
226-
} else {
227-
return core.raise(SyntaxError, e.getMessage());
228-
}
208+
return core.raise(SyntaxError, e.getMessage());
229209
}
230210

231211
private static PythonParseResult translateParseResult(PythonCore core, String name, ParserRuleContext input, Source source) {

0 commit comments

Comments
 (0)