一旦语法完成,如何最好地遍历ANTLR v4的语法树?

38

目标

我正在开发一个用于 Coldfusion CFscript 的 Varscoper 项目。基本上,这意味着检查源代码文件以确保开发人员正确地使用了 var 声明变量。

在使用 ANTLR V4 工作几天后,我拥有了一个语法,它在 GUI 视图中生成了一个非常好的解析树。现在,使用该树,我需要一种方法以编程方式向上和向下遍历节点,查找变量声明,并确保如果它们在函数内部,则具有适当的范围。如果可能,我不想在语法文件中执行此操作,因为这将要求将语言的定义与此特定任务混合。

我尝试过的方法

我最新的尝试是使用ParserRuleContext,并尝试通过getPayload()遍历它的children。检查getPayload()的类之后,我将得到一个ParserRuleContext对象或一个Token对象。不幸的是,我从来没有找到一种方法来获取特定节点的实际规则类型,只能获取其包含的文本。每个节点的规则类型都很重要,因为它们是被忽略的右手表达式、变量赋值还是函数声明。

问题

  1. 我非常新于ANTLR,这是正确的方法吗?还是有更好的方式遍历树结构?

这是我的Java示例代码:

Cfscript.java

import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.Trees;

public class Cfscript {
    public static void main(String[] args) throws Exception {
        ANTLRInputStream input = new ANTLRFileStream(args[0]);
        CfscriptLexer lexer = new CfscriptLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        CfscriptParser parser = new CfscriptParser(tokens);
        parser.setBuildParseTree(true);
        ParserRuleContext tree = parser.component();
        tree.inspect(parser); // show in gui
        /*
            Recursively go though tree finding function declarations and ensuring all variableDeclarations are varred
            but how?
        */
    }
}

Cfscript.g4

grammar Cfscript;

component
    : 'component' keyValue* '{' componentBody '}'
    ;

componentBody
    : (componentElement)*
    ;

componentElement
    : statement
    | functionDeclaration
    ;

functionDeclaration
    : Identifier? Identifier? 'function' Identifier argumentsDefinition '{' functionBody '}'
    ;

argumentsDefinition
    : '(' argumentDefinition (',' argumentDefinition)* ')'
    | '()'
    ;

argumentDefinition
    : Identifier? Identifier? argumentName ('=' expression)?
    ; 

argumentName
    : Identifier
    ;

functionBody
    : (statement)*
    ;

statement
    : variableStatement
    | nonVarVariableStatement
    | expressionStatement
    ;

variableStatement
    : 'var' variableName '=' expression ';'
    ;

nonVarVariableStatement
    : variableName '=' expression ';'
    ;

expressionStatement
    : expression ';'
    ;

expression
    : assignmentExpression
    | arrayLiteral
    | objectLiteral
    | StringLiteral
    | incrementExpression
    | decrementExpression
    | 'true' 
    | 'false'
    | Identifier
    ;

incrementExpression
    : variableName '++'
    ;

decrementExpression
    : variableName '--'
    ;

assignmentExpression
    : Identifier (assignmentExpressionSuffix)*
    | assignmentExpression (('+'|'-'|'/'|'*') assignmentExpression)+
    ;

assignmentExpressionSuffix
    : '.' assignmentExpression
    | ArrayIndex
    | ('()' | '(' expression (',' expression)* ')' )
    ;

methodCall
    : Identifier ('()' | '(' expression (',' expression)* ')' )
    ;

variableName
    : Identifier (variableSuffix)*
    ;

variableSuffix
    : ArrayIndex
    | '.' variableName
    ;

arrayLiteral
    : '[' expression (',' expression)* ']'
    ;

objectLiteral
    : '{' (Identifier '=' expression (',' Identifier '=' expression)*)? '}'
    ;

keyValue
    : Identifier '=' StringLiteral
    ;

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

 ArrayIndex
    : '[' [1-9] [0-9]* ']'
    | '[' StringLiteral ']'
    ;

Identifier
    : [a-zA-Z0-9]+
    ;

WS
    : [ \t\r\n]+ -> skip 
    ;

COMMENT 
    : '/*' .*? '*/'  -> skip
    ;

Test.cfc(测试代码文件)

component something = "foo" another = "more" persistent = "true" datasource = "#application.env.dsn#" {
    var method = something.foo.test1;
    testing = something.foo[10];
    testingagain = something.foo["this is a test"];
    nuts["testing"]++;
    blah.test().test3["test"]();

    var math = 1 + 2 - blah.test().test4["test"];

    var test = something;
    var testing = somethingelse;
    var testing = { 
        test = more, 
        mystuff = { 
            interior = test 
        },
        third = "third key"
    };
    other = "Idunno homie";
    methodCall(interiorMethod());

    public function bar() {
        var new = "somebody i used to know";
        something = [1, 2, 3];
    }

    function nuts(required string test1 = "first", string test = "second", test3 = "third") {

    }

    private boolean function baz() {
        var this = "something else";
    }
}

