/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package groovy.console.ui

import groovy.text.GStringTemplateEngine
import groovy.text.Template
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
import org.apache.groovy.io.StringBuilderWriter
import org.codehaus.groovy.GroovyBugError
import org.codehaus.groovy.ast.AnnotationNode
import org.codehaus.groovy.ast.ClassHelper
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.CodeVisitorSupport
import org.codehaus.groovy.ast.ConstructorNode
import org.codehaus.groovy.ast.DynamicVariable
import org.codehaus.groovy.ast.FieldNode
import org.codehaus.groovy.ast.InnerClassNode
import org.codehaus.groovy.ast.MethodNode
import org.codehaus.groovy.ast.ModuleNode
import org.codehaus.groovy.ast.Parameter
import org.codehaus.groovy.ast.PropertyNode
import org.codehaus.groovy.ast.RecordComponentNode
import org.codehaus.groovy.ast.expr.ArgumentListExpression
import org.codehaus.groovy.ast.expr.ArrayExpression
import org.codehaus.groovy.ast.expr.AttributeExpression
import org.codehaus.groovy.ast.expr.BinaryExpression
import org.codehaus.groovy.ast.expr.BitwiseNegationExpression
import org.codehaus.groovy.ast.expr.BooleanExpression
import org.codehaus.groovy.ast.expr.CastExpression
import org.codehaus.groovy.ast.expr.ClassExpression
import org.codehaus.groovy.ast.expr.ClosureExpression
import org.codehaus.groovy.ast.expr.ClosureListExpression
import org.codehaus.groovy.ast.expr.ConstantExpression
import org.codehaus.groovy.ast.expr.ConstructorCallExpression
import org.codehaus.groovy.ast.expr.DeclarationExpression
import org.codehaus.groovy.ast.expr.ElvisOperatorExpression
import org.codehaus.groovy.ast.expr.Expression
import org.codehaus.groovy.ast.expr.FieldExpression
import org.codehaus.groovy.ast.expr.GStringExpression
import org.codehaus.groovy.ast.expr.LambdaExpression
import org.codehaus.groovy.ast.expr.ListExpression
import org.codehaus.groovy.ast.expr.MapEntryExpression
import org.codehaus.groovy.ast.expr.MapExpression
import org.codehaus.groovy.ast.expr.MethodCallExpression
import org.codehaus.groovy.ast.expr.MethodPointerExpression
import org.codehaus.groovy.ast.expr.MethodReferenceExpression
import org.codehaus.groovy.ast.expr.NamedArgumentListExpression
import org.codehaus.groovy.ast.expr.NotExpression
import org.codehaus.groovy.ast.expr.PostfixExpression
import org.codehaus.groovy.ast.expr.PrefixExpression
import org.codehaus.groovy.ast.expr.PropertyExpression
import org.codehaus.groovy.ast.expr.RangeExpression
import org.codehaus.groovy.ast.expr.SpreadExpression
import org.codehaus.groovy.ast.expr.SpreadMapExpression
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression
import org.codehaus.groovy.ast.expr.TernaryExpression
import org.codehaus.groovy.ast.expr.TupleExpression
import org.codehaus.groovy.ast.expr.UnaryMinusExpression
import org.codehaus.groovy.ast.expr.UnaryPlusExpression
import org.codehaus.groovy.ast.expr.VariableExpression
import org.codehaus.groovy.ast.stmt.AssertStatement
import org.codehaus.groovy.ast.stmt.BlockStatement
import org.codehaus.groovy.ast.stmt.BreakStatement
import org.codehaus.groovy.ast.stmt.CaseStatement
import org.codehaus.groovy.ast.stmt.CatchStatement
import org.codehaus.groovy.ast.stmt.ContinueStatement
import org.codehaus.groovy.ast.stmt.DoWhileStatement
import org.codehaus.groovy.ast.stmt.EmptyStatement
import org.codehaus.groovy.ast.stmt.ExpressionStatement
import org.codehaus.groovy.ast.stmt.ForStatement
import org.codehaus.groovy.ast.stmt.IfStatement
import org.codehaus.groovy.ast.stmt.ReturnStatement
import org.codehaus.groovy.ast.stmt.Statement
import org.codehaus.groovy.ast.stmt.SwitchStatement
import org.codehaus.groovy.ast.stmt.SynchronizedStatement
import org.codehaus.groovy.ast.stmt.ThrowStatement
import org.codehaus.groovy.ast.stmt.TryCatchStatement
import org.codehaus.groovy.ast.stmt.WhileStatement
import org.codehaus.groovy.classgen.BytecodeExpression
import org.codehaus.groovy.classgen.GeneratorContext
import org.codehaus.groovy.classgen.asm.BytecodeHelper
import org.codehaus.groovy.control.CompilationFailedException
import org.codehaus.groovy.control.CompilationUnit
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.SourceUnit

