package org.rubypeople.rdt.internal.ti;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.jruby.ast.ArgsNode;
import org.jruby.ast.ArgumentNode;
import org.jruby.ast.CallNode;
import org.jruby.ast.ClassNode;
import org.jruby.ast.Colon2Node;
import org.jruby.ast.ConstNode;
import org.jruby.ast.DVarNode;
import org.jruby.ast.DefnNode;
import org.jruby.ast.DefsNode;
import org.jruby.ast.GlobalAsgnNode;
import org.jruby.ast.GlobalVarNode;
import org.jruby.ast.InstAsgnNode;
import org.jruby.ast.InstVarNode;
import org.jruby.ast.IterNode;
import org.jruby.ast.ListNode;
import org.jruby.ast.LocalAsgnNode;
import org.jruby.ast.LocalVarNode;
import org.jruby.ast.MethodDefNode;
import org.jruby.ast.ModuleNode;
import org.jruby.ast.Node;
import org.jruby.ast.RootNode;
import org.jruby.ast.VCallNode;
import org.jruby.ast.YieldNode;
import org.jruby.lexer.yacc.SyntaxException;
import org.rubypeople.rdt.core.IMethod;
import org.rubypeople.rdt.core.IRubyElement;
import org.rubypeople.rdt.core.RubyCore;
import org.rubypeople.rdt.core.search.CollectingSearchRequestor;
import org.rubypeople.rdt.core.search.IRubySearchConstants;
import org.rubypeople.rdt.core.search.IRubySearchScope;
import org.rubypeople.rdt.core.search.SearchEngine;
import org.rubypeople.rdt.core.search.SearchMatch;
import org.rubypeople.rdt.core.search.SearchParticipant;
import org.rubypeople.rdt.core.search.SearchPattern;
import org.rubypeople.rdt.internal.core.parser.RubyParser;
import org.rubypeople.rdt.internal.core.util.ASTUtil;
import org.rubypeople.rdt.internal.ti.data.LiteralNodeTypeNames;
import org.rubypeople.rdt.internal.ti.data.TypicalMethodReturnNames;
import org.rubypeople.rdt.internal.ti.util.ClosestSpanningNodeLocator;
import org.rubypeople.rdt.internal.ti.util.FirstPrecursorNodeLocator;
import org.rubypeople.rdt.internal.ti.util.INodeAcceptor;
import org.rubypeople.rdt.internal.ti.util.OffsetNodeLocator;
import org.rubypeople.rdt.internal.ti.util.ScopedNodeLocator;
public class DefaultTypeInferrer implements ITypeInferrer
{
private static final String CONSTRUCTOR_INVOKE_NAME = "new";
private RootNode rootNode;
private Set<Node> dontVisitNodes;
private HashSet<Node> fVisitedNodes;
private Map<String, RootNode> parsed;
/**
* Infers type inside the source at given offset.
*
* @return List of ITypeGuess objects.
*/
public Collection<ITypeGuess> infer(String source, int offset)
{
dontVisitNodes = new HashSet<Node>();
fVisitedNodes = new HashSet<Node>();
parsed = new HashMap<String, RootNode>();
try
{
rootNode = parse(source);
Node node = OffsetNodeLocator.Instance().getNodeAtOffset(rootNode.getBodyNode(), offset);
if (node == null)
{
return Collections.emptyList();
}
return infer(node);
}
catch (SyntaxException e)
{
return Collections.emptyList();
}
finally
{
parsed.clear();
dontVisitNodes.clear();
fVisitedNodes.clear();
}
}
/**
* Infers the type of the specified node.
*
* @param node
* Node to infer type of.
* @return List of ITypeGuess objects.
*/
private Set<ITypeGuess> infer(Node node)
{
// Try to avoid infinite loop
if (fVisitedNodes.contains(node))
return new HashSet<ITypeGuess>();
fVisitedNodes.add(node);
Set<ITypeGuess> guesses = new HashSet<ITypeGuess>();
tryLiteralNode(node, guesses);
tryAsgnNode(node, guesses);
// TODO refactor these 3 by common features into 1 (or 1+3) method(s)
tryDVarNode(node, guesses);
tryLocalVarNode(node, guesses);
tryInstVarNode(node, guesses);
tryGlobalVarNode(node, guesses);
tryMethodNode(node, guesses);
tryIterNode(node, guesses);
tryWellKnownMethodCalls(node, guesses);
if (node instanceof Colon2Node)
{ // if this is a constant, it may be the type name!
Colon2Node colonNode = (Colon2Node) node;
String name = ASTUtil.getFullyQualifiedName(colonNode);
guesses.add(new BasicTypeGuess(name, 100));
}
if (node instanceof ConstNode)
{ // if this is a constant, it may be the type name!
ConstNode constNode = (ConstNode) node;
// TODO See if constant is assigned to in scope, if it is, don't add it as a type guess.
String name = constNode.getName();
if (!name.equals("ARGV"))
guesses.add(new BasicTypeGuess(constNode.getName(), 100));
}
if (guesses.isEmpty())
{ // if we have no guesses..
if (node instanceof CallNode)
{ // and it's a method call, try inferring receiver type
CallNode call = (CallNode) node;
return infer(call.getReceiverNode());
}
}
return guesses;
}
private void tryDVarNode(Node node, Set<ITypeGuess> guesses)
{
if (!(node instanceof DVarNode))
return;
Node iterNode = ClosestSpanningNodeLocator.Instance().findClosestSpanner(rootNode,
node.getPosition().getStartOffset(), new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
return (node instanceof IterNode);
}
});
Node methodCall = OffsetNodeLocator.Instance().getNodeAtOffset(rootNode,
iterNode.getPosition().getStartOffset() - 1);
try
{
SearchEngine engine = new SearchEngine();
SearchPattern pattern = SearchPattern.createPattern(IRubyElement.METHOD, ASTUtil
.getNameReflectively(methodCall), IRubySearchConstants.DECLARATIONS, SearchPattern.R_EXACT_MATCH);
SearchParticipant[] participants = new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() };
IRubySearchScope scope = SearchEngine.createWorkspaceScope();
CollectingSearchRequestor requestor = new CollectingSearchRequestor();
engine.search(pattern, participants, scope, requestor, new NullProgressMonitor());
List<SearchMatch> matches = requestor.getResults();
for (SearchMatch match : matches)
{
IMethod method = (IMethod) match.getElement();
// Grab the method's source, search for yields of a var, and then return the inferred type of the
// yielded var
String src = method.getRubyScript().getSource();
Node otherRoot = parse(src);
Node methodNodeThing = OffsetNodeLocator.Instance().getNodeAtOffset(otherRoot,
method.getSourceRange().getOffset());
List<Node> yields = ScopedNodeLocator.Instance().findNodesInScope(methodNodeThing, new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
return node instanceof YieldNode;
}
});
if (yields == null)
continue;
for (Node yield : yields)
{
if (yield instanceof YieldNode)
{
YieldNode yieldNode = (YieldNode) yield;
Node argsNode = yieldNode.getArgsNode();
guesses.addAll(infer(src, argsNode.getPosition().getStartOffset()));
}
}
}
}
catch (CoreException e)
{
RubyCore.log(e);
}
}
private RootNode parse(String src)
{
if (parsed.containsKey(src))
{
return parsed.get(src);
}
RubyParser parser = new RubyParser();
RootNode root = (RootNode) parser.parse(src).getAST();
parsed.put(src, root);
return root;
}
private void tryIterNode(Node node, Set<ITypeGuess> guesses)
{
if (!(node instanceof IterNode))
return;
tryEnclosingType(node, guesses);
}
private void tryEnclosingType(Node node, Set<ITypeGuess> guesses)
{
Node typeNode = ClosestSpanningNodeLocator.Instance().findClosestSpanner(rootNode,
node.getPosition().getStartOffset(), new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
return (node instanceof ClassNode || node instanceof ModuleNode);
}
});
if (typeNode == null)
{
// top level
guesses.add(new BasicTypeGuess("Object", 100));
}
else
{
guesses.add(new BasicTypeGuess(ASTUtil.getFullyQualifiedTypeName(rootNode, typeNode), 100));
}
}
/**
* Resolve a method to it's surrounding type. If in top level, return "Object" as a guess.
*
* @param node
* @param guesses
*/
private void tryMethodNode(Node node, Set<ITypeGuess> guesses)
{
if (!(node instanceof MethodDefNode))
return;
tryEnclosingType(node, guesses);
}
/**
* Infers type if node is a literal node; i.e. 5, 'foo', [1,2,3]
*
* @param node
* Node to infer type of.
* @param guesses
* List of ITypeGuess objects to insert guesses into.
*/
private void tryLiteralNode(Node node, Collection<ITypeGuess> guesses)
{
// Try seeing if the rvalue is a constant (5, "foo", [1,2,3], etc.)
String concreteGuess = LiteralNodeTypeNames.get(node.getClass().getSimpleName());
if (concreteGuess != null)
{
guesses.add(new BasicTypeGuess(concreteGuess, 100));
}
}
/**
* Infers type if node is an assignment node; i.e. x = 5,
*
* @y = 'foo', $z = [1,2,3]
* @param node
* Node to infer type of.
* @param guesses
* List of ITypeGuess objects to insert guesses into.
*/
private void tryAsgnNode(Node node, Collection<ITypeGuess> guesses)
{
Node valueNode = null;
if (node instanceof LocalAsgnNode)
{
valueNode = ((LocalAsgnNode) node).getValueNode();
}
if (node instanceof InstAsgnNode)
{
valueNode = ((InstAsgnNode) node).getValueNode();
}
if (node instanceof GlobalAsgnNode)
{
valueNode = ((GlobalAsgnNode) node).getValueNode();
}
if (valueNode != null)
{
guesses.addAll(infer(valueNode));
}
}
private void tryInstVarNode(Node node, Collection<ITypeGuess> guesses)
{
if (!(node instanceof InstVarNode))
return;
final InstVarNode instVarNode = (InstVarNode) node;
// TODO: see if there is attr_reader/attr_writer, maybe?
// TODO: find calls to the reader/writers
// TODO: for STI on InstVar, find references within this ClassNode
// to this InstVar... record 'em
// Find first assignment to this var name that occurs before the
// reference
// TODO: This will find assignments in other local scopes that
// precede this reference but have the same variable name.
// To mitigate, ensure that the closest spanning ScopeNode for both
// this LocalVarNode and the AsgnNode are the name ScopeNode.
// Or scopingNode. Still not sure whether IterNodes count or not...
// silly block-local-var ambiguity ;)
// try and grab the assignment node if this reference is in an assignment, so we can "blacklist" it from being
// grabbed in next step where we grab all assignments to the instance variable
final Node assignmentNode = ClosestSpanningNodeLocator.Instance().findClosestSpanner(rootNode,
instVarNode.getPosition().getStartOffset(), new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
return node instanceof InstAsgnNode;
}
});
if (assignmentNode != null)
dontVisitNodes.add(assignmentNode);
List<Node> assignments = new ArrayList<Node>();
assignments.addAll(ScopedNodeLocator.Instance().findNodesInScope(rootNode, new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
return (node instanceof InstAsgnNode)
&& (((InstAsgnNode) node).getName().equals(instVarNode.getName()))
&& !dontVisitNodes.contains(node);
}
}));
for (Node assignNode : assignments)
{
tryAsgnNode(assignNode, guesses);
}
}
private void tryGlobalVarNode(Node node, Collection<ITypeGuess> guesses)
{
if (!(node instanceof GlobalVarNode))
return;
final GlobalVarNode globalVarNode = (GlobalVarNode) node;
int nodeStart = node.getPosition().getStartOffset();
// TODO: for STI on GlobalVar, find references within this ClassNode
// to this GlobalVar... record 'em
// TODO: p.s. globals are low-priority.
// Find first assignment to this var name that occurs before the
// reference
// TODO: This will find assignments in other local scopes that
// precede this reference but have the same variable name.
// To mitigate, ensure that the closest spanning ScopeNode for both
// this LocalVarNode and the AsgnNode are the name ScopeNode.
// Or scopingNode. Still not sure whether IterNodes count or not...
// silly block-local-var ambiguity ;)
Node initialAssignmentNode = FirstPrecursorNodeLocator.Instance().findFirstPrecursor(rootNode, nodeStart,
new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
String name = null;
if (node instanceof LocalAsgnNode)
name = ((LocalAsgnNode) node).getName();
if (node instanceof InstAsgnNode)
name = ((InstAsgnNode) node).getName();
if (node instanceof GlobalAsgnNode)
name = ((GlobalAsgnNode) node).getName();
return (name != null && name.equals(globalVarNode.getName()));
/**
* refactor to common INodeAcceptor for instVarName,localVarName,globalVarName
*/
}
});
if (initialAssignmentNode != null)
{
tryAsgnNode(initialAssignmentNode, guesses);
}
}
private void tryLocalVarNode(Node node, Collection<ITypeGuess> guesses)
{
if (node instanceof VCallNode)
{
// FIXME How do we handle local variables who show up as VCallNodes?
return;
}
if (!(node instanceof LocalVarNode))
return;
LocalVarNode localVarNode = (LocalVarNode) node;
int nodeStart = node.getPosition().getStartOffset();
final String localVarName = TypeInferenceHelper.Instance().getVarName(localVarNode);
// See if it has been assigned to, earlier [TODO: in this local scope].
// Find first assignment to this var name that occurs before the
// reference
// TODO: This will find assignments in other local scopes that
// precede this reference but have the same variable name.
// To mitigate, ensure that the closest spanning ScopeNode for both
// this LocalVarNode and the AsgnNode are the name ScopeNode.
// Or scopingNode. Still not sure whether IterNodes count or not...
// silly block-local-var ambiguity ;)
Node initialAssignmentNode = FirstPrecursorNodeLocator.Instance().findFirstPrecursor(rootNode, nodeStart,
new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
String name = null;
if (node instanceof LocalAsgnNode)
name = ((LocalAsgnNode) node).getName();
if (node instanceof InstAsgnNode)
name = ((InstAsgnNode) node).getName();
if (node instanceof GlobalAsgnNode)
name = ((GlobalAsgnNode) node).getName();
return (name != null && name.equals(localVarName));
}
});
if (initialAssignmentNode != null)
{
tryAsgnNode(initialAssignmentNode, guesses);
}
// See if it is a param into this scope
ArgsNode argsNode = (ArgsNode) FirstPrecursorNodeLocator.Instance().findFirstPrecursor(rootNode, nodeStart,
new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
return ((node instanceof ArgsNode) && (doesArgsNodeContainsVariable((ArgsNode) node,
localVarName)));
}
});
// If so, find its enclosing method
if (argsNode != null)
{
// Find enclosing method
Node defNode = FirstPrecursorNodeLocator.Instance().findFirstPrecursor(rootNode, nodeStart,
new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
ArgsNode argsNode = null;
if (node instanceof DefnNode)
argsNode = ((DefnNode) node).getArgsNode();
if (node instanceof DefsNode)
argsNode = ((DefsNode) node).getArgsNode();
return ((argsNode != null) && (doesArgsNodeContainsVariable(argsNode, localVarName)));
}
});
if (defNode != null)
{
String methodName = null;
if (defNode instanceof DefnNode)
methodName = ((DefnNode) defNode).getName();
if (defNode instanceof DefsNode)
methodName = ((DefsNode) defNode).getName();
// Find all invocations of the surrounding method.
// TODO: from easiest to hardest:
// It may be a global function, where simply a CallNode
// where method name must be matched.
// It may be a DefsNode static class method, where a
// CallNode whose receiverNode is a ConstNode whose name is
// the surrounding class
// It may be an DefnNode method defined in a class, where a
// CallNode whose receiverNode must be type-matched to the
// surrounding class
}
}
}
private void tryWellKnownMethodCalls(Node node, Collection<ITypeGuess> guesses)
{
if (!(node instanceof CallNode))
return;
CallNode callNode = (CallNode) node;
String method = callNode.getName();
if (method.equals(CONSTRUCTOR_INVOKE_NAME))
{
String name = null;
if (callNode.getReceiverNode() instanceof ConstNode)
{
name = ((ConstNode) callNode.getReceiverNode()).getName();
}
else if (callNode.getReceiverNode() instanceof Colon2Node)
{
name = ASTUtil.getFullyQualifiedName((Colon2Node) callNode.getReceiverNode());
}
if (name != null)
guesses.add(new BasicTypeGuess(name, 100));
}
else
{
guesses.addAll(TypicalMethodReturnNames.get(method));
}
}
/**
* Determine whether an ArgsNode contains a particular named argument
*
* @param argsNode
* ArgsNode to search
* @param argName
* Name of argument to find
* @return
*/
private boolean doesArgsNodeContainsVariable(ArgsNode argsNode, String argName)
{
if (argsNode == null)
return false;
if (argName == null)
return false;
return getArgumentIndex(argsNode, argName) >= 0;
}
/**
* Finds the index of an argument in an ArgsNode by name, -1 if it is not contained.
*
* @param argsNode
* ArgsNode to search
* @param argName
* Name of argument to find
* @return Index of argName in argsNode or -1 if it is not there.
*/
private int getArgumentIndex(ArgsNode argsNode, String argName)
{
int argNumber = 0;
ListNode args = argsNode.getArgs();
if (args == null)
return -1; // no args. Maybe we should check arity instead?
for (Iterator iter = args.childNodes().iterator(); iter.hasNext();)
{
ArgumentNode arg = (ArgumentNode) iter.next();
if (arg.getName().equals(argName))
{
break;
}
argNumber++;
}
if (argNumber == argsNode.getRequiredArgsCount())
{
return -1;
}
return argNumber;
}
}