顺带一提,你可能想使用 '(' ')' 而不是 '()',因为前者将不匹配间隔有空格的括号。 - Bart Kiers
1
如果你还不知道,Railo 4在其调试选项中拥有一个更普遍的作用域检查工具......这并没有完全使变量作用域检测工具过时,但它确实在解决问题方面存在重叠。 - Peter Boughton
1
此外,还有一个更完整/通用的ANTLR解析器可用 - 看看denstar完成的工作 - 可能提供一些快捷方式。 - Peter Boughton
@BartKiers 如果我在 argumentsDefinition 规则中使用 '(' ')',就会出现错误,提示输入的 '()' 无法识别。请尝试使用 antlr4 和我提供的文件进行测试。 - Owen Allen
@Nucleon,啊,好的,这意味着你的语法不正确,因为它无法正确解析像... function baz( ) { ...这样的输入。 - Bart Kiers
1个回答

50

如果我是您,我不会手动处理这个。在生成词法分析器和语法分析器之后,ANTLR 还会生成一个名为 CfscriptBaseListener 的文件,其中包含所有解析规则的空方法。您可以让 ANTLR 遍历树,并附加一个自定义的树监听器,在其中仅覆盖您感兴趣的那些方法/规则。

在您的情况下,您可能希望在创建新函数时(以创建新作用域)得到通知,并且可能会对变量赋值(variableStatementnonVarVariableStatement)感兴趣。当 ANTLR 遍历树时,您的监听器(设其名称为VarListener)将跟踪所有作用域。

我稍微改了一条规则(我添加了objectLiteralEntry):

objectLiteral
    : '{' (objectLiteralEntry (',' objectLiteralEntry)*)? '}'
    ;
objectLiteralEntry : Identifier '=' expression ;

这使得以下演示更加容易:

VarListener.java

public class VarListener extends CfscriptBaseListener {

    private Stack<Scope> scopes;

    public VarListener() {
        scopes = new Stack<Scope>();
        scopes.push(new Scope(null));
    } 

    @Override
    public void enterVariableStatement(CfscriptParser.VariableStatementContext ctx) {
        String varName = ctx.variableName().getText();
        Scope scope = scopes.peek();
        scope.add(varName);
    }

    @Override
    public void enterNonVarVariableStatement(CfscriptParser.NonVarVariableStatementContext ctx) {
        String varName = ctx.variableName().getText();
        checkVarName(varName);
    }

    @Override
    public void enterObjectLiteralEntry(CfscriptParser.ObjectLiteralEntryContext ctx) {
        String varName = ctx.Identifier().getText();
        checkVarName(varName);
    }

    @Override
    public void enterFunctionDeclaration(CfscriptParser.FunctionDeclarationContext ctx) {
        scopes.push(new Scope(scopes.peek()));
    }

    @Override
    public void exitFunctionDeclaration(CfscriptParser.FunctionDeclarationContext ctx) {
        scopes.pop();        
    }

    private void checkVarName(String varName) {
        Scope scope = scopes.peek();
        if(scope.inScope(varName)) {
            System.out.println("OK   : " + varName);
        }
        else {
            System.out.println("Oops : " + varName);
        }
    }
}

一个Scope对象可能就像这样简单:

Scope.java

class Scope extends HashSet<String> {

    final Scope parent;

    public Scope(Scope parent) {
        this.parent = parent;
    }

    boolean inScope(String varName) {
        if(super.contains(varName)) {
            return true;
        }
        return parent == null ? false : parent.inScope(varName);
    }
}

现在,为了测试这一切,这里有一个小的主类:

Main.java

import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.*;

public class Main {

    public static void main(String[] args) throws Exception {

        CfscriptLexer lexer = new CfscriptLexer(new ANTLRFileStream("Test.cfc"));
        CfscriptParser parser = new CfscriptParser(new CommonTokenStream(lexer));
        ParseTree tree = parser.component();
        ParseTreeWalker.DEFAULT.walk(new VarListener(), tree);
    }
}

如果您运行此 Main 类,将会打印出以下内容:

Oops : testing
Oops : testingagain
OK   : test
Oops : mystuff
Oops : interior
Oops : third
Oops : other
Oops : something

毫无疑问,这不完全是您想要的,我可能弄乱了一些Coldfusion的作用域规则。但我认为这将为您正确解决问题提供一些见解。我认为代码相当易于理解,但如果不是这种情况,请随时要求澄清。

希望对您有所帮助。


没问题@Nucleon。再次强调:如果我对某些方面过于草率,请随时要求澄清。 - Bart Kiers
2
提示:ParseTreeWalker.DEFAULT.walk(...) - Sam Harwell
@BartKiers 你好。我也是ANTLR的初学者。我正在尝试使用类似于您的主代码来遍历我的解析树,但是我的生成的gardenParser对象没有component()类。您有任何想法为什么会这样吗? - Zach H
@ZachH,解析器有一个“component”方法,因为语法有一个“component”规则。你的语法可能没有这样的规则。 - Bart Kiers
那就是为什么。我一直在想为什么我的解析器没有教程中提到的任何可用函数。谢谢! - Zach H
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接