import java.util.concurrent.atomic.AtomicBoolean

/**
 * This class controls the conversion from a Groovy script as a String into
 * a tree representation of the AST of that script. The script itself
 * will be a tree node, and each class in the script will be a tree node. The
 * conversion creates tree nodes for any concrete class found within an AST
 * visitor. So, if a tree node should be shown once for each ASTNode and the parent
 * types will not appear as nodes. Custom subclasses of expression types will
 * not appear in the tree.
 *
 * The String label of a tree node is defined by classname in AstBrowserProperties.properties.
 */
class ScriptToTreeNodeAdapter {

    /** Maps AST node class names to display templates. */
    static Properties classNameToStringForm
    /** Display flags that control which script structures are included. */
    boolean showScriptFreeForm, showScriptClass, showClosureClasses
    /** Class loader used for script compilation. */
    final GroovyClassLoader classLoader
    /** Factory used to create tree node instances. */
    final AstBrowserNodeMaker nodeMaker
    private final CompilerConfiguration config

    static {
        try {
            URL url =  ClassLoader.getSystemResource('groovy/console/ui/AstBrowserProperties.groovy')
            if (!url) {
                url = ScriptToTreeNodeAdapter.class.classLoader.getResource('groovy/console/ui/AstBrowserProperties.groovy')
            }

            def config = new ConfigSlurper().parse(url)
            classNameToStringForm = config.toProperties()

            String home = System.getProperty('user.home')
            if (home) {
                File userFile = new File(home + File.separator + '.groovy/AstBrowserProperties.groovy')
                if (userFile.exists()) {
                    def customConfig = new ConfigSlurper().parse(userFile.toURL())
                    // layer custom string forms onto defaults with putAll, do not replace them
                    classNameToStringForm.putAll(customConfig.toProperties())
                }
            }
        } catch(ex) {
            // on restricted environments like, such calls may fail, but that should not prevent the class
            // from being loaded. Tree nodes can still get rendered with their simple names.
            classNameToStringForm = new Properties()
        }
    }

    /**
     * Creates an adapter for the supplied compilation settings.
     *
     * @param classLoader class loader used for compilation
     * @param showScriptFreeForm whether to include free-form script statements
     * @param showScriptClass whether to include the generated script class
     * @param showClosureClasses whether to include generated closure classes
     * @param nodeMaker node factory used for tree creation
     * @param config optional compiler configuration
     */
    ScriptToTreeNodeAdapter(classLoader, showScriptFreeForm, showScriptClass, showClosureClasses, nodeMaker, config = null) {
        this.classLoader = classLoader ?: new GroovyClassLoader(getClass().classLoader)
        this.showScriptFreeForm = showScriptFreeForm
        this.showScriptClass = showScriptClass
        this.showClosureClasses = showClosureClasses
        this.nodeMaker = nodeMaker
        this.config = config
    }

