Как я могу изменить текст токенов в CommonTokenStream с помощью ANTLR?

Я пытаюсь изучить ANTLR и одновременно использовать его для текущего проекта.

Я дошел до того, что смог запустить лексер на куске кода и вывести его в CommonTokenStream. Это работает нормально, и я убедился, что исходный текст разбивается на соответствующие токены.

Теперь я хотел бы иметь возможность изменять текст некоторых токенов в этом потоке и отображать измененный исходный код.

Например я пробовал:

import org.antlr.runtime.*;
import java.util.*;

public class LexerTest
{
    public static final int IDENTIFIER_TYPE = 4;

    public static void main(String[] args)
    {
    String input = "public static void main(String[] args) { int myVar = 0; }";
    CharStream cs = new ANTLRStringStream(input);


        JavaLexer lexer = new JavaLexer(cs);
        CommonTokenStream tokens = new CommonTokenStream();
        tokens.setTokenSource(lexer);

        int size = tokens.size();
        for(int i = 0; i < size; i++)
        {
            Token token = (Token) tokens.get(i);
            if(token.getType() == IDENTIFIER_TYPE)
            {
                token.setText("V");
            }
        }
        System.out.println(tokens.toString());
    }  
}

Я пытаюсь установить для всего текста токена идентификатора строковый литерал "V".

  1. Почему мои изменения в тексте токена не отражаются при вызове tokens.toString()?

  2. Как мне узнать различные идентификаторы типа токена? Я прошел через мой отладчик и увидел, что ID для токенов IDENTIFIER был "4" (отсюда моя константа вверху). Но как бы я знал это иначе? Есть ли какой-либо другой способ сопоставления идентификаторов типа токена с именем токена?


РЕДАКТИРОВАТЬ:

Одна вещь, которая важна для меня, - я хочу, чтобы токены имели свои начальные и конечные позиции персонажей. То есть я не хочу, чтобы они отражали свои новые позиции с именами переменных, измененными на "V". Это так, я знаю, где токены были в исходном тексте.

3 ответа

ANTLR имеет способ сделать это в своем файле грамматики.

Допустим, вы разбираете строку, состоящую из чисел и строк, разделенных запятыми. Грамматика будет выглядеть так:

grammar Foo;

parse
  :  value ( ',' value )* EOF
  ;

value
  :  Number
  |  String
  ;

String
  :  '"' ( ~( '"' | '\\' ) | '\\\\' | '\\"' )* '"'
  ;

Number
  :  '0'..'9'+
  ;

Space
  :  ( ' ' | '\t' ) {skip();}
  ;

Все это должно выглядеть знакомым для вас. Допустим, вы хотите заключить квадратные скобки во все целочисленные значения. Вот как это сделать:

grammar Foo;

options {output=template; rewrite=true;} 

parse
  :  value ( ',' value )* EOF
  ;

value
  :  n=Number -> template(num={$n.text}) "[<num>]" 
  |  String
  ;

String
  :  '"' ( ~( '"' | '\\' ) | '\\\\' | '\\"' )* '"'
  ;

Number
  :  '0'..'9'+
  ;

Space
  :  ( ' ' | '\t' ) {skip();}
  ;

Как видите, я добавил несколько options вверху и добавил правило перезаписи (все после ->) после Number в value правило парсера.

Теперь, чтобы проверить все это, скомпилируйте и запустите этот класс:

import org.antlr.runtime.*;

public class FooTest {
  public static void main(String[] args) throws Exception {
    String text = "12, \"34\", 56, \"a\\\"b\", 78";
    System.out.println("parsing: "+text);
    ANTLRStringStream in = new ANTLRStringStream(text);
    FooLexer lexer = new FooLexer(in);
    CommonTokenStream tokens = new TokenRewriteStream(lexer); // Note: a TokenRewriteStream!
    FooParser parser = new FooParser(tokens);
    parser.parse();
    System.out.println("tokens: "+tokens.toString());
  }
}

