package org.rubypeople.rdt.internal.ti;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import org.jruby.ast.AssignableNode;
import org.jruby.ast.CallNode;
import org.jruby.ast.ClassNode;
import org.jruby.ast.ClassVarAsgnNode;
import org.jruby.ast.ClassVarDeclNode;
import org.jruby.ast.ClassVarNode;
import org.jruby.ast.Colon2Node;
import org.jruby.ast.ConstNode;
import org.jruby.ast.DAsgnNode;
import org.jruby.ast.DVarNode;
import org.jruby.ast.DefnNode;
import org.jruby.ast.DefsNode;
import org.jruby.ast.FCallNode;
import org.jruby.ast.GlobalAsgnNode;
import org.jruby.ast.GlobalVarNode;
import org.jruby.ast.InstAsgnNode;
import org.jruby.ast.InstVarNode;
import org.jruby.ast.ListNode;
import org.jruby.ast.LocalAsgnNode;
import org.jruby.ast.LocalVarNode;
import org.jruby.ast.ModuleNode;
import org.jruby.ast.Node;
import org.jruby.ast.ReturnNode;
import org.jruby.ast.SelfNode;
import org.jruby.ast.VCallNode;
import org.jruby.lexer.yacc.SyntaxException;
import org.rubypeople.rdt.core.RubyCore;
import org.rubypeople.rdt.core.parser.ReturnVisitor;
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.INodeAcceptor;
import org.rubypeople.rdt.internal.ti.util.MethodDefinitionLocator;
import org.rubypeople.rdt.internal.ti.util.MethodInvocationLocator;
import org.rubypeople.rdt.internal.ti.util.OffsetNodeLocator;
import org.rubypeople.rdt.internal.ti.util.ScopedNodeLocator;
public class DataFlowTypeInferrer implements ITypeInferrer
{
private static final boolean VERBOSE = false;
private void sysout(String string)
{
if (VERBOSE)
{
System.out.println(string);
}
}
private void prettyPrint(Node node)
{
sysout("----------------------------------------\n" + "Node: " + node.getClass().getSimpleName() + "\n"
+ "Source:\n[" + node.getPosition().getStartOffset() + ".." + node.getPosition().getEndOffset() + "]\n"
+ source.substring(node.getPosition().getStartOffset(), node.getPosition().getEndOffset()) + "\n"
+ "----------------------------------------");
}
TypeInferenceHelper helper;
private String source;
private Node rootNode;
// To detect cycles in dataflow graph
private List<Node> inferNodeStack;
public List<ITypeGuess> infer(String source, int offset)
{
if (source == null)
return Collections.emptyList();
this.rootNode = null;
try {
this.rootNode = (new RubyParser()).parse(source).getAST();
}
catch (SyntaxException se)
{
return Collections.emptyList();
}
catch (Exception e) {
RubyCore.log(e);
return Collections.emptyList();
}
List<ITypeGuess> guesses = new LinkedList<ITypeGuess>();
this.helper = TypeInferenceHelper.Instance();
this.source = source;
this.inferNodeStack = new LinkedList<Node>();
Node node = OffsetNodeLocator.Instance().getNodeAtOffset(rootNode, offset);
if (node == null)
{
return null;
}
guesses = inferNodeType(node);
guesses = redistributeGuessConfidences(guesses);
guesses = combineSameGuesses(guesses);
return guesses;
}
private List<ITypeGuess> combineSameGuesses(List<ITypeGuess> guesses)
{
// TODO If we have two String guesses at 50%, add them back together as one String guess at 100%!
Map<String, Integer> combined = new HashMap<String, Integer>();
for (ITypeGuess typeGuess : guesses)
{
Integer percent = combined.get(typeGuess.getType());
if (percent == null)
{
combined.put(typeGuess.getType(), typeGuess.getConfidence());
}
else
{
combined.put(typeGuess.getType(), percent + typeGuess.getConfidence());
}
}
List<ITypeGuess> combinedGuesses = new ArrayList<ITypeGuess>(combined.size());
for (String type : combined.keySet())
{
combinedGuesses.add(new BasicTypeGuess(type, combined.get(type)));
}
return combinedGuesses;
}
/**
* Redistribute the confidence percentages. I.e. if guesses contains three guesses, each at 100%, they will now each
* be 33%.
*
* @param guesses
* Guesses to redistribute
* @return Guesses with confidences redistributes
*/
private List<ITypeGuess> redistributeGuessConfidences(List<ITypeGuess> guesses)
{
int sum = 0;
for (ITypeGuess guess : guesses)
{
if (guess == null)
continue;
sum += guess.getConfidence();
}
List<ITypeGuess> newGuesses = new ArrayList<ITypeGuess>(guesses.size());
for (ITypeGuess guess : guesses)
{
if (guess == null)
continue;
ITypeGuess newGuess = new BasicTypeGuess(guess.getType(), (int) (((double) guess.getConfidence())
/ ((double) sum) * 100.0));
newGuesses.add(newGuess);
}
return newGuesses;
}
// Infer the type of specified node
private List<ITypeGuess> inferNodeType(Node node)
{
if (node == null)
{
return Collections.emptyList();
}
sysout("Inferring node: " + node.getClass().getSimpleName());
List<ITypeGuess> guesses = new ArrayList<ITypeGuess>(1);
// Detect cycles in data flow graph
if (inferNodeStack.indexOf(node) != -1)
{
sysout("Data flow graph cycle detected:");
prettyPrint(node);
return guesses;
}
// Push node onto stack
inferNodeStack.add(0, node);
if (isSelfReferenceNode(node))
{
ITypeGuess guess = getSelfReferenceNodeType(node);
if (guess != null)
guesses.add(guess);
}
if (isAssignmentNode(node))
{
guesses.addAll(inferNodeType(getAssignmentNodeValueNode(node)));
}
if (isTypeDefinitionNode(node))
{
ITypeGuess guess = getTypeDefinitionNodeType(node);
if (guess != null)
guesses.add(guess);
}
if (isConstantNode(node))
{
guesses.add(getConstantNodeType(node));
}
if (node instanceof LocalVarNode)
{
guesses.addAll(getLocalVarReferenceNodeTypes((LocalVarNode) node));
}
if (node instanceof DVarNode)
{
guesses.addAll(getDVarReferenceNodeTypes((DVarNode) node));
}
if (node instanceof InstVarNode)
{
guesses.addAll(getInstanceVarReferenceNodeTypes((InstVarNode) node));
}
if (node instanceof ClassVarNode)
{
guesses.addAll(getClassVarReferenceNodeTypes((ClassVarNode) node));
}
if (node instanceof GlobalVarNode)
{
guesses.addAll(getGlobalVarReferenceNodeTypes((GlobalVarNode) node));
}
if (isCallNode(node))
{
guesses.addAll(getCallNodeTypes(node));
}
// PSEUDOCODE:
// if ( element is_a(instvar or global)) {
// ALREADY USING THIS:
// types = sum(typeOfEach(getThingsAssignedInto(element, :within => element.lexicalScope)));
// return types if any found;
// CAN STILL FALLBACK TO:
// //otherwise
// usages = list of places where element is passed as a param;
// sameTypedElements = list of elements passed into the same method-param-location element is;
// types = sum(typeOfEach(sameTypedElements));
// return types if any found;
// CAN STILL FALLBACK TO:
// //otherwise
// methods = list of methods invoked against element;
// types = sum(typesRespondingToAllOfThese);
// return types;
// }
// if ( element is_a(method invocation)) {
// ALREADY USING THIS:
// klass = typeOf(receiver);
//
// // nice special case: check for accessors/mutators
// // hook for "magic" combinations; (class << ActiveRecord::Base).find(*) => ArrayOf[klass], etc.
//
// defNode = findDefinition(receiver,method_name);
// return findReturnedTypesInDefNode();
// }
// Pop node from stack
inferNodeStack.remove(0);
return guesses;
}
private boolean isConstantNode(Node node)
{
return (node instanceof Colon2Node) || (node instanceof ConstNode)
|| (null != LiteralNodeTypeNames.get(node.getClass().getSimpleName()));
}
// Look up from LiteralNodeTypeNames
private ITypeGuess getConstantNodeType(Node node)
{
if (node instanceof ConstNode)
{
return new BasicTypeGuess(((ConstNode) node).getName(), 100);
}
if (node instanceof Colon2Node)
{
String name = ASTUtil.getFullyQualifiedName((Colon2Node) node);
return new BasicTypeGuess(name, 100);
}
return new BasicTypeGuess(LiteralNodeTypeNames.get(node.getClass().getSimpleName()), 100);
}
private boolean isTypeDefinitionNode(Node node)
{
return (node instanceof ClassNode) || (node instanceof ModuleNode);
}
private ITypeGuess getTypeDefinitionNodeType(Node node)
{
String typeNodeName = helper.getTypeNodeName(node);
if (typeNodeName != null)
{
return new BasicTypeGuess(typeNodeName, 100);
}
RubyCore.log("Unable to determine type node's name: " + node);
return null;
}
private boolean isSelfReferenceNode(Node node)
{
return (node instanceof SelfNode);
}
private ITypeGuess getSelfReferenceNodeType(Node node)
{
Node enclosingTypeNode = findEnclosingTypeNode(node);
return getTypeDefinitionNodeType(enclosingTypeNode);
}
private List<Node> findAllSendersOfMethod(String typeName, String methodName)
{
return MethodInvocationLocator.Instance().findMethodInvocations(rootNode, typeName, methodName,
new DataFlowTypeInferrer());
}
private List<Node> findAllMethodDefinitions(String typeName, String methodName)
{
return MethodDefinitionLocator.Instance().findMethodDefinitions(rootNode, typeName, methodName);
}
private List<Node> findRetvalExprs(Node methodNode)
{
ReturnVisitor visitor = new ReturnVisitor();
visitor.acceptNode(methodNode);
List<Node> returnNodes = visitor.getReturnValues();
List<Node> retvalExprs = new ArrayList<Node>(returnNodes.size());
for (Node returnNode : returnNodes)
{
if (returnNode instanceof ReturnNode)
{
retvalExprs.add(((ReturnNode) returnNode).getValueNode());
}
else
{
retvalExprs.add(returnNode);
}
}
sysout("Found " + retvalExprs.size() + " + retval exprs in method "
+ helper.getMethodDefinitionNodeName(methodNode));
return retvalExprs;
}
private Node findEnclosingMethodNode(Node node)
{
Node enclosingScopeNode = ClosestSpanningNodeLocator.Instance().findClosestSpanner(rootNode,
node.getPosition().getStartOffset(), new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
return (node instanceof DefnNode) || (node instanceof DefsNode);
}
});
if (enclosingScopeNode == null)
{
enclosingScopeNode = rootNode;
}
return enclosingScopeNode;
}
private Node findEnclosingTypeNode(Node node)
{
Node enclosingTypeNode = ClosestSpanningNodeLocator.Instance().findClosestSpanner(rootNode,
node.getPosition().getStartOffset(), new INodeAcceptor()
{
public boolean doesAccept(Node node)
{
return (node instanceof ClassNode) || (node instanceof ModuleNode);
}
});
// TODO: Handle reference inside metaclass block:
// class << foo; [[INFER]] .....
if (enclosingTypeNode == null)
{
enclosingTypeNode = rootNode;
}
return enclosingTypeNode;
}
private List<ITypeGuess> getLocalVarReferenceNodeTypes(LocalVarNode node)
{
List<ITypeGuess> possibleTypes = new ArrayList<ITypeGuess>(1);
// Get enclosing scope
Node enclosingScopeNode = findEnclosingMethodNode(node);
if (enclosingScopeNode == rootNode)
{
sysout("localvarnode outside a method!");
enclosingScopeNode = findEnclosingTypeNode(node);
}
// TODO: ScopedNodeLocator doesn't ensure that returned asgns are prior to the ref... relevant to algo?
// Are there prior assigns into this ref within the scope?
final String localVarName = helper.getVarName(node);
CopyOnWriteArrayList<Node> localAssignsIntoNode = new CopyOnWriteArrayList<Node>(ScopedNodeLocator.Instance().findNodesInScope(enclosingScopeNode,
new INodeAcceptor()
{
public boolean doesAccept(Node acceptNode)
{
if (acceptNode instanceof LocalAsgnNode)
{
return (((LocalAsgnNode) acceptNode).getName().equals(localVarName));
}
return false;
}
}));
// If so, return the sum of the RHSes' type inferences
if ((localAssignsIntoNode != null) && (localAssignsIntoNode.size() > 0))
{
for (Node asgnNode : localAssignsIntoNode)
{
possibleTypes.addAll(inferNodeType(((AssignableNode) asgnNode).getValueNode()));
}
return possibleTypes;
}
// No prior assigns; if is an arg, find send-exprs into that arg, return sum of their inferences
if (helper.isArgumentInMethod(localVarName, enclosingScopeNode))
{
// Rename for clarity
Node enclosingMethodNode = enclosingScopeNode;
sysout("Is arg in method");
// Get enclosing type name
Node enclosingTypeNode = findEnclosingTypeNode(node);
String enclosingTypeName = "Kernel";
if (enclosingTypeNode != rootNode)
{
enclosingTypeName = helper.getTypeNodeName(enclosingTypeNode);
}
// Get enclosing method name
String enclosingMethodName = helper.getMethodDefinitionNodeName(enclosingMethodNode);
sysout("Inferring type of argument " + localVarName + " in method " + enclosingMethodName);
// Find index of param
ListNode argsListNode = helper.getArgsListNode(enclosingMethodNode);
int paramIndex = helper.getArgIndex(argsListNode, localVarName);
// Find all send-exprs to the enclosing method
List<Node> sendExprs = findAllSendersOfMethod(enclosingTypeName, enclosingMethodName);
sysout("Found " + sendExprs.size() + " senders: ");
// Find all arg-exprs in the send-exprs that flow into the local var
List<Node> argExprs = new ArrayList<Node>(sendExprs.size());
for (Node sendExpr : sendExprs)
{
prettyPrint(sendExpr);
argExprs.add(helper.findNthArgExprInSendExpr(paramIndex, sendExpr));
}
sysout("Inflowing argexprs:" + argExprs.size());
// Sum the inferred type of each arg-exprs that flows into the local var
for (Node argExpr : argExprs)
{
prettyPrint(argExpr);
possibleTypes.addAll(inferNodeType(argExpr));
}
return possibleTypes;
}
sysout("bottom");
// No prior assigns and is not an arg; return empty set of guesses.
return possibleTypes;
}
private List<ITypeGuess> getDVarReferenceNodeTypes(DVarNode node)
{
List<ITypeGuess> possibleTypes = new ArrayList<ITypeGuess>(1);
Node enclosingScopeNode = findEnclosingMethodNode(node);
if (enclosingScopeNode == rootNode)
{
enclosingScopeNode = findEnclosingTypeNode(node);
}
// Find assignments into this variable
final String varName = node.getName();
List<Node> dynAsgnNodes = ScopedNodeLocator.Instance().findNodesInScope(enclosingScopeNode, new INodeAcceptor()
{
public boolean doesAccept(Node acceptNode)
{
if (acceptNode instanceof DAsgnNode)
{
return (((DAsgnNode) acceptNode).getName().equals(varName));
}
return false;
}
});
// Sum the inferred type of assignment RHSes
if (dynAsgnNodes != null)
{
for (Node dynAsgnNode : dynAsgnNodes)
{
possibleTypes.addAll(inferNodeType(((DAsgnNode) dynAsgnNode).getValueNode()));
}
}
return possibleTypes;
}
private List<ITypeGuess> getInstanceVarReferenceNodeTypes(InstVarNode node)
{
List<ITypeGuess> possibleTypes = new ArrayList<ITypeGuess>(1);
Node enclosingTypeNode = findEnclosingTypeNode(node);
// Find assignments into this variable
final String instanceVarName = helper.getVarName(node);
List<Node> instAsgnNodes = ScopedNodeLocator.Instance().findNodesInScope(enclosingTypeNode, new INodeAcceptor()
{
public boolean doesAccept(Node acceptNode)
{
if (acceptNode instanceof InstAsgnNode)
{
return (((InstAsgnNode) acceptNode).getName().equals(instanceVarName));
}
return false;
}
});
// TODO: also collect calls to [parentype].instvarname=
// Sum the inferred type of assignment RHSes
if (instAsgnNodes != null)
{
for (Node instAsgnNode : instAsgnNodes)
{
possibleTypes.addAll(inferNodeType(((InstAsgnNode) instAsgnNode).getValueNode()));
}
}
return possibleTypes;
}
private List<ITypeGuess> getClassVarReferenceNodeTypes(ClassVarNode node)
{
List<ITypeGuess> possibleTypes = new ArrayList<ITypeGuess>(1);
Node enclosingTypeNode = findEnclosingTypeNode(node);
// Find assignments into this variable
final String classVarName = helper.getVarName(node);
prettyPrint(enclosingTypeNode);
List<Node> classAsgnNodes = ScopedNodeLocator.Instance().findNodesInScope(enclosingTypeNode,
new INodeAcceptor()
{
public boolean doesAccept(Node acceptNode)
{
if (acceptNode instanceof ClassVarAsgnNode)
{
return (((ClassVarAsgnNode) acceptNode).getName().equals(classVarName));
}
else if (acceptNode instanceof ClassVarDeclNode)
{
return (((ClassVarDeclNode) acceptNode).getName().equals(classVarName));
}
return false;
}
});
// TODO: class Klass;@@x=5;@@x=6;@@x;end # @@x=5 is parsed as a ClassDeclNode, and so is @@x=6. Do
// ClassAsgnNodes ever pop up???
// TODO: also collect calls to [parentypeklass].classvarname=
// Sum the inferred type of assignment RHSes
if (classAsgnNodes != null)
{
sysout("asgns not null: " + classAsgnNodes.size());
for (Node classAsgnNode : classAsgnNodes)
{
if (classAsgnNode instanceof ClassVarAsgnNode)
{
possibleTypes.addAll(inferNodeType(((ClassVarAsgnNode) classAsgnNode).getValueNode()));
}
if (classAsgnNode instanceof ClassVarDeclNode)
{
possibleTypes.addAll(inferNodeType(((ClassVarDeclNode) classAsgnNode).getValueNode()));
}
}
}
return possibleTypes;
}
private List<ITypeGuess> getGlobalVarReferenceNodeTypes(GlobalVarNode node)
{
List<ITypeGuess> possibleTypes = new ArrayList<ITypeGuess>(1);
// Find assignments into this variable
final String globalVarName = helper.getVarName(node);
List<Node> globalAsgnNodes = ScopedNodeLocator.Instance().findNodesInScope(rootNode, new INodeAcceptor()
{
public boolean doesAccept(Node acceptNode)
{
if (acceptNode instanceof GlobalAsgnNode)
{
return (((GlobalAsgnNode) acceptNode).getName().equals(globalVarName));
}
return false;
}
});
// Sum the inferred type of assignment RHSes
for (Node globalAsgnNode : globalAsgnNodes)
{
possibleTypes.addAll(inferNodeType(((GlobalAsgnNode) globalAsgnNode).getValueNode()));
}
return possibleTypes;
}
private boolean isAssignmentNode(Node node)
{
return (node instanceof LocalAsgnNode) || (node instanceof InstAsgnNode) || (node instanceof GlobalAsgnNode);
}
private Node getAssignmentNodeValueNode(Node node)
{
if (node instanceof InstAsgnNode)
{
return ((InstAsgnNode) node).getValueNode();
}
if (node instanceof LocalAsgnNode)
{
return ((LocalAsgnNode) node).getValueNode();
}
if (node instanceof GlobalAsgnNode)
{
return ((GlobalAsgnNode) node).getValueNode();
}
return null;
}
private boolean isCallNode(Node node)
{
return (node instanceof CallNode) || (node instanceof FCallNode) || (node instanceof VCallNode);
}
private List<ITypeGuess> getCallNodeTypes(Node node)
{
String methodName = helper.getCallNodeMethodName(node);
// Handle class instantiations separately
if (methodName.equals("new"))
{
return getInstantiationCallNodeTypes(node);
}
List<ITypeGuess> possibleTypes = new LinkedList<ITypeGuess>();
// heuristic for common method which should return known types, i.e to_s, to_i
possibleTypes.addAll(TypicalMethodReturnNames.get(methodName));
List<String> receiverTypes = new ArrayList<String>();
if (node instanceof CallNode)
{
List<ITypeGuess> receiverTypeInferences = inferNodeType(((CallNode) node).getReceiverNode());
if (receiverTypeInferences != null && receiverTypeInferences.size() > 0)
{
for (ITypeGuess typeGuess : receiverTypeInferences)
{
if (typeGuess == null)
continue;
receiverTypes.add(typeGuess.getType());
}
}
}
else if ((node instanceof FCallNode) || (node instanceof VCallNode))
{
String receiverTypeName = helper.getTypeNodeName(findEnclosingTypeNode(node));
if (receiverTypeName == null)
{
receiverTypeName = "Kernel";
}
receiverTypes.add(receiverTypeName);
}
List<Node> defnNodes = new ArrayList<Node>();
for (String receiverType : receiverTypes)
{
// TODO: Find method defnnode, sum types of its retval-exprs
List<Node> result = findAllMethodDefinitions(receiverType, methodName);
defnNodes.addAll(result);
sysout("Receiver type name: " + receiverType);
sysout(" " + result.size() + " defnnodes found");
}
// For each send-expr, collect all retval-exprs
List<Node> retvalExprs = new LinkedList<Node>();
for (Node defnNode : defnNodes)
{
retvalExprs.addAll(findRetvalExprs(defnNode));
}
// Sum possible types for all retval-exprs
for (Node retvalExpr : retvalExprs)
{
possibleTypes.addAll(inferNodeType(retvalExpr));
}
return possibleTypes;
}
private List<ITypeGuess> getInstantiationCallNodeTypes(Node node)
{
List<ITypeGuess> possibleTypes = new ArrayList<ITypeGuess>(1);
if (node instanceof CallNode)
{
Node receiverNode = ((CallNode) node).getReceiverNode();
return inferNodeType(receiverNode);
}
if (node instanceof FCallNode)
{
Node enclosingTypeNode = findEnclosingTypeNode(node);
possibleTypes.add(getTypeDefinitionNodeType(enclosingTypeNode));
}
if (node instanceof VCallNode)
{
Node enclosingTypeNode = findEnclosingTypeNode(node);
possibleTypes.add(getTypeDefinitionNodeType(enclosingTypeNode));
}
return possibleTypes;
}
}