    /**
     * Performs the conversion from script to TreeNode.
     *
     * @param script
     *      a Groovy script in String form
     * @param compilePhase
     *      the int based CompilePhase to compile it to.
     * @return the generated tree root
     */
    def compile(String script, int compilePhase) {
        def scriptName = 'script' + System.currentTimeMillis() + '.groovy'
        GroovyCodeSource codeSource = new GroovyCodeSource(script, scriptName, '/groovy/script')
        CompilerConfiguration cc = new CompilerConfiguration(config ?: CompilerConfiguration.DEFAULT)
        if (config) {
            cc.addCompilationCustomizers(*config.compilationCustomizers)
        }
        CompilationUnit cu = new CompilationUnit(cc, codeSource.codeSource, classLoader)
        cu.setClassgenCallback(classLoader.createCollector(cu, null))

        TreeNodeBuildingNodeOperation operation = new TreeNodeBuildingNodeOperation(this, showScriptFreeForm, showScriptClass, showClosureClasses)
        cu.addPhaseOperation(operation, compilePhase)
        cu.addSource(codeSource.getName(), script)
        try {
            cu.compile(compilePhase)
        } catch (CompilationFailedException cfe) {
            operation.root.add(nodeMaker.makeNode('Unable to produce AST for this phase due to earlier compilation error:'))
            cfe.message.eachLine {
                operation.root.add(nodeMaker.makeNode(it))
            }
            operation.root.add(nodeMaker.makeNode('Fix the above error(s) and then press Refresh'))
        } catch (Throwable t) {
            operation.root.add(nodeMaker.makeNode('Unable to produce AST for this phase due to an error:'))
            operation.root.add(nodeMaker.makeNode(t))
            operation.root.add(nodeMaker.makeNode('Fix the above error(s) and then press Refresh'))
        }
        return operation.root
    }

    /**
     * Creates a tree node for the supplied AST node.
     *
     * @param node AST node to adapt
     * @return the created tree node
     */
    def make(node) {
        makeWithTable(node, getPropertyTable(node))
    }

    /**
     * Creates a tree node for a method node with its descriptor metadata.
     *
     * @param node method node to adapt
     * @return the created tree node
     */
    def make(MethodNode node) {
        def table = getPropertyTable(node)
        extendMethodNodePropertyTable(table, node)
        makeWithTable(node, table)
    }

    private makeWithTable(node, List<List<?>> table) {
        nodeMaker.makeNodeWithProperties(getStringForm(node), table)
    }

    /**
     * Extends the method node property table by adding custom properties.
     *
     * @param table property table to extend
     * @param node method node supplying the descriptor
     */
    void extendMethodNodePropertyTable(List<List<?>> table, MethodNode node) {
        def descriptor = BytecodeHelper.getMethodDescriptor(node)
        table << ['descriptor', descriptor, 'String', descriptor]
    }

    /**
     * Creates the property table for the node so that the properties view can display nicely.
     */
    private List<List<?>> getPropertyTable(node) {
        node.metaClass.properties.findResults { mp ->
            if (mp instanceof MetaBeanProperty && mp.getter) {
                String name = mp.name, type = mp.type.simpleName
                Object valueAsString = null
                Object value = null
                try {
                    // multiple assignment statements cannot be cast to VariableExpression so
                    // instead reference the value through the leftExpression property, which is the same
                    if (node instanceof DeclarationExpression && (name == 'variableExpression' || name == 'tupleExpression')) {
                        value = node.leftExpression
                    } else {
                        value = mp.getProperty(node)
                    }
                    valueAsString = toString(value)
                } catch (GroovyBugError ignore) {
                    // compiler throws error if it thinks a field is being accessed
                    // before it is set under certain conditions. It wasn't designed
                    // to be walked reflectively like this.
                }
                [name, valueAsString, type, value]
            }
        }.sort { list -> list[0] } // sort by name
    }

    /**
     * Returns a string representation without crossing visibility boundaries.
     *
     * @param o value to convert
     * @return string form of the supplied value
     */
    // GROOVY-8339: to avoid illegal access to a non-visible implementation class - can be removed if a more general solution is found
    @CompileStatic
    String toString(Object o) {
        o.toString()
    }

    /**
     * Handles the property file templating for node types.
     */
    private String getStringForm(node) {
        String templateTextForNode = classNameToStringForm[node.class.name]
        if (templateTextForNode) {
            GStringTemplateEngine engine = new GStringTemplateEngine()
            Template template = engine.createTemplate(templateTextForNode)
            Writable writable = template.make([expression: node])
            Writer result = new StringBuilderWriter()
            writable.writeTo(result)
            result.toString()
        } else {
            node.class.simpleName
        }
    }
}

/**
 * This Node Operation builds up a root tree node for the viewer.
 */
class TreeNodeBuildingNodeOperation implements CompilationUnit.IPrimaryClassNodeOperation {

    /** Root node for the generated tree. */
    final root
    /** Tracks whether free-form source nodes were already collected. */
    final sourceCollected = new AtomicBoolean(false)
    /** Adapter used to create child nodes. */
    final ScriptToTreeNodeAdapter adapter

