org.cloudsmith.geppetto.ruby.jrubyparser.RubyRakefileTaskFinder.java Source code

Java tutorial

Introduction

Here is the source code for org.cloudsmith.geppetto.ruby.jrubyparser.RubyRakefileTaskFinder.java

Source

/**
 * Copyright (c) 2011 Cloudsmith Inc. and other contributors, as listed below.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *   Cloudsmith
 * 
 */
package org.cloudsmith.geppetto.ruby.jrubyparser;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.jrubyparser.ast.ArrayNode;
import org.jrubyparser.ast.CallNode;
import org.jrubyparser.ast.FCallNode;
import org.jrubyparser.ast.HashNode;
import org.jrubyparser.ast.ListNode;
import org.jrubyparser.ast.NewlineNode;
import org.jrubyparser.ast.Node;
import org.jrubyparser.ast.NodeType;

import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

/**
 * Finds Rakefile task creations in a parsed ruby AST. An instance of this class can be reused,
 * but it is not threadsafe.
 * 
 * TODO: text from CallFinder - change this
 * 
 * Calls are found using a FQN - i.e. a sequence of module/receiver names, where
 * the last name segment is the name of the function. The search will find
 * function calls irrespective of call type (e.g. a FCallNode (where receiver is
 * implied), or CallNode where receiver is explicit. All FQN names are appended
 * to the current scope - i.e. if a call is made to X::Y::foo() in module A, it
 * will be found by a search of A::X::Y::foo().
 * 
 * TODO: global references are not handled - i.e. if a call to ::X::Y::foo() is
 * made inside module A, it will be recognized (in error) as A::X::Y::foo() -
 * also see {@link ConstEvaluator}.
 */
public class RubyRakefileTaskFinder {

    /**
     * Keeps track of where processing is in the node model.
     */
    private LinkedList<Object> stack = null;

    /**
     * Keeps track of the scope name.
     */
    private LinkedList<Object> nameStack = null;

    /**
     * The wanted FQN
     */
    // private List<String> qualifiedName = null;

    private List<String> cucumberTask = Lists.newArrayList("Cucumber", "Rake", "Task");

    private List<String> rspecTask = Lists.newArrayList("RSpec", "Core", "RakeTask");

    /**
     * An evaluator of constant ruby expressions
     */
    private ConstEvaluator constEvaluator = new ConstEvaluator();

    // public List<GenericCallNode> findCalls(Node root, String... qualifiedName) {
    // if(qualifiedName.length < 1)
    // throw new IllegalArgumentException("qualifiedName can not be empty");
    //
    // this.stack = Lists.newLinkedList();
    // this.nameStack = Lists.newLinkedList();
    //
    // this.qualifiedName = Lists.reverse(Lists.newArrayList(qualifiedName));
    //
    // // TODO: make this return more than one
    // return findTaskInternal(root, false);
    // }
    private String lastDesc;

    public Map<String, String> findTasks(Node root) {
        // use a linked map to get entries in the order they are added
        Map<String, String> resultMap = Maps.newLinkedHashMap();

        this.stack = Lists.newLinkedList();
        this.nameStack = Lists.newLinkedList();

        // this.qualifiedName = Lists.reverse(Lists.newArrayList(qualifiedName));
        if (root != null)
            findTasksInternal(root, resultMap);
        return resultMap;
    }

    private void findTasksInternal(Node root, Map<String, String> resultMap) {
        SEARCH: {
            switch (root.getNodeType()) {
            case MODULENODE:
                lastDesc = ""; // not valid now

                break SEARCH; // don't know what to do when user declares a module

            case CLASSNODE:
                lastDesc = ""; // not valid now

                // don't know what to do when user declares a class, any calls to
                // create tasks could be inside a loop etc. Just impossible to figure out
                break SEARCH;

            case CALLNODE:
                // don't know yet what the calls look like - if they are calls to "do" with
                // the interesting part on the left
                // or what...
                //
                // CallNode callNode = (CallNode) root;
                if (!processCallNode((CallNode) root, resultMap))
                    break SEARCH;
                break;

            // if(!callNode.getName().equals(qualifiedName.get(0)))
            // break SEARCH;
            // pushNames(constEvaluator.stringList(constEvaluator.eval(callNode.getReceiverNode())));
            // if(inWantedScope())
            // return Lists.newArrayList(new GenericCallNode(callNode));
            // pop(root); // clear the pushed names
            // push(root); // push it again
            // break; // continue search inside the function

            case FCALLNODE:
                // Don't know what the interesting calls look like
                if (!processFCallNode((FCallNode) root, resultMap))
                    break SEARCH;
                break;
            // FCallNode fcallNode = (FCallNode) root;
            // if(!fcallNode.getName().equals(qualifiedName.get(0)))
            // break SEARCH;
            // if(inWantedScope())
            // return Lists.newArrayList(new GenericCallNode(fcallNode));
            // break; // continue search inside the function
            default:
                break;
            }

            for (Node n : root.childNodes()) {
                if (n.getNodeType() == NodeType.NEWLINENODE)
                    n = ((NewlineNode) n).getNextNode();
                findTasksInternal(n, resultMap);
            }
        } // SEARCH

    }

    /**
     * If the argsNode is an Array with a Hash, then return the node for the first key (the name).
     * This construct is found for input task :foo => 'dependson', or :foo => ['depdendson', 'andonthis']
     * 
     * @param argsNode
     * @return
     */
    private Node getTaskNameNodeFromArgNode(Node argsNode) {
        if (argsNode instanceof ArrayNode) {
            ArrayNode arrayNode = (ArrayNode) argsNode;
            if (arrayNode.size() > 0) {
                Node a = arrayNode.get(0);
                if (a instanceof HashNode) {
                    HashNode argsHash = (HashNode) a;
                    ListNode listNode = argsHash.getListNode();
                    if (listNode.size() > 0)
                        argsNode = listNode.get(0);
                }
            }
        }
        return argsNode;
    }

