Java com ANTLR

Java com ANTLR

1. Visão geral

Neste tutorial, faremos uma rápida visão geral do gerador de analisadorANTLR e mostraremos alguns aplicativos do mundo real.

2. ANTLR

O ANTLR (outra ferramenta para reconhecimento de idiomas) é uma ferramenta para processar texto estruturado.

Isso é feito fornecendo acesso a primitivas de processamento de idiomas, como lexers, gramáticas e analisadores, bem como o tempo de execução para processar o texto com relação a eles.

Muitas vezes é usado para construir ferramentas e estruturas. Por exemplo, o Hibernate usa o ANTLR para analisar e processar consultas HQL e o Elasticsearch para o Painless.

E Java é apenas uma ligação. O ANTLR também oferece ligações para C #, Python, JavaScript, Go, C ++ e Swift.

3. Configuração

Primeiro de tudo, vamos começar adicionandoantlr-runtime ao nossopom.xml:


    org.antlr
    antlr4-runtime
    4.7.1

E também oantlr-maven-plugin:


    org.antlr
    antlr4-maven-plugin
    4.7.1
    
        
            
                antlr4
            
        
    

É função do plug-in gerar código a partir das gramáticas que especificamos.

4. Como funciona?

Basicamente, quando queremos criar o analisador usandoANTLR Maven plugin, precisamos seguir três etapas simples:

  • preparar um arquivo de gramática

  • gerar fontes

  • crie o ouvinte

Então, vamos ver essas etapas em ação.

5. Usando uma gramática existente

Vamos primeiro usar ANTLR para analisar o código de métodos com caixa incorreta:

public class SampleClass {

    public void DoSomethingElse() {
        //...
    }
}

Resumindo, vamos validar se todos os nomes de métodos em nosso código começam com uma letra minúscula.

5.1. Prepare um arquivo de gramática

O que é bom é que já existem vários arquivos de gramática por aí que podem atender aos nossos propósitos.

Podemos criar o diretóriosrc/main/antlr4 e baixá-lo lá.

5.2. Gerar fontes

O ANTLR funciona gerando código Java correspondente aos arquivos de gramática que fornecemos, e o plug-in maven facilita:

mvn package

Por padrão, isso irá gerar vários arquivos no diretóriotarget/generated-sources/antlr4:

  • Java8.interp

  • Java8Listener.java

  • Java8BaseListener.java

  • Java8Lexer.java

  • Java8Lexer.interp

  • Java8Parser.java

  • Java8.tokens

  • Java8Lexer.tokens

Observe que os nomes desses arquivosare based on the name of the grammar file.

Precisaremos queJava8Lexer lixe os arquivosJava8Parser mais tarde, quando testarmos. Por enquanto, porém, precisamos doJava8BaseListener para criar nossoMethodUppercaseListener.

5.3. CriandoMethodUppercaseListener

Com base na gramática Java8 que usamos,Java8BaseListener tem vários métodos que podemos substituir, cada um correspondendo a um título no arquivo de gramática.

Por exemplo, a gramática define o nome do método, a lista de parâmetros e lança a cláusula da seguinte forma:

methodDeclarator
    :   Identifier '(' formalParameterList? ')' dims?
    ;

E entãoJava8BaseListener  tem um métodoenterMethodDeclarator que será invocado cada vez que esse padrão for encontrado.

Então, vamos substituirenterMethodDeclarator, retirarIdentifier e realizar nossa verificação:

public class UppercaseMethodListener extends Java8BaseListener {

    private List errors = new ArrayList<>();

    // ... getter for errors

    @Override
    public void enterMethodDeclarator(Java8Parser.MethodDeclaratorContext ctx) {
        TerminalNode node = ctx.Identifier();
        String methodName = node.getText();

        if (Character.isUpperCase(methodName.charAt(0))) {
            String error = String.format("Method %s is uppercased!", methodName);
            errors.add(error);
        }
    }
}

5.4. Teste

Agora, vamos fazer alguns testes. Primeiro, construímos o lexer:

String javaClassContent = "public class SampleClass { void DoSomething(){} }";
Java8Lexer java8Lexer = new Java8Lexer(CharStreams.fromString(javaClassContent));

Em seguida, instanciamos o analisador:

CommonTokenStream tokens = new CommonTokenStream(lexer);
Java8Parser parser = new Java8Parser(tokens);
ParseTree tree = parser.compilationUnit();

E então, o caminhante e o ouvinte:

ParseTreeWalker walker = new ParseTreeWalker();
UppercaseMethodListener listener= new UppercaseMethodListener();

Por último, dizemos ao ANTLR para percorrer nossa classe de amostra:

walker.walk(listener, tree);

assertThat(listener.getErrors().size(), is(1));
assertThat(listener.getErrors().get(0),
  is("Method DoSomething is uppercased!"));

6. Construindo nossa gramática

Agora, vamos tentar algo um pouco mais complexo, como analisar arquivos de log:

