当解析器不知道该做什么时,默认行为是向终端打印消息,如:
第1:23行在"}"处缺少DECIMAL
这是一个很好的信息,但放错了地方。我宁愿把这个作为例外。
我尝试过使用BailErrorStrategy
,但这会抛出一个没有消息的ParseCancellationException
(由InputMismatchException
引起,也没有消息)。
有没有一种方法可以让它通过异常报告错误,同时保留消息中的有用信息?
以下是我真正想要的——我通常使用规则中的操作来构建一个对象:
dataspec returns [DataExtractor extractor]
@init {
DataExtractorBuilder builder = new DataExtractorBuilder(layout);
}
@after {
$extractor = builder.create();
}
: first=expr { builder.addAll($first.values); } (COMMA next=expr { builder.addAll($next.values); })* EOF
;
expr returns [List<ValueExtractor> values]
: a=atom { $values = Arrays.asList($a.val); }
| fields=fieldrange { $values = values($fields.fields); }
| '%' { $values = null; }
| ASTERISK { $values = values(layout); }
;
然后,当我调用解析器时,我会做这样的事情:
public static DataExtractor create(String dataspec) {
CharStream stream = new ANTLRInputStream(dataspec);
DataSpecificationLexer lexer = new DataSpecificationLexer(stream);
CommonTokenStream tokens = new CommonTokenStream(lexer);
DataSpecificationParser parser = new DataSpecificationParser(tokens);
return parser.dataspec().extractor;
}
我真正想要的只是
- 当输入无法解析时,
dataspec()
调用抛出异常(理想情况下为已检查的异常) - 使该异常具有有用的消息,并提供对发现问题的行号和位置的访问
然后,我会让这个异常出现在最适合向用户显示有用消息的调用堆栈中——就像我处理断开的网络连接、读取损坏的文件等一样。
我确实看到,在ANTLR4中,行动现在被认为是"高级"的,所以也许我正在以一种奇怪的方式处理事情,但我还没有研究什么是"非高级"的方式,因为这种方式一直很好地满足我们的需求。
由于我对现有的两个答案有点纠结,所以我想分享我最终得到的解决方案。
首先,我创建了自己版本的ErrorListener,就像Sam Harwell建议的那样:
public class ThrowingErrorListener extends BaseErrorListener {
public static final ThrowingErrorListener INSTANCE = new ThrowingErrorListener();
@Override
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e)
throws ParseCancellationException {
throw new ParseCancellationException("line " + line + ":" + charPositionInLine + " " + msg);
}
}
注意使用ParseCancellationException
而不是RecognitionException
,因为DefaultErrorStrategy会捕获后者,并且它永远不会到达您自己的代码。
像Brad Mace建议的那样创建一个全新的ErrorStrategy是没有必要的,因为默认情况下DefaultErrorStrategy会生成非常好的错误消息。
然后,我在解析函数中使用自定义ErrorListener:
public static String parse(String text) throws ParseCancellationException {
MyLexer lexer = new MyLexer(new ANTLRInputStream(text));
lexer.removeErrorListeners();
lexer.addErrorListener(ThrowingErrorListener.INSTANCE);
CommonTokenStream tokens = new CommonTokenStream(lexer);
MyParser parser = new MyParser(tokens);
parser.removeErrorListeners();
parser.addErrorListener(ThrowingErrorListener.INSTANCE);
ParserRuleContext tree = parser.expr();
MyParseRules extractor = new MyParseRules();
return extractor.visit(tree);
}
(有关MyParseRules
功能的更多信息,请参阅此处。)
这将为您提供与默认情况下打印到控制台相同的错误消息,只是以适当的异常形式。
使用DefaultErrorStrategy
或BailErrorStrategy
时,会为生成的分析树中发生错误的任何分析树节点设置ParserRuleContext.exception
字段。该字段的文档如下(对于不想点击额外链接的人):
强制返回此规则的异常。如果规则成功完成,则该规则为
null
。
编辑:如果使用DefaultErrorStrategy
,解析上下文异常不会一直传播到调用代码,因此您可以直接检查exception
字段。如果您使用BailErrorStrategy
,那么如果您调用getCause()
,它抛出的ParseCancellationException
将包括一个RecognitionException
。
if (pce.getCause() instanceof RecognitionException) {
RecognitionException re = (RecognitionException)pce.getCause();
ParserRuleContext context = (ParserRuleContext)re.getCtx();
}
编辑2:根据您的另一个答案,您似乎实际上并不想要异常,但您想要的是报告错误的不同方式。在这种情况下,您将对ANTLRErrorListener
接口更感兴趣。您需要调用parser.removeErrorListeners()
来删除写入控制台的默认侦听器,然后为您自己的特殊侦听器调用parser.addErrorListener(listener)
。我经常使用以下侦听器作为起点,因为它包含消息的源文件的名称。
public class DescriptiveErrorListener extends BaseErrorListener {
public static DescriptiveErrorListener INSTANCE = new DescriptiveErrorListener();
@Override
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol,
int line, int charPositionInLine,
String msg, RecognitionException e)
{
if (!REPORT_SYNTAX_ERRORS) {
return;
}
String sourceName = recognizer.getInputStream().getSourceName();
if (!sourceName.isEmpty()) {
sourceName = String.format("%s:%d:%d: ", sourceName, line, charPositionInLine);
}
System.err.println(sourceName+"line "+line+":"+charPositionInLine+" "+msg);
}
}
有了这个类,你可以使用下面的方法来使用它
lexer.removeErrorListeners();
lexer.addErrorListener(DescriptiveErrorListener.INSTANCE);
parser.removeErrorListeners();
parser.addErrorListener(DescriptiveErrorListener.INSTANCE);
一个更为复杂的错误侦听器示例是TestPerformance
中的SummarizingDiagnosticErrorListener
类,我用它来识别导致语法非SLL的歧义。
到目前为止,我所想到的是基于扩展DefaultErrorStrategy
并覆盖它的reportXXX
方法(尽管我完全有可能使事情变得比必要的更复杂):
public class ExceptionErrorStrategy extends DefaultErrorStrategy {
@Override
public void recover(Parser recognizer, RecognitionException e) {
throw e;
}
@Override
public void reportInputMismatch(Parser recognizer, InputMismatchException e) throws RecognitionException {
String msg = "mismatched input " + getTokenErrorDisplay(e.getOffendingToken());
msg += " expecting one of "+e.getExpectedTokens().toString(recognizer.getTokenNames());
RecognitionException ex = new RecognitionException(msg, recognizer, recognizer.getInputStream(), recognizer.getContext());
ex.initCause(e);
throw ex;
}
@Override
public void reportMissingToken(Parser recognizer) {
beginErrorCondition(recognizer);
Token t = recognizer.getCurrentToken();
IntervalSet expecting = getExpectedTokens(recognizer);
String msg = "missing "+expecting.toString(recognizer.getTokenNames()) + " at " + getTokenErrorDisplay(t);
throw new RecognitionException(msg, recognizer, recognizer.getInputStream(), recognizer.getContext());
}
}
这会抛出带有有用消息的异常,问题的行和位置可以从offending
令牌中获取,如果未设置,则可以通过在RecognitionException
上使用((Parser) re.getRecognizer()).getCurrentToken()
从current
令牌中获取。
我对它的工作方式很满意,尽管有六个reportX
方法可以覆盖让我觉得有更好的方法。
对于任何感兴趣的人来说,以下是ANTLR4 C#等价于Sam Harwell的答案:
using System; using System.IO; using Antlr4.Runtime;
public class DescriptiveErrorListener : BaseErrorListener, IAntlrErrorListener<int>
{
public static DescriptiveErrorListener Instance { get; } = new DescriptiveErrorListener();
public void SyntaxError(TextWriter output, IRecognizer recognizer, int offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e) {
if (!REPORT_SYNTAX_ERRORS) return;
string sourceName = recognizer.InputStream.SourceName;
// never ""; might be "<unknown>" == IntStreamConstants.UnknownSourceName
sourceName = $"{sourceName}:{line}:{charPositionInLine}";
Console.Error.WriteLine($"{sourceName}: line {line}:{charPositionInLine} {msg}");
}
public override void SyntaxError(TextWriter output, IRecognizer recognizer, Token offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e) {
this.SyntaxError(output, recognizer, 0, line, charPositionInLine, msg, e);
}
static readonly bool REPORT_SYNTAX_ERRORS = true;
}
lexer.RemoveErrorListeners();
lexer.AddErrorListener(DescriptiveErrorListener.Instance);
parser.RemoveErrorListeners();
parser.AddErrorListener(DescriptiveErrorListener.Instance);
对于使用Python的人来说,以下是Python 3中基于Mouagip答案的解决方案。
首先,定义一个自定义错误侦听器:
from antlr4.error.ErrorListener import ErrorListener
from antlr4.error.Errors import ParseCancellationException
class ThrowingErrorListener(ErrorListener):
def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
ex = ParseCancellationException(f'line {line}: {column} {msg}')
ex.line = line
ex.column = column
raise ex
然后将其设置为lexer和解析器:
lexer = MyScriptLexer(script)
lexer.removeErrorListeners()
lexer.addErrorListener(ThrowingErrorListener())
token_stream = CommonTokenStream(lexer)
parser = MyScriptParser(token_stream)
parser.removeErrorListeners()
parser.addErrorListener(ThrowingErrorListener())
tree = parser.script()