    /**
     * If in the exact scope, or if scope is an outer scope of the wanted scope.
     * 
     * @return true if wanted or outer scope of wanted
     */
    /*
    private boolean inCompatibleScope() {
       // if an inner module of the wanted module is found
       // i.e. we find module a::b::c::d(::_)* when we are looking for
       // a::b::c::FUNC
       //
       if(nameStack.size() >= qualifiedName.size())
     return false; // will not be found in this module - it is out of
                 // scope
        
       // the module's name is shorter than wanted, does it match so far?
       // i.e. we find module a::b when we are looking for a::b::c::FUNC
       //
       int sizeX = qualifiedName.size();
       int sizeY = nameStack.size();
       try {
     return qualifiedName.subList(sizeX - sizeY, sizeX).equals(nameStack)
           ? true
           : false;
       }
       catch(IndexOutOfBoundsException e) {
     return false;
       }
    }*/

    /**
     * Returns true if the current scope is the wanted scope.
     * 
     * @return
     */
    /*
    private boolean inWantedScope() {
       // we are in wanted scope if all segments (except the function name)
       // match.
       try {
     return qualifiedName.subList(1, qualifiedName.size()).equals(nameStack)
           ? true
           : false;
       }
       catch(IndexOutOfBoundsException e) {
     return false;
       }
    }*/

    /**
     * Pops the stack until we are the previous scope.
     * 
     * @param n
     */
    private void pop(Node n) {
        while (stack.peek() != n) {
            Object x = stack.pop();
            if (x instanceof String)
                popName();
        }
        stack.pop();
    }

    /**
     * Pops a name of the namestack - should not be called without also managing
     * the scope stack.
     */
    private void popName() {
        nameStack.pop();
    }

    /**
     * @param root
     * @return
     */
    private boolean processCallNode(CallNode root, Map<String, String> resultMap) {
        String mName = root.getName();
        try {
            if (mName.equals("new")) {
                List<String> receiver = constEvaluator.stringList(constEvaluator.eval(root.getReceiver()));
                boolean isRspec = receiver.equals(rspecTask);
                boolean isCucumber = !isRspec && receiver.equals(cucumberTask);
                if (isRspec || isCucumber) {
                    // recognized as a task
                    Node argsNode = getTaskNameNodeFromArgNode(root.getArgs());
                    List<String> nameList = constEvaluator.stringList(constEvaluator.eval(argsNode));
                    if (nameList.size() < 1) {
                        if (isRspec)
                            nameList = Lists.newArrayList("spec");
                        else
                            nameList = Lists.newArrayList("cucumber");
                    }
                    String taskName = Joiner.on(":").join(Iterables.concat(Lists.reverse(nameStack), nameList));
                    resultMap.put(taskName, lastDesc);
                    // System.err.println("Added task: " + taskName + " with description: " + lastDesc);
                    lastDesc = ""; // consumed
                }
            }
        } catch (RuntimeException e) {
            // Failed to handle some constant evaluation - not sure what, should not fail, could be
            // caused by faulty ruby code (syntax errors etc.) causing a strange model.
            // Should be handled elsewhere.
            return false;
        }
        return false;
    }

    /**
     * @param root
     * @return
     */
    private boolean processFCallNode(FCallNode root, Map<String, String> resultMap) {
        final String fName = root.getName();
        if (fName.equals("namespace")) {
            // System.err.println("Found NAMESPACE (Fcall)");
            // push the name on the nameStack
            // this is the argument to the namespace function
            push(root);
            pushNames(constEvaluator.stringList(constEvaluator.eval(root.getArgs())));

            for (Node n : root.childNodes()) {
                if (n.getNodeType() == NodeType.NEWLINENODE)
                    n = ((NewlineNode) n).getNextNode();
                findTasksInternal(n, resultMap);
            }
            pop(root);
        } else if (fName.equals("task")) {
            // System.err.println("Found TASK (Fcall)");
            // argument is the name of the task
            // prepend with name parts from namespaces
            // pick up lastDesc
            Node argsNode = getTaskNameNodeFromArgNode(root.getArgs());
            String taskName = Joiner.on(":").join(Iterables.concat(Lists.reverse(nameStack),
                    constEvaluator.stringList(constEvaluator.eval(argsNode))));

            resultMap.put(taskName, lastDesc);
            // System.err.println("Added task: " + taskName + " with description: " + lastDesc);
            lastDesc = ""; // consumed
        } else if (fName.equals("desc")) {
            // argument is the description
            lastDesc = Joiner.on("").join(constEvaluator.stringList(constEvaluator.eval(root.getArgs())));
        } else {
            lastDesc = ""; // consume, not valid any more
        }
        return false;
    }

    /**
     * Push node so we know where we are in scope.
     * 
     * @param n
     */
    private void push(Node n) {
        stack.push(n);
    }

    /**
     * Pushes a name onto the name stack (and the scope stack, as we need to
     * discard the names when leaving a named scope).
     * 
     * @param name
     */
    private void pushName(String name) {
        stack.push(name);
        nameStack.push(name);
    }

    /**
     * Convenience for pushing 0-n names in a list. Same as call {@link #pushName(String)} for each.
     * 
     * @param names
     */
    private void pushNames(List<String> names) {
        for (String name : names)
            pushName(name);
    }
}