web-dev-qa-db-ja.com

文法が完成したら、ANTLR v4ツリーを歩くための最良の方法は何ですか?

目標

Coldfusion CFscript用のVarscoperを作成するプロジェクトに取り組んでいます。基本的に、これは、ソースコードファイルをチェックして、開発者が変数を適切にvarしたことを確認することを意味します。

ANTLR V4で2日間作業した後、GUIビューに非常にナイスな解析ツリーを生成する文法があります。次に、そのツリーを使用して、ノードをプログラムでクロールして変数宣言を探し、それらが関数内にある場合は適切なスコープが設定されていることを確認する方法が必要です。可能であれば、文法ファイルではこれを行わないでください。言語の定義とこの特定のタスクを混在させる必要があるためです。

私が試したこと

私の最新の試みはParserRuleContextを使用しており、それをchildren経由でgetPayload()を介して試みました。 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";
    }
}
33
Nucleon

もし私があなただったら、私はこれを手動で歩かなかったでしょう。レクサーとパーサーを生成した後、ANTLRはCfscriptBaseListenerというファイルも生成します。このファイルには、すべてのパーサールールに対して空のメソッドがあります。 ANTLRにツリーをウォークさせて、関心のあるメソッド/ルールのみをオーバーライドするカスタムツリーリスナーをアタッチできます。

あなたのケースでは、(新しいスコープを作成するために)新しい関数が作成されるたびに通知を受け取る必要があり、変数の割り当て(variableStatementおよびnonVarVariableStatement)に興味がある可能性があります。リスナーはVarListenerを呼び出しましょう。ANTLRがツリーをウォークするときにすべてのスコープを追跡します。

私は1つのルールを少し変更しました(objectLiteralEntryを追加しました):

objectLiteral 
: '{'(objectLiteralEntry( '、' objectLiteralEntry)*)? '}' 
; 
 
 objectLiteralEntry 
:識別子 '='式
; 
 

これは、次のデモで人生を楽にします:

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クラスを実行すると、次のように出力されます。

エラー:テスト
エラー:テスト中
 OK:テスト
エラー:mystuff 
エラー:内部
エラー:3番目
エラー:その他
おっと:何か

間違いなく、これはまさにあなたが望むものではありません、そして私はおそらくColdfusionのいくつかのスコープルールを間違えました。しかし、これにより、問題を適切に解決する方法についての洞察が得られると思います。コードはかなり自明だと思いますが、そうでない場合は、遠慮なく説明を求めてください。

HTH

39
Bart Kiers