package org.rubypeople.rdt.internal.codeassist; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.jruby.ast.AliasNode; import org.jruby.ast.ArgumentNode; 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.ConstDeclNode; 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.InstAsgnNode; import org.jruby.ast.InstVarNode; import org.jruby.ast.LocalAsgnNode; import org.jruby.ast.LocalVarNode; import org.jruby.ast.ModuleNode; import org.jruby.ast.Node; import org.jruby.ast.RootNode; import org.jruby.ast.StrNode; import org.jruby.ast.VCallNode; import org.jruby.ast.types.INameNode; import org.rubypeople.rdt.core.ILoadpathEntry; import org.rubypeople.rdt.core.IMethod; import org.rubypeople.rdt.core.IParent; import org.rubypeople.rdt.core.IRubyElement; import org.rubypeople.rdt.core.IRubyScript; import org.rubypeople.rdt.core.ISourceFolder; import org.rubypeople.rdt.core.ISourceFolderRoot; import org.rubypeople.rdt.core.IType; import org.rubypeople.rdt.core.ITypeHierarchy; import org.rubypeople.rdt.core.RubyCore; import org.rubypeople.rdt.core.RubyModelException; import org.rubypeople.rdt.core.codeassist.CodeResolver; import org.rubypeople.rdt.core.codeassist.ResolveContext; 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.RubyScript; import org.rubypeople.rdt.internal.core.search.BasicSearchEngine; import org.rubypeople.rdt.internal.core.util.ASTUtil; import org.rubypeople.rdt.internal.core.util.Util; import org.rubypeople.rdt.internal.ti.ITypeGuess; import org.rubypeople.rdt.internal.ti.ITypeInferrer; 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; public class RubyCodeResolver extends CodeResolver { private static final String OBJECT = "Object"; private static final String NEW = "new"; private static final String DEFAULT_FILE_EXTENSION = ".rb"; private static final String LOAD = "load"; private static final String REQUIRE = "require"; private static final String INITIALIZE = "initialize"; private HashSet<IType> fVisitedTypes; @Override public void select(ResolveContext context) throws RubyModelException { Node selected = OffsetNodeLocator.Instance().getNodeAtOffset(context.getAST(), context.getStartOffset()); if (selected instanceof StrNode) { // Go to file in a 'require' or 'load' call resolveString(context, selected); return; } if (selected instanceof AliasNode) { resolveAlias(context, selected); return; } if (selected instanceof Colon2Node) { resolveColon2Node(context, selected); return; } if (selected instanceof DVarNode) { resolveDynamicVar(context, selected); return; } if (selected instanceof ConstNode) { resolveConstant(context, selected); return; } if (isLocalVarRef(selected)) { resolveLocalVar(context, selected); return; } if (isInstanceVarRef(selected)) { resolveInstanceVar(context, selected); return; } if (isClassVarRef(selected)) { resolveClassVarRef(context, selected); return; } if (isDeclaration(selected)) { resolveDeclaration(context); return; } if (isMethodCall(selected)) { resolveMethodCall(context, selected); return; } } private boolean isDeclaration(Node selected) { return (selected instanceof DefnNode) || (selected instanceof DefsNode) || (selected instanceof ConstDeclNode) || (selected instanceof ClassNode) || (selected instanceof ModuleNode) || (selected instanceof ClassVarDeclNode); } protected void resolveString(ResolveContext context, Node selected) throws RubyModelException { StrNode string = (StrNode) selected; FCallNode fcall = (FCallNode) ClosestSpanningNodeLocator.Instance().findClosestSpanner(context.getAST(), string.getPosition().getStartOffset(), new INodeAcceptor() { public boolean doesAccept(Node node) { return node instanceof FCallNode; } }); if (fcall == null) return; IRubyScript script = context.getScript(); if (fcall.getName().equals(REQUIRE) || fcall.getName().equals(LOAD)) { String value = string.getValue().toString(); if (!value.endsWith(DEFAULT_FILE_EXTENSION)) { value += DEFAULT_FILE_EXTENSION; } ILoadpathEntry[] entries = script.getRubyProject().getResolvedLoadpath(true); for (int i = 0; i < entries.length; i++) { IPath path = entries[i].getPath().append(value); if (path.toFile().exists()) { // If it's in the workspace, it's relatively easy... IFile file = null; if (path.isAbsolute()) { file = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation(path); } else { file = ResourcesPlugin.getWorkspace().getRoot().getFile(path); } if (file != null) { putResolved(context, new IRubyElement[] { RubyCore.create(file) }); return; } // ...Otherwise we need to deal with opening an external ruby script by traversing the model of // the project ISourceFolderRoot sfRoot = script.getRubyProject().getSourceFolderRoot( entries[i].getPath().toPortableString()); String[] parts = value.split("[\\|/]"); String[] minusFileName; if (parts.length == 1) { minusFileName = new String[0]; } else { minusFileName = new String[parts.length - 1]; System.arraycopy(parts, 0, minusFileName, 0, minusFileName.length); } ISourceFolder folder = sfRoot.getSourceFolder(minusFileName); putResolved(context, new IRubyElement[] { folder.getRubyScript(path.lastSegment()) }); return; } } } } protected void putResolved(ResolveContext context, IRubyElement[] resolved) { if (resolved != null && resolved.length > 0) context.putResolved(resolved); } protected void resolveMethodCall(ResolveContext context, Node selected) throws RubyModelException { String methodName = getName(selected); if (methodName.equals(NEW)) // Special case where new resolves to initialize methodName = INITIALIZE; Set<IRubyElement> possible = new HashSet<IRubyElement>(); IType[] types = getReceiver(context, selected); for (int i = 0; i < types.length; i++) { IType type = types[i]; if (fVisitedTypes == null) { fVisitedTypes = new HashSet<IType>(); } // keep track of types so we don't get into infinite loop Collection<IMethod> methods = suggestMethods(type); fVisitedTypes.clear(); for (IMethod method : methods) { if (method.getElementName().equals(methodName)) possible.add(method); } } if (possible.isEmpty()) { // If trying to resolve new/initialize and we have one type, just resolve to type decl! Set<String> uniqueTypeNames = uniqueTypeNames(types); if (uniqueTypeNames.size() == 1) { if (methodName.equals(INITIALIZE)) { putResolved(context, types); return; } else { // limit search to just this type! try { List<SearchMatch> results = search(IRubyElement.METHOD, uniqueTypeNames.iterator().next() + "." + methodName, IRubySearchConstants.DECLARATIONS, SearchPattern.R_EXACT_MATCH); for (SearchMatch match : results) { IRubyElement element = (IRubyElement) match.getElement(); possible.add(element); } } catch (CoreException e) { RubyCore.log(e); } } } else { // do a global search for method declarations matching this name try { List<SearchMatch> results = search(IRubyElement.METHOD, methodName, IRubySearchConstants.DECLARATIONS, SearchPattern.R_EXACT_MATCH); for (SearchMatch match : results) { IRubyElement element = (IRubyElement) match.getElement(); possible.add(element); } } catch (CoreException e) { RubyCore.log(e); } } } putResolved(context, possible.toArray(new IRubyElement[possible.size()])); } protected void resolveInstanceVar(ResolveContext context, Node selected) throws RubyModelException { List<IRubyElement> possible = getChildrenWithName(context.getScript().getChildren(), IRubyElement.INSTANCE_VAR, getName(selected)); putResolved(context, possible.toArray(new IRubyElement[possible.size()])); } protected void resolveLocalVar(ResolveContext context, Node selected) throws RubyModelException { IRubyScript script = context.getScript(); IRubyElement spanner = script.getElementAt(selected.getPosition().getStartOffset()); List<IRubyElement> possible = new ArrayList<IRubyElement>(); if (spanner instanceof IParent) { IParent parent = (IParent) spanner; possible = getChildrenWithName(parent.getChildren(), IRubyElement.LOCAL_VARIABLE, getName(selected)); } if (possible.isEmpty()) { possible = getChildrenWithName(script.getChildren(), IRubyElement.LOCAL_VARIABLE, getName(selected)); } putResolved(context, possible.toArray(new IRubyElement[possible.size()])); } protected void resolveConstant(ResolveContext context, Node selected) throws RubyModelException { ConstNode constNode = (ConstNode) selected; String name = constNode.getName(); IRubyScript script = context.getScript(); // Try to find a matching constant in this script // TODO Use convention of all caps versus camelcase to decided which to search for first? try { // Search script for constant IRubySearchScope scope = SearchEngine.createRubySearchScope(new IRubyElement[] { script }); List<SearchMatch> matches = search(scope, IRubyElement.CONSTANT, name, IRubySearchConstants.DECLARATIONS, SearchPattern.R_EXACT_MATCH); if (matches.isEmpty()) { // none in script, expand search to project scope = SearchEngine.createRubySearchScope(new IRubyElement[] { script.getRubyProject() }); matches = search(scope, IRubyElement.CONSTANT, name, IRubySearchConstants.DECLARATIONS, SearchPattern.R_EXACT_MATCH); } for (SearchMatch match : matches) { IRubyElement element = (IRubyElement) match.getElement(); if (element != null) { putResolved(context, new IRubyElement[] { element }); return; } } } catch (CoreException e) { RubyCore.log(e); } // Now search for a type in this script try { IRubySearchScope scope = SearchEngine.createRubySearchScope(new IRubyElement[] { script }); List<SearchMatch> matches = search(scope, IRubyElement.TYPE, name, IRubySearchConstants.DECLARATIONS, SearchPattern.R_EXACT_MATCH); for (SearchMatch match : matches) { IRubyElement element = (IRubyElement) match.getElement(); if (element != null) { putResolved(context, new IRubyElement[] { element }); return; } } } catch (CoreException e) { RubyCore.log(e); } RubyElementRequestor completer = new RubyElementRequestor(script); String fullyQualifiedName = getFullyQualifiedName(context.getAST(), constNode.getPosition().getStartOffset(), name); if (fullyQualifiedName != null) { IType[] types = completer.findType(fullyQualifiedName); if (types != null && types.length > 0) { putResolved(context, types); return; } } putResolved(context, completer.findType(name)); } protected void resolveDynamicVar(ResolveContext context, Node selected) throws RubyModelException { final String name = ((DVarNode) selected).getName(); Node assignment = FirstPrecursorNodeLocator.Instance().findFirstPrecursor(context.getAST(), context.getStartOffset(), new INodeAcceptor() { public boolean doesAccept(Node node) { return (node instanceof DAsgnNode) && ((DAsgnNode) node).getName().equals(name); } }); putResolved(context, new IRubyElement[] { context.getScript().getElementAt( assignment.getPosition().getStartOffset()) }); } protected void resolveClassVarRef(ResolveContext context, Node selected) throws RubyModelException { List<IRubyElement> possible = getChildrenWithName(context.getScript().getChildren(), IRubyElement.CLASS_VAR, getName(selected)); putResolved(context, possible.toArray(new IRubyElement[possible.size()])); } protected void resolveDeclaration(ResolveContext context) throws RubyModelException { IRubyElement element = ((RubyScript) context.getScript()).getElementAt(context.getStartOffset()); if (element != null) putResolved(context, new IRubyElement[] { element }); } protected void resolveColon2Node(ResolveContext context, Node selected) { String simpleName = ((Colon2Node) selected).getName(); String fullyQualifiedName = ASTUtil.getFullyQualifiedName((Colon2Node) selected); IRubyScript script = context.getScript(); IRubyElement element = findChild(simpleName, IRubyElement.TYPE, script); if (element != null && Util.parentsMatch((IType) element, fullyQualifiedName)) { putResolved(context, new IRubyElement[] { element }); return; } RubyElementRequestor completer = new RubyElementRequestor(script); putResolved(context, completer.findType(fullyQualifiedName)); } protected void resolveAlias(ResolveContext context, Node selected) { // figure out if we're pointing at new name or old name. AliasNode aliasNode = (AliasNode) selected; int startOffset = aliasNode.getPosition().getStartOffset(); int diff = context.getStartOffset() - startOffset; if (diff < (6 + aliasNode.getNewName().length() + 1)) return; // if we're not over the old name, don't resolve this to anything! FIXME Resolve it to the new // method! String methodName = aliasNode.getOldName(); // FIXME Only search within the current class/module scope! // do a global search for method declarations matching this name List<IRubyElement> possible = new ArrayList<IRubyElement>(); try { List<SearchMatch> results = search(IRubyElement.METHOD, methodName, IRubySearchConstants.DECLARATIONS, SearchPattern.R_EXACT_MATCH); for (SearchMatch match : results) { IRubyElement element = (IRubyElement) match.getElement(); possible.add(element); } } catch (CoreException e) { RubyCore.log(e); } putResolved(context, possible.toArray(new IRubyElement[possible.size()])); return; } private Set<String> uniqueTypeNames(IType[] types) { Set<String> names = new HashSet<String>(); if (types == null) return names; for (IType type : types) { names.add(type.getFullyQualifiedName()); } return names; } private String getFullyQualifiedName(Node root, int offset, String name) { String namespace = ASTUtil.getNamespace(root, offset); if (namespace == null || namespace.trim().length() == 0) { return name; } return namespace + "::" + name; } protected List<SearchMatch> search(int type, String patternString, int limitTo, int matchRule) throws CoreException { return search(SearchEngine.createWorkspaceScope(), type, patternString, limitTo, matchRule); } protected List<SearchMatch> search(IRubySearchScope scope, int type, String patternString, int limitTo, int matchRule) throws CoreException { SearchEngine engine = new SearchEngine(); SearchPattern pattern = SearchPattern.createPattern(type, patternString, limitTo, matchRule); SearchParticipant[] participants = new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() }; CollectingSearchRequestor requestor = new CollectingSearchRequestor(); engine.search(pattern, participants, scope, requestor, null); return requestor.getResults(); } private IType[] getReceiver(ResolveContext context, Node selected) throws RubyModelException { List<IType> types = new ArrayList<IType>(); if ((selected instanceof FCallNode) || (selected instanceof VCallNode)) { types = resolveImplicitReceiver(context, selected); } else { int start = context.getStartOffset(); if (selected instanceof CallNode) { // The problem here is that we want to infer the type of the receiver, not the method (which would // give us its return types). So we need to grab the offset of the receiver and infer on that node CallNode call = (CallNode) selected; Node receiver = call.getReceiverNode(); start = receiver.getPosition().getStartOffset(); } IRubyScript script = context.getScript(); ITypeInferrer inferrer = RubyCore.getTypeInferrer(); Collection<ITypeGuess> guesses = new ArrayList<ITypeGuess>(); try { guesses = inferrer.infer(script.getSource(), start); } catch (RubyModelException e1) { RubyCore.log(e1); } // TODO If guesses are empty, just do a global search for this method? if (guesses.isEmpty()) { String methodName = ASTUtil.getNameReflectively(selected); IRubySearchScope scope = SearchEngine.createRubySearchScope(new IRubyElement[] { script .getRubyProject() }); CollectingSearchRequestor requestor = new CollectingSearchRequestor(); SearchPattern pattern = SearchPattern.createPattern(IRubyElement.METHOD, methodName, IRubySearchConstants.DECLARATIONS, SearchPattern.R_EXACT_MATCH); SearchParticipant[] participants = { BasicSearchEngine.getDefaultSearchParticipant() }; try { new BasicSearchEngine().search(pattern, participants, scope, requestor, null); } catch (CoreException e) { RubyCore.log(e); } List<SearchMatch> matches = requestor.getResults(); if (matches == null || matches.isEmpty()) return new IType[0]; for (SearchMatch match : matches) { IMethod method = (IMethod) match.getElement(); types.add(method.getDeclaringType()); } } else { RubyElementRequestor requestor = new RubyElementRequestor(script); for (ITypeGuess guess : guesses) { String name = guess.getType(); IType[] tmpTypes = requestor.findType(name); for (int i = 0; i < tmpTypes.length; i++) { types.add(tmpTypes[i]); } } } } return types.toArray(new IType[types.size()]); } protected List<IType> resolveImplicitReceiver(ResolveContext context, Node selected) throws RubyModelException { IRubyScript script = context.getScript(); RootNode root = context.getAST(); int start = context.getStartOffset(); List<IType> types = new ArrayList<IType>(); Node receiver = ClosestSpanningNodeLocator.Instance().findClosestSpanner(root, start, new INodeAcceptor() { public boolean doesAccept(Node node) { return (node instanceof ClassNode || node instanceof ModuleNode); } }); IRubySearchScope scope = SearchEngine.createRubySearchScope(new IRubyElement[] { script }); String typeName = ASTUtil.getNameReflectively(receiver); if (typeName == null) typeName = OBJECT; try { List<SearchMatch> matches = search(scope, IRubyElement.TYPE, typeName, IRubySearchConstants.DECLARATIONS, SearchPattern.R_EXACT_MATCH); if (matches == null || matches.isEmpty()) return Collections.emptyList(); // TODO Check up the type hierarchy! for (SearchMatch match : matches) { types.add((IType) match.getElement()); } } catch (CoreException e) { RubyCore.log(e); } return types; } // FIXME Just create the type heirarchy, the add all the types into a search scope and search for an exact match to // the method name! private Collection<IMethod> suggestMethods(IType type) throws RubyModelException { if (type == null) return Collections.emptyList(); if (fVisitedTypes == null) fVisitedTypes = new HashSet<IType>(); // FIXME We want to avoid visiting the same types across the guesses too! List<IMethod> proposals = new ArrayList<IMethod>(); ITypeHierarchy hierarchy = type.newSupertypeHierarchy(null); IType[] all = new IType[] { type }; if (hierarchy != null) { all = hierarchy.getAllSupertypes(type); // Apparently getAllTypes is returning types that are in files chedk // that are related to type hierarchy, but aren't supertypes of // focus! So I had to switch to getAllSupertypes(focus); } for (int j = 0; j < all.length; j++) { IType currentType = all[j]; if (fVisitedTypes.contains(currentType)) continue; fVisitedTypes.add(currentType); IMethod[] methods = currentType.getMethods(); if (methods != null) { for (int k = 0; k < methods.length; k++) { if (methods[k] == null) continue; proposals.add(methods[k]); } } } fVisitedTypes.clear(); return proposals; } private IRubyElement findChild(String name, int type, IParent parent) { try { IRubyElement[] children = parent.getChildren(); for (int j = 0; j < children.length; j++) { IRubyElement child = children[j]; if (child.getElementName().equals(name) && child.isType(type)) return child; if (child instanceof IParent) { IRubyElement found = findChild(name, type, (IParent) child); if (found != null) return found; } } } catch (RubyModelException e) { RubyCore.log(e); } return null; } private boolean isMethodCall(Node selected) { return (selected instanceof VCallNode) || (selected instanceof FCallNode) || (selected instanceof CallNode); } private List<IRubyElement> getChildrenWithName(IRubyElement[] children, int type, String name) throws RubyModelException { List<IRubyElement> possible = new ArrayList<IRubyElement>(); for (int i = 0; i < children.length; i++) { IRubyElement child = children[i]; if (child.getElementType() == type) { if (child.getElementName().equals(name)) possible.add(child); } if (child instanceof IParent) { possible.addAll(getChildrenWithName(((IParent) child).getChildren(), type, name)); } } return possible; } private String getName(Node node) { if (node instanceof INameNode) { return ((INameNode) node).getName(); } if (node instanceof ClassVarNode) { return ((ClassVarNode) node).getName(); } return ""; } private boolean isInstanceVarRef(Node node) { return ((node instanceof InstAsgnNode) || (node instanceof InstVarNode)); } private boolean isClassVarRef(Node node) { return ((node instanceof ClassVarAsgnNode) || (node instanceof ClassVarNode)); } private boolean isLocalVarRef(Node node) { return ((node instanceof LocalAsgnNode) || (node instanceof ArgumentNode) || (node instanceof LocalVarNode)); } }