Java с ANTLR

Java с ANTLR

1. обзор

В этом руководстве мы сделаем краткий обзор генератора парсераANTLR и покажем некоторые реальные приложения.

2. ANTLR

ANTLR (ANother Tool for Language Recognition) - это инструмент для обработки структурированного текста.

Это достигается за счет предоставления нам доступа к примитивам языковой обработки, таким как лексеры, грамматики и парсеры, а также к среде выполнения для обработки текста против них.

Его часто используют для создания инструментов и фреймворков. Например, Hibernate использует ANTLR для анализа и обработки запросов HQL, а Elasticsearch - для Painless.

И Java - это всего лишь одно обязательное условие. ANTLR также предлагает привязки для C #, Python, JavaScript, Go, C ++ и Swift.

3. конфигурация

Прежде всего, давайте начнем с добавленияantlr-runtime к нашемуpom.xml:


    org.antlr
    antlr4-runtime
    4.7.1

А такжеantlr-maven-plugin:


    org.antlr
    antlr4-maven-plugin
    4.7.1
    
        
            
                antlr4
            
        
    

Задача плагина - генерировать код из указанных грамматик.

4. Как это работает?

По сути, когда мы хотим создать парсер с помощьюANTLR Maven plugin, нам нужно выполнить три простых шага:

  • подготовить файл грамматики

  • генерировать источники

  • создать слушателя

Итак, давайте посмотрим на эти шаги в действии.

5. Использование существующей грамматики

Давайте сначала воспользуемся ANTLR для анализа кода методов с плохим регистром:

public class SampleClass {

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

Проще говоря, мы проверим, что все имена методов в нашем коде начинаются с строчной буквы.

5.1. Подготовьте файл грамматики

Приятно то, что уже существует несколько файлов грамматики, которые могут удовлетворить наши цели.

Давайте использоватьthe Java8.g4 grammar file, которое мы нашли вANTLR’s Github grammar repo.

Мы можем создать каталогsrc/main/antlr4 и загрузить его туда.

5.2. Создать источники

ANTLR работает, генерируя код Java, соответствующий файлам грамматики, которые мы передаем ему, и плагин maven облегчает это:

mvn package

По умолчанию в каталогеtarget/generated-sources/antlr4 будет создано несколько файлов:

  • Java8.interp

  • Java8Listener.java

  • Java8BaseListener.java

  • Java8Lexer.java

  • Java8Lexer.interp

  • Java8Parser.java

  • Java8.tokens

  • Java8Lexer.tokens

Обратите внимание, что имена этих файловare based on the name of the grammar file.

Нам понадобятся файлыJava8Lexer иJava8Parser позже, когда мы будем тестировать. А пока нам нужныJava8BaseListener для создания нашегоMethodUppercaseListener.

5.3. СозданиеMethodUppercaseListener

На основе грамматики Java8, которую мы использовали,Java8BaseListener имеет несколько методов, которые мы можем переопределить, каждый из которых соответствует заголовку в файле грамматики.

Например, грамматика определяет имя метода, список параметров и предложение throws следующим образом:

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

Итак,Java8BaseListener - это методenterMethodDeclarator w, который будет вызываться каждый раз, когда встречается этот шаблон.

Итак, давайте переопределимenterMethodDeclarator, вытащимIdentifier и выполним нашу проверку:

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. тестирование

А теперь давайте проведем небольшое тестирование. Сначала мы строим лексер:

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

Затем мы создаем экземпляр парсера:

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

И затем, ходок и слушатель:

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

Наконец, мы говорим ANTLR пройти через наш образец класса:

walker.walk(listener, tree);

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

6. Создание нашей грамматики

Теперь давайте попробуем кое-что посложнее, например, проанализируем файлы журнала:

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

Поскольку у нас есть собственный формат журнала, нам сначала нужно создать собственную грамматику.

6.1. Подготовьте файл грамматики

Во-первых, давайте посмотрим, сможем ли мы создать мысленную карту того, как выглядит каждая строка журнала в нашем файле.

<дата> <уровень> <сообщение>

Или, если мы углубимся еще на один уровень, мы можем сказать:

<дата>: = <год> <тире> <месяц >< тире> <день>…

И так далее. Это важно учитывать, чтобы мы могли решить, на каком уровне детализации мы хотим анализировать текст.

Файл грамматики в основном представляет собой набор правил лексера и парсера. Simply put, lexer rules describe the syntax of the grammar while parser rules describe the semantics.с

Начнем с определения фрагментовreusable building blocks for lexer rules.

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

Затем давайте определим правила лексера остатка:

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

С этими строительными блоками мы можем построить правила парсера для базовой структуры:

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

А затем добавим детали дляtimestamp:

timestamp : DATE ' ' TIME;

Дляlevel:

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

И дляmessage:

message : (TEXT | ' ')+;

Вот и все! Наша грамматика готова к использованию. Как и прежде, мы поместим его в каталогsrc/main/antlr4.

6.2. Создать источники

Напомним, что это всего лишь быстрыйmvn package, и это создаст несколько файлов, таких какLogBaseListener,LogParser и т. Д., На основе имени нашей грамматики.

6.3. Создайте наш прослушиватель журнала

Теперь мы готовы реализовать наш слушатель, который в конечном итоге будем использовать для синтаксического анализа файла журнала на объекты Java.

Итак, давайте начнем с простого класса модели для записи журнала:

public class LogEntry {

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

    // getters and setters
}

Теперь нам нужно создать подклассLogBaseListener, как и раньше:

public class LogListener extends LogBaseListener {

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

current будет удерживать текущую строку журнала, которую мы можем повторно инициализировать каждый раз, когда вводимlogEntry, again на основе нашей грамматики:

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

Затем мы будем использоватьenterTimestamp,enterLevel, иenterMessage для установки соответствующих свойствLogEntry:

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

И наконец, давайте воспользуемся методомexitEntry , чтобы создать и добавить наш новыйLogEntry:

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

Обратите внимание, кстати, что нашLogListener  не является потокобезопасным!

6.4. тестирование

И теперь мы можем проверить снова, как в прошлый раз:

@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. Заключение

В этой статье мы сосредоточились на том, как создать собственный синтаксический анализатор для своего языка с помощью ANTLR.

Мы также увидели, как использовать существующие файлы грамматики и применять их для очень простых задач, таких как кодирование текста.

Как всегда, весь используемый здесь код находится вover on GitHub.