Manejo de errores en ANTLR4

83

El comportamiento predeterminado cuando el analizador no sabe qué hacer es imprimir mensajes en la terminal como:

línea 1:23 falta DECIMAL en '}'

Este es un buen mensaje, pero en el lugar equivocado. Prefiero recibir esto como una excepción.

Intenté usar el BailErrorStrategy, pero esto arroja un ParseCancellationExceptionsin un mensaje (causado por un InputMismatchException, también sin un mensaje).

¿Hay alguna manera de que pueda informar errores a través de excepciones mientras conserve la información útil en el mensaje?


Esto es lo que realmente busco: normalmente uso acciones en reglas para construir un objeto:

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); }
    ;

Luego, cuando invoco el analizador, hago algo como esto:

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;
}

Todo lo que realmente quiero es

  • para que la dataspec()llamada arroje una excepción (idealmente una marcada) cuando la entrada no se puede analizar
  • para que esa excepción tenga un mensaje útil y proporcione acceso al número de línea y la posición donde se encontró el problema

Luego dejaré que esa excepción burbujee en la pila de llamadas donde sea más adecuado para presentar un mensaje útil al usuario, de la misma manera que manejaría una conexión de red caída, leer un archivo corrupto, etc.

Vi que las acciones ahora se consideran "avanzadas" en ANTLR4, así que tal vez estoy haciendo las cosas de una manera extraña, pero no he investigado cuál sería la forma "no avanzada" de hacer esto desde esta manera. ha funcionado bien para nuestras necesidades.

Brad Mace
fuente

Respuestas:

98

Como he tenido un poco de dificultad con las dos respuestas existentes, me gustaría compartir la solución con la que terminé.

En primer lugar, creé mi propia versión de ErrorListener como sugirió 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);
      }
}

Tenga en cuenta el uso de a en ParseCancellationExceptionlugar de a, RecognitionExceptionya que DefaultErrorStrategy detectaría este último y nunca alcanzaría su propio código.

Creando una estrategia de error completamente nueva como Brad MaceNo es necesario sugirió ya que DefaultErrorStrategy produce mensajes de error bastante buenos por defecto.

Luego uso el ErrorListener personalizado en mi función de análisis:

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);
}

(Para obtener más información sobre lo que MyParseRuleshace, consulte aquí ).

Esto le dará los mismos mensajes de error que se imprimirían en la consola de forma predeterminada, solo en forma de excepciones adecuadas.

Mouagip
fuente
3
Probé esto y confirmo que funcionó bien. Creo que esta es la más fácil de las 3 soluciones propuestas.
Kami
1
Éste es el camino correcto a seguir. La forma más sencilla de hacerlo. El "problema" ocurre en el lexer y tiene sentido informarlo en ese mismo momento si es importante que la entrada sea válida antes de intentar analizar. ++
RubberDuck
¿Hay alguna razón en particular para usar la ThrowingErrorListenerclase como Singleton?
RonyHe
@RonyHe No, esto es solo una adaptación del código de Sam Harwells .
Mouagip
Esta solución funcionó para mí con una advertencia: estamos tratando de analizar usando SLL y luego retroceder a LL, y resulta que al hacerlo no se produjo ningún error al realizar el análisis alternativo. La solución fue construir un analizador completamente nuevo para el segundo intento en lugar de restablecer el analizador; aparentemente, restablecer el analizador no restablece algún estado importante.
Trejkaz
51

Cuando usa DefaultErrorStrategyo el BailErrorStrategy, el ParserRuleContext.exceptioncampo se establece para cualquier nodo del árbol de análisis en el árbol de análisis resultante donde se produjo un error. La documentación de este campo dice (para las personas que no quieren hacer clic en un enlace adicional):

La excepción que obligó a esta regla a volver. Si la regla se completó con éxito, esto es null.

Editar: si lo usa DefaultErrorStrategy, la excepción de contexto de análisis no se propagará hasta el código de llamada, por lo que podrá examinar el exceptioncampo directamente. Si lo usa BailErrorStrategy, el ParseCancellationExceptionarrojado por él incluirá un RecognitionExceptionif you call getCause().

if (pce.getCause() instanceof RecognitionException) {
    RecognitionException re = (RecognitionException)pce.getCause();
    ParserRuleContext context = (ParserRuleContext)re.getCtx();
}

Edición 2: según su otra respuesta, parece que en realidad no desea una excepción, pero lo que desea es una forma diferente de informar los errores. En ese caso, estará más interesado en la ANTLRErrorListenerinterfaz. Desea llamar parser.removeErrorListeners()para eliminar el oyente predeterminado que escribe en la consola y luego llamar parser.addErrorListener(listener)a su propio oyente especial. A menudo utilizo el siguiente oyente como punto de partida, ya que incluye el nombre del archivo fuente con los mensajes.

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);
    }
}

Con esta clase disponible, puede usar lo siguiente para usarla.

lexer.removeErrorListeners();
lexer.addErrorListener(DescriptiveErrorListener.INSTANCE);
parser.removeErrorListeners();
parser.addErrorListener(DescriptiveErrorListener.INSTANCE);

Un ejemplo mucho más complicado de un detector de errores que utilizo para identificar ambigüedades que hacen que una gramática no sea SLL es la SummarizingDiagnosticErrorListenerclase enTestPerformance .

Sam Harwell
fuente
Ok ... ¿cómo hago uso de eso? ¿Se supone que debo usar algo como ((InputMismatchException) pce.getCause()).getCtx().exceptionpara llegar al útil mensaje de error?
Brad Mace
1
Experimenté un poco lanzando la excepción desde el oyente de errores, pero la excepción nunca parece aparecer. Acabo de terminar con NPE de las acciones en la gramática debido a las coincidencias fallidas. Agregué una historia de fondo a la pregunta, ya que parece que estoy nadando contra la corriente.
Brad Mace
Simplemente debe escribir una clase de utilidad para devolver la "línea", la "columna" y el "mensaje" de un RecognitionException. La información que desea está disponible en la excepción que ya se lanzó.
Sam Harwell
Amable lector, si eres como yo, te estarás preguntando de qué se trata REPORT_SYNTAX_ERRORS. Aquí está la respuesta: stackoverflow.com/questions/18581880/handling-errors-in-antlr-4
james.garriss
Este ejemplo es realmente útil. Creo que debería estar en algún lugar de la documentación oficial , parece que le falta una página para el manejo de errores. Sería bueno mencionar al menos a los oyentes de errores.
geekley
10

Lo que se me ocurrió hasta ahora se basa en extender DefaultErrorStrategyy anular sus reportXXXmétodos (aunque es muy posible que esté haciendo las cosas más complicadas de lo necesario):

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());
    }
}

Esto arroja excepciones con mensajes útiles, y la línea y la posición del problema se pueden obtener del offendingtoken o, si no está configurado, del currenttoken mediante el uso ((Parser) re.getRecognizer()).getCurrentToken()de RecognitionException.

Estoy bastante contento con cómo funciona esto, aunque tener seis reportXmétodos para anular me hace pensar que hay una mejor manera.

Brad Mace
fuente
funciona mejor para c #, la respuesta aceptada y más votada tenía errores de compilación en c #, cierta incompatibilidad del argumento genérico IToken vs int
sarh
0

Para cualquiera que esté interesado, aquí está el equivalente ANTLR4 C # de la respuesta de 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);
geekley
fuente