Jac*_*cob 7 parsing code-analysis antlr antlr3
我正在编写一个程序,我需要解析一个JavaScript源文件,提取一些事实,并插入/替换部分代码.根据以下代码,我需要做的事情的简化描述:
foo(['a', 'b', 'c']);
Run Code Online (Sandbox Code Playgroud)
提取'a'
,'b'
和'c'
,并将代码重写为:
foo('bar', [0, 1, 2]);
Run Code Online (Sandbox Code Playgroud)
我正在使用ANTLR来解析我的需求,生成C#3代码.其他人已经提供了JavaScript语法.解析源代码是有效的.
我遇到的问题是弄清楚如何实际正确分析和修改源文件.我尝试实际解决问题的每种方法都让我陷入了死胡同.我不禁想到我没有按照预期使用该工具,或者在处理AST时只是太过新手.
我的第一种方法是使用a解析TokenRewriteStream
并实现EnterRule_*
我感兴趣的规则的部分方法.虽然这似乎使得令牌流的修改变得非常简单,但是我的分析没有足够的上下文信息.似乎所有我可以访问的是一个平坦的令牌流,这并没有告诉我足够的整个代码结构.例如,要检测是否foo
正在调用该函数,只需查看第一个令牌就行不通,因为它也会错误地匹配:
a.b.foo();
Run Code Online (Sandbox Code Playgroud)
为了让我能够进行更复杂的代码分析,我的第二种方法是使用重写规则修改语法以生成更多的树.现在,第一个示例代码块产生:
Program CallExpression Identifier('foo') ArgumentList ArrayLiteral StringLiteral('a') StringLiteral('b') StringLiteral('c')
这对于分析代码非常有用.但是,现在我无法轻易地重写代码.当然,我可以修改树结构来表示我想要的代码,但我不能用它来输出源代码.我曾希望与每个节点相关联的令牌至少能给我足够的信息来知道我需要在原始文本中进行修改的位置,但我得到的只是令牌索引或行/列号.要使用行号和列号,我必须在源代码中进行尴尬的第二次传递.
我怀疑我在理解如何正确使用ANTLR做我需要的东西时遗漏了一些东西.有没有更合适的方法来解决这个问题?
您要做的是称为程序转换,即从另一个程序自动生成一个程序.你正在做什么"错误"是假设解析器是你所需要的,发现它不是,你必须填补空白.
这样做的工具有解析器(构建AST),意味着修改AST(程序和模式定向),以及将(修改的)AST转换回合法源代码的漂亮打印机.你似乎正在努力解决这样一个事实:ANTLR并没有带有漂亮的打印机; 这不是其哲学的一部分; ANTLR是一个(精细的)解析器生成器.其他答案建议使用ANTLR的"字符串模板",它们本身不是漂亮的打印机,但可以用来实现一个,以实现一个.这比看起来更难做; 请参阅我的回答,将AST编译回源代码.
这里真正的问题是广泛制作但错误的假设,"如果我有一个解析器,那么我正在构建复杂的程序分析和转换工具." 请参阅我的解析后的生活文章,对此进行长时间的讨论; 基本上,除了你想要自己重建基础架构的很大一部分而不是继续执行任务之外,你需要更多的工具"只"一个解析器才能做到这一点.实际程序转换系统的其他有用特征通常包括源到源转换,这大大简化了查找和替换树中复杂模式的问题.
例如,如果您具有源到源转换功能(我们的工具,DMS软件重新设计工具包),您将能够使用这些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";
Run Code Online (Sandbox Code Playgroud)
使用规则外部"元"程序"first_arg"(知道如何计算"bar"给定标识符"foo"[我猜你想要这样做],而"compute_index_for"给出一个字符串文字,知道什么整数替换它.
个别重写规则具有的参数列表"(....)"中,表示子树槽命名,作为模式匹配,并作为替换的右手侧的左手侧,两者通常引述迈达克 "这从目标语言(例如JavaScript)文本中分离重写规则语言文本.在元引用中找到了许多元转义**,表示特殊的重写规则语言项.通常这些是参数名称,并表示任何类型的参数表示的名称树,或表示外部元过程调用(例如first_arg;您将注意其参数列表(,)是元引用!),或者最后是"标记",例如"替换",这是一个特殊种类的树,代表着未来进行更多变革的意图.
这组特定的规则通过用条形化版本替换候选函数调用来工作,附加意图"替换"以转换列表.另外两个转换通过一次一个地处理列表中的元素来转换"替换"来实现意图,并且将替换进一步向下推到列表中直到它最终脱离结束并且替换完成.(这是循环的转换等价物).
您的具体示例可能会有所不同,因为您对细节的确不准确.
应用这些规则来修改已解析的树后,DMS可以简单地将结果打印出来(某些配置中的默认行为是"解析到AST,应用规则直到耗尽,漂亮的AST",因为这很方便).
您可以在(高中)代数中看到"定义语言","定义重写规则","应用规则和漂亮印刷"的完整过程作为DMS域.
其他程序转换系统包括TXL和Stratego.我们将DMS视为这些工业强度版本,其中我们构建了所有基础设施,包括许多标准语言解析器和漂亮打印机.
所以事实证明我实际上可以使用重写树语法并使用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();
Run Code Online (Sandbox Code Playgroud)