/**
* Aptana Studio
* Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the GNU Public License (GPL) v3 (with exceptions).
* Please see the license.html included with this distribution for details.
* Any modifications to this file must keep this entire header intact.
*/
package com.aptana.ruby.internal.core.inference;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.jrubyparser.CompatVersion;
import org.jrubyparser.Parser;
import org.jrubyparser.ast.AssignableNode;
import org.jrubyparser.ast.CallNode;
import org.jrubyparser.ast.ClassVarNode;
import org.jrubyparser.ast.Colon2Node;
import org.jrubyparser.ast.ConstDeclNode;
import org.jrubyparser.ast.ConstNode;
import org.jrubyparser.ast.DefnNode;
import org.jrubyparser.ast.INameNode;
import org.jrubyparser.ast.InstVarNode;
import org.jrubyparser.ast.LocalAsgnNode;
import org.jrubyparser.ast.LocalVarNode;
import org.jrubyparser.ast.Node;
import org.jrubyparser.ast.NodeType;
import org.jrubyparser.ast.ReturnNode;
import org.jrubyparser.lexer.SyntaxException;
import org.jrubyparser.parser.ParserConfiguration;
import com.aptana.core.logging.IdeLog;
import com.aptana.core.util.StringUtil;
import com.aptana.index.core.Index;
import com.aptana.index.core.QueryResult;
import com.aptana.index.core.SearchPattern;
import com.aptana.ruby.core.IRubyConstants;
import com.aptana.ruby.core.RubyCorePlugin;
import com.aptana.ruby.core.ast.ASTUtils;
import com.aptana.ruby.core.ast.ClosestSpanningNodeLocator;
import com.aptana.ruby.core.ast.FirstPrecursorNodeLocator;
import com.aptana.ruby.core.ast.INodeAcceptor;
import com.aptana.ruby.core.ast.NamespaceVisitor;
import com.aptana.ruby.core.ast.OffsetNodeLocator;
import com.aptana.ruby.core.ast.ScopedNodeLocator;
import com.aptana.ruby.core.index.IRubyIndexConstants;
import com.aptana.ruby.core.index.RubyIndexUtil;
import com.aptana.ruby.core.inference.ITypeGuess;
import com.aptana.ruby.core.inference.ITypeInferrer;
@SuppressWarnings("nls")
public class TypeInferrer implements ITypeInferrer
{
/**
* Hard-coded mapping from common method names to their possible return types.
*/
private static final Map<String, Collection<ITypeGuess>> TYPICAL_METHOD_RETURN_TYPE_NAMES = new HashMap<String, Collection<ITypeGuess>>();
static
{
// TODO Read this in from some config file/property file rather than hardcode it!
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("capitalize", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("capitalize!", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("ceil", createSet(IRubyConstants.FIXNUM));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("center", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("chomp", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("chomp!", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("chop", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("chop!", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("concat", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("count", createSet(IRubyConstants.FIXNUM));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("crypt", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("downcase", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("downcase!", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("dump", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("floor", createSet(IRubyConstants.FIXNUM));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("gets", createSet(IRubyConstants.STRING, IRubyConstants.NIL_CLASS));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("gsub", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("gsub!", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("hash", createSet(IRubyConstants.FIXNUM));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("index", createSet(IRubyConstants.FIXNUM));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("inspect", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("intern", createSet(IRubyConstants.SYMBOL));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("length", createSet(IRubyConstants.FIXNUM));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("now", createSet(IRubyConstants.TIME));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("round", createSet(IRubyConstants.FIXNUM));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("size", createSet(IRubyConstants.FIXNUM));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put(
"slice",
createSet(IRubyConstants.STRING, IRubyConstants.ARRAY, IRubyConstants.NIL_CLASS, IRubyConstants.OBJECT,
IRubyConstants.FIXNUM));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put(
"slice!",
createSet(IRubyConstants.STRING, IRubyConstants.ARRAY, IRubyConstants.NIL_CLASS, IRubyConstants.OBJECT,
IRubyConstants.FIXNUM));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("strip", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("strip!", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("sub", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("sub!", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("swapcase", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("swapcase!", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("to_a", createSet(IRubyConstants.ARRAY));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("to_ary", createSet(IRubyConstants.ARRAY));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("to_i", createSet(IRubyConstants.FIXNUM));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("to_int", createSet(IRubyConstants.FIXNUM));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("to_f", createSet(IRubyConstants.FLOAT));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("to_proc", createSet(IRubyConstants.PROC));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("to_s", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("to_str", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("to_string", createSet(IRubyConstants.STRING));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("to_sym", createSet(IRubyConstants.SYMBOL));
TYPICAL_METHOD_RETURN_TYPE_NAMES.put("unpack", createSet(IRubyConstants.ARRAY));
}
private IProject project;
public TypeInferrer(IProject project)
{
this.project = project;
}
private static Set<ITypeGuess> createSet(String... strings)
{
if (strings == null || strings.length == 0)
{
return Collections.emptySet();
}
// TODO Allow for un-equal weighting of types!
int weight = 100 / strings.length;
Set<ITypeGuess> set = new HashSet<ITypeGuess>();
for (String string : strings)
{
set.add(new BasicTypeGuess(string, weight, true));
}
return set;
}
private Collection<ITypeGuess> createSet(Map<String, Boolean> types)
{
if (types == null || types.isEmpty())
{
return Collections.emptySet();
}
int weight = 100 / types.size();
Set<ITypeGuess> set = new HashSet<ITypeGuess>();
for (Map.Entry<String, Boolean> entry : types.entrySet())
{
set.add(new BasicTypeGuess(entry.getKey(), weight, entry.getValue()));
}
return set;
}
public Collection<ITypeGuess> infer(String source, int offset)
{
Parser parser = new Parser();
Reader reader = new BufferedReader(new StringReader(source));
Node root = null;
try
{
root = parser.parse(StringUtil.EMPTY, reader, new ParserConfiguration(0, CompatVersion.BOTH));
}
finally
{
try
{
reader.close();
}
catch (IOException e)
{
// ignore
}
}
if (root == null)
{
return Collections.emptyList();
}
Node atOffset = new OffsetNodeLocator().find(root, offset);
if (atOffset == null)
{
return Collections.emptyList();
}
return infer(root, atOffset);
}
public Collection<ITypeGuess> infer(Node rootNode, Node toInfer)
{
if (toInfer == null)
{
return createSet(IRubyConstants.OBJECT);
}
switch (toInfer.getNodeType())
{
case CONSTNODE:
return inferConstant(rootNode, (ConstNode) toInfer);
case CALLNODE:
case FCALLNODE:
case VCALLNODE:
return inferMethod(rootNode, (INameNode) toInfer);
case SYMBOLNODE:
case DSYMBOLNODE:
return createSet(IRubyConstants.SYMBOL);
case ARRAYNODE:
case ZARRAYNODE:
return createSet(IRubyConstants.ARRAY);
case BIGNUMNODE:
return createSet(IRubyConstants.BIGNUM);
case FIXNUMNODE:
return createSet(IRubyConstants.FIXNUM);
case FLOATNODE:
return createSet(IRubyConstants.FLOAT);
case HASHNODE:
return createSet(IRubyConstants.HASH);
case DREGEXPNODE:
case REGEXPNODE:
return createSet(IRubyConstants.REGEXP);
case TRUENODE:
return createSet(IRubyConstants.TRUE_CLASS);
case FALSENODE:
return createSet(IRubyConstants.FALSE_CLASS);
case NILNODE:
return createSet(IRubyConstants.NIL_CLASS);
case DSTRNODE:
case DXSTRNODE:
case STRNODE:
case XSTRNODE:
return createSet(IRubyConstants.STRING);
case LOCALVARNODE:
return inferLocal(rootNode, (LocalVarNode) toInfer);
case INSTVARNODE:
return inferInstance(rootNode, (InstVarNode) toInfer);
case CLASSVARNODE:
return inferClassVar(rootNode, (ClassVarNode) toInfer);
case COLON2NODE:
return inferColon2Node((Colon2Node) toInfer);
case CLASSVARASGNNODE:
case CLASSVARDECLNODE:
case CONSTDECLNODE:
case DASGNNODE:
case GLOBALASGNNODE:
case INSTASGNNODE:
case LOCALASGNNODE:
case MULTIPLEASGN19NODE:
case MULTIPLEASGNNODE:
AssignableNode assignable = (AssignableNode) toInfer;
return infer(rootNode, assignable.getValueNode());
default:
break;
}
return createSet(IRubyConstants.OBJECT);
}
private Collection<ITypeGuess> inferConstant(Node rootNode, ConstNode toInfer)
{
NamespaceVisitor visitor = new NamespaceVisitor();
String implicitNamespace = visitor.getNamespace(rootNode, toInfer.getPosition().getStartOffset());
String constantName = toInfer.getName();
// First search for types and constants in the implicit namespace
// if no match, then look for them in toplevel
Map<String, Boolean> types = matchingTypes(implicitNamespace + IRubyConstants.NAMESPACE_DELIMETER
+ constantName);
if (types.isEmpty())
{
// TODO If no matching types, search constants and then infer any matches!
// types = inferMatchingConstants(implicitNamespace + IRubyConstants.NAMESPACE_DELIMETER +
// constantName);
// no matching types or constants, try without implicit namespace
if (implicitNamespace.length() > 0)
{
types = matchingTypes(constantName);
// TODO If no matching types, search constants!
// if (types.isEmpty())
// {
// types = inferMatchingConstants(constantName);
// }
}
}
if (types.isEmpty())
{
// Fell all the way through, fall back and just assume constant text is a type
// FIXME We're assuming this is a class and not a module, may want to do some verification?
return createSet(constantName);
}
return createSet(types);
}
/**
* Returns a map from the type name to a boolean indicating if it's a class (true) or Module (false).
*
* @param fullyQualifiedName
* @return
*/
private Map<String, Boolean> matchingTypes(String fullyQualifiedName)
{
Map<String, Boolean> matches = new HashMap<String, Boolean>();
if (fullyQualifiedName.startsWith(IRubyConstants.NAMESPACE_DELIMETER))
{
fullyQualifiedName = fullyQualifiedName.substring(2);
}
String typeName = fullyQualifiedName;
String namespace = StringUtil.EMPTY;
int lastNS = typeName.lastIndexOf(IRubyConstants.NAMESPACE_DELIMETER);
if (lastNS != -1)
{
namespace = typeName.substring(0, lastNS);
typeName = typeName.substring(lastNS + 2);
}
// Build query key
StringBuilder builder = new StringBuilder();
builder.append('^'); // start matching at beginning of key
builder.append(typeName);
builder.append(IRubyIndexConstants.SEPARATOR);
builder.append(namespace);
builder.append(IRubyIndexConstants.SEPARATOR);
builder.append(".+$");
String key = builder.toString();
for (Index index : getAllIndicesForProject())
{
if (index == null)
{
continue;
}
List<QueryResult> results = index.query(new String[] { IRubyIndexConstants.TYPE_DECL }, key,
SearchPattern.REGEX_MATCH | SearchPattern.CASE_SENSITIVE);
if (results == null)
{
continue;
}
for (QueryResult result : results)
{
String word = result.getWord();
String[] parts = word.split(Character.toString(IRubyIndexConstants.SEPARATOR));
StringBuilder fullName = new StringBuilder();
if (parts[1].length() > 0)
{
fullName.append(parts[1]);
fullName.append(IRubyConstants.NAMESPACE_DELIMETER);
}
fullName.append(parts[0]);
boolean isClass = parts[2].equals(IRubyIndexConstants.CLASS_SUFFIX);
matches.put(fullName.toString(), isClass);
}
}
return matches;
}
private Collection<ITypeGuess> inferInstance(Node rootNode, InstVarNode toInfer)
{
return inferClassOrInstanceVar(rootNode, toInfer, NodeType.INSTASGNNODE);
}
private Collection<ITypeGuess> inferClassVar(Node rootNode, ClassVarNode toInfer)
{
return inferClassOrInstanceVar(rootNode, toInfer, NodeType.CLASSVARASGNNODE, NodeType.CLASSVARDECLNODE);
}
private Collection<ITypeGuess> inferClassOrInstanceVar(Node rootNode, final INameNode varRefNode,
final NodeType... nodeTypes)
{
final String varName = varRefNode.getName();
Node enclosingTypeNode = enclosingType(rootNode, ((Node) varRefNode).getPosition().getStartOffset());
if (enclosingTypeNode == null)
{
enclosingTypeNode = rootNode;
}
List<Node> assigns = new ScopedNodeLocator().find(enclosingTypeNode, new INodeAcceptor()
{
public boolean accepts(Node node)
{
boolean found = false;
for (NodeType type : nodeTypes)
{
if (node.getNodeType() == type)
{
found = true;
break;
}
}
if (!found)
{
return false;
}
return ((INameNode) node).getName().equals(varName);
}
});
if (assigns == null)
{
return createSet(IRubyConstants.OBJECT);
}
Collection<ITypeGuess> guesses = new ArrayList<ITypeGuess>();
for (Node assignment : assigns)
{
AssignableNode assignmentNode = (AssignableNode) assignment;
guesses.addAll(infer(rootNode, assignmentNode.getValueNode()));
}
return guesses;
}
private Node enclosingType(Node rootNode, int startOffset)
{
return new ClosestSpanningNodeLocator().find(rootNode, startOffset, new INodeAcceptor()
{
public boolean accepts(Node node)
{
return node.getNodeType() == NodeType.CLASSNODE || node.getNodeType() == NodeType.MODULENODE;
}
});
}
private Collection<ITypeGuess> inferColon2Node(Colon2Node toInfer)
{
// Break name into constant name, type base name, type namespace. Ugh!
String fullName = ASTUtils.getFullyQualifiedName(toInfer);
String namespace = StringUtil.EMPTY;
String typeName = StringUtil.EMPTY;
String constantName = fullName;
int namespaceIndex = fullName.lastIndexOf(IRubyConstants.NAMESPACE_DELIMETER);
if (namespaceIndex != -1)
{
typeName = fullName.substring(0, namespaceIndex);
constantName = fullName.substring(namespaceIndex + 2);
namespaceIndex = typeName.lastIndexOf(IRubyConstants.NAMESPACE_DELIMETER);
if (namespaceIndex != -1)
{
namespace = typeName.substring(0, namespaceIndex);
typeName = typeName.substring(namespaceIndex + 2);
}
}
// TODO Check the indices to see if this is a constant or a type! If constant, we need to infer that constant
// decl!
final String key = constantName + IRubyIndexConstants.SEPARATOR + typeName + IRubyIndexConstants.SEPARATOR
+ namespace;
String matchingDocURI = null;
for (Index index : getAllIndicesForProject())
{
if (index == null)
{
continue;
}
List<QueryResult> results = index.query(new String[] { IRubyIndexConstants.CONSTANT_DECL }, key,
SearchPattern.EXACT_MATCH | SearchPattern.CASE_SENSITIVE);
if (results == null || results.isEmpty())
{
continue;
}
for (QueryResult result : results)
{
// Found a match! Exit early, don't keep searching indices...
matchingDocURI = result.getDocuments().iterator().next();
break;
}
if (matchingDocURI != null)
{
break;
}
}
if (matchingDocURI != null)
{
Reader reader = null;
try
{
// TODO Move parsing code into one method, and try to use the parser pool
IFileStore store = EFS.getStore(URI.create(matchingDocURI));
InputStream stream = store.openInputStream(EFS.NONE, new NullProgressMonitor());
reader = new BufferedReader(new InputStreamReader(stream));
Parser parser = new Parser();
Node root = parser.parse(StringUtil.EMPTY, reader, new ParserConfiguration(0, CompatVersion.BOTH));
if (root == null)
{
return Collections.emptyList();
}
final String theConstantName = constantName;
List<Node> decls = new ScopedNodeLocator().find(root, new INodeAcceptor()
{
public boolean accepts(Node node)
{
if (!(node instanceof ConstDeclNode))
{
return false;
}
ConstDeclNode declNode = (ConstDeclNode) node;
return declNode.getName().equals(theConstantName);
}
});
if (decls == null || decls.isEmpty())
{
return Collections.emptyList();
}
return infer(root, decls.iterator().next());
}
catch (SyntaxException e) // $codepro.audit.disable emptyCatchClause
{
// ignore if syntax is busted.
}
catch (CoreException e)
{
IdeLog.log(RubyCorePlugin.getDefault(), e.getStatus());
}
finally
{
if (reader != null)
{
try
{
reader.close();
}
catch (IOException e) // $codepro.audit.disable emptyCatchClause
{
// ignore
}
}
}
}
// FIXME We're assuming this is a class, and not a module. May want to look up in indices and verify!
// It appears to be a type and not a constant, so just return the actual text as the resulting Type inferred
return createSet(fullName);
}
protected Collection<Index> getAllIndicesForProject()
{
return RubyIndexUtil.allIndices(project);
}
private Collection<ITypeGuess> inferLocal(Node rootNode, LocalVarNode toInfer)
{
final String varName = toInfer.getName();
Node precedingAssignment = new FirstPrecursorNodeLocator().find(rootNode, toInfer.getPosition()
.getStartOffset() - 1, new INodeAcceptor()
{
public boolean accepts(Node node)
{
return node.getNodeType() == NodeType.LOCALASGNNODE && ((INameNode) node).getName().equals(varName);
}
});
if (precedingAssignment != null)
{
LocalAsgnNode assign = (LocalAsgnNode) precedingAssignment;
return infer(rootNode, assign.getValueNode());
}
return createSet(IRubyConstants.OBJECT);
}
private Collection<ITypeGuess> inferMethod(Node rootNode, INameNode toInfer)
{
final String methodName = toInfer.getName();
if (methodName.endsWith("?"))
{
return createSet(IRubyConstants.TRUE_CLASS, IRubyConstants.FALSE_CLASS);
}
Collection<ITypeGuess> guesses = TYPICAL_METHOD_RETURN_TYPE_NAMES.get(methodName);
if (guesses == null)
{
if (toInfer instanceof CallNode)
{
if ("new".equals(methodName))
{
Node receiver = ((CallNode) toInfer).getReceiverNode();
return infer(rootNode, receiver);
}
// else
// {
// FIXME We need to gather the return type of the method if receiver is a method
// }
}
else
{
guesses = new ArrayList<ITypeGuess>();
Node enclosingType = enclosingType(rootNode, ((Node) toInfer).getPosition().getStartOffset());
List<Node> methods = new ScopedNodeLocator().find(enclosingType, new INodeAcceptor()
{
public boolean accepts(Node node)
{
return NodeType.DEFNNODE == node.getNodeType()
&& methodName.equals(((DefnNode) node).getName());
}
});
if (!methods.isEmpty())
{
for (Node methodNode : methods)
{
List<Node> returnNodes = new ScopedNodeLocator().find(methodNode, new INodeAcceptor()
{
public boolean accepts(Node node)
{
return NodeType.RETURNNODE == node.getNodeType();
}
});
if (!returnNodes.isEmpty())
{
for (Node returnNode : returnNodes)
{
ReturnNode blah = (ReturnNode) returnNode;
guesses.addAll(infer(rootNode, blah.getValueNode()));
}
}
// # Get method body as a BlockNode, grab last child, that's the implicit return.
// implicit_return = last_statement(methods.first.body_node)
// if implicit_return
// case implicit_return.node_type
// when org.jrubyparser.ast.NodeType::IFNODE
// types << infer(last_statement(implicit_return.then_body)) if implicit_return.then_body
// types << infer(last_statement(implicit_return.else_body)) if implicit_return.else_body
// when org.jrubyparser.ast.NodeType::CASENODE
// implicit_return.cases.child_nodes.each do |c|
// types << infer(last_statement(c.body_node)) if c
// end
// types << infer(last_statement(implicit_return.else_node)) if implicit_return.else_node
}
}
}
return Collections.emptySet();
}
return guesses;
}
}