/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. * * Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * Contributor(s): * * The Original Software is NetBeans. The Initial Developer of the Original * Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun * Microsystems, Inc. All Rights Reserved. * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. */ package org.netbeans.modules.ruby; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import org.jrubyparser.ast.AliasNode; import org.jrubyparser.ast.ArgsNode; import org.jrubyparser.ast.ArgumentNode; import org.jrubyparser.ast.BlockArgNode; import org.jrubyparser.ast.CallNode; import org.jrubyparser.ast.ClassNode; import org.jrubyparser.ast.ClassVarDeclNode; import org.jrubyparser.ast.ClassVarNode; import org.jrubyparser.ast.Colon2Node; import org.jrubyparser.ast.ConstNode; import org.jrubyparser.ast.DAsgnNode; import org.jrubyparser.ast.DVarNode; import org.jrubyparser.ast.FCallNode; import org.jrubyparser.ast.GlobalAsgnNode; import org.jrubyparser.ast.GlobalVarNode; import org.jrubyparser.ast.HashNode; import org.jrubyparser.ast.InstAsgnNode; import org.jrubyparser.ast.InstVarNode; import org.jrubyparser.ast.ListNode; import org.jrubyparser.ast.LocalAsgnNode; import org.jrubyparser.ast.LocalVarNode; import org.jrubyparser.ast.MethodDefNode; import org.jrubyparser.ast.Node; import org.jrubyparser.ast.StrNode; import org.jrubyparser.ast.SymbolNode; import org.jrubyparser.ast.VCallNode; import org.jrubyparser.ast.INameNode; import org.jrubyparser.ast.NodeType; import org.jrubyparser.ast.SuperNode; import org.jrubyparser.ast.ZSuperNode; import org.netbeans.api.lexer.Token; import org.netbeans.api.lexer.TokenHierarchy; import org.netbeans.api.lexer.TokenId; import org.netbeans.api.lexer.TokenSequence; import org.netbeans.editor.BaseDocument; import org.netbeans.editor.Utilities; import org.netbeans.modules.csl.api.DeclarationFinder; import org.netbeans.modules.csl.api.DeclarationFinder.AlternativeLocation; import org.netbeans.modules.csl.api.DeclarationFinder.DeclarationLocation; import org.netbeans.modules.csl.api.OffsetRange; import org.netbeans.modules.csl.spi.ParserResult; import org.netbeans.modules.parsing.spi.Parser; import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport; import org.netbeans.modules.ruby.elements.IndexedClass; import org.netbeans.modules.ruby.elements.IndexedElement; import org.netbeans.modules.ruby.elements.IndexedField; import org.netbeans.modules.ruby.elements.IndexedMethod; import org.netbeans.modules.ruby.lexer.LexUtilities; import org.netbeans.modules.ruby.lexer.Call; import org.netbeans.modules.ruby.lexer.RubyCommentTokenId; import org.netbeans.modules.ruby.lexer.RubyTokenId; import org.openide.filesystems.FileObject; import org.openide.util.Exceptions; /** * Find a declaration from an element in the JRuby AST. * * @todo Look at the target to see which method to choose. For example, if * you do Foo.new, I should locate "initialize" in Foo, not somewhere else. * @todo Don't include inexact matches like alias nodes when searching first; * only if a search for actual declaration nodes fail should I revert to looking * for aliases! * @todo If you're looking for a local class, such as a Rails model, I should * find those first! * @todo Within a gem, prefer other matches within the same gem or gem cluster * @todo Prefer files named after the class! (e.g. SchemaStatements in schema_statements.rb) * * @author Tor Norbye */ public class RubyDeclarationFinder extends RubyDeclarationFinderHelper implements DeclarationFinder { /** An increasing number; I will be using this number modulo the */ private static int methodSelector = 0; /** When true, don't match alias nodes as reads. Used during traversal of the AST. */ private boolean ignoreAlias; private RubyIndex rubyIndex; private static final String PARTIAL = "partial"; //NOI18N private static final String CONTROLLER = "controller"; //NOI18N private static final String ACTION = "action";//NOI18N private static final String TEMPLATE = "template";//NOI18N private static final String[] RAILS_TARGET_RAW_NAMES = new String[] {PARTIAL, CONTROLLER, ACTION, TEMPLATE}; private static final List<String> RAILS_TARGETS = initRailsTargets(); private static List<String> initRailsTargets() { List<String> result = new ArrayList<String>(RAILS_TARGET_RAW_NAMES.length * 4); for (String target : RAILS_TARGET_RAW_NAMES) { result.add(":" + target + " => "); result.add(":" + target + "=> "); result.add(":" + target + " =>"); result.add(":" + target + "=>"); } return result; } /** Creates a new instance of RubyDeclarationFinder */ public RubyDeclarationFinder() { } private RubyIndex getIndex(ParserResult result) { if (rubyIndex == null) { rubyIndex = RubyIndex.get(result); } return rubyIndex; } public OffsetRange getReferenceSpan(Document document, int lexOffset) { TokenHierarchy<Document> th = TokenHierarchy.get(document); BaseDocument doc = (BaseDocument)document; FileObject fo = RubyUtils.getFileObject(document); if (RubyUtils.isRhtmlDocument(doc) || (fo != null && RubyUtils.isRailsProject(fo))) { RailsTarget target = findRailsTarget(doc, th, lexOffset); if (target != null) { return target.range; } } TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(th, lexOffset); if (ts == null) { return OffsetRange.NONE; } ts.move(lexOffset); if (!ts.moveNext() && !ts.movePrevious()) { return OffsetRange.NONE; } // Determine whether the caret position is right between two tokens boolean isBetween = (lexOffset == ts.offset()); OffsetRange range = getReferenceSpan(ts, th, lexOffset); if ((range == OffsetRange.NONE) && isBetween) { // The caret is between two tokens, and the token on the right // wasn't linkable. Try on the left instead. if (ts.movePrevious()) { range = getReferenceSpan(ts, th, lexOffset); } } return range; } private OffsetRange getReferenceSpan(TokenSequence<?> ts, TokenHierarchy<Document> th, int lexOffset) { Token<?> token = ts.token(); TokenId id = token.id(); if (id == RubyTokenId.IDENTIFIER) { if (token.length() == 1 && id == RubyTokenId.IDENTIFIER && token.text().toString().equals(",")) { return OffsetRange.NONE; } } // TODO: Tokens.THIS, Tokens.SELF ... if ((id == RubyTokenId.IDENTIFIER) || (id == RubyTokenId.CLASS_VAR) || (id == RubyTokenId.GLOBAL_VAR) || (id == RubyTokenId.CONSTANT) || (id == RubyTokenId.TYPE_SYMBOL) || (id == RubyTokenId.INSTANCE_VAR) || (id == RubyTokenId.SUPER)) { return new OffsetRange(ts.offset(), ts.offset() + token.length()); } // Look for embedded RDoc comments: TokenSequence<?> embedded = ts.embedded(); if (embedded != null) { ts = embedded; embedded.move(lexOffset); if (embedded.moveNext()) { Token<?> embeddedToken = embedded.token(); if (embeddedToken.id() == RubyCommentTokenId.COMMENT_LINK) { return new OffsetRange(embedded.offset(), embedded.offset() + embeddedToken.length()); } // Recurse into the range - perhaps there is Ruby code (identifiers // etc.) to follow there OffsetRange range = getReferenceSpan(embedded, th, lexOffset); if (range != OffsetRange.NONE) { return range; } } } // Allow hyperlinking of some literal strings too, such as require strings if ((id == RubyTokenId.QUOTED_STRING_LITERAL) || (id == RubyTokenId.STRING_LITERAL)) { int requireStart = LexUtilities.getRequireStringOffset(lexOffset, th); if (requireStart != -1) { String require = LexUtilities.getStringAt(lexOffset, th); if (require != null) { return new OffsetRange(requireStart, requireStart + require.length()); } } int classNameStart = LexUtilities.getClassNameStringOffset(lexOffset, th); if (classNameStart != -1) { String className = LexUtilities.getStringAt(lexOffset, th); if (className != null) { return new OffsetRange(classNameStart, classNameStart + className.length()); } } } return OffsetRange.NONE; } public DeclarationLocation findDeclaration(ParserResult parserResult, int lexOffset) { // Is this a require-statement? If so, jump to the required file try { Document document = RubyUtils.getDocument(parserResult, true); if (document == null) { return DeclarationLocation.NONE; } TokenHierarchy<Document> th = TokenHierarchy.get(document); BaseDocument doc = (BaseDocument)document; int astOffset = AstUtilities.getAstOffset(parserResult, lexOffset); if (astOffset == -1) { return DeclarationLocation.NONE; } boolean view = RubyUtils.isRhtmlFile(RubyUtils.getFileObject(parserResult)); if (view || RubyUtils.isRailsProject(RubyUtils.getFileObject(parserResult))) { DeclarationLocation loc = findRailsFile(parserResult, doc, th, lexOffset, astOffset, view); if (loc != DeclarationLocation.NONE) { return loc; } } OffsetRange range = getReferenceSpan(doc, lexOffset); if (range == OffsetRange.NONE) { return DeclarationLocation.NONE; } // Determine the bias (if the caret is between two tokens, did we // click on a link for the left or the right? boolean leftSide = range.getEnd() <= lexOffset; Node root = AstUtilities.getRoot(parserResult); RubyIndex index = getIndex(parserResult); if (root == null) { // No parse tree - try to just use the syntax info to do a simple index lookup // for methods and classes String text = doc.getText(range.getStart(), range.getLength()); if ((index == null) || (text.length() == 0)) { return DeclarationLocation.NONE; } if (Character.isUpperCase(text.charAt(0))) { // A Class or Constant? Set<IndexedClass> classes = index.getClasses(text, QuerySupport.Kind.EXACT, true, false, false); if (classes.size() == 0) { return DeclarationLocation.NONE; } RubyClassDeclarationFinder cdf = new RubyClassDeclarationFinder(null, null, null, index, null); DeclarationLocation l = cdf.getElementDeclaration(classes, null); if (l != null) { return l; } } else { // A method? Set<IndexedMethod> methods = index.getMethods(text, (String) null, QuerySupport.Kind.EXACT); if (methods.size() == 0) { methods = index.getMethods(text, QuerySupport.Kind.EXACT); } DeclarationLocation l = getMethodDeclaration(parserResult, text, methods, null, null, index, astOffset, lexOffset); if (l != null) { return l; } } // TODO: @ - field? return DeclarationLocation.NONE; } int tokenOffset = lexOffset; if (leftSide && (tokenOffset > 0)) { tokenOffset--; } // See if the hyperlink is for the string in a require statement int requireStart = LexUtilities.getRequireStringOffset(tokenOffset, th); if (requireStart != -1) { String require = LexUtilities.getStringAt(tokenOffset, th); if (require != null) { FileObject fo = index.getRequiredFile(require); if (fo != null) { return new DeclarationLocation(fo, 0); } } // It's in a require string so no possible other matches return DeclarationLocation.NONE; } AstPath path = new AstPath(root, astOffset); Node closest = path.leaf(); if (closest == null) { return DeclarationLocation.NONE; } // See if the hyperlink is over a method reference in an rdoc comment DeclarationLocation rdoc = findRDocMethod(parserResult, doc, astOffset, lexOffset, root, path, closest, index); if (rdoc != DeclarationLocation.NONE) { return fix(rdoc, parserResult); } // Look at the parse tree; find the closest node and jump based on the context if (closest instanceof LocalVarNode || closest instanceof LocalAsgnNode) { // A local variable read or a parameter read, or an assignment to one of these String name = ((INameNode)closest).getName(); Node method = AstUtilities.findLocalScope(closest, path); return fix(findLocal(parserResult, method, name), parserResult); } else if (closest instanceof DVarNode) { // A dynamic variable read or assignment String name = ((DVarNode)closest).getName(); // Does not implement INameNode Node block = AstUtilities.findDynamicScope(closest, path); return fix(findDynamic(parserResult, block, name), parserResult); } else if (closest instanceof DAsgnNode) { // A dynamic variable read or assignment String name = ((INameNode)closest).getName(); Node block = AstUtilities.findDynamicScope(closest, path); return fix(findDynamic(parserResult, block, name), parserResult); } else if (closest instanceof InstVarNode) { // A field variable read String name = ((INameNode)closest).getName(); return findInstanceFromIndex(parserResult, name, path, index, false); } else if (closest instanceof ClassVarNode) { // A class variable read String name = ((INameNode)closest).getName(); return findInstanceFromIndex(parserResult, name, path, index, false); } else if (closest instanceof GlobalVarNode) { // A global variable read String name = ((GlobalVarNode)closest).getName(); // GlobalVarNode does not implement INameNode return fix(findGlobal(parserResult, root, name), parserResult); } else if (closest instanceof FCallNode || closest instanceof VCallNode || closest instanceof CallNode) { // A method call String name = ((INameNode)closest).getName(); Call call = Call.getCallType(doc, th, lexOffset); RubyType type = call.getType(); String lhs = call.getLhs(); if (!type.isKnown() && lhs != null && closest != null && call.isSimpleIdentifier()) { Node method = AstUtilities.findLocalScope(closest, path); if (method != null) { // TODO - if the lhs is "foo.bar." I need to split this // up and do it a bit more cleverly ContextKnowledge knowledge = new ContextKnowledge( index, root, method, astOffset, lexOffset, parserResult); RubyTypeInferencer inferencer = RubyTypeInferencer.create(knowledge); type = inferencer.inferType(lhs); } } // Constructors: "new" ends up calling "initialize". // Actually, it's more complicated than this: a method CAN override new // in which case I should show it, but that is discouraged and people // SHOULD override initialize, which is what the default new method will // call for initialization. if (!type.isKnown()) { // search locally if (name.equals("new")) { // NOI18N name = "initialize"; // NOI18N } Arity arity = Arity.getCallArity(closest); DeclarationLocation loc = fix(findMethod(parserResult, root, name, arity), parserResult); if (loc != DeclarationLocation.NONE) { return loc; } } String fqn = AstUtilities.getFqnName(path); if (call == Call.LOCAL && fqn != null && fqn.length() == 0) { fqn = "Object"; } return findMethod(name, fqn, type, call, parserResult, astOffset, lexOffset, path, closest, index); } else if (closest instanceof ConstNode || closest instanceof Colon2Node) { // try Class usage RubyClassDeclarationFinder classDF = new RubyClassDeclarationFinder(parserResult, root, path, index, closest); DeclarationLocation decl = classDF.findClassDeclaration(); if (decl != DeclarationLocation.NONE) { return decl; } // try Constant usage RubyConstantDeclarationFinder constantDF = new RubyConstantDeclarationFinder(parserResult, root, path, index, closest); return constantDF.findConstantDeclaration(); } else if (closest instanceof SymbolNode) { String name = ((SymbolNode)closest).getName(); // Search for methods, fields, etc. Arity arity = Arity.UNKNOWN; DeclarationLocation location = findMethod(parserResult, root, name, arity); // search for AR associations if (location == DeclarationLocation.NONE) { location = new ActiveRecordAssociationFinder(index, (SymbolNode) closest, root, path).findAssociationLocation(); } // search for helpers if (location == DeclarationLocation.NONE) { location = new HelpersFinder(index, (SymbolNode) closest, root, path).findHelperLocation(); } if (location == DeclarationLocation.NONE) { location = findInstance(parserResult, root, name, index); } if (location == DeclarationLocation.NONE) { location = findClassVar(parserResult, root, name); } if (location == DeclarationLocation.NONE) { location = findGlobal(parserResult, root, name); } if (location == DeclarationLocation.NONE) { RubyClassDeclarationFinder cdf = new RubyClassDeclarationFinder(); Node clz = cdf.findClass(root, ((INameNode)closest).getName(), ignoreAlias); if (clz != null) { location = getLocation(parserResult, clz); } } // methods if (location == DeclarationLocation.NONE) { location = findInstanceMethodsFromIndex(parserResult, name, path, index); } // fields if (location == DeclarationLocation.NONE) { location = findInstanceFromIndex(parserResult, name, path, index, true); } return fix(location, parserResult); } else if (closest instanceof AliasNode) { AliasNode an = (AliasNode)closest; // TODO - determine if the click is over the new name or the old name String newName = AstUtilities.getNameOrValue(an.getNewName()); if (newName == null) { return DeclarationLocation.NONE; } // XXX I don't know where the old and new names are since the user COULD // have used more than one whitespace character for separation. For now I'll // just have to assume it's the normal case with one space: alias new old. // I -could- use the getPosition.getEndOffset() to see if this looks like it's // the case (e.g. node length != "alias ".length + old.length+new.length+1). // In this case I could go peeking in the source buffer to see where the // spaces are - between alias and the first word or between old and new. XXX. int newLength = newName.length(); int aliasPos = an.getPosition().getStartOffset(); if (astOffset > aliasPos+6) { // 6: "alias ".length() if (astOffset > (aliasPos + 6 + newLength)) { // It's over the old word: this counts as a usage. // The problem is that we don't know if it's a local, a dynamic, an instance // variable, etc. (The $ and @ parts are not included in the alias statement). // First see if it's a local variable. String name = AstUtilities.getNameOrValue(an.getOldName()); if (name == null) { return DeclarationLocation.NONE; } ignoreAlias = true; try { DeclarationLocation location = findLocal(parserResult, AstUtilities.findLocalScope(closest, path), name); if (location == DeclarationLocation.NONE) { location = findDynamic(parserResult, AstUtilities.findDynamicScope(closest, path), name); } if (location == DeclarationLocation.NONE) { location = findMethod(parserResult, root, name, Arity.UNKNOWN); } if (location == DeclarationLocation.NONE) { location = findInstance(parserResult, root, name, index); } if (location == DeclarationLocation.NONE) { location = findClassVar(parserResult, root, name); } if (location == DeclarationLocation.NONE) { location = findGlobal(parserResult, root, name); } if (location == DeclarationLocation.NONE) { RubyClassDeclarationFinder cdf = new RubyClassDeclarationFinder(); Node clz = cdf.findClass(root, name, ignoreAlias); if (clz != null) { location = getLocation(parserResult, clz); } } // TODO - what if we're aliasing another alias? I think that should show up in the various // other nodes if (location == DeclarationLocation.NONE) { return location; } else { return fix(location, parserResult); } } finally { ignoreAlias = false; } } else { // It's over the new word: this counts as a declaration. Nothing to do here except // maybe jump right back to the beginning. return new DeclarationLocation(RubyUtils.getFileObject(parserResult), aliasPos + 4); } } } else if (closest instanceof ArgumentNode) { // A method name (if under a DefnNode or DefsNode) or a parameter (if indirectly under an ArgsNode) String name = ((ArgumentNode)closest).getName(); // ArgumentNode doesn't implement INameNode Node parent = path.leafParent(); if (parent != null) { if (parent instanceof MethodDefNode) { // It's a method name return DeclarationLocation.NONE; } else { // Parameter (check to see if its under ArgumentNode) Node method = AstUtilities.findLocalScope(closest, path); return fix(findLocal(parserResult, method, name), parserResult); } } } else if (closest instanceof StrNode) { // See if the hyperlink is for the string that is the value for :class_name => int classNameStart = LexUtilities.getClassNameStringOffset(astOffset, th); if (classNameStart != -1) { String className = LexUtilities.getStringAt(tokenOffset, th); if (className != null) { return getLocation(index.getClasses(className, QuerySupport.Kind.EXACT, true, false, false)); } } } else if (closest instanceof SuperNode || closest instanceof ZSuperNode) { Node scope = AstUtilities.findLocalScope(closest, path); String fqn = AstUtilities.getFqnName(path); switch (scope.getNodeType()) { case SCLASSNODE: case MODULENODE: case CLASSNODE: { IndexedClass superClass = index.getSuperclass(fqn); if (superClass != null) { return getLocation(Collections.singleton(superClass)); } break; } case DEFNNODE: case DEFSNODE: { MethodDefNode methodDef = AstUtilities.findMethod(path); IndexedMethod superMethod = index.getSuperMethod(fqn, methodDef.getName(), true); if (superMethod != null) { return getLocation(Collections.singleton(superMethod)); } break; } } } } catch (BadLocationException ble) { // do nothing - see #154991 } return DeclarationLocation.NONE; } /** * Compute the declaration location for a test string (such as MosModule::TestBaz/test_qux). * * @param fileInProject a file in the project where to perform the search * @param testString a string represening a test class and method, such as TestFoo/test_bar * @param classLocation if true, returns the location of the class rather then the method. */ public static DeclarationLocation getTestDeclaration(FileObject fileInProject, String testString, boolean classLocation) { return getTestDeclaration(fileInProject, testString, classLocation, true); } public static DeclarationLocation getTestDeclaration(FileObject fileInProject, String testString, boolean classLocation, boolean requireDeclaredClass) { int methodIndex = testString.indexOf('/'); //NOI18N if (methodIndex == -1) { return DeclarationLocation.NONE; } RubyIndex index = RubyIndex.get(QuerySupport.findRoots(fileInProject, Collections.singleton(RubyLanguage.SOURCE), Collections.singleton(RubyLanguage.BOOT), Collections.<String>emptySet())); if (index == null) { return DeclarationLocation.NONE; } String className = testString.substring(0, methodIndex); String methodName = testString.substring(methodIndex+1); Set<IndexedMethod> methods = index.getMethods(methodName, className, QuerySupport.Kind.EXACT); DeclarationLocation methodLocation = getLocation(methods); if (!classLocation) { if (DeclarationLocation.NONE == methodLocation && !requireDeclaredClass) { // the test method is not defined in the class methodLocation = getLocation(index.getMethods(methodName, QuerySupport.Kind.EXACT)); } return methodLocation; } Set<IndexedClass> classes = index.getClasses(className, QuerySupport.Kind.EXACT, false, false, true, null); DeclarationLocation classDeclarationLocation = getLocation(classes); if (DeclarationLocation.NONE == methodLocation && classLocation) { return classDeclarationLocation; } if (methodLocation.getFileObject().equals(classDeclarationLocation.getFileObject())) { return classDeclarationLocation; } for (AlternativeLocation alt : classDeclarationLocation.getAlternativeLocations()) { if (methodLocation.getFileObject().equals(alt.getLocation().getFileObject())) { return alt.getLocation(); } } return classDeclarationLocation; } static DeclarationLocation getLocation(Set<? extends IndexedElement> elements) { DeclarationLocation loc = DeclarationLocation.NONE; for (IndexedElement element : elements) { FileObject fo = element.getFileObject(); if (fo == null) { continue; } if (loc == DeclarationLocation.NONE) { int offset = -1; Node node = AstUtilities.getForeignNode(element); if (node != null) { offset = AstUtilities.getRange(node).getStart(); } loc = new DeclarationLocation(fo, offset, element); loc.addAlternative(new RubyAltLocation(element, false)); } else { AlternativeLocation alternate = new RubyAltLocation(element, false); loc.addAlternative(alternate); } } return loc; } private DeclarationLocation findRailsFile(ParserResult info, BaseDocument doc, TokenHierarchy<Document> th, int lexOffset, int astOffset, boolean fromView) { RailsTarget target = findRailsTarget(doc, th, lexOffset); if (target != null) { String type = target.type; if (type.indexOf(PARTIAL) != -1 || type.indexOf(TEMPLATE) != -1) { // NOI18N boolean template = type.indexOf(TEMPLATE) != -1; FileObject dir; String name; int slashIndex = target.name.lastIndexOf('/'); if (slashIndex != -1) { FileObject app = RubyUtils.getAppDir(RubyUtils.getFileObject(info)); if (app == null) { return DeclarationLocation.NONE; } String relativePath = target.name.substring(0, slashIndex); dir = app.getFileObject("views/" + relativePath); // NOI18N if (dir == null) { return DeclarationLocation.NONE; } name = target.name.substring(slashIndex+1); // NOI18N } else { dir = RubyUtils.getFileObject(info).getParent(); name = target.name; // NOI18N } if (!template) { name = "_" + name; } DeclarationLocation partialLocation = findPartial(name, dir); if (partialLocation != DeclarationLocation.NONE) { return partialLocation; } } else if (type.indexOf(CONTROLLER) != -1 || type.indexOf(ACTION) != -1) { // NOI18N // Look for the controller file in the corresponding directory FileObject file = RubyUtils.getFileObject(info); file = file.getParent(); //FileObject dir = file.getParent(); String action = null; String fileName = file.getName(); boolean isController = type.indexOf(CONTROLLER) != -1; // NOI18N String path = ""; // NOI18N if (isController) { path = target.name; } else { if (!fileName.startsWith("_")) { // NOI18N // For partials like "_foo", just use the surrounding view path = fileName; action = RubyUtils.getFileObject(info).getName(); } } // The hyperlink has either the controller or the action, but I should // look at the AST to find the other such that the navigation works // better. E.g. if you click on :controller=>'foo', and the statement // also has an :action=>'bar', we not only jump to FooController we go to // the "def bar" in it as well (and vice versa if you click on just :action=>'bar'; // this normally assumes its the controller associated with the RHTML file unless // a different controller is specified int delta = target.range.getStart() - lexOffset; String[] controllerAction = findControllerAction(info, lexOffset+delta, astOffset+delta); if (controllerAction[0] != null) { path = controllerAction[0]; } if (controllerAction[1] != null) { action = controllerAction[1]; } if (!fromView) { // uh, this is getting really messy - hard to add funtionality here // without breaking existing functionality. this an attempt to fix // IZ 172679 w/o affect navigation from views. the class is in // need of serious refactoring. String controllerName = null; if (controllerAction[0] != null) { controllerName = controllerAction[0]; } else if (isController) { controllerName = target.name; } else { controllerName = RubyUtils.getFileObject(info).getName(); } return findActionLocation(asControllerClass(controllerName), action, info); } // Find app dir, and build up a relative path to the view file in the process FileObject app = file.getParent(); while (app != null) { if (app.getName().equals("views") && // NOI18N ((app.getParent() == null) || app.getParent().getName().equals("app"))) { // NOI18N app = app.getParent(); break; } path = app.getNameExt() + "/" + path; // NOI18N app = app.getParent(); } if (app != null) { FileObject controllerFile = app.getFileObject("controllers/" + path + "_controller.rb"); // NOI18N if (controllerFile != null) { int offset = 0; if (action != null) { offset = AstUtilities.findOffset(controllerFile, action); if (offset < 0) { offset = 0; } } return new DeclarationLocation(controllerFile, offset); } } } } return DeclarationLocation.NONE; } private static String asControllerClass(String controllerName) { String suffix = controllerName.endsWith("_controller") ? "" : "_controller";//NOI18N return RubyUtils.underlinedNameToCamel(controllerName + suffix); } private DeclarationLocation findActionLocation(String controllerName, String actionName, ParserResult result) { RubyIndex index = getIndex(result); Set<IndexedMethod> methods = index.getMethods(actionName, controllerName, QuerySupport.Kind.EXACT); return getLocation(methods); } /** * Finds the location of the partial matching the given <code>name</code> in the * given <code>dir</code>. * * @param name * @param dir * @return */ private DeclarationLocation findPartial(String name, FileObject dir) { // Try to find the partial file FileObject partial = dir.getFileObject(name); if (partial != null) { return new DeclarationLocation(partial, 0); } // try extensions for (String ext : RubyUtils.RUBY_VIEW_EXTS) { partial = dir.getFileObject(name + ext); if (partial != null) { return new DeclarationLocation(partial, 0); } } // Handle some other file types for the partials for (FileObject child : dir.getChildren()) { if (child.isValid() && !child.isFolder() && child.getName().equals(name)) { return new DeclarationLocation(child, 0); } } // finally, try matching just the first part of the file name for (FileObject child : dir.getChildren()) { if (child.isValid() && !child.isFolder()) { String fileName = child.getName(); int firstDot = fileName.indexOf('.'); if (firstDot != -1 && name.equals(fileName.substring(0, firstDot))) { return new DeclarationLocation(child, 0); } } } return DeclarationLocation.NONE; } /** Locate the :action and :controller strings in the hash list that is under the * given offsets * @return A string[2] where string[0] is the controller or null, and string[1] is the * action or null */ private String[] findControllerAction(ParserResult info, int lexOffset, int astOffset) { String[] result = new String[2]; Node root = AstUtilities.getRoot(info); if (root == null) { return result; } AstPath path = new AstPath(root, astOffset); Iterator<Node> it = path.leafToRoot(); Node prev = null; while (it.hasNext()) { Node n = it.next(); if (n instanceof HashNode) { if (prev instanceof ListNode) { // uhm... why am I going back to prev? List<Node> hashItems = prev.childNodes(); Iterator<Node> hi = hashItems.iterator(); while (hi.hasNext()) { String from = null; String to = null; Node f = hi.next(); if (f instanceof SymbolNode) { from = ((SymbolNode)f).getName(); } if (hi.hasNext()) { Node t = hi.next(); if (t instanceof StrNode) { to = ((StrNode)t).getValue().toString(); } } if ("controller".equals(from)) { // NOI18N result[0] = to; } else if ("action".equals(from)) { // NOI18N result[1] = to; } } break; } } prev = n; } return result; } /** A result from findRailsTarget which computes sections that have special * hyperlink semantics - like link_to, render :partial, render :action, :controller etc. */ private static class RailsTarget { RailsTarget(String type, String name, OffsetRange range) { this.type = type; this.range = range; this.name = name; } @Override public String toString() { return "RailsTarget(" + type + ", " + name + ", " + range + ")"; } String name; OffsetRange range; String type; } private boolean fastCheckIsRailsTarget(String s) { for (String targetName : RAILS_TARGET_RAW_NAMES) { if (s.indexOf(targetName) != -1) { return true; } } return false; } private RailsTarget findRailsTarget(BaseDocument doc, TokenHierarchy<Document> th, int lexOffset) { try { doc.readLock(); // TODO - limit this to RHTML files only? int begin = Utilities.getRowStart(doc, lexOffset); if (begin != -1) { int end = Utilities.getRowEnd(doc, lexOffset); String s = doc.getText(begin, end-begin); // TODO - limit to a narrower region around the caret? if (!fastCheckIsRailsTarget(s)) { return null; } for (String target : RAILS_TARGETS) { int index = s.indexOf(target); if (index != -1) { // Find string int nameOffset = begin+index+target.length(); TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(th, nameOffset); if (ts == null) { return null; } ts.move(nameOffset); StringBuilder sb = new StringBuilder(); boolean started = false; while (ts.moveNext() && ts.offset() < end) { started = true; Token<?> token = ts.token(); TokenId id = token.id(); if (id == RubyTokenId.STRING_LITERAL || id == RubyTokenId.QUOTED_STRING_LITERAL) { sb.append(token.text().toString()); } if (!"string".equals(id.primaryCategory())) { break; } } if (!started) { return null; } int rangeEnd = ts.offset(); String name = sb.toString(); if (lexOffset <= rangeEnd && lexOffset >= begin+index) { OffsetRange range = new OffsetRange(begin+index, rangeEnd); return new RailsTarget(target, name, range); } } } } } catch (BadLocationException ble) { Exceptions.printStackTrace(ble); } finally { doc.readUnlock(); } return null; } private DeclarationLocation findMethod(String name, String possibleFqn, RubyType type, Call call, ParserResult info, int caretOffset, int lexOffset, AstPath path, Node closest, RubyIndex index) { Set<IndexedMethod> methods = getApplicableMethods(name, possibleFqn, type, call, index); int astOffset = caretOffset; DeclarationLocation l = getMethodDeclaration(info, name, methods, path, closest, index, astOffset, lexOffset); return l; } private Set<IndexedMethod> getApplicableMethods(String name, String possibleFqn, RubyType type, Call call, RubyIndex index) { Set<IndexedMethod> methods = new HashSet<IndexedMethod>(); String fqn = possibleFqn; if (!type.isKnown() && possibleFqn != null && call.getLhs() == null && call != Call.UNKNOWN) { fqn = possibleFqn; // Possibly a class on the left hand side: try searching with the class as a qualifier. // Try with the LHS + current FQN recursively. E.g. if we're in // Test::Unit when there's a call to Foo.x, we'll try // Test::Unit::Foo, and Test::Foo while (methods.size() == 0 && (fqn.length() > 0)) { methods = index.getInheritedMethods(fqn, name, QuerySupport.Kind.EXACT); int f = fqn.lastIndexOf("::"); if (f == -1) { break; } else { fqn = fqn.substring(0, f); } } } if (type.isKnown() && methods.size() == 0) { fqn = possibleFqn; // Possibly a class on the left hand side: try searching with the class as a qualifier. // Try with the LHS + current FQN recursively. E.g. if we're in // Test::Unit when there's a call to Foo.x, we'll try // Test::Unit::Foo, and Test::Foo while (methods.size() == 0 && fqn != null && (fqn.length() > 0)) { for (String realType : type.getRealTypes()) { methods.addAll(index.getInheritedMethods(fqn + "::" + realType, name, QuerySupport.Kind.EXACT)); } int f = fqn.lastIndexOf("::"); if (f == -1) { break; } else { fqn = fqn.substring(0, f); } } if (methods.size() == 0) { // Add methods in the class (without an FQN) for (String realType : type.getRealTypes()) { methods.addAll(index.getInheritedMethods(realType, name, QuerySupport.Kind.EXACT)); } if (methods.size() == 0) { for (String realType : type.getRealTypes()) { assert realType != null : "Should not be null"; if (realType.indexOf("::") == -1) { // Perhaps we specified a class without its FQN, such as "TableDefinition" // -- go and look for the full FQN and add in all the matches from there Set<IndexedClass> classes = index.getClasses(realType, QuerySupport.Kind.EXACT, false, false, false); Set<String> fqns = new HashSet<String>(); for (IndexedClass cls : classes) { String f = cls.getFqn(); if (f != null) { fqns.add(f); } } for (String f : fqns) { if (!f.equals(realType)) { methods.addAll(index.getInheritedMethods(f, name, QuerySupport.Kind.EXACT)); } } } } } } // Fall back to ALL methods across classes // Try looking at the libraries too if (methods.size() == 0) { methods.addAll(index.getMethods(name, QuerySupport.Kind.EXACT)); } } if (methods.size() == 0) { if (!type.isKnown()) { methods.addAll(index.getMethods(name, QuerySupport.Kind.EXACT)); } else { methods.addAll(index.getMethods(name, type.getRealTypes(), QuerySupport.Kind.EXACT)); } if (methods.size() == 0 && type.isKnown()) { methods = index.getMethods(name, QuerySupport.Kind.EXACT); } } return methods; } private DeclarationLocation getMethodDeclaration(ParserResult info, String name, Set<IndexedMethod> methods, AstPath path, Node closest, RubyIndex index, int astOffset, int lexOffset) { BaseDocument doc = RubyUtils.getDocument(info); if (doc == null) { return DeclarationLocation.NONE; } IndexedMethod candidate = findBestMethodMatch(name, methods, doc, astOffset, lexOffset, path, closest, index); if (candidate != null) { FileObject fileObject = candidate.getFileObject(); if (fileObject == null) { return DeclarationLocation.NONE; } Node node = AstUtilities.getForeignNode(candidate); int nodeOffset = 0; if (node != null) { nodeOffset = node.getPosition().getStartOffset(); if (node.getNodeType() == NodeType.ALIASNODE) { nodeOffset += 6; // 6 = lenght of 'alias ' } } DeclarationLocation loc = new DeclarationLocation( fileObject, nodeOffset, candidate); if (!CHOOSE_ONE_DECLARATION && methods.size() > 1) { // Could the :nodoc: alternatives: if there is only one nodoc'ed alternative // don't ask user! int not_nodoced = 0; for (final IndexedMethod mtd : methods) { if (!mtd.isNoDoc()) { not_nodoced++; } } if (not_nodoced >= 2) { for (final IndexedMethod mtd : methods) { loc.addAlternative(new RubyAltLocation(mtd, mtd == candidate)); } } } return loc; } return DeclarationLocation.NONE; } /** Locate the method declaration for the given method call */ public IndexedMethod findMethodDeclaration(Parser.Result parserResult, Node callNode, AstPath path, Set<IndexedMethod>[] alternativesHolder) { int astOffset = AstUtilities.getCallRange(callNode).getStart(); // Is this a require-statement? If so, jump to the required file try { Document doc = RubyUtils.getDocument(parserResult); if (doc == null) { return null; } // Determine the bias (if the caret is between two tokens, did we // click on a link for the left or the right? int lexOffset = LexUtilities.getLexerOffset(parserResult, astOffset); if (lexOffset == -1) { return null; } OffsetRange range = getReferenceSpan(doc, lexOffset); if (range == OffsetRange.NONE) { return null; } boolean leftSide = range.getEnd() <= astOffset; Node root = AstUtilities.getRoot(parserResult); RubyIndex index = RubyIndex.get(parserResult); if (root == null) { // No parse tree - try to just use the syntax info to do a simple index lookup // for methods and classes String text = doc.getText(range.getStart(), range.getLength()); if ((index == null) || (text.length() == 0)) { return null; } if (Character.isUpperCase(text.charAt(0))) { // A Class or Constant? // Not a method call return null; } else { // A method? Set<IndexedMethod> methods = index.getMethods(text, QuerySupport.Kind.EXACT); BaseDocument bdoc = (BaseDocument)doc; IndexedMethod candidate = findBestMethodMatch(text, methods, bdoc, astOffset, lexOffset, null, null, index); return candidate; } // TODO: @ - field? } TokenHierarchy<Document> th = TokenHierarchy.get(doc); int tokenOffset = astOffset; if (leftSide && (tokenOffset > 0)) { tokenOffset--; } // A method call String name = ((INameNode)callNode).getName(); String fqn = AstUtilities.getFqnName(path); if ((fqn == null) || (fqn.length() == 0)) { fqn = "Object"; // NOI18N } Call call = Call.getCallType((BaseDocument)doc, th, lexOffset); boolean skipPrivate = true; boolean done = call.isMethodExpected(); boolean skipInstanceMethods = call.isStatic(); RubyType type = call.getType(); String lhs = call.getLhs(); QuerySupport.Kind kind = QuerySupport.Kind.EXACT; Node node = callNode; if ((!type.isKnown()) && (lhs != null) && (node != null) && call.isSimpleIdentifier()) { Node method = AstUtilities.findLocalScope(node, path); if (method != null) { // TODO - if the lhs is "foo.bar." I need to split this // up and do it a bit more cleverly ContextKnowledge knowledge = new ContextKnowledge( index, root, method, astOffset, lexOffset, AstUtilities.getParseResult(parserResult)); RubyTypeInferencer inferencer = RubyTypeInferencer.create(knowledge); type = inferencer.inferType(lhs); } } // I'm not doing any data flow analysis at this point, so // I can't do anything with a LHS like "foo.". Only actual types. if (type.isKnown()) { if ("self".equals(lhs)) { type = RubyType.create(fqn); skipPrivate = false; } else if ("super".equals(lhs)) { skipPrivate = false; IndexedClass sc = index.getSuperclass(fqn); if (sc != null) { type = RubyType.create(sc.getFqn()); } else { ClassNode cls = AstUtilities.findClass(path); if (cls != null) { type = RubyType.create(AstUtilities.getSuperclass(cls)); } } if (!type.isKnown()) { type = RubyType.OBJECT; // NOI18N } } } if (call == Call.LOCAL && fqn != null && fqn.length() == 0) { fqn = "Object"; } Set<IndexedMethod> methods = getApplicableMethods(name, fqn, type, call, index); if (name.equals("new")) { // NOI18N // Also look for initialize Set<IndexedMethod> initializeMethods = getApplicableMethods("initialize", fqn, type, call, index); methods.addAll(initializeMethods); } IndexedMethod candidate = findBestMethodMatch(name, methods, (BaseDocument)doc, astOffset, lexOffset, path, callNode, index); if (alternativesHolder != null) { alternativesHolder[0] = methods; } return candidate; } catch (BadLocationException ble) { // do nothing - see #154991 } return null; } @SuppressWarnings("empty-statement") private DeclarationLocation findRDocMethod(ParserResult info, Document doc, int astOffset, int lexOffset, Node root, AstPath path, Node closest, RubyIndex index) { TokenHierarchy<Document> th = TokenHierarchy.get(doc); TokenSequence<?> ts = LexUtilities.getRubyTokenSequence((BaseDocument)doc, lexOffset); if (ts == null) { return DeclarationLocation.NONE; } ts.move(lexOffset); if (!ts.moveNext() && !ts.movePrevious()) { return DeclarationLocation.NONE; } Token<?> token = ts.token(); TokenSequence<?> embedded = ts.embedded(); if (embedded != null) { ts = embedded; embedded.move(lexOffset); if (!embedded.moveNext() && !embedded.movePrevious()) { return DeclarationLocation.NONE; } token = embedded.token(); } // Is this a comment? If so, possibly do rdoc-method reference jump if ((token != null) && (token.id() == RubyCommentTokenId.COMMENT_LINK)) { // TODO - use findLinkedMethod String method = token.text().toString(); if (method.startsWith("#")) { method = method.substring(1); DeclarationLocation loc = findMethod(info, root, method, Arity.UNKNOWN); // It looks like "#foo" can refer not just to methods (as rdoc suggested) // but to attributes as well - in Rails' initializer.rb this is used // in a number of places. if (loc == DeclarationLocation.NONE) { loc = findInstance(info, root, "@" + method, index); } return loc; } else { // A URL such as http://netbeans.org - try to open it in a browser! try { URL url = new URL(method); return new DeclarationLocation(url); } catch (MalformedURLException mue) { // URL is from user source... don't complain with exception dialogs etc. ; } } // Probably a Class#method int methodIndex = method.indexOf("#"); if (methodIndex != -1 && methodIndex < method.length()-1) { String clz = method.substring(0, methodIndex); method = method.substring(methodIndex+1); return findMethod(method, null, RubyType.create(clz), Call.UNKNOWN, info, astOffset, lexOffset, path, closest, index); } } return DeclarationLocation.NONE; } @SuppressWarnings("empty-statement") DeclarationLocation findLinkedMethod(ParserResult info, String method) { Node root = AstUtilities.getRoot(info); AstPath path = new AstPath(); path.descend(root); Node closest = root; int astOffset = 0; int lexOffset = 0; RubyIndex index = getIndex(info); if (root == null) { return DeclarationLocation.NONE; } if (method.startsWith("#")) { method = method.substring(1); DeclarationLocation loc = findMethod(info, root, method, Arity.UNKNOWN); // It looks like "#foo" can refer not just to methods (as rdoc suggested) // but to attributes as well - in Rails' initializer.rb this is used // in a number of places. if (loc == DeclarationLocation.NONE) { loc = findInstance(info, root, "@" + method, index); } return loc; } else { // A URL such as http://netbeans.org - try to open it in a browser! try { URL url = new URL(method); return new DeclarationLocation(url); } catch (MalformedURLException mue) { // URL is from user source... don't complain with exception dialogs etc. ; } } // Probably a Class#method int methodIndex = method.indexOf("#"); if (methodIndex != -1 && methodIndex < method.length()-1) { String clz = method.substring(0, methodIndex); method = method.substring(methodIndex+1); return findMethod(method, null, RubyType.create(clz), Call.UNKNOWN, info, astOffset, lexOffset, path, closest, index); } return DeclarationLocation.NONE; } IndexedMethod findBestMethodMatch(String name, Set<IndexedMethod> methodSet, BaseDocument doc, int astOffset, int lexOffset, AstPath path, Node call, RubyIndex index) { // Make sure that the best fit method actually has a corresponding valid source location // and parse tree Set<IndexedMethod> methods = new HashSet<IndexedMethod>(methodSet); while (!methods.isEmpty()) { IndexedMethod method = findBestMethodMatchHelper(name, methods, doc, astOffset, lexOffset, path, call, index); Node node = AstUtilities.getForeignNode(method); if (node != null) { return method; } if (!methods.contains(method)) { // Avoid infinite loop when we somehow don't find the node for // the best method and we keep trying it methods.remove(methods.iterator().next()); } else { methods.remove(method); } } // Dynamic methods that don't have source (such as the TableDefinition methods "binary", "boolean", etc. if (methodSet.size() > 0) { return methodSet.iterator().next(); } return null; } private IndexedMethod findBestMethodMatchHelper(String name, Set<IndexedMethod> methods, BaseDocument doc, int astOffset, int lexOffset, AstPath path, Node callNode, RubyIndex index) { Set<IndexedMethod> candidates = new HashSet<IndexedMethod>(); // 1. First see if the reference is fully qualified. If so the job should // be easier: prune the result set down // If I have the fqn, I can also call RubyIndex.getRDocLocation to pick the // best candidate if (callNode instanceof CallNode) { Node node = ((CallNode)callNode).getReceiverNode(); String fqn = null; if (node instanceof Colon2Node) { fqn = AstUtilities.getFqn((Colon2Node)node); } else if (node instanceof ConstNode) { fqn = ((ConstNode)node).getName(); } if (fqn != null) { while ((fqn != null) && (fqn.length() > 0)) { for (IndexedMethod method : methods) { if (fqn.equals(method.getClz())) { candidates.add(method); } } // Check inherited methods; for example, if we've determined // that you're looking for Integer::foo, I should happily match // Numeric::foo. IndexedClass superClass = index.getSuperclass(fqn); if (superClass != null) { fqn = superClass.getSignature(); } else { break; } } } } if (candidates.size() == 1) { return candidates.iterator().next(); } else if (!candidates.isEmpty()) { methods = candidates; } // 2. See if the reference is not qualified (no :: or . prior to // the method call; if so it must be an inherited method (or a local // method, but we've already checked that possibility before getting // into the index search) TokenHierarchy<Document> th = TokenHierarchy.get((Document)doc); Call call = Call.getCallType(doc, th, lexOffset); boolean skipPrivate = true; if ((path != null) && (callNode != null) && (call != Call.LOCAL) && (call != Call.NONE)) { boolean skipInstanceMethods = call.isStatic(); candidates = new HashSet<IndexedMethod>(); RubyType type = call.getType(); // I'm not doing any data flow analysis at this point, so // I can't do anything with a LHS like "foo.". Only actual types. if (type.isKnown()) { String lhs = call.getLhs(); String fqn = AstUtilities.getFqnName(path); // TODO for self and super, rather than computing ALL inherited methods // (and picking just one of them), I should use the FIRST match as the // one to show! (closest super class or include definition) if ("self".equals(lhs)) { type = RubyType.create(fqn); skipPrivate = false; } else if ("super".equals(lhs)) { skipPrivate = false; IndexedClass sc = index.getSuperclass(fqn); if (sc != null) { type = RubyType.create(sc.getFqn()); } else { ClassNode cls = AstUtilities.findClass(path); if (cls != null) { type = RubyType.create(AstUtilities.getSuperclass(cls)); } } } if (type.isKnown()) { // Possibly a class on the left hand side: try searching with the class as a qualifier. // Try with the LHS + current FQN recursively. E.g. if we're in // Test::Unit when there's a call to Foo.x, we'll try // Test::Unit::Foo, and Test::Foo while (candidates.size() == 0) { candidates = index.getInheritedMethods(fqn + "::" + type, name, QuerySupport.Kind.EXACT); int f = fqn.lastIndexOf("::"); if (f == -1) { break; } else { fqn = fqn.substring(0, f); } } // Add methods in the class (without an FQN) if (candidates.size() == 0) { candidates = index.getInheritedMethods(type, name, QuerySupport.Kind.EXACT); } } } if (skipPrivate || skipInstanceMethods) { Set<IndexedMethod> m = new HashSet<IndexedMethod>(); for (IndexedMethod method : candidates) { // Don't include private or protected methods on other objects if (skipPrivate && (method.isPrivate() && !"new".equals(method.getName()))) { // TODO - "initialize" removal here should not be necessary since they should // be marked as private, but index doesn't contain that yet continue; } // We can only call static methods if (skipInstanceMethods && !method.isStatic()) { continue; } m.add(method); } candidates = m; } // First try to limit the candidates down to the ones that match the lhs type, if we // are calling new or initialize if (type != null /* && ("new".equals(name) || "initialize".equals(name))*/) { // NOI18N Set<IndexedMethod> cs = new HashSet<IndexedMethod>(); for (IndexedMethod m : candidates) { // AppendIO might be the lhs - e.g. AppendIO.new, yet its FQN is Shell::AppendIO // so do suffix comparison if ((m.getIn() != null) && type.isSingleton() && m.getIn().endsWith(type.first())) { cs.add(m); } } if (cs.size() < candidates.size()) { candidates = cs; } } } if (candidates.size() == 1) { return candidates.iterator().next(); } else if (!candidates.isEmpty()) { methods = candidates; } // 3. Prefer methods with extra index attributes since these tend to be important // methods (e.g. pick ActiveRecord::ConnectionAdapters::SchemaStatements instead // of the many overrides of that method // (A more general solution would be to prefer ancestor classes' implementations // over superclasses' implementations candidates = new HashSet<IndexedMethod>(); for (IndexedMethod method : methods) { String attributes = method.getEncodedAttributes(); if (attributes != null && attributes.length() > 3) { candidates.add(method); } } if (candidates.size() == 1) { return candidates.iterator().next(); } else if (!candidates.isEmpty()) { methods = candidates; } // 4. Use method arity to rule out mismatches // TODO - this is tricky since Ruby lets you specify more or fewer // parameters with some reasonable behavior... // Possibly I should do this check further down since the // other heuristics may work better as a first-level disambiguation // 4. Check to see which classes are required directly from this file, and // prefer matches that are in this set of classes Set<String> requires = null; if (path != null) { candidates = new HashSet<IndexedMethod>(); requires = AstUtilities.getRequires(path.root()); for (IndexedMethod method : methods) { String require = method.getRequire(); if (requires.contains(require)) { candidates.add(method); } } if (candidates.size() == 1) { return candidates.iterator().next(); } else if (!candidates.isEmpty()) { methods = candidates; } } // 3. See if any of the methods are in "kernel" classes (builtins) and for these // go to the known locations candidates = new HashSet<IndexedMethod>(); for (IndexedMethod method : methods) { String url = method.getFileUrl(); if (RubyUtils.isRubyStubsURL(url)) { candidates.add(method); } } if (candidates.size() == 1) { return candidates.iterator().next(); } else if (!candidates.isEmpty()) { methods = candidates; } // 4. See which methods are documented, and prefer those over undocumented methods candidates = new HashSet<IndexedMethod>(); int longestDocLength = 0; for (IndexedMethod method : methods) { int length = method.getDocumentationLength(); if (length > longestDocLength) { candidates.clear(); candidates.add(method); longestDocLength = length; } else if ((length > 0) && (length == longestDocLength)) { candidates.add(method); } } if (candidates.size() == 1) { return candidates.iterator().next(); } else if (!candidates.isEmpty()) { methods = candidates; } // 5. Look at transitive closure of require statements and see which files // are most likely candidates if ((index != null) && (requires != null)) { candidates = new HashSet<IndexedMethod>(); Set<String> allRequires = index.getRequiresTransitively(requires); for (IndexedMethod method : methods) { String require = method.getRequire(); if (allRequires.contains(require)) { candidates.add(method); } } if (candidates.size() == 1) { return candidates.iterator().next(); } else if (!candidates.isEmpty()) { methods = candidates; } } // 6. Other heuristics: Look at the method definition with the // class with most methods associated with it. Look at other uses of this // method in this parse tree and see if I can figure out the containing class // or rule out other candidates based on that // 7. Look at superclasses and consider -their- requires to figure out // which class we're looking for methods in // TODO // Pick one arbitrarily if (methods.size() > 0) { return methods.iterator().next(); } else { return null; } } private DeclarationLocation findLocal(ParserResult info, Node node, String name) { if (node instanceof LocalAsgnNode) { if (((INameNode)node).getName().equals(name)) { return getLocation(info, node); } } else if (!ignoreAlias && node instanceof AliasNode) { String newName = AstUtilities.getNameOrValue(((AliasNode)node).getNewName()); if (name.equals(newName)) { return getLocation(info, node); } } else if (node instanceof ArgsNode) { ArgsNode an = (ArgsNode)node; if (an.getRequiredCount() > 0) { List<Node> args = an.childNodes(); for (Node arg : args) { if (arg instanceof ListNode) { List<Node> args2 = arg.childNodes(); for (Node arg2 : args2) { if (arg2 instanceof ArgumentNode) { if (((ArgumentNode)arg2).getName().equals(name)) { return getLocation(info, arg2); } } else if (arg2 instanceof LocalAsgnNode) { if (((LocalAsgnNode)arg2).getName().equals(name)) { return getLocation(info, arg2); } } } } } } // Rest args if (an.getRest() != null) { ArgumentNode bn = an.getRest(); if (bn.getName().equals(name)) { return getLocation(info, bn); } } // Block args if (an.getBlock() != null) { BlockArgNode bn = an.getBlock(); if (bn.getName().equals(name)) { return getLocation(info, bn); } } } List<Node> list = node.childNodes(); for (Node child : list) { if (child.isInvisible()) { continue; } DeclarationLocation location = findLocal(info, child, name); if (location != DeclarationLocation.NONE) { return location; } } return DeclarationLocation.NONE; } private DeclarationLocation findDynamic(ParserResult info, Node node, String name) { if (node instanceof DAsgnNode) { if (((INameNode)node).getName().equals(name)) { return getLocation(info, node); } } else if (!ignoreAlias && node instanceof AliasNode) { String newName = AstUtilities.getNameOrValue(((AliasNode)node).getNewName()); if (name.equals(newName)) { return getLocation(info, node); } } List<Node> list = node.childNodes(); for (Node child : list) { if (child.isInvisible()) { continue; } DeclarationLocation location = findDynamic(info, child, name); if (location != DeclarationLocation.NONE) { return location; } } return DeclarationLocation.NONE; } private DeclarationLocation findInstance(ParserResult info, Node node, String name, RubyIndex index) { if (node instanceof InstAsgnNode) { if (((INameNode)node).getName().equals(name)) { return getLocation(info, node); } } else if (!ignoreAlias && node instanceof AliasNode) { String newName = AstUtilities.getNameOrValue(((AliasNode)node).getNewName()); if (name.equals(newName)) { return getLocation(info, node); } } else if (AstUtilities.isAttr(node)) { // TODO: Compute the symbols and check for equality // attr_reader, attr_accessor, attr_writer SymbolNode[] symbols = AstUtilities.getAttrSymbols(node); for (int i = 0; i < symbols.length; i++) { // possibly an instance variable referred by attr_accessor and like if (name.equals(symbols[i].getName())) { Node root = AstUtilities.getRoot(info); DeclarationLocation location = findInstanceFromIndex(info, name, new AstPath(root, node), index, true); if (location != DeclarationLocation.NONE) { return location; } return getLocation(info, symbols[i]); } } } List<Node> list = node.childNodes(); for (Node child : list) { if (child.isInvisible()) { continue; } DeclarationLocation location = findInstance(info, child, name, index); if (location != DeclarationLocation.NONE) { return location; } } return DeclarationLocation.NONE; } private DeclarationLocation findClassVar(ParserResult info, Node node, String name) { if (node instanceof ClassVarDeclNode) { if (((INameNode)node).getName().equals(name)) { return getLocation(info, node); } } else if (!ignoreAlias && node instanceof AliasNode) { String newName = AstUtilities.getNameOrValue(((AliasNode)node).getNewName()); if (name.equals(newName)) { return getLocation(info, node); } // TODO: Are there attr readers and writers for class variables? // } else if (AstUtilities.isAttrReader(node) || AstUtilities.isAttrWriter(node)) { // // TODO: Compute the symbols and check for equality // // attr_reader, attr_accessor, attr_writer // SymbolNode[] symbols = AstUtilities.getAttrSymbols(node); // // for (int i = 0; i < symbols.length; i++) { // if (name.equals("@" + symbols[i].getName())) { // return getLocation(info, symbols[i]); // } // } } List<Node> list = node.childNodes(); for (Node child : list) { if (child.isInvisible()) { continue; } DeclarationLocation location = findClassVar(info, child, name); if (location != DeclarationLocation.NONE) { return location; } } return DeclarationLocation.NONE; } private DeclarationLocation findInstanceFromIndex(ParserResult info, String name, AstPath path, RubyIndex index, boolean inherited) { String fqn = AstUtilities.getFqnName(path); // TODO - if fqn has multiple ::'s, try various combinations? or is // add inherited already doing that? Set<IndexedField> f = index.getInheritedFields(fqn, name, QuerySupport.Kind.EXACT, inherited); for (IndexedField field : f) { // How do we choose one? // For now, just pick the first one Node node = AstUtilities.getForeignNode(field); if (node != null) { return new DeclarationLocation(field.getFileObject(), node.getPosition().getStartOffset(), field); } } return DeclarationLocation.NONE; } private DeclarationLocation findInstanceMethodsFromIndex(ParserResult info, String name, AstPath path, RubyIndex index) { String fqn = AstUtilities.getFqnName(path); Set<IndexedMethod> methods = index.getInheritedMethods(fqn, name, QuerySupport.Kind.EXACT); return getLocation(methods); } private DeclarationLocation findGlobal(ParserResult info, Node node, String name) { if (node instanceof GlobalAsgnNode) { if (((INameNode)node).getName().equals(name)) { return getLocation(info, node); } } else if (!ignoreAlias && node instanceof AliasNode) { String newName = AstUtilities.getNameOrValue(((AliasNode)node).getNewName()); if (name.equals(newName)) { return getLocation(info, node); } } List<Node> list = node.childNodes(); for (Node child : list) { if (child.isInvisible()) { continue; } DeclarationLocation location = findGlobal(info, child, name); if (location != DeclarationLocation.NONE) { return location; } } return DeclarationLocation.NONE; } private DeclarationLocation findMethod(ParserResult info, Node node, String name, Arity arity) { // Recursively search for methods or method calls that match the name and arity if (node instanceof MethodDefNode) { if (((MethodDefNode)node).getName().equals(name) && Arity.matches(arity, Arity.getDefArity(node))) { return getLocation(info, node); } } else if (!ignoreAlias && node instanceof AliasNode) { String newName = AstUtilities.getNameOrValue(((AliasNode)node).getNewName()); if (name.equals(newName)) { // No obvious way to check arity return getLocation(info, node); } } List<Node> list = node.childNodes(); for (Node child : list) { if (child.isInvisible()) { continue; } DeclarationLocation location = findMethod(info, child, name, arity); if (location != DeclarationLocation.NONE) { return location; } } return DeclarationLocation.NONE; } }