web-dev-qa-db-fra.com

Une fois la grammaire terminée, quelle est la meilleure façon de parcourir un arbre ANTLR v4?

Objectif

Je travaille sur un projet pour créer un Varscoper pour Coldfusion CFscript. Fondamentalement, cela signifie vérifier les fichiers de code source pour s'assurer que les développeurs ont correctement var 'leurs variables.

Après quelques jours de travail avec ANTLR V4, j'ai une grammaire qui génère un arbre d'analyse très agréable dans la vue de l'interface graphique. Maintenant, en utilisant cet arbre, j'ai besoin d'un moyen d'explorer de haut en bas les nœuds à la recherche de déclarations de variables par programme et de m'assurer que s'ils sont à l'intérieur des fonctions, ils ont la portée appropriée. Si possible, je préfère ne PAS le faire dans le fichier de grammaire car cela nécessiterait de mélanger la définition de la langue avec cette tâche spécifique.

Ce que j'ai essayé

Ma dernière tentative a été d'utiliser le ParserRuleContext et d'essayer de passer par son children via getPayload(). Après avoir vérifié la classe de getPayLoad() j'aurais soit un objet ParserRuleContext soit un objet Token. Malheureusement, en utilisant cela, je n'ai jamais pu trouver un moyen d'obtenir le type de règle réel pour un nœud spécifique, mais il contient du texte. Le type de règle pour chaque nœud est nécessaire car il importe que ce nœud de texte soit une expression de droite ignorée, une affectation de variable ou une déclaration de fonction.

Questions

  1. Je suis très nouveau sur ANTLR, est-ce même la bonne approche ou existe-t-il une meilleure façon de traverser l'arbre?

Voici mon exemple Java code:

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 (fichier de code de test)

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

Je ne marcherais pas manuellement si j'étais toi. Après avoir généré un lexer et un analyseur, ANTLR aurait également généré un fichier appelé CfscriptBaseListener qui a des méthodes vides pour toutes vos règles d'analyseur. Vous pouvez laisser ANTLR parcourir votre arbre et attacher un écouteur d'arborescence personnalisé dans lequel vous ne remplacez que les méthodes/règles qui vous intéressent.

Dans votre cas, vous voudrez probablement être averti chaque fois qu'une nouvelle fonction est créée (pour créer une nouvelle étendue) et vous serez probablement intéressé par les affectations de variables (variableStatement et nonVarVariableStatement). Votre auditeur, appelons est VarListener gardera une trace de toutes les étendues pendant que ANTLR parcourt l'arbre.

J'ai légèrement modifié 1 règle (j'ai ajouté objectLiteralEntry):

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

ce qui vous facilite la vie dans la démo suivante:

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);
        }
    }
}

Un objet Scope pourrait être aussi simple que:

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);
    }
}

Maintenant, pour tester tout cela, voici une petite classe principale:

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);
    }
}

Si vous exécutez cette classe Main, les éléments suivants seront imprimés:

Oups: test 
 Oups: test contre 
 OK: test 
 Oups: mystuff 
 Oups: intérieur 
 Oups: troisième 
 Oups: autre 
 Oups: quelque chose

Sans aucun doute, ce n'est pas exactement ce que vous voulez et j'ai probablement bafoué quelques règles de cadrage de Coldfusion. Mais je pense que cela vous donnera un aperçu de la façon de résoudre correctement votre problème. Je pense que le code est assez explicite, mais si ce n'est pas le cas, n'hésitez pas à demander des éclaircissements.

HTH

39
Bart Kiers