    /** Whether free-form script statements should be included. */
    final showScriptFreeForm
    /** Whether the generated script class should be included. */
    final showScriptClass
    /** Whether generated closure classes should be included. */
    final showClosureClasses

    /** Factory used to create tree nodes. */
    final nodeMaker

    /**
     * Creates an operation with closure class nodes hidden.
     *
     * @param adapter adapter used to create tree nodes
     * @param showScriptFreeForm whether free-form script statements should be shown
     * @param showScriptClass whether the generated script class should be shown
     */
    TreeNodeBuildingNodeOperation(ScriptToTreeNodeAdapter adapter, showScriptFreeForm, showScriptClass) {
        this(adapter, showScriptFreeForm, showScriptClass, false)
    }

    /**
     * Creates an operation for building the AST tree.
     *
     * @param adapter adapter used to create tree nodes
     * @param showScriptFreeForm whether free-form script statements should be shown
     * @param showScriptClass whether the generated script class should be shown
     * @param showClosureClasses whether generated closure classes should be shown
     */
    TreeNodeBuildingNodeOperation(ScriptToTreeNodeAdapter adapter, showScriptFreeForm, showScriptClass, showClosureClasses) {
        if (!adapter) throw new IllegalArgumentException('Null: adapter')
        this.adapter = adapter
        this.showScriptFreeForm = showScriptFreeForm
        this.showScriptClass = showScriptClass
        this.showClosureClasses = showClosureClasses
        nodeMaker = adapter.nodeMaker
        root = nodeMaker.makeNode('root')
    }

    /**
     * Adds the current class node and optional script statements to the tree.
     */
    @Override
    void call(SourceUnit source, GeneratorContext context, ClassNode classNode) {
        // module node
        if (!sourceCollected.getAndSet(true) && showScriptFreeForm) {
            // display the source unit AST
            ModuleNode ast = source.getAST()
            TreeNodeBuildingVisitor visitor = new TreeNodeBuildingVisitor(adapter)
            ast.getStatementBlock().visit(visitor)
            if (visitor.currentNode) root.add(visitor.currentNode)
            collectModuleNodeMethodData('Methods', ast.getMethods())
        }

        if(classNode.isScript() && !showScriptClass) return

        def child = adapter.make(classNode)
        root.add(child)

        collectConstructorData(child, 'Constructors', classNode)
        collectObjectInitializers(child, 'Object Initializers', classNode)
        collectMethodData(child, 'Methods', classNode)
        collectFieldData(child, 'Fields', classNode)
        collectPropertyData(child, 'Properties', classNode)
        collectRecordComponentData(child, 'Record Components', classNode)

        if (showClosureClasses)  {
            makeClosureClassTreeNodes(classNode)
        }
    }

    /**
     * Adds generated closure and lambda classes for the supplied outer class.
     *
     * @param classNode outer class being inspected
     */
    protected void makeClosureClassTreeNodes(ClassNode classNode) {
        def compileUnit = classNode.compileUnit
        if (!compileUnit.generatedInnerClasses) return

        def innerClassNodes = compileUnit.generatedInnerClasses.values().sort { it.name }
        innerClassNodes.each { InnerClassNode innerClassNode ->
            if (!ClassHelper.isGeneratedFunction(innerClassNode)) return
            if (innerClassNode.outerMostClass != classNode) return

            def child = adapter.make(innerClassNode)
            root.add(child)

            collectConstructorData(child, 'Constructors', innerClassNode)
            collectObjectInitializers(child, 'Object Initializers', innerClassNode)
            collectMethodData(child, 'Methods', innerClassNode)
            collectFieldData(child, 'Fields', innerClassNode)
            collectPropertyData(child, 'Properties', innerClassNode)
            collectRecordComponentData(child, 'Record Components', classNode)
        }
    }

    private void collectPropertyData(parent, String name, ClassNode classNode) {
        if (classNode.properties) {
            def allProperties = nodeMaker.makeNode(name)
            parent.add(allProperties)
            classNode.properties.each { PropertyNode propertyNode ->
                def ggrandchild = adapter.make(propertyNode)
                allProperties.add(ggrandchild)
            }
        }
    }