который производит:

parsing: 12, "34", 56, "a\"b", 78
tokens: [12],"34",[56],"a\"b",[78]

В ANTLR 4 есть новое средство, использующее прослушиватели дерева разбора и TokenStreamRewriter (обратите внимание на разницу имен), которое можно использовать для наблюдения или преобразования деревьев. (Ответы, предлагающие TokenRewriteStream, применяются к ANTLR 3 и не будут работать с ANTLR 4.)

В ANTL4 класс XXXBaseListener создается для вас с обратными вызовами для входа и выхода из каждого нетерминального узла в грамматике (например, enterClassDeclaration()).

Вы можете использовать Слушатель двумя способами:

1) В качестве наблюдателя - просто переопределяя методы для создания произвольного вывода, связанного с вводимым текстом - например, переопределите enterClassDeclaration () и выведите строку для каждого класса, объявленного в вашей программе.

2) В качестве преобразователя, использующего TokenRewriteStream для изменения исходного текста при его прохождении. Для этого вы используете переписывающее устройство для внесения модификаций (добавления, удаления, замены) в методы обратного вызова, а переписывающее устройство и конец используются для вывода измененного текста.

См. Следующие примеры из книги ANTL4 для примера того, как выполнять преобразования:

https://github.com/mquinn/ANTLR4/blob/master/book_code/tour/InsertSerialIDListener.java

а также

https://github.com/mquinn/ANTLR4/blob/master/book_code/tour/InsertSerialID.java

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

Использование TokenRewriteStream позволяет гибко изменять текст только в определенных контекстах.

Это можно сделать с помощью подкласса класса потока токенов, который вы использовали. Вместо использования CommonTokenStream класс вы можете использовать TokenRewriteStream,

Таким образом, у вас будет TokenRewriteStream, потребляющий лексер, а затем вы запустите свой парсер.

В вашей грамматике вы обычно делаете замену так:

/** Convert "int foo() {...}" into "float foo();" */
function
:
{
    RefTokenWithIndex t(LT(1));  // copy the location of the token you want to replace
    engine.replace(t, "float");
}
type id:ID LPAREN (formalParameter (COMMA formalParameter)*)? RPAREN
    block[true]
;

Здесь мы заменили токен int, которому мы сопоставили текст с плавающей точкой. Информация о местоположении сохраняется, но текст, который она "соответствует", был изменен.

Чтобы проверить поток токенов после того, как вы используете тот же код, что и раньше.

Я использовал образец грамматики Java для создания сценария ANTLR для обработки R.java файл и перепишите все шестнадцатеричные значения в декомпилированном Android-приложении со значениями формы R.string.*, R.id.*, R.layout.* и так далее.

Ключ использует TokenStreamRewriter для обработки токенов и вывода результата.

Проект (Python) называется RestoreR

Модифицированный прослушиватель ANTLR для перезаписи

Я анализирую с помощью слушателя для чтения в файле R.java и создаю сопоставление от целого числа к строке, а затем заменяю шестнадцатеричные значения, поскольку я анализирую java-файлы программ другим слушателем, содержащим экземпляр перезаписывающего устройства.

class RValueReplacementListener(ParseTreeListener):
    replacements = 0
    r_mapping = {}
    rewriter = None

    def __init__(self, tokens):
        self.rewriter = TokenStreamRewriter(tokens)

    // Code removed for the sake of brevity

    # Enter a parse tree produced by JavaParser#integerLiteral.
    def enterIntegerLiteral(self, ctx:JavaParser.IntegerLiteralContext):
        hex_literal = ctx.HEX_LITERAL()
        if hex_literal is not None:
            int_literal = int(hex_literal.getText(), 16)
            if int_literal in self.r_mapping:
                # print('Replace: ' + ctx.getText() + ' with ' + self.r_mapping[int_literal])
                self.rewriter.replaceSingleToken(ctx.start, self.r_mapping[int_literal])
                self.replacements += 1
Другие вопросы по тегам