Как я могу изменить текст токенов в 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".
Почему мои изменения в тексте токена не отражаются при вызове tokens.toString()?
Как мне узнать различные идентификаторы типа токена? Я прошел через мой отладчик и увидел, что 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