    private void collectRecordComponentData(parent, String name, ClassNode classNode) {
        if (classNode.recordComponents) {
            def allRecords = nodeMaker.makeNode(name)
            parent.add(allRecords)
            classNode.recordComponents.each { RecordComponentNode recordComponentNode ->
                def ggrandchild = adapter.make(recordComponentNode)
                allRecords.add(ggrandchild)
            }
        }
    }

    private void collectFieldData(parent, String name, ClassNode classNode) {
        if (classNode.fields) {
            def allFields = nodeMaker.makeNode(name)
            parent.add(allFields)
            classNode.fields.each { FieldNode fieldNode ->
                def ggrandchild = adapter.make(fieldNode)
                allFields.add(ggrandchild)
            }
        }
    }

    private void collectMethodData(parent, String name, ClassNode classNode) {
        def allMethods = nodeMaker.makeNode(name)
        if (classNode.methods) parent.add(allMethods)

        doCollectMethodData(allMethods, classNode.methods)
    }

    private void collectModuleNodeMethodData(String name, List methods) {
        if(!methods) return
        def allMethods = nodeMaker.makeNode(name)
        root.add(allMethods)

        doCollectMethodData(allMethods, methods)
    }

    private void doCollectMethodData(allMethods, List methods) {
        methods?.each { MethodNode methodNode ->
            def ggrandchild = adapter.make(methodNode)
            allMethods.add(ggrandchild)
            collectParametersData(ggrandchild, methodNode)
            collectBodyData(ggrandchild, methodNode.code)
        }
    }

    private void collectBodyData(ggrandchild, Statement code) {
        if (code) {
            TreeNodeBuildingVisitor visitor = new TreeNodeBuildingVisitor(adapter)
            code.visit(visitor)
            if (visitor.currentNode) {
                def body = nodeMaker.makeNode('Body')
                ggrandchild.add(body)
                body.add(visitor.currentNode)
            }
        }
    }

    private void collectParametersData(parent, MethodNode methodNode) {
        if (methodNode.parameters) {
            def allParamNodes = nodeMaker.makeNode('Parameters')
            parent.add(allParamNodes)
            methodNode.parameters.each { Parameter parameter ->
                def paramNode = adapter.make(parameter)
                allParamNodes.add(paramNode)
            }
        }
    }

    private void collectConstructorData(parent, String name, ClassNode classNode) {
        def allCtors = nodeMaker.makeNode(name)
        if (classNode.declaredConstructors) parent.add(allCtors)
        classNode.declaredConstructors?.each { ConstructorNode ctorNode ->
            def ggrandchild = adapter.make(ctorNode)
            allCtors.add(ggrandchild)
            collectParametersData(ggrandchild, ctorNode)
            collectBodyData(ggrandchild, ctorNode.code)
        }

    }

    private void collectObjectInitializers(parent, String name, ClassNode node) {
        List<Statement> initStatements = node.getObjectInitializerStatements()
        if (!initStatements) {
            return
        }
        def allInitializers = nodeMaker.makeNode(name)
        parent.add(allInitializers)
        for (Statement stmt : initStatements) {
            TreeNodeBuildingVisitor visitor = new TreeNodeBuildingVisitor(adapter)
            stmt.visit(visitor)
            if (visitor.currentNode) {
                allInitializers.add(visitor.currentNode)
            }
        }
    }

}

/**
 * This AST visitor builds up a TreeNode.
 */
@PackageScope
class TreeNodeBuildingVisitor extends CodeVisitorSupport {

    /** Node currently being populated by the visitor. */
    def currentNode
    private final adapter

    /**
     * Creates the visitor. A file named AstBrowserProperties.groovy is located which is
     * a property files the describes how to represent ASTNode types as Strings.
     */
    TreeNodeBuildingVisitor(adapter) {
        if (!adapter) throw new IllegalArgumentException('Null: adapter')
        this.adapter = adapter
    }

    /**
     * This method looks at the AST node and decides how to represent it in a TreeNode, then it
     * continues walking the tree. If the node and the expectedSubclass are not exactly the same
     * Class object then the node is not added to the tree. This is to eliminate seeing duplicate
     * nodes, for instance seeing an ArgumentListExpression and a TupleExpression in the tree, when
     * an ArgumentList is-a Tuple.
     */
    private void addNode(node, Class expectedSubclass, Closure superMethod) {

        if (expectedSubclass.getName() == node.getClass().getName()) {
            if (currentNode == null) {
                currentNode = adapter.make(node)
                superMethod.call(node)
            } else {
                // visitor works off void methods... so we have to
                // perform a swap to get accumulation like behavior.
                def temp = currentNode
                currentNode = adapter.make(node)

                temp.add(currentNode)
                currentNode.parent = temp
                superMethod.call(node)
                currentNode = temp
            }
        } else {
            superMethod.call(node)
        }
    }

