处理ANTLR4中的错误
parsing器不知道该怎么做的默认行为是将消息打印到terminal,如:
行1:23在'}'缺lessDECIMAL
这是一个好消息,但在错误的地方。 我宁愿接受这个例外。
我试过使用BailErrorStrategy
,但是这会抛出一个ParseCancellationException
没有消息(由InputMismatchException
引起,也没有消息)。
有没有办法让我能通过例外来报告错误,同时保留消息中的有用信息?
这就是我真正的追求 – 我通常使用规则中的动作来build立一个对象:
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); } ;
然后当我调用parsing器时,我做了这样的事情:
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()
调用抛出一个exception(理想情况下是一个检查一个),当input不能被parsing - 为了这个例外,有一个有用的信息,并提供访问行号和发现问题的位置
然后,我会让这个exception向上调用callstack,以便向用户展示一个有用的消息 – 就像我处理丢弃的networking连接,阅读损坏的文件等一样。
在ANTLR4中,我确实已经看到这些行为被认为是“先进的”,所以也许我正在以一种奇怪的方式来讨论事情,但是我没有看到这样做的“非高级”方法是什么,一直在为我们的需求工作。
由于我已经与现有的两个答案进行了一些斗争,所以我想分享一下我最终的解决scheme。
首先我创build了我自己的一个像Sam Harwell的ErrorListener版本,build议:
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会捕获后者,并且永远不会到达您自己的代码。
创build像Brad Mace这样的全新的ErrorStrategy是没有必要的,因为DefaultErrorStrategy默认会产生相当不错的错误信息。
然后我在parsing函数中使用自定义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
字段将针对发生错误的结果分析树中的任何分析树节点进行设置。 此字段的文档(针对不想单击额外链接的人员):
强制此规则返回的exception。 如果规则成功完成,则为
null
。
编辑:如果您使用DefaultErrorStrategy
,则分析上下文exception将不会传播到调用代码,因此您将能够直接检查exception
字段。 如果使用BailErrorStrategy
,则抛出的ParseCancellationException
将包含一个RecognitionException
如果调用getCause()
。
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);
一个更复杂的错误侦听器例子,我用它来识别模糊语言非SLL是TestPerformance
的SummarizingDiagnosticErrorListener
类 。
到目前为止我所提出的基于扩展DefaultErrorStrategy
并覆盖它的reportXXX
方法(尽pipe完全有可能使事情比必要的更复杂):
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()); } }
这会抛出有用消息的exception,并且通过使用((Parser) re.getRecognizer()).getCurrentToken()
可以从有问题的令牌或者如果没有设置,从current
令牌获得问题的线和位置((Parser) re.getRecognizer()).getCurrentToken()
关于RecognitionException
。
我很满意这是如何工作的,尽pipe有六种reportX
方法可以覆盖,这让我觉得还有更好的办法。