我正在编写一个程序,我需要解析JavaScript源文件,提取一些事实,并插入/替换部分代码。根据以下代码,我需要做的事情的简化描述是:
foo(['a', 'b', 'c']);
提取'a'
,'b'
和'c'
并将代码重写为:
foo('bar', [0, 1, 2]);
我正在使用ANTLR来解析我的需求,生成C#3代码。其他人已经提供了JavaScript语法。解析源代码是有效的。
我遇到的问题是弄清楚如何正确分析和修改源文件。我尝试实际解决问题的每种方法都让我陷入了死胡同。我不禁想到我没有按照预期使用该工具,或者在处理AST时只是太过新手。
我的第一种方法是使用TokenRewriteStream
进行解析并为我感兴趣的规则实现EnterRule_*
部分方法。虽然这似乎使得令牌流的修改非常简单,但是没有足够的上下文信息供我分析。似乎所有我可以访问的是一个平坦的令牌流,这并没有告诉我足够的整个代码结构。例如,要检测是否正在调用foo
函数,只需查看第一个令牌就行不通,因为它也会错误地匹配:
a.b.foo();
为了让我能够进行更复杂的代码分析,我的第二种方法是使用重写规则修改语法以生成更多的树。现在,第一个示例代码块产生了这个:
Program CallExpression Identifier('foo') ArgumentList ArrayLiteral StringLiteral('a') StringLiteral('b') StringLiteral('c')
这对于分析代码非常有用。但是,现在我无法轻易地重写代码。当然,我可以修改树结构来表示我想要的代码,但我不能用它来输出源代码。我曾希望与每个节点相关联的令牌至少能给我足够的信息来知道我需要在原始文本中进行修改的位置,但我得到的只是令牌索引或行/列号。要使用行号和列号,我必须在源代码中进行尴尬的第二次传递。
我怀疑我在理解如何正确使用ANTLR来完成我需要的工作时遗漏了一些东西。有没有更合适的方法来解决这个问题?
答案 0 :(得分:7)
您要做的是program transformation,即从另一个程序自动生成一个程序。你所做的“错误”是假设解析器就是你所需要的,发现它不是,你必须填补空白。
这样做的工具有解析器(构建AST),意味着修改AST(程序和模式定向),以及 prettyprinters 将(修改的)AST转换回合法来源码。你似乎正在努力解决这样一个事实:ANTLR并没有带有漂亮的打印机;这不是其哲学的一部分; ANTLR是一个(精细的)解析器生成器。其他答案建议使用ANTLR的“字符串模板”,它们本身不是漂亮的打印机,但可以用它来实现一个,但要付出代价。这比看起来更难做;在compiling an AST back to source code看到我的答案。
这里真正的问题是广泛制作的 false 假设“如果我有一个解析器,那么我正在构建复杂的程序分析和转换工具。”请参阅我在Life After Parsing上的文章,对此进行长时间的讨论;基本上,除了你想要自己重建基础架构的很大一部分而不是继续执行任务之外,你需要更多的工具“只”一个解析器才能做到这一点。实际程序转换系统的其他有用功能通常包括源到源转换,这大大简化了查找和替换树中复杂模式的问题。
例如,如果您具有源代码转换功能(我们的工具DMS Software Reengineering Toolkit),您就可以使用这些DMS转换编写部分示例代码更改:
domain ECMAScript.
tag replace; -- says this is a special kind of temporary tree
rule barize(function_name:IDENTIFIER,list:expression_list,b:body):
expression->expression
= " \function_name ( '[' \list ']' ) "
-> "\function_name( \firstarg\(\function_name\), \replace\(\list\))";
rule replace_unit_list(s:character_literal):
expression_list -> expression_list
replace(s) -> compute_index_for(s);
rule replace_long_list(s:character_list, list:expression_list):
expression_list -> expression_list
"\replace\(\s\,\list)-> "compute_index_for\(\s\),\list";
使用rule-external“meta”程序“first_arg”(知道如何计算“bar”给定标识符“foo”[我猜你想要这样做],以及“compute_index_for”给出一个字符串文字,知道要用它替换它的整数。
个别重写规则具有参数列表“(....)”,其中表示子树的槽被命名,左侧作为匹配的模式,右侧作为替换,两者通常引用metaquotes “从目标语言(例如JavaScript)文本中分离重写规则语言文本。在metaquotes中找到了许多元转义**,表示特殊的重写规则语言项。这些是参数名称,表示参数表示的任何类型的名称树,或者表示外部元过程调用(例如first_arg;您将注意其参数列表(,)是元引用!),或者最后是“标记” “如”替换“,这是一种特殊的树,代表着未来进行更多转变的意图。
这一组特殊规则的工作原理是通过条形化版本替换候选函数调用,附加意图“替换”以转换列表。另外两个转换通过一次一个地处理列表的元素来转换“替换”来实现意图,并且将替换进一步向下推到列表中直到它最终脱离结束并且替换完成。 (这是循环的转换等价物。)
您的具体示例可能会有所不同,因为您对细节的确不准确。
应用这些规则来修改已解析的树后,DMS可以轻松地将结果打印出来(某些配置中的默认行为是“解析为AST,应用规则直到耗尽,漂亮的AST”,因为这很方便)。
您可以在(High School) Algebra as a DMS domain看到“定义语言”,“定义重写规则”,“应用规则和漂亮印刷”的完整过程。
其他程序转换系统包括TXL和Stratego。我们将DMS视为这些工业强度版本,我们在其中构建了所有基础设施,包括many standard language parsers and prettyprinters。
答案 1 :(得分:4)
所以我发现我实际上可以使用重写树语法并使用TokenRewriteStream
插入/替换令牌。而且,它实际上很容易做到。我的代码类似于以下内容:
var charStream = new ANTLRInputStream(stream);
var lexer = new JavaScriptLexer(charStream);
var tokenStream = new TokenRewriteStream(lexer);
var parser = new JavaScriptParser(tokenStream);
var program = parser.program().Tree as Program;
var dependencies = new List<IModule>();
var functionCall = (
from callExpression in program.Children.OfType<CallExpression>()
where callExpression.Children[0].Text == "foo"
select callExpression
).Single();
var argList = functionCall.Children[1] as ArgumentList;
var array = argList.Children[0] as ArrayLiteral;
tokenStream.InsertAfter(argList.Token.TokenIndex, "'bar', ");
for (var i = 0; i < array.Children.Count(); i++)
{
tokenStream.Replace(
(array.Children[i] as StringLiteral).Token.TokenIndex,
i.ToString());
}
var rewrittenCode = tokenStream.ToString();
答案 2 :(得分:2)
您是否查看了string template库。编写ANTLR的人是同一个人,他们打算一起工作。听起来它适合做你想要的东西,即。将匹配的语法规则输出为格式化文本。