Too much stuff in one commit. Rename blocks now follows language semantics. Removal of more ASTPath consumers
2 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4 * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
6 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
7 * Other names may be trademarks of their respective owners.
9 * The contents of this file are subject to the terms of either the GNU
10 * General Public License Version 2 only ("GPL") or the Common
11 * Development and Distribution License("CDDL") (collectively, the
12 * "License"). You may not use this file except in compliance with the
13 * License. You can obtain a copy of the License at
14 * http://www.netbeans.org/cddl-gplv2.html
15 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
16 * specific language governing permissions and limitations under the
17 * License. When distributing the software, include this License Header
18 * Notice in each file and include the License file at
19 * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
20 * particular file as subject to the "Classpath" exception as provided
21 * by Oracle in the GPL Version 2 section of the License file that
22 * accompanied this code. If applicable, add the following below the
23 * License Header, with the fields enclosed by brackets [] replaced by
24 * your own identifying information:
25 * "Portions Copyrighted [year] [name of copyright owner]"
29 * The Original Software is NetBeans. The Initial Developer of the Original
30 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun
31 * Microsystems, Inc. All Rights Reserved.
33 * If you wish your version of this file to be governed by only the CDDL
34 * or only the GPL Version 2, indicate your decision by adding
35 * "[Contributor] elects to include this software in this distribution
36 * under the [CDDL or GPL Version 2] license." If you do not indicate a
37 * single choice of license, a recipient has the option to distribute
38 * your version of this file under either the CDDL, the GPL Version 2 or
39 * to extend the choice of license to its licensees as provided above.
40 * However, if you add GPL Version 2 code and therefore, elected the GPL
41 * Version 2 license, then the option applies only if the new code is
42 * made subject to such option by the copyright holder.
44 package org.netbeans.modules.ruby;
46 import java.net.MalformedURLException;
48 import java.util.ArrayList;
49 import java.util.Collections;
50 import java.util.HashSet;
51 import java.util.Iterator;
52 import java.util.List;
54 import java.util.concurrent.atomic.AtomicReference;
56 import javax.swing.text.BadLocationException;
57 import javax.swing.text.Document;
59 import org.jrubyparser.ast.AliasNode;
60 import org.jrubyparser.ast.ArgsNode;
61 import org.jrubyparser.ast.ArgumentNode;
62 import org.jrubyparser.ast.BlockArgNode;
63 import org.jrubyparser.ast.CallNode;
64 import org.jrubyparser.ast.ClassNode;
65 import org.jrubyparser.ast.ClassVarDeclNode;
66 import org.jrubyparser.ast.ClassVarNode;
67 import org.jrubyparser.ast.Colon2Node;
68 import org.jrubyparser.ast.ConstNode;
69 import org.jrubyparser.ast.DAsgnNode;
70 import org.jrubyparser.ast.FCallNode;
71 import org.jrubyparser.ast.GlobalAsgnNode;
72 import org.jrubyparser.ast.GlobalVarNode;
73 import org.jrubyparser.ast.HashNode;
74 import org.jrubyparser.ast.ILocalVariable;
75 import org.jrubyparser.ast.InstAsgnNode;
76 import org.jrubyparser.ast.InstVarNode;
77 import org.jrubyparser.ast.ListNode;
78 import org.jrubyparser.ast.LocalAsgnNode;
79 import org.jrubyparser.ast.MethodDefNode;
80 import org.jrubyparser.ast.Node;
81 import org.jrubyparser.ast.StrNode;
82 import org.jrubyparser.ast.SymbolNode;
83 import org.jrubyparser.ast.VCallNode;
84 import org.jrubyparser.ast.INameNode;
85 import org.jrubyparser.ast.IParameterScope;
86 import org.jrubyparser.ast.IterNode;
87 import org.jrubyparser.ast.NodeType;
88 import org.jrubyparser.ast.SuperNode;
89 import org.jrubyparser.ast.ZSuperNode;
90 import org.netbeans.api.lexer.Token;
91 import org.netbeans.api.lexer.TokenHierarchy;
92 import org.netbeans.api.lexer.TokenId;
93 import org.netbeans.api.lexer.TokenSequence;
94 import org.netbeans.editor.BaseDocument;
95 import org.netbeans.editor.Utilities;
96 import org.netbeans.modules.csl.api.DeclarationFinder;
97 import org.netbeans.modules.csl.api.DeclarationFinder.AlternativeLocation;
98 import org.netbeans.modules.csl.api.DeclarationFinder.DeclarationLocation;
99 import org.netbeans.modules.csl.api.OffsetRange;
100 import org.netbeans.modules.csl.spi.ParserResult;
101 import org.netbeans.modules.parsing.spi.Parser;
102 import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport;
103 import static org.netbeans.modules.ruby.RubyDeclarationFinderHelper.getLocation;
104 import org.netbeans.modules.ruby.elements.IndexedClass;
105 import org.netbeans.modules.ruby.elements.IndexedElement;
106 import org.netbeans.modules.ruby.elements.IndexedField;
107 import org.netbeans.modules.ruby.elements.IndexedMethod;
108 import org.netbeans.modules.ruby.lexer.LexUtilities;
109 import org.netbeans.modules.ruby.lexer.Call;
110 import org.netbeans.modules.ruby.lexer.RubyCommentTokenId;
111 import org.netbeans.modules.ruby.lexer.RubyTokenId;
112 import org.openide.filesystems.FileObject;
113 import org.openide.util.Exceptions;
117 * Find a declaration from an element in the JRuby AST.
119 * @todo Look at the target to see which method to choose. For example, if
120 * you do Foo.new, I should locate "initialize" in Foo, not somewhere else.
121 * @todo Don't include inexact matches like alias nodes when searching first;
122 * only if a search for actual declaration nodes fail should I revert to looking
124 * @todo If you're looking for a local class, such as a Rails model, I should
126 * @todo Within a gem, prefer other matches within the same gem or gem cluster
127 * @todo Prefer files named after the class! (e.g. SchemaStatements in schema_statements.rb)
131 public class RubyDeclarationFinder extends RubyDeclarationFinderHelper implements DeclarationFinder {
133 /** An increasing number; I will be using this number modulo the */
134 private static int methodSelector = 0;
136 /** When true, don't match alias nodes as reads. Used during traversal of the AST. */
137 private boolean ignoreAlias;
139 private RubyIndex rubyIndex;
141 private static final String PARTIAL = "partial"; //NOI18N
142 private static final String CONTROLLER = "controller"; //NOI18N
143 private static final String ACTION = "action";//NOI18N
144 private static final String TEMPLATE = "template";//NOI18N
145 private static final String[] RAILS_TARGET_RAW_NAMES = new String[] {PARTIAL, CONTROLLER, ACTION, TEMPLATE};
147 private static final List<String> RAILS_TARGETS = initRailsTargets();
149 private static List<String> initRailsTargets() {
150 List<String> result = new ArrayList<String>(RAILS_TARGET_RAW_NAMES.length * 4);
151 for (String target : RAILS_TARGET_RAW_NAMES) {
152 result.add(":" + target + " => ");
153 result.add(":" + target + "=> ");
154 result.add(":" + target + " =>");
155 result.add(":" + target + "=>");
159 /** Creates a new instance of RubyDeclarationFinder */
160 public RubyDeclarationFinder() {
163 private RubyIndex getIndex(ParserResult result) {
164 if (rubyIndex == null) {
165 rubyIndex = RubyIndex.get(result);
171 public OffsetRange getReferenceSpan(Document document, int lexOffset) {
172 TokenHierarchy<Document> th = TokenHierarchy.get(document);
174 BaseDocument doc = (BaseDocument)document;
175 FileObject fo = RubyUtils.getFileObject(document);
176 if (RubyUtils.isRhtmlDocument(doc) || (fo != null && RubyUtils.isRailsProject(fo))) {
177 RailsTarget target = findRailsTarget(doc, th, lexOffset);
178 if (target != null) {
183 TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(th, lexOffset);
186 return OffsetRange.NONE;
191 if (!ts.moveNext() && !ts.movePrevious()) {
192 return OffsetRange.NONE;
195 // Determine whether the caret position is right between two tokens
196 boolean isBetween = (lexOffset == ts.offset());
198 OffsetRange range = getReferenceSpan(ts, th, lexOffset);
200 if ((range == OffsetRange.NONE) && isBetween) {
201 // The caret is between two tokens, and the token on the right
202 // wasn't linkable. Try on the left instead.
203 if (ts.movePrevious()) {
204 range = getReferenceSpan(ts, th, lexOffset);
211 private OffsetRange getReferenceSpan(TokenSequence<?> ts,
212 TokenHierarchy<Document> th, int lexOffset) {
213 Token<?> token = ts.token();
214 TokenId id = token.id();
216 if (id == RubyTokenId.IDENTIFIER) {
217 if (token.length() == 1 && id == RubyTokenId.IDENTIFIER && token.text().toString().equals(",")) {
218 return OffsetRange.NONE;
222 // TODO: Tokens.THIS, Tokens.SELF ...
223 if ((id == RubyTokenId.IDENTIFIER) || (id == RubyTokenId.CLASS_VAR) ||
224 (id == RubyTokenId.GLOBAL_VAR) || (id == RubyTokenId.CONSTANT) ||
225 (id == RubyTokenId.TYPE_SYMBOL) || (id == RubyTokenId.INSTANCE_VAR) ||
226 (id == RubyTokenId.SUPER)) {
227 return new OffsetRange(ts.offset(), ts.offset() + token.length());
230 // Look for embedded RDoc comments:
231 TokenSequence<?> embedded = ts.embedded();
233 if (embedded != null) {
235 embedded.move(lexOffset);
237 if (embedded.moveNext()) {
238 Token<?> embeddedToken = embedded.token();
240 if (embeddedToken.id() == RubyCommentTokenId.COMMENT_LINK) {
241 return new OffsetRange(embedded.offset(),
242 embedded.offset() + embeddedToken.length());
244 // Recurse into the range - perhaps there is Ruby code (identifiers
246 // etc.) to follow there
247 OffsetRange range = getReferenceSpan(embedded, th, lexOffset);
249 if (range != OffsetRange.NONE) {
255 // Allow hyperlinking of some literal strings too, such as require strings
256 if ((id == RubyTokenId.QUOTED_STRING_LITERAL) || (id == RubyTokenId.STRING_LITERAL)) {
257 int requireStart = LexUtilities.getRequireStringOffset(lexOffset, th);
259 if (requireStart != -1) {
260 String require = LexUtilities.getStringAt(lexOffset, th);
262 if (require != null) {
263 return new OffsetRange(requireStart, requireStart + require.length());
267 int classNameStart = LexUtilities.getClassNameStringOffset(lexOffset, th);
268 if (classNameStart != -1) {
269 String className = LexUtilities.getStringAt(lexOffset, th);
270 if (className != null) {
271 return new OffsetRange(classNameStart, classNameStart + className.length());
276 return OffsetRange.NONE;
280 public DeclarationLocation findDeclaration(final ParserResult parserResult, final int lexOffset) {
281 // Is this a require-statement? If so, jump to the required file
282 final Document document = RubyUtils.getDocument(parserResult, true);
283 if (document == null) return DeclarationLocation.NONE;
285 final AtomicReference<DeclarationLocation> out = new AtomicReference<DeclarationLocation>(DeclarationLocation.NONE);
287 // This will only change out if the result is different than NONE.
288 document.render(new Runnable() {
292 TokenHierarchy<Document> th = TokenHierarchy.get(document);
293 BaseDocument doc = (BaseDocument)document;
295 int astOffset = AstUtilities.getAstOffset(parserResult, lexOffset);
296 if (astOffset == -1) return;
298 boolean view = RubyUtils.isRhtmlFile(RubyUtils.getFileObject(parserResult));
299 if (view || RubyUtils.isRailsProject(RubyUtils.getFileObject(parserResult))) {
300 DeclarationLocation loc = findRailsFile(parserResult, doc, th, lexOffset, astOffset, view);
302 if (loc != DeclarationLocation.NONE) {
308 OffsetRange range = getReferenceSpan(doc, lexOffset);
310 if (range == OffsetRange.NONE) return;
312 // Determine the bias (if the caret is between two tokens, did we
313 // click on a link for the left or the right?
314 boolean leftSide = range.getEnd() <= lexOffset;
316 Node root = AstUtilities.getRoot(parserResult);
318 RubyIndex index = getIndex(parserResult);
320 // No parse tree - try to just use the syntax info to do a simple index lookup
321 // for methods and classes
322 String text = doc.getText(range.getStart(), range.getLength());
324 if (index == null || text.length() == 0) return;
326 if (Character.isUpperCase(text.charAt(0))) {
327 // A Class or Constant?
328 Set<IndexedClass> classes = index.getClasses(text, QuerySupport.Kind.EXACT, true, false, false);
330 if (classes.isEmpty()) return;
332 RubyClassDeclarationFinder cdf = new RubyClassDeclarationFinder(null, null, null, index, null);
333 DeclarationLocation l = cdf.getElementDeclaration(classes, null);
341 Set<IndexedMethod> methods = index.getMethods(text, (String) null, QuerySupport.Kind.EXACT);
343 if (methods.isEmpty()) {
344 methods = index.getMethods(text, QuerySupport.Kind.EXACT);
347 DeclarationLocation l = getMethodDeclaration(parserResult, text, methods,
348 null, null, index, astOffset, lexOffset);
354 } // TODO: @ - field?
359 int tokenOffset = lexOffset;
361 if (leftSide && (tokenOffset > 0)) tokenOffset--;
363 // See if the hyperlink is for the string in a require statement
364 int requireStart = LexUtilities.getRequireStringOffset(tokenOffset, th);
366 if (requireStart != -1) {
367 String require = LexUtilities.getStringAt(tokenOffset, th);
369 if (require != null) {
370 FileObject fo = index.getRequiredFile(require);
373 out.set(new DeclarationLocation(fo, 0));
378 // It's in a require string so no possible other matches
382 AstPath path = new AstPath(root, astOffset);
383 Node closest = AstUtilities.findNodeAtOffset(root, astOffset);
384 if (closest == null) return;
386 // See if the hyperlink is over a method reference in an rdoc comment
387 DeclarationLocation rdoc = findRDocMethod(parserResult, doc, astOffset, lexOffset, root, path, closest, index);
389 if (rdoc != DeclarationLocation.NONE) {
390 out.set(fix(rdoc, parserResult));
394 if (closest instanceof ILocalVariable) {
395 ILocalVariable declaration = ((ILocalVariable) closest).getDeclaration();
397 out.set(fix(getLocation(parserResult, (Node) declaration), parserResult));
398 } else if (closest instanceof InstVarNode || closest instanceof ClassVarNode) { // A field/class variable read
399 String name = ((INameNode)closest).getLexicalName();
400 out.set(findInstanceFromIndex(parserResult, name, path, index, false));
401 } else if (closest instanceof GlobalVarNode) {
402 // A global variable read
403 String name = ((INameNode)closest).getLexicalName();
404 out.set(fix(findGlobal(parserResult, root, name), parserResult));
405 } else if (closest instanceof FCallNode || closest instanceof VCallNode ||
406 closest instanceof CallNode) {
408 String name = ((INameNode)closest).getName();
409 Call call = Call.getCallType(doc, th, lexOffset);
410 RubyType type = call.getType();
411 String lhs = call.getLhs();
413 if (!type.isKnown() && lhs != null && call.isSimpleIdentifier()) {
414 Node method = AstUtilities.findLocalScope(closest, path);
416 // TODO - if the lhs is "foo.bar." I need to split this
417 // up and do it a bit more cleverly
418 ContextKnowledge knowledge = new ContextKnowledge(
419 index, root, method, astOffset, lexOffset, parserResult);
420 type = RubyTypeInferencer.create(knowledge).inferType(lhs);
423 // Constructors: "new" ends up calling "initialize".
424 // Actually, it's more complicated than this: a method CAN override new
425 // in which case I should show it, but that is discouraged and people
426 // SHOULD override initialize, which is what the default new method will
427 // call for initialization.
428 if (!type.isKnown()) { // search locally
430 if (name.equals("new")) name = "initialize"; // NOI18N
432 Arity arity = Arity.getCallArity(closest);
434 DeclarationLocation loc = fix(findMethod(parserResult, root, name, arity), parserResult);
436 if (loc != DeclarationLocation.NONE) {
442 String fqn = AstUtilities.getFqnName(path);
443 if (call == Call.LOCAL && fqn != null && fqn.length() == 0) fqn = "Object"; // NOI18N
445 out.set(findMethod(name, fqn, type, call, parserResult, astOffset, lexOffset, path, closest, index));
446 } else if (closest instanceof ConstNode || closest instanceof Colon2Node) {
448 RubyClassDeclarationFinder classDF = new RubyClassDeclarationFinder(parserResult, root, path, index, closest);
449 DeclarationLocation decl = classDF.findClassDeclaration();
450 if (decl != DeclarationLocation.NONE) {
454 // try Constant usage
455 RubyConstantDeclarationFinder constantDF = new RubyConstantDeclarationFinder(parserResult, root, path, index, closest);
456 out.set(constantDF.findConstantDeclaration());
457 } else if (closest instanceof SymbolNode) {
458 String name = ((SymbolNode)closest).getName();
460 // Search for methods, fields, etc.
461 Arity arity = Arity.UNKNOWN;
462 DeclarationLocation location = findMethod(parserResult, root, name, arity);
464 // search for AR associations
465 if (location == DeclarationLocation.NONE) {
466 location = new ActiveRecordAssociationFinder(index, (SymbolNode) closest, root, path).findAssociationLocation();
469 // search for helpers
470 if (location == DeclarationLocation.NONE) {
471 location = new HelpersFinder(index, (SymbolNode) closest, path).findHelperLocation();
474 if (location == DeclarationLocation.NONE) {
475 location = findInstance(parserResult, root, name, index);
478 if (location == DeclarationLocation.NONE) {
479 location = findClassVar(parserResult, root, name);
482 if (location == DeclarationLocation.NONE) {
483 location = findGlobal(parserResult, root, name);
486 if (location == DeclarationLocation.NONE) {
487 RubyClassDeclarationFinder cdf = new RubyClassDeclarationFinder();
488 Node clz = cdf.findClass(root, ((INameNode)closest).getName(), ignoreAlias);
490 if (clz != null) location = getLocation(parserResult, clz);
494 if (location == DeclarationLocation.NONE) {
495 location = findInstanceMethodsFromIndex(parserResult, name, path, index);
498 if (location == DeclarationLocation.NONE) {
499 location = findInstanceFromIndex(parserResult, name, path, index, true);
502 out.set(fix(location, parserResult));
503 } else if (closest instanceof AliasNode) {
504 AliasNode an = (AliasNode)closest;
506 // TODO - determine if the click is over the new name or the old name
507 String newName = AstUtilities.getNameOrValue(an.getNewName());
508 if (newName == null) return;
510 // XXX I don't know where the old and new names are since the user COULD
511 // have used more than one whitespace character for separation. For now I'll
512 // just have to assume it's the normal case with one space: alias new old.
513 // I -could- use the getPosition.getEndOffset() to see if this looks like it's
514 // the case (e.g. node length != "alias ".length + old.length+new.length+1).
515 // In this case I could go peeking in the source buffer to see where the
516 // spaces are - between alias and the first word or between old and new. XXX.
517 int newLength = newName.length();
518 int aliasPos = an.getPosition().getStartOffset();
520 if (astOffset > aliasPos + 6) { // 6: "alias ".length()
521 if (astOffset > (aliasPos + 6 + newLength)) {
522 // It's over the old word: this counts as a usage.
523 // The problem is that we don't know if it's a local, a dynamic, an instance
524 // variable, etc. (The $ and @ parts are not included in the alias statement).
525 // First see if it's a local variable.
526 String name = AstUtilities.getNameOrValue(an.getOldName());
527 if (name == null) return;
531 DeclarationLocation location =
532 findLocal(parserResult, AstUtilities.findLocalScope(closest, path), name);
534 if (location == DeclarationLocation.NONE) {
535 location = findDynamic(parserResult, AstUtilities.findDynamicScope(closest, path),
539 if (location == DeclarationLocation.NONE) {
540 location = findMethod(parserResult, root, name, Arity.UNKNOWN);
543 if (location == DeclarationLocation.NONE) {
544 location = findInstance(parserResult, root, name, index);
547 if (location == DeclarationLocation.NONE) {
548 location = findClassVar(parserResult, root, name);
551 if (location == DeclarationLocation.NONE) {
552 location = findGlobal(parserResult, root, name);
555 if (location == DeclarationLocation.NONE) {
556 RubyClassDeclarationFinder cdf = new RubyClassDeclarationFinder();
557 Node clz = cdf.findClass(root, name, ignoreAlias);
560 location = getLocation(parserResult, clz);
564 // TODO - what if we're aliasing another alias? I think that should show up in the various
566 if (location == DeclarationLocation.NONE) return;
568 out.set(fix(location, parserResult));
573 // It's over the new word: this counts as a declaration. Nothing to do here except
574 // maybe jump right back to the beginning.
575 out.set(new DeclarationLocation(RubyUtils.getFileObject(parserResult), aliasPos + 4));
578 } else if (closest instanceof ArgumentNode) {
579 // A method name (if under a DefnNode or DefsNode) or a parameter (if indirectly under an ArgsNode)
580 String name = ((ArgumentNode)closest).getName(); // ArgumentNode doesn't implement INameNode
582 Node parent = path.leafParent();
584 if (parent != null) {
585 if (parent instanceof MethodDefNode) return; // It's a method name
587 // Parameter (check to see if its under ArgumentNode)
588 Node method = AstUtilities.findLocalScope(closest, path);
590 out.set(fix(findLocal(parserResult, method, name), parserResult));
592 } else if (closest instanceof StrNode) {
593 // See if the hyperlink is for the string that is the value for :class_name =>
594 int classNameStart = LexUtilities.getClassNameStringOffset(astOffset, th);
595 if (classNameStart != -1) {
596 String className = LexUtilities.getStringAt(tokenOffset, th);
597 if (className != null) {
598 out.set(getLocation(index.getClasses(className, QuerySupport.Kind.EXACT, true, false, false)));
601 } else if (closest instanceof SuperNode || closest instanceof ZSuperNode) {
602 Node scope = AstUtilities.findLocalScope(closest, path);
603 String fqn = AstUtilities.getFqnName(path);
604 switch (scope.getNodeType()) {
608 IndexedClass superClass = index.getSuperclass(fqn);
609 if (superClass != null) {
610 out.set(getLocation(Collections.singleton(superClass)));
616 MethodDefNode methodDef = (MethodDefNode) scope;
617 IndexedMethod superMethod = index.getSuperMethod(fqn, methodDef.getName(), true);
618 if (superMethod != null) {
619 out.set(getLocation(Collections.singleton(superMethod)));
625 } catch (BadLocationException ble) {
626 // do nothing - see #154991
635 * Compute the declaration location for a test string (such as MosModule::TestBaz/test_qux).
637 * @param fileInProject a file in the project where to perform the search
638 * @param testString a string represening a test class and method, such as TestFoo/test_bar
639 * @param classLocation if true, returns the location of the class rather then the method.
641 public static DeclarationLocation getTestDeclaration(FileObject fileInProject, String testString, boolean classLocation) {
642 return getTestDeclaration(fileInProject, testString, classLocation, true);
645 public static DeclarationLocation getTestDeclaration(FileObject fileInProject, String testString,
646 boolean classLocation, boolean requireDeclaredClass) {
648 int methodIndex = testString.indexOf('/'); //NOI18N
649 if (methodIndex == -1) return DeclarationLocation.NONE;
651 RubyIndex index = RubyIndex.get(QuerySupport.findRoots(fileInProject,
652 Collections.singleton(RubyLanguage.SOURCE),
653 Collections.singleton(RubyLanguage.BOOT),
654 Collections.<String>emptySet()));
656 if (index == null) return DeclarationLocation.NONE;
658 String className = testString.substring(0, methodIndex);
659 String methodName = testString.substring(methodIndex+1);
661 Set<IndexedMethod> methods = index.getMethods(methodName, className, QuerySupport.Kind.EXACT);
662 DeclarationLocation methodLocation = getLocation(methods);
663 if (!classLocation) {
664 if (DeclarationLocation.NONE == methodLocation && !requireDeclaredClass) {
665 // the test method is not defined in the class
666 methodLocation = getLocation(index.getMethods(methodName, QuerySupport.Kind.EXACT));
668 return methodLocation;
670 Set<IndexedClass> classes =
671 index.getClasses(className, QuerySupport.Kind.EXACT, false, false, true, null);
672 DeclarationLocation classDeclarationLocation = getLocation(classes);
674 if (DeclarationLocation.NONE == methodLocation && classLocation) {
675 return classDeclarationLocation;
677 if (methodLocation.getFileObject().equals(classDeclarationLocation.getFileObject())) {
678 return classDeclarationLocation;
681 for (AlternativeLocation alt : classDeclarationLocation.getAlternativeLocations()) {
682 if (methodLocation.getFileObject().equals(alt.getLocation().getFileObject())) {
683 return alt.getLocation();
687 return classDeclarationLocation;
690 static DeclarationLocation getLocation(Set<? extends IndexedElement> elements) {
691 DeclarationLocation loc = DeclarationLocation.NONE;
692 for (IndexedElement element : elements) {
693 FileObject fo = element.getFileObject();
697 if (loc == DeclarationLocation.NONE) {
699 Node node = AstUtilities.getForeignNode(element);
701 offset = AstUtilities.getRange(node).getStart();
703 loc = new DeclarationLocation(fo, offset, element);
704 loc.addAlternative(new RubyAltLocation(element, false));
706 AlternativeLocation alternate = new RubyAltLocation(element, false);
707 loc.addAlternative(alternate);
713 private DeclarationLocation findRailsFile(ParserResult info, BaseDocument doc,
714 TokenHierarchy<Document> th, int lexOffset, int astOffset, boolean fromView) {
715 RailsTarget target = findRailsTarget(doc, th, lexOffset);
716 if (target != null) {
717 String type = target.type;
718 if (type.indexOf(PARTIAL) != -1 || type.indexOf(TEMPLATE) != -1) { // NOI18N
720 boolean template = type.indexOf(TEMPLATE) != -1;
723 int slashIndex = target.name.lastIndexOf('/');
724 if (slashIndex != -1) {
726 FileObject app = RubyUtils.getAppDir(RubyUtils.getFileObject(info));
728 return DeclarationLocation.NONE;
731 String relativePath = target.name.substring(0, slashIndex);
732 dir = app.getFileObject("views/" + relativePath); // NOI18N
734 return DeclarationLocation.NONE;
736 name = target.name.substring(slashIndex+1); // NOI18N
739 dir = RubyUtils.getFileObject(info).getParent();
740 name = target.name; // NOI18N
747 DeclarationLocation partialLocation = findPartial(name, dir);
748 if (partialLocation != DeclarationLocation.NONE) {
749 return partialLocation;
751 } else if (type.indexOf(CONTROLLER) != -1 || type.indexOf(ACTION) != -1) { // NOI18N
752 // Look for the controller file in the corresponding directory
753 FileObject file = RubyUtils.getFileObject(info);
754 file = file.getParent();
755 //FileObject dir = file.getParent();
757 String action = null;
758 String fileName = file.getName();
759 boolean isController = type.indexOf(CONTROLLER) != -1; // NOI18N
760 String path = ""; // NOI18N
764 if (!fileName.startsWith("_")) { // NOI18N
765 // For partials like "_foo", just use the surrounding view
767 action = RubyUtils.getFileObject(info).getName();
771 // The hyperlink has either the controller or the action, but I should
772 // look at the AST to find the other such that the navigation works
773 // better. E.g. if you click on :controller=>'foo', and the statement
774 // also has an :action=>'bar', we not only jump to FooController we go to
775 // the "def bar" in it as well (and vice versa if you click on just :action=>'bar';
776 // this normally assumes its the controller associated with the RHTML file unless
777 // a different controller is specified
778 int delta = target.range.getStart() - lexOffset;
779 String[] controllerAction = findControllerAction(info, lexOffset+delta, astOffset+delta);
780 if (controllerAction[0] != null) {
781 path = controllerAction[0];
783 if (controllerAction[1] != null) {
784 action = controllerAction[1];
788 // uh, this is getting really messy - hard to add funtionality here
789 // without breaking existing functionality. this an attempt to fix
790 // IZ 172679 w/o affect navigation from views. the class is in
791 // need of serious refactoring.
792 String controllerName;
793 if (controllerAction[0] != null) {
794 controllerName = controllerAction[0];
795 } else if (isController) {
796 controllerName = target.name;
798 controllerName = RubyUtils.getFileObject(info).getName();
800 return findActionLocation(asControllerClass(controllerName), action, info);
803 // Find app dir, and build up a relative path to the view file in the process
804 FileObject app = file.getParent();
806 while (app != null) {
807 if (app.getName().equals("views") && // NOI18N
808 ((app.getParent() == null) || app.getParent().getName().equals("app"))) { // NOI18N
809 app = app.getParent();
814 path = app.getNameExt() + "/" + path; // NOI18N
815 app = app.getParent();
819 FileObject controllerFile = app.getFileObject("controllers/" + path + "_controller.rb"); // NOI18N
820 if (controllerFile != null) {
822 if (action != null) {
823 offset = AstUtilities.findOffset(controllerFile, action);
829 return new DeclarationLocation(controllerFile, offset);
835 return DeclarationLocation.NONE;
838 private static String asControllerClass(String controllerName) {
839 String suffix = controllerName.endsWith("_controller") ? "" : "_controller";//NOI18N
840 return RubyUtils.underlinedNameToCamel(controllerName + suffix);
843 private DeclarationLocation findActionLocation(String controllerName, String actionName, ParserResult result) {
844 RubyIndex index = getIndex(result);
845 Set<IndexedMethod> methods = index.getMethods(actionName, controllerName, QuerySupport.Kind.EXACT);
846 return getLocation(methods);
850 * Finds the location of the partial matching the given <code>name</code> in the
851 * given <code>dir</code>.
857 private DeclarationLocation findPartial(String name, FileObject dir) {
858 // Try to find the partial file
859 FileObject partial = dir.getFileObject(name);
860 if (partial != null) {
861 return new DeclarationLocation(partial, 0);
864 for (String ext : RubyUtils.RUBY_VIEW_EXTS) {
865 partial = dir.getFileObject(name + ext);
866 if (partial != null) {
867 return new DeclarationLocation(partial, 0);
871 // Handle some other file types for the partials
872 for (FileObject child : dir.getChildren()) {
873 if (child.isValid() && !child.isFolder() && child.getName().equals(name)) {
874 return new DeclarationLocation(child, 0);
878 // finally, try matching just the first part of the file name
879 for (FileObject child : dir.getChildren()) {
880 if (child.isValid() && !child.isFolder()) {
881 String fileName = child.getName();
882 int firstDot = fileName.indexOf('.');
883 if (firstDot != -1 && name.equals(fileName.substring(0, firstDot))) {
884 return new DeclarationLocation(child, 0);
888 return DeclarationLocation.NONE;
891 /** Locate the :action and :controller strings in the hash list that is under the
893 * @return A string[2] where string[0] is the controller or null, and string[1] is the
896 private String[] findControllerAction(ParserResult info, int lexOffset, int astOffset) {
897 String[] result = new String[2];
899 Node root = AstUtilities.getRoot(info);
903 AstPath path = new AstPath(root, astOffset);
904 Iterator<Node> it = path.leafToRoot();
906 while (it.hasNext()) {
909 if (n instanceof HashNode) {
910 if (prev instanceof ListNode) { // uhm... why am I going back to prev?
911 List<Node> hashItems = prev.childNodes();
913 Iterator<Node> hi = hashItems.iterator();
914 while (hi.hasNext()) {
919 if (f instanceof SymbolNode) {
920 from = ((SymbolNode)f).getName();
925 if (t instanceof StrNode) {
926 to = ((StrNode)t).getValue().toString();
930 if ("controller".equals(from)) { // NOI18N
932 } else if ("action".equals(from)) { // NOI18N
946 /** A result from findRailsTarget which computes sections that have special
947 * hyperlink semantics - like link_to, render :partial, render :action, :controller etc.
949 private static class RailsTarget {
950 RailsTarget(String type, String name, OffsetRange range) {
957 public String toString() {
958 return "RailsTarget(" + type + ", " + name + ", " + range + ")";
966 private boolean fastCheckIsRailsTarget(String s) {
967 for (String targetName : RAILS_TARGET_RAW_NAMES) {
968 if (s.indexOf(targetName) != -1) {
976 private RailsTarget findRailsTarget(BaseDocument doc, TokenHierarchy<Document> th, int lexOffset) {
979 // TODO - limit this to RHTML files only?
980 int begin = Utilities.getRowStart(doc, lexOffset);
982 int end = Utilities.getRowEnd(doc, lexOffset);
983 String s = doc.getText(begin, end-begin); // TODO - limit to a narrower region around the caret?
984 if (!fastCheckIsRailsTarget(s)) {
987 for (String target : RAILS_TARGETS) {
988 int index = s.indexOf(target);
991 int nameOffset = begin+index+target.length();
992 TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(th, nameOffset);
999 StringBuilder sb = new StringBuilder();
1000 boolean started = false;
1001 while (ts.moveNext() && ts.offset() < end) {
1003 Token<?> token = ts.token();
1004 TokenId id = token.id();
1005 if (id == RubyTokenId.STRING_LITERAL || id == RubyTokenId.QUOTED_STRING_LITERAL) {
1006 sb.append(token.text().toString());
1009 if (!"string".equals(id.primaryCategory())) {
1017 int rangeEnd = ts.offset();
1019 String name = sb.toString();
1021 if (lexOffset <= rangeEnd && lexOffset >= begin+index) {
1022 OffsetRange range = new OffsetRange(begin+index, rangeEnd);
1023 return new RailsTarget(target, name, range);
1028 } catch (BadLocationException ble) {
1029 Exceptions.printStackTrace(ble);
1037 private DeclarationLocation findMethod(String name, String possibleFqn, RubyType type, Call call,
1038 ParserResult info, int caretOffset, int lexOffset, AstPath path, Node closest, RubyIndex index) {
1039 Set<IndexedMethod> methods = getApplicableMethods(name, possibleFqn, type, call, index);
1041 int astOffset = caretOffset;
1042 DeclarationLocation l = getMethodDeclaration(info, name, methods,
1043 path, closest, index, astOffset, lexOffset);
1048 private Set<IndexedMethod> getApplicableMethods(String name, String possibleFqn,
1049 RubyType type, Call call, RubyIndex index) {
1050 Set<IndexedMethod> methods = new HashSet<IndexedMethod>();
1052 if (!type.isKnown() && possibleFqn != null && call.getLhs() == null && call != Call.UNKNOWN) {
1055 // Possibly a class on the left hand side: try searching with the class as a qualifier.
1056 // Try with the LHS + current FQN recursively. E.g. if we're in
1057 // Test::Unit when there's a call to Foo.x, we'll try
1058 // Test::Unit::Foo, and Test::Foo
1059 while (methods.isEmpty() && (fqn.length() > 0)) {
1060 methods = index.getInheritedMethods(fqn, name, QuerySupport.Kind.EXACT);
1062 int f = fqn.lastIndexOf("::");
1067 fqn = fqn.substring(0, f);
1072 if (type.isKnown() && methods.isEmpty()) {
1075 // Possibly a class on the left hand side: try searching with the class as a qualifier.
1076 // Try with the LHS + current FQN recursively. E.g. if we're in
1077 // Test::Unit when there's a call to Foo.x, we'll try
1078 // Test::Unit::Foo, and Test::Foo
1079 while (methods.isEmpty() && fqn != null && (fqn.length() > 0)) {
1080 for (String realType : type.getRealTypes()) {
1081 methods.addAll(index.getInheritedMethods(fqn + "::" + realType, name, QuerySupport.Kind.EXACT));
1084 int f = fqn.lastIndexOf("::");
1089 fqn = fqn.substring(0, f);
1093 if (methods.isEmpty()) {
1094 // Add methods in the class (without an FQN)
1095 for (String realType : type.getRealTypes()) {
1096 methods.addAll(index.getInheritedMethods(realType, name, QuerySupport.Kind.EXACT));
1099 if (methods.isEmpty()) {
1100 for (String realType : type.getRealTypes()) {
1101 assert realType != null : "Should not be null";
1102 if (realType.indexOf("::") == -1) {
1103 // Perhaps we specified a class without its FQN, such as "TableDefinition"
1104 // -- go and look for the full FQN and add in all the matches from there
1105 Set<IndexedClass> classes = index.getClasses(realType, QuerySupport.Kind.EXACT, false, false, false);
1106 Set<String> fqns = new HashSet<String>();
1107 for (IndexedClass cls : classes) {
1108 String f = cls.getFqn();
1113 for (String f : fqns) {
1114 if (!f.equals(realType)) {
1115 methods.addAll(index.getInheritedMethods(f, name, QuerySupport.Kind.EXACT));
1123 // Fall back to ALL methods across classes
1124 // Try looking at the libraries too
1125 if (methods.isEmpty()) {
1126 methods.addAll(index.getMethods(name, QuerySupport.Kind.EXACT));
1130 if ( methods.isEmpty()) {
1131 if (!type.isKnown()) {
1132 methods.addAll(index.getMethods(name, QuerySupport.Kind.EXACT));
1134 methods.addAll(index.getMethods(name, type.getRealTypes(), QuerySupport.Kind.EXACT));
1136 if (methods.isEmpty() && type.isKnown()) {
1137 methods = index.getMethods(name, QuerySupport.Kind.EXACT);
1144 private DeclarationLocation getMethodDeclaration(ParserResult info, String name, Set<IndexedMethod> methods,
1145 AstPath path, Node closest, RubyIndex index, int astOffset, int lexOffset) {
1146 BaseDocument doc = RubyUtils.getDocument(info);
1148 return DeclarationLocation.NONE;
1151 IndexedMethod candidate =
1152 findBestMethodMatch(name, methods, doc,
1153 astOffset, lexOffset, path, closest, index);
1155 if (candidate != null) {
1156 FileObject fileObject = candidate.getFileObject();
1157 if (fileObject == null) {
1158 return DeclarationLocation.NONE;
1161 Node node = AstUtilities.getForeignNode(candidate);
1164 nodeOffset = node.getPosition().getStartOffset();
1165 if (node.getNodeType() == NodeType.ALIASNODE) {
1166 nodeOffset += 6; // 6 = lenght of 'alias '
1170 DeclarationLocation loc = new DeclarationLocation(
1171 fileObject, nodeOffset, candidate);
1173 if (!CHOOSE_ONE_DECLARATION && methods.size() > 1) {
1174 // Could the :nodoc: alternatives: if there is only one nodoc'ed alternative
1176 int not_nodoced = 0;
1177 for (final IndexedMethod mtd : methods) {
1178 if (!mtd.isNoDoc()) {
1182 if (not_nodoced >= 2) {
1183 for (final IndexedMethod mtd : methods) {
1184 loc.addAlternative(new RubyAltLocation(mtd, mtd == candidate));
1192 return DeclarationLocation.NONE;
1195 /** Locate the method declaration for the given method call */
1196 public IndexedMethod findMethodDeclaration(Parser.Result parserResult, Node callNode, AstPath path,
1197 Set<IndexedMethod>[] alternativesHolder) {
1198 int astOffset = AstUtilities.getCallRange(callNode).getStart();
1200 // Is this a require-statement? If so, jump to the required file
1202 Document doc = RubyUtils.getDocument(parserResult);
1207 // Determine the bias (if the caret is between two tokens, did we
1208 // click on a link for the left or the right?
1209 int lexOffset = LexUtilities.getLexerOffset(parserResult, astOffset);
1210 if (lexOffset == -1) {
1213 OffsetRange range = getReferenceSpan(doc, lexOffset);
1215 if (range == OffsetRange.NONE) {
1219 boolean leftSide = range.getEnd() <= astOffset;
1221 Node root = AstUtilities.getRoot(parserResult);
1223 RubyIndex index = RubyIndex.get(parserResult);
1225 // No parse tree - try to just use the syntax info to do a simple index lookup
1226 // for methods and classes
1227 String text = doc.getText(range.getStart(), range.getLength());
1230 if ((index == null) || (text.length() == 0)) {
1234 if (Character.isUpperCase(text.charAt(0))) {
1235 // A Class or Constant?
1236 // Not a method call
1240 Set<IndexedMethod> methods = index.getMethods(text, QuerySupport.Kind.EXACT);
1242 BaseDocument bdoc = (BaseDocument)doc;
1243 IndexedMethod candidate =
1244 findBestMethodMatch(text, methods, bdoc,
1245 astOffset, lexOffset, null, null, index);
1248 } // TODO: @ - field?
1251 TokenHierarchy<Document> th = TokenHierarchy.get(doc);
1253 int tokenOffset = astOffset;
1255 if (leftSide && (tokenOffset > 0)) {
1260 String name = ((INameNode)callNode).getName();
1261 String fqn = AstUtilities.getFqnName(path);
1263 if ((fqn == null) || (fqn.length() == 0)) {
1264 fqn = "Object"; // NOI18N
1267 Call call = Call.getCallType((BaseDocument)doc, th, lexOffset);
1268 boolean skipPrivate = true;
1269 boolean done = call.isMethodExpected();
1270 boolean skipInstanceMethods = call.isStatic();
1272 RubyType type = call.getType();
1273 String lhs = call.getLhs();
1274 QuerySupport.Kind kind = QuerySupport.Kind.EXACT;
1276 Node node = callNode;
1277 if ((!type.isKnown()) && (lhs != null) && (node != null) && call.isSimpleIdentifier()) {
1278 Node method = AstUtilities.findLocalScope(node, path);
1280 if (method != null) {
1281 // TODO - if the lhs is "foo.bar." I need to split this
1282 // up and do it a bit more cleverly
1283 ContextKnowledge knowledge = new ContextKnowledge(
1284 index, root, method, astOffset, lexOffset, AstUtilities.getParseResult(parserResult));
1285 RubyTypeInferencer inferencer = RubyTypeInferencer.create(knowledge);
1286 type = inferencer.inferType(lhs);
1290 // I'm not doing any data flow analysis at this point, so
1291 // I can't do anything with a LHS like "foo.". Only actual types.
1292 if (type.isKnown()) {
1293 if ("self".equals(lhs)) {
1294 type = RubyType.create(fqn);
1295 skipPrivate = false;
1296 } else if ("super".equals(lhs)) {
1297 skipPrivate = false;
1299 IndexedClass sc = index.getSuperclass(fqn);
1302 type = RubyType.create(sc.getFqn());
1304 ClassNode cls = AstUtilities.findClass(path);
1307 type = RubyType.create(AstUtilities.getSuperclass(cls));
1311 if (!type.isKnown()) {
1312 type = RubyType.OBJECT; // NOI18N
1316 if (call == Call.LOCAL && fqn != null && fqn.length() == 0) {
1320 Set<IndexedMethod> methods = getApplicableMethods(name, fqn, type, call, index);
1322 if (name.equals("new")) { // NOI18N
1323 // Also look for initialize
1324 Set<IndexedMethod> initializeMethods = getApplicableMethods("initialize", fqn, type, call, index);
1325 methods.addAll(initializeMethods);
1328 IndexedMethod candidate =
1329 findBestMethodMatch(name, methods, (BaseDocument)doc,
1330 astOffset, lexOffset, path, callNode, index);
1332 if (alternativesHolder != null) {
1333 alternativesHolder[0] = methods;
1336 } catch (BadLocationException ble) {
1337 // do nothing - see #154991
1343 @SuppressWarnings("empty-statement")
1344 private DeclarationLocation findRDocMethod(ParserResult info, Document doc, int astOffset, int lexOffset,
1345 Node root, AstPath path, Node closest, RubyIndex index) {
1346 // TokenHierarchy<Document> th = TokenHierarchy.get(doc);
1348 TokenSequence<?> ts = LexUtilities.getRubyTokenSequence((BaseDocument)doc, lexOffset);
1349 if (ts == null) return DeclarationLocation.NONE;
1353 if (!ts.moveNext() && !ts.movePrevious()) return DeclarationLocation.NONE;
1355 Token<?> token = ts.token();
1356 TokenSequence<?> embedded = ts.embedded();
1358 if (embedded != null) {
1359 embedded.move(lexOffset);
1361 if (!embedded.moveNext() && !embedded.movePrevious()) return DeclarationLocation.NONE;
1363 token = embedded.token();
1366 // Is this a comment? If so, possibly do rdoc-method reference jump
1367 if (token != null && token.id() == RubyCommentTokenId.COMMENT_LINK) {
1368 // TODO - use findLinkedMethod
1369 String method = token.text().toString();
1371 if (method.startsWith("#")) {
1372 method = method.substring(1);
1374 DeclarationLocation loc = findMethod(info, root, method, Arity.UNKNOWN);
1376 // It looks like "#foo" can refer not just to methods (as rdoc suggested) but to
1377 // attributes as well - in Rails' initializer.rb this is used in a number of places.
1378 return loc != DeclarationLocation.NONE ? loc : findInstance(info, root, "@" + method, index);
1380 // A URL such as http://netbeans.org - try to open it in a browser!
1382 return new DeclarationLocation(new URL(method));
1383 } catch (MalformedURLException mue) {
1384 // URL is from user source... don't complain with exception dialogs etc.
1389 // Probably a Class#method
1390 int methodIndex = method.indexOf("#");
1391 if (methodIndex != -1 && methodIndex < method.length()-1) {
1392 String clz = method.substring(0, methodIndex);
1393 method = method.substring(methodIndex+1);
1395 return findMethod(method, null, RubyType.create(clz), Call.UNKNOWN,
1396 info, astOffset, lexOffset, path, closest, index);
1400 return DeclarationLocation.NONE;
1403 @SuppressWarnings("empty-statement")
1404 DeclarationLocation findLinkedMethod(ParserResult info, String method) {
1405 Node root = AstUtilities.getRoot(info);
1406 AstPath path = new AstPath();
1408 Node closest = root;
1411 RubyIndex index = getIndex(info);
1414 return DeclarationLocation.NONE;
1417 if (method.startsWith("#")) {
1418 method = method.substring(1);
1420 DeclarationLocation loc = findMethod(info, root, method, Arity.UNKNOWN);
1422 // It looks like "#foo" can refer not just to methods (as rdoc suggested)
1423 // but to attributes as well - in Rails' initializer.rb this is used
1424 // in a number of places.
1425 if (loc == DeclarationLocation.NONE) {
1426 loc = findInstance(info, root, "@" + method, index);
1431 // A URL such as http://netbeans.org - try to open it in a browser!
1433 URL url = new URL(method);
1435 return new DeclarationLocation(url);
1436 } catch (MalformedURLException mue) {
1437 // URL is from user source... don't complain with exception dialogs etc.
1442 // Probably a Class#method
1443 int methodIndex = method.indexOf("#");
1444 if (methodIndex != -1 && methodIndex < method.length()-1) {
1445 String clz = method.substring(0, methodIndex);
1446 method = method.substring(methodIndex+1);
1448 return findMethod(method, null, RubyType.create(clz), Call.UNKNOWN, info, astOffset, lexOffset, path, closest, index);
1451 return DeclarationLocation.NONE;
1454 IndexedMethod findBestMethodMatch(String name, Set<IndexedMethod> methodSet,
1455 BaseDocument doc, int astOffset, int lexOffset, AstPath path, Node call, RubyIndex index) {
1456 // Make sure that the best fit method actually has a corresponding valid source location
1459 Set<IndexedMethod> methods = new HashSet<IndexedMethod>(methodSet);
1461 while (!methods.isEmpty()) {
1462 IndexedMethod method =
1463 findBestMethodMatchHelper(name, methods, doc, astOffset, lexOffset, path, call, index);
1464 Node node = AstUtilities.getForeignNode(method);
1470 if (!methods.contains(method)) {
1471 // Avoid infinite loop when we somehow don't find the node for
1472 // the best method and we keep trying it
1473 methods.remove(methods.iterator().next());
1475 methods.remove(method);
1479 // Dynamic methods that don't have source (such as the TableDefinition methods "binary", "boolean", etc.
1480 if (methodSet.size() > 0) {
1481 return methodSet.iterator().next();
1487 private IndexedMethod findBestMethodMatchHelper(String name, Set<IndexedMethod> methods,
1488 BaseDocument doc, int astOffset, int lexOffset, AstPath path, Node callNode, RubyIndex index) {
1489 Set<IndexedMethod> candidates = new HashSet<IndexedMethod>();
1491 // 1. First see if the reference is fully qualified. If so the job should
1492 // be easier: prune the result set down
1493 // If I have the fqn, I can also call RubyIndex.getRDocLocation to pick the
1495 if (callNode instanceof CallNode) {
1496 Node node = ((CallNode)callNode).getReceiver();
1499 if (node instanceof Colon2Node) {
1500 fqn = AstUtilities.getFqn((Colon2Node)node);
1501 } else if (node instanceof ConstNode) {
1502 fqn = ((ConstNode)node).getName();
1506 while ((fqn != null) && (fqn.length() > 0)) {
1507 for (IndexedMethod method : methods) {
1508 if (fqn.equals(method.getClz())) {
1509 candidates.add(method);
1513 // Check inherited methods; for example, if we've determined
1514 // that you're looking for Integer::foo, I should happily match
1516 IndexedClass superClass = index.getSuperclass(fqn);
1518 if (superClass != null) {
1519 fqn = superClass.getSignature();
1527 if (candidates.size() == 1) {
1528 return candidates.iterator().next();
1529 } else if (!candidates.isEmpty()) {
1530 methods = candidates;
1533 // 2. See if the reference is not qualified (no :: or . prior to
1534 // the method call; if so it must be an inherited method (or a local
1535 // method, but we've already checked that possibility before getting
1536 // into the index search)
1537 TokenHierarchy<Document> th = TokenHierarchy.get((Document)doc);
1539 Call call = Call.getCallType(doc, th, lexOffset);
1540 boolean skipPrivate = true;
1542 if ((path != null) && (callNode != null) && (call != Call.LOCAL) && (call != Call.NONE)) {
1543 boolean skipInstanceMethods = call.isStatic();
1545 candidates = new HashSet<IndexedMethod>();
1547 RubyType type = call.getType();
1549 // I'm not doing any data flow analysis at this point, so
1550 // I can't do anything with a LHS like "foo.". Only actual types.
1551 if (type.isKnown()) {
1552 String lhs = call.getLhs();
1554 String fqn = AstUtilities.getFqnName(path);
1556 // TODO for self and super, rather than computing ALL inherited methods
1557 // (and picking just one of them), I should use the FIRST match as the
1558 // one to show! (closest super class or include definition)
1559 if ("self".equals(lhs)) {
1560 type = RubyType.create(fqn);
1561 skipPrivate = false;
1562 } else if ("super".equals(lhs)) {
1563 skipPrivate = false;
1565 IndexedClass sc = index.getSuperclass(fqn);
1568 type = RubyType.create(sc.getFqn());
1570 ClassNode cls = AstUtilities.findClass(path);
1573 type = RubyType.create(AstUtilities.getSuperclass(cls));
1578 if (type.isKnown()) {
1579 // Possibly a class on the left hand side: try searching with the class as a qualifier.
1580 // Try with the LHS + current FQN recursively. E.g. if we're in
1581 // Test::Unit when there's a call to Foo.x, we'll try
1582 // Test::Unit::Foo, and Test::Foo
1583 while (candidates.isEmpty()) {
1584 candidates = index.getInheritedMethods(fqn + "::" + type, name,
1585 QuerySupport.Kind.EXACT);
1587 int f = fqn.lastIndexOf("::");
1592 fqn = fqn.substring(0, f);
1596 // Add methods in the class (without an FQN)
1597 if (candidates.isEmpty()) {
1598 candidates = index.getInheritedMethods(type, name, QuerySupport.Kind.EXACT);
1603 if (skipPrivate || skipInstanceMethods) {
1604 Set<IndexedMethod> m = new HashSet<IndexedMethod>();
1606 for (IndexedMethod method : candidates) {
1607 // Don't include private or protected methods on other objects
1608 if (skipPrivate && (method.isPrivate() && !"new".equals(method.getName()))) {
1609 // TODO - "initialize" removal here should not be necessary since they should
1610 // be marked as private, but index doesn't contain that yet
1614 // We can only call static methods
1615 if (skipInstanceMethods && !method.isStatic()) {
1625 // First try to limit the candidates down to the ones that match the lhs type, if we
1626 // are calling new or initialize
1627 Set<IndexedMethod> cs = new HashSet<IndexedMethod>();
1629 for (IndexedMethod m : candidates) {
1630 // AppendIO might be the lhs - e.g. AppendIO.new, yet its FQN is Shell::AppendIO
1631 // so do suffix comparison
1632 if ((m.getIn() != null) && type.isSingleton() && m.getIn().endsWith(type.first())) {
1637 if (cs.size() < candidates.size()) candidates = cs;
1640 if (candidates.size() == 1) {
1641 return candidates.iterator().next();
1642 } else if (!candidates.isEmpty()) {
1643 methods = candidates;
1646 // 3. Prefer methods with extra index attributes since these tend to be important
1647 // methods (e.g. pick ActiveRecord::ConnectionAdapters::SchemaStatements instead
1648 // of the many overrides of that method
1649 // (A more general solution would be to prefer ancestor classes' implementations
1650 // over superclasses' implementations
1651 candidates = new HashSet<IndexedMethod>();
1653 for (IndexedMethod method : methods) {
1654 String attributes = method.getEncodedAttributes();
1655 if (attributes != null && attributes.length() > 3) {
1656 candidates.add(method);
1660 if (candidates.size() == 1) {
1661 return candidates.iterator().next();
1662 } else if (!candidates.isEmpty()) {
1663 methods = candidates;
1666 // 4. Use method arity to rule out mismatches
1667 // TODO - this is tricky since Ruby lets you specify more or fewer
1668 // parameters with some reasonable behavior...
1669 // Possibly I should do this check further down since the
1670 // other heuristics may work better as a first-level disambiguation
1672 // 4. Check to see which classes are required directly from this file, and
1673 // prefer matches that are in this set of classes
1674 Set<String> requires = null;
1677 candidates = new HashSet<IndexedMethod>();
1679 requires = AstUtilities.getRequires(path.root());
1681 for (IndexedMethod method : methods) {
1682 String require = method.getRequire();
1684 if (requires.contains(require)) {
1685 candidates.add(method);
1689 if (candidates.size() == 1) {
1690 return candidates.iterator().next();
1691 } else if (!candidates.isEmpty()) {
1692 methods = candidates;
1696 // 3. See if any of the methods are in "kernel" classes (builtins) and for these
1697 // go to the known locations
1698 candidates = new HashSet<IndexedMethod>();
1700 for (IndexedMethod method : methods) {
1701 String url = method.getFileUrl();
1703 if (RubyUtils.isRubyStubsURL(url)) {
1704 candidates.add(method);
1708 if (candidates.size() == 1) {
1709 return candidates.iterator().next();
1710 } else if (!candidates.isEmpty()) {
1711 methods = candidates;
1714 // 4. See which methods are documented, and prefer those over undocumented methods
1715 candidates = new HashSet<IndexedMethod>();
1717 int longestDocLength = 0;
1719 for (IndexedMethod method : methods) {
1720 int length = method.getDocumentationLength();
1722 if (length > longestDocLength) {
1724 candidates.add(method);
1725 longestDocLength = length;
1726 } else if ((length > 0) && (length == longestDocLength)) {
1727 candidates.add(method);
1731 if (candidates.size() == 1) {
1732 return candidates.iterator().next();
1733 } else if (!candidates.isEmpty()) {
1734 methods = candidates;
1737 // 5. Look at transitive closure of require statements and see which files
1738 // are most likely candidates
1739 if ((index != null) && (requires != null)) {
1740 candidates = new HashSet<IndexedMethod>();
1742 Set<String> allRequires = index.getRequiresTransitively(requires);
1744 for (IndexedMethod method : methods) {
1745 String require = method.getRequire();
1747 if (allRequires.contains(require)) {
1748 candidates.add(method);
1752 if (candidates.size() == 1) {
1753 return candidates.iterator().next();
1754 } else if (!candidates.isEmpty()) {
1755 methods = candidates;
1759 // 6. Other heuristics: Look at the method definition with the
1760 // class with most methods associated with it. Look at other uses of this
1761 // method in this parse tree and see if I can figure out the containing class
1762 // or rule out other candidates based on that
1764 // 7. Look at superclasses and consider -their- requires to figure out
1765 // which class we're looking for methods in
1768 // Pick one arbitrarily
1769 if (methods.size() > 0) {
1770 return methods.iterator().next();
1776 private DeclarationLocation findLocal(ParserResult info, Node node, String name) {
1777 if (node instanceof LocalAsgnNode) {
1778 if (((INameNode)node).getName().equals(name)) return getLocation(info, node);
1779 } else if (!ignoreAlias && node instanceof AliasNode) {
1780 String newName = AstUtilities.getNameOrValue(((AliasNode)node).getNewName());
1782 if (name.equals(newName)) return getLocation(info, node);
1783 } else if (node instanceof IParameterScope) {
1784 ILocalVariable parameter = ((IParameterScope) node).getParameterNamed(name);
1786 return parameter != null ? getLocation(info, (Node) parameter) : DeclarationLocation.NONE;
1789 List<Node> list = node.childNodes();
1791 for (Node child : list) {
1792 DeclarationLocation location = findLocal(info, child, name);
1794 if (location != DeclarationLocation.NONE) return location;
1797 return DeclarationLocation.NONE;
1800 private DeclarationLocation findDynamic(ParserResult info, Node node, String name) {
1801 if (node instanceof DAsgnNode || node instanceof ArgumentNode && node.isBlockParameter()) {
1802 if (((INameNode)node).getName().equals(name)) return getLocation(info, node);
1803 } else if (!ignoreAlias && node instanceof AliasNode) {
1804 if (name.equals(AstUtilities.getNameOrValue(((AliasNode)node).getNewName()))) return getLocation(info, node);
1807 for (Node child : node.childNodes()) {
1808 DeclarationLocation location = findDynamic(info, child, name);
1810 if (location != DeclarationLocation.NONE) return location;
1813 return DeclarationLocation.NONE;
1816 private DeclarationLocation findInstance(ParserResult info, Node node, String name, RubyIndex index) {
1817 if (node instanceof InstAsgnNode) {
1818 if (((INameNode)node).getName().equals(name)) {
1819 return getLocation(info, node);
1821 } else if (!ignoreAlias && node instanceof AliasNode) {
1822 String newName = AstUtilities.getNameOrValue(((AliasNode)node).getNewName());
1823 if (name.equals(newName)) {
1824 return getLocation(info, node);
1826 } else if (AstUtilities.isAttr(node)) {
1827 // TODO: Compute the symbols and check for equality
1828 // attr_reader, attr_accessor, attr_writer
1829 SymbolNode[] symbols = AstUtilities.getAttrSymbols(node);
1831 for (int i = 0; i < symbols.length; i++) {
1832 // possibly an instance variable referred by attr_accessor and like
1833 if (name.equals(symbols[i].getName())) {
1834 Node root = AstUtilities.getRoot(info);
1835 DeclarationLocation location =
1836 findInstanceFromIndex(info, name, new AstPath(root, node), index, true);
1837 if (location != DeclarationLocation.NONE) {
1840 return getLocation(info, symbols[i]);
1845 for (Node child : node.childNodes()) {
1846 DeclarationLocation location = findInstance(info, child, name, index);
1848 if (location != DeclarationLocation.NONE) return location;
1851 return DeclarationLocation.NONE;
1854 private DeclarationLocation findClassVar(ParserResult info, Node node, String name) {
1855 if (node instanceof ClassVarDeclNode) {
1856 if (((INameNode)node).getName().equals(name)) {
1857 return getLocation(info, node);
1859 } else if (!ignoreAlias && node instanceof AliasNode) {
1860 String newName = AstUtilities.getNameOrValue(((AliasNode)node).getNewName());
1861 if (name.equals(newName)) {
1862 return getLocation(info, node);
1865 // TODO: Are there attr readers and writers for class variables?
1866 // } else if (AstUtilities.isAttrReader(node) || AstUtilities.isAttrWriter(node)) {
1867 // // TODO: Compute the symbols and check for equality
1868 // // attr_reader, attr_accessor, attr_writer
1869 // SymbolNode[] symbols = AstUtilities.getAttrSymbols(node);
1871 // for (int i = 0; i < symbols.length; i++) {
1872 // if (name.equals("@" + symbols[i].getName())) {
1873 // return getLocation(info, symbols[i]);
1878 for (Node child : node.childNodes()) {
1879 DeclarationLocation location = findClassVar(info, child, name);
1881 if (location != DeclarationLocation.NONE) return location;
1884 return DeclarationLocation.NONE;
1887 private DeclarationLocation findInstanceFromIndex(ParserResult info, String name, AstPath path, RubyIndex index, boolean inherited) {
1888 String fqn = AstUtilities.getFqnName(path);
1890 // TODO - if fqn has multiple ::'s, try various combinations? or is
1891 // add inherited already doing that?
1892 Set<IndexedField> f = index.getInheritedFields(fqn, name, QuerySupport.Kind.EXACT, inherited);
1893 for (IndexedField field : f) {
1894 // How do we choose one?
1895 // For now, just pick the first one
1897 Node node = AstUtilities.getForeignNode(field);
1900 return new DeclarationLocation(field.getFileObject(),
1901 node.getPosition().getStartOffset(), field);
1905 return DeclarationLocation.NONE;
1908 private DeclarationLocation findInstanceMethodsFromIndex(ParserResult info, String name, AstPath path, RubyIndex index) {
1909 String fqn = AstUtilities.getFqnName(path);
1910 Set<IndexedMethod> methods = index.getInheritedMethods(fqn, name, QuerySupport.Kind.EXACT);
1911 return getLocation(methods);
1914 private DeclarationLocation findGlobal(ParserResult info, Node node, String name) {
1915 if (node instanceof GlobalAsgnNode) {
1916 if (((INameNode)node).getName().equals(name)) {
1917 return getLocation(info, node);
1919 } else if (!ignoreAlias && node instanceof AliasNode) {
1920 String newName = AstUtilities.getNameOrValue(((AliasNode)node).getNewName());
1921 if (name.equals(newName)) {
1922 return getLocation(info, node);
1926 for (Node child : node.childNodes()) {
1927 DeclarationLocation location = findGlobal(info, child, name);
1929 if (location != DeclarationLocation.NONE) return location;
1932 return DeclarationLocation.NONE;
1935 private DeclarationLocation findMethod(ParserResult info, Node node, String name, Arity arity) {
1936 // Recursively search for methods or method calls that match the name and arity
1937 if (node instanceof MethodDefNode) {
1938 if (((MethodDefNode)node).getName().equals(name) &&
1939 Arity.matches(arity, Arity.getArity((MethodDefNode) node))) {
1940 return getLocation(info, node);
1942 } else if (!ignoreAlias && node instanceof AliasNode) {
1943 String newName = AstUtilities.getNameOrValue(((AliasNode)node).getNewName());
1944 if (name.equals(newName)) {
1945 // No obvious way to check arity
1946 return getLocation(info, node);
1950 for (Node child : node.childNodes()) {
1951 DeclarationLocation location = findMethod(info, child, name, arity);
1953 if (location != DeclarationLocation.NONE) return location;
1956 return DeclarationLocation.NONE;