    /** Adds a block statement node to the tree. */
    @Override
    void visitBlockStatement(BlockStatement node) {
        addNode(node, BlockStatement, { super.visitBlockStatement(it) })
    }

    /** Adds a for-loop node to the tree. */
    @Override
    void visitForLoop(ForStatement node) {
        addNode(node, ForStatement, { super.visitForLoop(it) })
    }

    /** Adds a while-loop node to the tree. */
    @Override
    void visitWhileLoop(WhileStatement node) {
        addNode(node, WhileStatement, { super.visitWhileLoop(it) })
    }

    /** Adds a do/while-loop node to the tree. */
    @Override
    void visitDoWhileLoop(DoWhileStatement node) {
        addNode(node, DoWhileStatement, { super.visitDoWhileLoop(it) })
    }

    /** Adds an if/else node to the tree. */
    @Override
    void visitIfElse(IfStatement node) {
        addNode(node, IfStatement, { super.visitIfElse(it) })
    }

    /** Adds an expression statement node to the tree. */
    @Override
    void visitExpressionStatement(ExpressionStatement node) {
        addNode(node, ExpressionStatement, { super.visitExpressionStatement(it) })
    }

    /** Adds a return statement node to the tree. */
    @Override
    void visitReturnStatement(ReturnStatement node) {
        addNode(node, ReturnStatement, { super.visitReturnStatement(it) })
    }

    /** Adds an assert statement node to the tree. */
    @Override
    void visitAssertStatement(AssertStatement node) {
        addNode(node, AssertStatement, { super.visitAssertStatement(it) })
    }

    /** Adds a try/catch/finally node to the tree. */
    @Override
    void visitTryCatchFinally(TryCatchStatement node) {
        addNode(node, TryCatchStatement, { super.visitTryCatchFinally(it) })
    }

    /** Adds an empty statement node to the tree. */
    @Override
    void visitEmptyStatement(EmptyStatement node) {
        addNode(node, EmptyStatement, { super.visitEmptyStatement(it) })
    }

    /** Adds a switch node to the tree. */
    @Override
    void visitSwitch(SwitchStatement node) {
        addNode(node, SwitchStatement, { super.visitSwitch(it) })
    }

    /** Adds a case statement node to the tree. */
    @Override
    void visitCaseStatement(CaseStatement node) {
        addNode(node, CaseStatement, { super.visitCaseStatement(it) })
    }

    /** Adds a break statement node to the tree. */
    @Override
    void visitBreakStatement(BreakStatement node) {
        addNode(node, BreakStatement, { super.visitBreakStatement(it) })
    }

    /** Adds a continue statement node to the tree. */
    @Override
    void visitContinueStatement(ContinueStatement node) {
        addNode(node, ContinueStatement, { super.visitContinueStatement(it) })
    }

    /** Adds a synchronized statement node to the tree. */
    @Override
    void visitSynchronizedStatement(SynchronizedStatement node) {
        addNode(node, SynchronizedStatement, { super.visitSynchronizedStatement(it) })
    }

    /** Adds a throw statement node to the tree. */
    @Override
    void visitThrowStatement(ThrowStatement node) {
        addNode(node, ThrowStatement, { super.visitThrowStatement(it) })
    }

    /** Adds a method call expression node to the tree. */
    @Override
    void visitMethodCallExpression(MethodCallExpression node) {
        addNode(node, MethodCallExpression, { super.visitMethodCallExpression(it) })
    }

    /** Adds a static method call node to the tree. */
    @Override
    void visitStaticMethodCallExpression(StaticMethodCallExpression node) {
        addNode(node, StaticMethodCallExpression, { super.visitStaticMethodCallExpression(it) })
    }

    /** Adds a constructor call node to the tree. */
    @Override
    void visitConstructorCallExpression(ConstructorCallExpression node) {
        addNode(node, ConstructorCallExpression, { super.visitConstructorCallExpression(it) })
    }