2018-May-05 14:20:18 INFO some error occurred
2018-May-05 14:20:19 INFO yet another error
2018-May-05 14:20:20 INFO some method started
2018-May-05 14:20:21 DEBUG another method started
2018-May-05 14:20:21 DEBUG entering awesome method
2018-May-05 14:20:24 ERROR Bad thing happened

Como temos um formato de registro personalizado, primeiro precisamos criar nossa própria gramática.

6.1. Prepare um arquivo de gramática

Primeiro, vamos ver se podemos criar um mapa mental de como cada linha de registro se parece em nosso arquivo.

Ou, se formos mais um nível, poderemos dizer:

: =

E assim por diante. É importante considerar isso para que possamos decidir em que nível de granularidade queremos analisar o texto.

Um arquivo de gramática é basicamente um conjunto de regras lexer e parser. Simply put, lexer rules describe the syntax of the grammar while parser rules describe the semantics.

Vamos começar definindo fragmentos que sãoreusable building blocks for lexer rules.

fragment DIGIT : [0-9];
fragment TWODIGIT : DIGIT DIGIT;
fragment LETTER : [A-Za-z];

A seguir, vamos definir as regras restantes do lexer:

DATE : TWODIGIT TWODIGIT '-' LETTER LETTER LETTER '-' TWODIGIT;
TIME : TWODIGIT ':' TWODIGIT ':' TWODIGIT;
TEXT   : LETTER+ ;
CRLF : '\r'? '\n' | '\r';

Com esses blocos de construção, podemos criar regras do analisador para a estrutura básica:

log : entry+;
entry : timestamp ' ' level ' ' message CRLF;

E então adicionaremos os detalhes paratimestamp:

timestamp : DATE ' ' TIME;

Paralevel:

level : 'ERROR' | 'INFO' | 'DEBUG';

E paramessage:

message : (TEXT | ' ')+;

E é isso! Nossa gramática está pronta para uso. Vamos colocá-lo no diretóriosrc/main/antlr4 como antes.

6.2. Gerar fontes

Lembre-se de que este é apenas ummvn package rápido e que criará vários arquivos comoLogBaseListener,LogParser e assim por diante, com base no nome de nossa gramática.

6.3. Crie nosso Log Listener

Agora, estamos prontos para implementar nosso ouvinte, que finalmente usaremos para analisar um arquivo de log em objetos Java.

Então, vamos começar com uma classe de modelo simples para a entrada de registro:

public class LogEntry {

    private LogLevel level;
    private String message;
    private LocalDateTime timestamp;

    // getters and setters
}

Agora, precisamos criar a subclasseLogBaseListener como antes:

public class LogListener extends LogBaseListener {

    private List entries = new ArrayList<>();
    private LogEntry current;

current manterá a linha de registro atual, que podemos reinicializar cada vez que inserirmos umlogEntry,  novamente com base em nossa gramática:

    @Override
    public void enterEntry(LogParser.EntryContext ctx) {
        this.current = new LogEntry();
    }

A seguir, usaremosenterTimestamp,enterLevel,eenterMessage para definir as propriedadesLogEntry apropriadas:

    @Override
    public void enterTimestamp(LogParser.TimestampContext ctx) {
        this.current.setTimestamp(
          LocalDateTime.parse(ctx.getText(), DEFAULT_DATETIME_FORMATTER));
    }

    @Override
    public void enterMessage(LogParser.MessageContext ctx) {
        this.current.setMessage(ctx.getText());
    }

    @Override
    public void enterLevel(LogParser.LevelContext ctx) {
        this.current.setLevel(LogLevel.valueOf(ctx.getText()));
    }

E, finalmente, vamos usar o métodoexitEntry para criar e adicionar nosso novoLogEntry:

    @Override
    public void exitLogEntry(LogParser.EntryContext ctx) {
        this.entries.add(this.current);
    }

Observe, a propósito, que nossoLogListener  não é seguro para threads!

6.4. Teste

E agora podemos testar novamente, como fizemos da última vez:

@Test
public void whenLogContainsOneErrorLogEntry_thenOneErrorIsReturned()
  throws Exception {

    String logLine ="2018-May-05 14:20:24 ERROR Bad thing happened";

    // instantiate the lexer, the parser, and the walker
    LogListener listener = new LogListener();
    walker.walk(listener, logParser.log());
    LogEntry entry = listener.getEntries().get(0);

    assertThat(entry.getLevel(), is(LogLevel.ERROR));
    assertThat(entry.getMessage(), is("Bad thing happened"));
    assertThat(entry.getTimestamp(), is(LocalDateTime.of(2018,5,5,14,20,24)));
}

7. Conclusão

Neste artigo, focamos em como criar o analisador personalizado para o próprio idioma usando o ANTLR.

Também vimos como usar arquivos gramaticais existentes e aplicá-los a tarefas muito simples, como a quebra de código.

Como sempre, todo o código usado aqui pode ser encontradoover on GitHub.