8

我正在编写一个程序,我需要解析 JavaScript 源文件、提取一些事实并插入/替换部分代码。给定以下代码,我需要做的各种事情的简化描述是:

foo(['a', 'b', 'c']);

提取'a', 'b', 并将'c'代码重写为:

foo('bar', [0, 1, 2]);

我正在使用 ANTLR 来满足我的解析需求,生成 C# 3 代码。其他人已经贡献了 JavaScript 语法。源代码的解析正在工作。

我遇到的问题是弄清楚如何真正正确地分析和修改源文件。我在实际解决问题时尝试采用的每种方法都将我带入了死胡同。我不禁认为我没有按预期使用该工具,或者在处理 AST 时我只是一个新手。

我的第一种方法是使用 a 解析TokenRewriteStream并实现EnterRule_*我感兴趣的规则的部分方法。虽然这似乎使修改令牌流变得非常容易,但我的分析没有足够的上下文信息。似乎我只能访问一个扁平的令牌流,这并不能告诉我关于整个代码结构的足够信息。例如,要检测foo函数是否被调用,仅仅查看第一个标记是行不通的,因为它也会错误地匹配:

a.b.foo();

为了让我能够进行更复杂的代码分析,我的第二种方法是使用重写规则修改语法以生成更多的树。现在,第一个示例代码块生成:

程序
    呼叫表达
        标识符('foo')
        参数列表
            数组字面量
                字符串文字('a')
                字符串文字('b')
                字符串文字('c')

这对于分析代码非常有用。但是,现在我无法轻松地重写代码。当然,我可以修改树结构来表示我想要的代码,但我不能用它来输出源代码。我曾希望与每个节点关联的标记至少能给我足够的信息,让我知道我需要在原始文本中的哪个位置进行修改,但我得到的只是标记索引或行/列号。要使用行号和列号,我将不得不对源代码进行一次尴尬的第二次遍历。

我怀疑我在理解如何正确使用 ANTLR 来做我需要的事情时遗漏了一些东西。我有没有更合适的方法来解决这个问题?

4

3 回答 3

9

您正在尝试做的事情称为程序转换,即从另一个程序自动生成一个程序。您所做的“错误”是假设解析器就是您所需要的,并且发现它不是并且您必须填补空白。

可以很好地做到这一点的工具有解析器(构建 AST),意味着修改 AST(过程和模式定向),以及将(修改后的)AST 转换回合法源代码的漂亮打印机。您似乎在为 ANTLR 不附带漂亮打印机这一事实而苦苦挣扎。这不是其哲学的一部分;ANTLR 是一个(精细的)解析器生成器。其他答案建议使用 ANTLR 的“字符串模板”,它们本身不是漂亮的打印机,但可以用来实现一个,但代价是实现一个。这比看起来更难做到;请参阅我关于将 AST 编译回源代码的答案。

这里真正的问题是广为流传但错误的假设,即“如果我有一个解析器,我就可以很好地构建复杂的程序分析和转换工具”。请参阅我关于解析后的生活的文章对此进行了长时间的讨论;基本上,您需要更多“只是”解析器的工具来执行此操作,除非您想自己重建大部分基础架构而不是继续您的任务。实际程序转换系统的其他有用特征包括典型的源到源转换,这大大简化了在树中查找和替换复杂模式的问题。

例如,如果您具有源到源转换功能(我们的工具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";

使用规则外部“元”程序“first_arg”(知道如何在给定标识符“foo”的情况下计算“bar”[我猜你想这样做),以及给定字符串文字的“compute_index_for”,知道什么整数来替换它。

单独的重写规则具有参数列表“(....)”,其中表示子树的插槽被命名,左侧充当匹配模式,右侧充当替换,两者通常在元引号中 引用将重写规则语言文本与目标语言(例如 JavaScript)文本分开。元引号内有很多元转义 **,表示特殊的重写规则语言项。通常这些是参数名称,代表任何类型的参数表示的名称树,或表示外部元过程调用(例如 first_arg;您会注意到它的参数列表( , )是元引用的!),或者最后是一个“标记”,例如“替换”,这是一种特殊的树,代表了未来进行更多转换的意图。

这组特定的规则通过用 barized 版本替换候选函数调用来工作,并带有额外的意图“替换”来转换列表。其他两个转换通过一次处理一个列表的元素来转换“替换”来实现意图,并将替换进一步推到列表的下方,直到它最终掉到最后并完成替换。(这是循环的转换等价物)。

您的具体示例可能会有所不同,因为您确实对细节并不精确。

应用这些规则来修改解析树后,DMS 可以简单地漂亮打印结果(某些配置中的默认行为是“解析到 AST,应用规则直到耗尽,漂亮打印 AST”,因为这很方便)。

你可以在(高中)代数作为DMS域看到“定义语言”、“定义重写规则”、“应用规则和漂亮打印”的完整过程。

其他程序转换系统包括TXLStratego。我们将 DMS 想象成这些的工业级版本,我们在其中构建了所有基础设施,包括许多标准语言解析器和漂亮的打印机。

于 2012-07-23T13:14:25.220 回答
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();
于 2012-07-24T04:35:15.783 回答
2

你看过字符串模板库吗?它是由编写 ANTLR 的同一个人编写的,他们打算一起工作。听起来它会适合你正在寻找的东西。将匹配的语法规则输出为格式化文本。

这是一篇关于通过 ANTLR 翻译的文章

于 2012-07-23T08:45:35.473 回答