    /** Adds a binary expression node to the tree. */
    @Override
    void visitBinaryExpression(BinaryExpression node) {
        addNode(node, BinaryExpression, { super.visitBinaryExpression(it) })
    }

    /** Adds a ternary expression node to the tree. */
    @Override
    void visitTernaryExpression(TernaryExpression node) {
        addNode(node, TernaryExpression, { super.visitTernaryExpression(it) })
    }

    /** Adds an Elvis expression node to the tree. */
    @Override
    void visitShortTernaryExpression(ElvisOperatorExpression node) {
        addNode(node, ElvisOperatorExpression, { super.visitShortTernaryExpression(it) })
    }

    /** Adds a postfix expression node to the tree. */
    @Override
    void visitPostfixExpression(PostfixExpression node) {
        addNode(node, PostfixExpression, { super.visitPostfixExpression(it) })
    }

    /** Adds a prefix expression node to the tree. */
    @Override
    void visitPrefixExpression(PrefixExpression node) {
        addNode(node, PrefixExpression, { super.visitPrefixExpression(it) })
    }

    /** Adds a boolean expression node to the tree. */
    @Override
    void visitBooleanExpression(BooleanExpression node) {
        addNode(node, BooleanExpression, { super.visitBooleanExpression(it) })
    }

    /** Adds a logical negation expression node to the tree. */
    @Override
    void visitNotExpression(NotExpression node) {
        addNode(node, NotExpression, { super.visitNotExpression(it) })
    }

    /** Adds a closure expression node to the tree. */
    @Override
    void visitClosureExpression(ClosureExpression node) {
        addNode(node, ClosureExpression, {
            it.parameters?.each { parameter -> visitParameter(parameter) }
            super.visitClosureExpression(it)
        })
    }

    /** Adds a lambda expression node to the tree. */
    @Override
    void visitLambdaExpression(LambdaExpression node) {
        addNode(node, LambdaExpression, {
            // params will be catered for by super call
            //it.parameters?.each { parameter -> visitParameter(parameter) }
            super.visitLambdaExpression(it)
        })
    }

    /**
     * Makes walking parameters look like others in the visitor.
     *
     * @param node parameter node to add
     */
    void visitParameter(Parameter node) {
        addNode(node, Parameter, { })
    }

    /** Adds a tuple expression node to the tree. */
    @Override
    void visitTupleExpression(TupleExpression node) {
        addNode(node, TupleExpression, { super.visitTupleExpression(it) })
    }

    /** Adds a list expression node to the tree. */
    @Override
    void visitListExpression(ListExpression node) {
        addNode(node, ListExpression, { super.visitListExpression(it) })
    }

    /** Adds an array expression node to the tree. */
    @Override
    void visitArrayExpression(ArrayExpression node) {
        addNode(node, ArrayExpression, { super.visitArrayExpression(it) })
    }

    /** Adds a map expression node to the tree. */
    @Override
    void visitMapExpression(MapExpression node) {
        addNode(node, MapExpression, { super.visitMapExpression(it) })
    }

    /** Adds a map entry expression node to the tree. */
    @Override
    void visitMapEntryExpression(MapEntryExpression node) {
        addNode(node, MapEntryExpression, { super.visitMapEntryExpression(it) })
    }

    /** Adds a range expression node to the tree. */
    @Override
    void visitRangeExpression(RangeExpression node) {
        addNode(node, RangeExpression, { super.visitRangeExpression(it) })
    }

    /** Adds a spread expression node to the tree. */
    @Override
    void visitSpreadExpression(SpreadExpression node) {
        addNode(node, SpreadExpression, { super.visitSpreadExpression(it) })
    }

    /** Adds a spread-map expression node to the tree. */
    @Override
    void visitSpreadMapExpression(SpreadMapExpression node) {
        addNode(node, SpreadMapExpression, { super.visitSpreadMapExpression(it) })
    }

    /** Adds a method pointer expression node to the tree. */
    @Override
    void visitMethodPointerExpression(MethodPointerExpression node) {
        addNode(node, MethodPointerExpression, { super.visitMethodPointerExpression(it) })
    }

    /** Adds a method reference expression node to the tree. */
    @Override
    void visitMethodReferenceExpression(MethodReferenceExpression node) {
        addNode(node, MethodReferenceExpression, { super.visitMethodReferenceExpression(it) })
    }

    /** Adds a unary minus expression node to the tree. */
    @Override
    void visitUnaryMinusExpression(UnaryMinusExpression node) {
        addNode(node, UnaryMinusExpression, { super.visitUnaryMinusExpression(it) })
    }

    /** Adds a unary plus expression node to the tree. */
    @Override
    void visitUnaryPlusExpression(UnaryPlusExpression node) {
        addNode(node, UnaryPlusExpression, { super.visitUnaryPlusExpression(it) })
    }

    /** Adds a bitwise negation expression node to the tree. */
    @Override
    void visitBitwiseNegationExpression(BitwiseNegationExpression node) {
        addNode(node, BitwiseNegationExpression, { super.visitBitwiseNegationExpression(it) })
    }

    /** Adds a cast expression node to the tree. */
    @Override
    void visitCastExpression(CastExpression node) {
        addNode(node, CastExpression, { super.visitCastExpression(it) })
    }

    /** Adds a constant expression node to the tree. */
    @Override
    void visitConstantExpression(ConstantExpression node) {
        addNode(node, ConstantExpression, { super.visitConstantExpression(it) })
    }

    /** Adds a class expression node to the tree. */
    @Override
    void visitClassExpression(ClassExpression node) {
        addNode(node, ClassExpression, { super.visitClassExpression(it) })
    }

    /** Adds a variable expression node to the tree. */
    @Override
    void visitVariableExpression(VariableExpression node) {
        addNode(node, VariableExpression, { VariableExpression it ->
            if (it.accessedVariable) {
                if (it.accessedVariable instanceof Parameter) {
                    visitParameter((Parameter)it.accessedVariable)
                } else if (it.accessedVariable instanceof DynamicVariable) {
                    addNode(it.accessedVariable, DynamicVariable,{ it.initialExpression?.visit(this)})
                }
            }
        })
    }

    /** Adds a declaration expression node to the tree. */
    @Override
    void visitDeclarationExpression(DeclarationExpression node) {
        addNode(node, DeclarationExpression, { super.visitDeclarationExpression(it) })
    }

    /** Adds a property expression node to the tree. */
    @Override
    void visitPropertyExpression(PropertyExpression node) {
        addNode(node, PropertyExpression, { super.visitPropertyExpression(it) })
    }

    /** Adds an attribute expression node to the tree. */
    @Override
    void visitAttributeExpression(AttributeExpression node) {
        addNode(node, AttributeExpression, { super.visitAttributeExpression(it) })
    }

    /** Adds a field expression node to the tree. */
    @Override
    void visitFieldExpression(FieldExpression node) {
        addNode(node, FieldExpression, { super.visitFieldExpression(it) })
    }

    /** Adds a GString expression node to the tree. */
    @Override
    void visitGStringExpression(GStringExpression node) {
        addNode(node, GStringExpression, { super.visitGStringExpression(it) })
    }

    /** Adds a catch clause node to the tree. */
    @Override
    void visitCatchStatement(CatchStatement node) {
        addNode(node, CatchStatement, {
            if (it.variable) visitParameter(it.variable)
            super.visitCatchStatement(it)
        })
    }

    /** Adds an argument list expression node to the tree. */
    @Override
    void visitArgumentlistExpression(ArgumentListExpression node) {
        addNode(node, ArgumentListExpression, { super.visitArgumentlistExpression(it) })
    }

    /** Adds a closure list expression node to the tree. */
    @Override
    void visitClosureListExpression(ClosureListExpression node) {
        addNode(node, ClosureListExpression, { super.visitClosureListExpression(it) })
    }

    /** Adds a bytecode expression node to the tree. */
    @Override
    void visitBytecodeExpression(BytecodeExpression node) {
        addNode(node, BytecodeExpression, { super.visitBytecodeExpression(it) })
    }

    /** Adds each expression from the supplied list to the tree. */
    @Override
    void visitListOfExpressions(List<? extends Expression> list) {
        list.each { Expression node ->
            if (node instanceof NamedArgumentListExpression ) {
                addNode(node, NamedArgumentListExpression, { it.visit(this) })
            } else {
                node.visit(this)
            }
        }
    }
}
