ruby/src/org/netbeans/modules/ruby/RubyDeclarationFinder.java
author enebo@netbeans.org
Sun, 08 Dec 2013 11:39:16 -0600
changeset 4554 07958c1ff402
parent 4553 32cac83e9ae7
permissions -rw-r--r--
Too much stuff in one commit. Rename blocks now follows language semantics. Removal of more ASTPath consumers
     1 /*
     2  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     3  *
     4  * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
     5  *
     6  * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
     7  * Other names may be trademarks of their respective owners.
     8  *
     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]"
    26  *
    27  * Contributor(s):
    28  *
    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.
    32  *
    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.
    43  */
    44 package org.netbeans.modules.ruby;
    45 
    46 import java.net.MalformedURLException;
    47 import java.net.URL;
    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;
    53 import java.util.Set;
    54 import java.util.concurrent.atomic.AtomicReference;
    55 
    56 import javax.swing.text.BadLocationException;
    57 import javax.swing.text.Document;
    58 
    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;
   114 
   115 
   116 /**
   117  * Find a declaration from an element in the JRuby AST.
   118  *
   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
   123  *   for aliases!
   124  * @todo If you're looking for a local class, such as a Rails model, I should
   125  *   find those first!
   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)
   128  * 
   129  * @author Tor Norbye
   130  */
   131 public class RubyDeclarationFinder extends RubyDeclarationFinderHelper implements DeclarationFinder {
   132 
   133     /** An increasing number; I will be using this number modulo the  */
   134     private static int methodSelector = 0;
   135 
   136     /** When true, don't match alias nodes as reads. Used during traversal of the AST. */
   137     private boolean ignoreAlias;
   138 
   139     private RubyIndex rubyIndex;
   140 
   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};
   146 
   147     private static final List<String> RAILS_TARGETS = initRailsTargets();
   148 
   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 + "=>");
   156         }
   157         return result;
   158     }
   159     /** Creates a new instance of RubyDeclarationFinder */
   160     public RubyDeclarationFinder() {
   161     }
   162 
   163     private RubyIndex getIndex(ParserResult result) {
   164         if (rubyIndex == null) {
   165             rubyIndex = RubyIndex.get(result);
   166         }
   167         return rubyIndex;
   168     }
   169 
   170     @Override
   171     public OffsetRange getReferenceSpan(Document document, int lexOffset) {
   172         TokenHierarchy<Document> th = TokenHierarchy.get(document);
   173         
   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) {
   179                 return target.range;
   180             }
   181         }
   182         
   183         TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(th, lexOffset);
   184 
   185         if (ts == null) {
   186             return OffsetRange.NONE;
   187         }
   188 
   189         ts.move(lexOffset);
   190 
   191         if (!ts.moveNext() && !ts.movePrevious()) {
   192             return OffsetRange.NONE;
   193         }
   194 
   195         // Determine whether the caret position is right between two tokens
   196         boolean isBetween = (lexOffset == ts.offset());
   197 
   198         OffsetRange range = getReferenceSpan(ts, th, lexOffset);
   199 
   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);
   205             }
   206         }
   207 
   208         return range;
   209     }
   210 
   211     private OffsetRange getReferenceSpan(TokenSequence<?> ts,
   212         TokenHierarchy<Document> th, int lexOffset) {
   213         Token<?> token = ts.token();
   214         TokenId id = token.id();
   215 
   216         if (id == RubyTokenId.IDENTIFIER) {
   217             if (token.length() == 1 && id == RubyTokenId.IDENTIFIER && token.text().toString().equals(",")) {
   218                 return OffsetRange.NONE;
   219             }
   220         }
   221 
   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());
   228         }
   229 
   230         // Look for embedded RDoc comments:
   231         TokenSequence<?> embedded = ts.embedded();
   232 
   233         if (embedded != null) {
   234             ts = embedded;
   235             embedded.move(lexOffset);
   236 
   237             if (embedded.moveNext()) {
   238                 Token<?> embeddedToken = embedded.token();
   239 
   240                 if (embeddedToken.id() == RubyCommentTokenId.COMMENT_LINK) {
   241                     return new OffsetRange(embedded.offset(),
   242                         embedded.offset() + embeddedToken.length());
   243                 }
   244                 // Recurse into the range - perhaps there is Ruby code (identifiers
   245 
   246                 // etc.) to follow there
   247                 OffsetRange range = getReferenceSpan(embedded, th, lexOffset);
   248 
   249                 if (range != OffsetRange.NONE) {
   250                     return range;
   251                 }
   252             }
   253         }
   254 
   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);
   258 
   259             if (requireStart != -1) {
   260                 String require = LexUtilities.getStringAt(lexOffset, th);
   261 
   262                 if (require != null) {
   263                     return new OffsetRange(requireStart, requireStart + require.length());
   264                 }
   265             }
   266             
   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());
   272                 }
   273             }
   274         }
   275 
   276         return OffsetRange.NONE;
   277     }
   278 
   279     @Override
   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;
   284 
   285             final AtomicReference<DeclarationLocation> out = new AtomicReference<DeclarationLocation>(DeclarationLocation.NONE);
   286 
   287             // This will only change out if the result is different than NONE.
   288             document.render(new Runnable() {
   289                 @Override
   290                 public void run() {
   291                     try {
   292                         TokenHierarchy<Document> th = TokenHierarchy.get(document);
   293                         BaseDocument doc = (BaseDocument)document;
   294 
   295                         int astOffset = AstUtilities.getAstOffset(parserResult, lexOffset);
   296                         if (astOffset == -1) return;
   297 
   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);
   301 
   302                             if (loc != DeclarationLocation.NONE) {
   303                                 out.set(loc);
   304                                 return;
   305                             }
   306                         }
   307 
   308                         OffsetRange range = getReferenceSpan(doc, lexOffset);
   309 
   310                         if (range == OffsetRange.NONE) return;
   311 
   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;
   315 
   316                         Node root = AstUtilities.getRoot(parserResult);
   317 
   318                         RubyIndex index = getIndex(parserResult);
   319                         if (root == null) {
   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());
   323 
   324                             if (index == null || text.length() == 0) return;
   325 
   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);
   329 
   330                                 if (classes.isEmpty()) return;
   331 
   332                                 RubyClassDeclarationFinder cdf = new RubyClassDeclarationFinder(null, null, null, index, null);
   333                                 DeclarationLocation l = cdf.getElementDeclaration(classes, null);
   334 
   335                                 if (l != null) {
   336                                     out.set(l);
   337                                     return;
   338                                 }
   339                             } else {
   340                                 // A method?
   341                                 Set<IndexedMethod> methods = index.getMethods(text, (String) null, QuerySupport.Kind.EXACT);
   342 
   343                                 if (methods.isEmpty()) {
   344                                     methods = index.getMethods(text, QuerySupport.Kind.EXACT);
   345                                 }
   346 
   347                                 DeclarationLocation l = getMethodDeclaration(parserResult, text, methods,
   348                                         null, null, index, astOffset, lexOffset);
   349 
   350                                 if (l != null) {
   351                                     out.set(l);
   352                                     return;
   353                                 }
   354                             } // TODO: @ - field?
   355 
   356                             return;
   357                         }
   358 
   359                         int tokenOffset = lexOffset;
   360 
   361                         if (leftSide && (tokenOffset > 0)) tokenOffset--;
   362 
   363                         // See if the hyperlink is for the string in a require statement
   364                         int requireStart = LexUtilities.getRequireStringOffset(tokenOffset, th);
   365 
   366                         if (requireStart != -1) {
   367                             String require = LexUtilities.getStringAt(tokenOffset, th);
   368 
   369                             if (require != null) {
   370                                 FileObject fo = index.getRequiredFile(require);
   371 
   372                                 if (fo != null) {
   373                                     out.set(new DeclarationLocation(fo, 0));
   374                                     return;
   375                                 }
   376                             }
   377 
   378                             // It's in a require string so no possible other matches
   379                             return;
   380                         }
   381 
   382                         AstPath path = new AstPath(root, astOffset);
   383                         Node closest = AstUtilities.findNodeAtOffset(root, astOffset);
   384                         if (closest == null) return;
   385 
   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);
   388 
   389                         if (rdoc != DeclarationLocation.NONE) {
   390                             out.set(fix(rdoc, parserResult));
   391                             return;
   392                         }
   393 
   394                         if (closest instanceof ILocalVariable) {
   395                             ILocalVariable declaration = ((ILocalVariable) closest).getDeclaration();
   396                             
   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) {
   407                             // A method call
   408                             String name = ((INameNode)closest).getName();
   409                             Call call = Call.getCallType(doc, th, lexOffset);
   410                             RubyType type = call.getType();
   411                             String lhs = call.getLhs();
   412 
   413                             if (!type.isKnown() && lhs != null && call.isSimpleIdentifier()) {
   414                                 Node method = AstUtilities.findLocalScope(closest, path);
   415 
   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);
   421                             }
   422 
   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
   429 
   430                                 if (name.equals("new")) name = "initialize"; // NOI18N
   431 
   432                                 Arity arity = Arity.getCallArity(closest);
   433 
   434                                 DeclarationLocation loc = fix(findMethod(parserResult, root, name, arity), parserResult);
   435 
   436                                 if (loc != DeclarationLocation.NONE) {
   437                                     out.set(loc);
   438                                     return;
   439                                 }
   440                             }
   441 
   442                             String fqn = AstUtilities.getFqnName(path);
   443                             if (call == Call.LOCAL && fqn != null && fqn.length() == 0) fqn = "Object"; // NOI18N
   444 
   445                             out.set(findMethod(name, fqn, type, call, parserResult, astOffset, lexOffset, path, closest, index));
   446                         } else if (closest instanceof ConstNode || closest instanceof Colon2Node) {
   447                             // try Class usage
   448                             RubyClassDeclarationFinder classDF = new RubyClassDeclarationFinder(parserResult, root, path, index, closest);
   449                             DeclarationLocation decl = classDF.findClassDeclaration();
   450                             if (decl != DeclarationLocation.NONE) {
   451                                 out.set(decl);
   452                                 return;
   453                             }
   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();
   459 
   460                             // Search for methods, fields, etc.
   461                             Arity arity = Arity.UNKNOWN;
   462                             DeclarationLocation location = findMethod(parserResult, root, name, arity);
   463 
   464                             // search for AR associations
   465                             if (location == DeclarationLocation.NONE) {
   466                                 location = new ActiveRecordAssociationFinder(index, (SymbolNode) closest, root, path).findAssociationLocation();
   467                             }
   468 
   469                             // search for helpers
   470                             if (location == DeclarationLocation.NONE) {
   471                                 location = new HelpersFinder(index, (SymbolNode) closest, path).findHelperLocation();
   472                             }
   473 
   474                             if (location == DeclarationLocation.NONE) {
   475                                 location = findInstance(parserResult, root, name, index);
   476                             }
   477 
   478                             if (location == DeclarationLocation.NONE) {
   479                                 location = findClassVar(parserResult, root, name);
   480                             }
   481 
   482                             if (location == DeclarationLocation.NONE) {
   483                                 location = findGlobal(parserResult, root, name);
   484                             }
   485 
   486                             if (location == DeclarationLocation.NONE) {
   487                                 RubyClassDeclarationFinder cdf = new RubyClassDeclarationFinder();
   488                                 Node clz = cdf.findClass(root, ((INameNode)closest).getName(), ignoreAlias);
   489 
   490                                 if (clz != null) location = getLocation(parserResult, clz);
   491                             }
   492 
   493                             // methods
   494                             if (location == DeclarationLocation.NONE) {
   495                                 location = findInstanceMethodsFromIndex(parserResult, name, path, index);
   496                             }
   497                             // fields
   498                             if (location == DeclarationLocation.NONE) {
   499                                 location = findInstanceFromIndex(parserResult, name, path, index, true);
   500                             }
   501 
   502                             out.set(fix(location, parserResult));
   503                         } else if (closest instanceof AliasNode) {
   504                             AliasNode an = (AliasNode)closest;
   505 
   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;
   509 
   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();
   519 
   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;
   528                                     ignoreAlias = true;
   529 
   530                                     try {
   531                                         DeclarationLocation location =
   532                                                 findLocal(parserResult, AstUtilities.findLocalScope(closest, path), name);
   533 
   534                                         if (location == DeclarationLocation.NONE) {
   535                                             location = findDynamic(parserResult, AstUtilities.findDynamicScope(closest, path),
   536                                                     name);
   537                                         }
   538 
   539                                         if (location == DeclarationLocation.NONE) {
   540                                             location = findMethod(parserResult, root, name, Arity.UNKNOWN);
   541                                         }
   542 
   543                                         if (location == DeclarationLocation.NONE) {
   544                                             location = findInstance(parserResult, root, name, index);
   545                                         }
   546 
   547                                         if (location == DeclarationLocation.NONE) {
   548                                             location = findClassVar(parserResult, root, name);
   549                                         }
   550 
   551                                         if (location == DeclarationLocation.NONE) {
   552                                             location = findGlobal(parserResult, root, name);
   553                                         }
   554 
   555                                         if (location == DeclarationLocation.NONE) {
   556                                             RubyClassDeclarationFinder cdf = new RubyClassDeclarationFinder();
   557                                             Node clz = cdf.findClass(root, name, ignoreAlias);
   558 
   559                                             if (clz != null) {
   560                                                 location = getLocation(parserResult, clz);
   561                                             }
   562                                         }
   563 
   564                                         // TODO - what if we're aliasing another alias? I think that should show up in the various
   565                                         // other nodes
   566                                         if (location == DeclarationLocation.NONE) return;
   567 
   568                                         out.set(fix(location, parserResult));
   569                                     } finally {
   570                                         ignoreAlias = false;
   571                                     }
   572                                 } else {
   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));
   576                                 }
   577                             }
   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
   581 
   582                             Node parent = path.leafParent();
   583                             
   584                             if (parent != null) {
   585                                 if (parent instanceof MethodDefNode) return; // It's a method name
   586 
   587                                 // Parameter (check to see if its under ArgumentNode)
   588                                 Node method = AstUtilities.findLocalScope(closest, path);
   589 
   590                                 out.set(fix(findLocal(parserResult, method, name), parserResult));
   591                             } 
   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)));
   599                                 }
   600                             }
   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()) {
   605                                 case SCLASSNODE:
   606                                 case MODULENODE:
   607                                 case CLASSNODE: {
   608                                     IndexedClass superClass = index.getSuperclass(fqn);
   609                                     if (superClass != null) {
   610                                         out.set(getLocation(Collections.singleton(superClass)));
   611                                     }
   612                                     break;
   613                                 }
   614                                 case DEFNNODE:
   615                                 case DEFSNODE: {
   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)));
   620                                     }
   621                                     break;
   622                                 }
   623                             }
   624                         }
   625                     } catch (BadLocationException ble) {
   626                         // do nothing - see #154991
   627                     }
   628                 }
   629             });
   630 
   631             return out.get();
   632     }
   633 
   634     /** 
   635      * Compute the declaration location for a test string (such as MosModule::TestBaz/test_qux).
   636      * 
   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.
   640      */
   641     public static DeclarationLocation getTestDeclaration(FileObject fileInProject, String testString, boolean classLocation) {
   642         return getTestDeclaration(fileInProject, testString, classLocation, true);
   643     }
   644 
   645     public static DeclarationLocation getTestDeclaration(FileObject fileInProject, String testString, 
   646             boolean classLocation, boolean requireDeclaredClass) {
   647         
   648         int methodIndex = testString.indexOf('/'); //NOI18N
   649         if (methodIndex == -1) return DeclarationLocation.NONE;
   650 
   651         RubyIndex index = RubyIndex.get(QuerySupport.findRoots(fileInProject,
   652                 Collections.singleton(RubyLanguage.SOURCE),
   653                 Collections.singleton(RubyLanguage.BOOT),
   654                 Collections.<String>emptySet()));
   655 
   656         if (index == null) return DeclarationLocation.NONE;
   657 
   658         String className = testString.substring(0, methodIndex);
   659         String methodName = testString.substring(methodIndex+1);
   660 
   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));
   667             }
   668             return methodLocation;
   669         }
   670         Set<IndexedClass> classes =
   671                 index.getClasses(className, QuerySupport.Kind.EXACT, false, false, true, null);
   672         DeclarationLocation classDeclarationLocation = getLocation(classes);
   673         
   674         if (DeclarationLocation.NONE == methodLocation && classLocation) {
   675             return classDeclarationLocation;
   676         }
   677         if (methodLocation.getFileObject().equals(classDeclarationLocation.getFileObject())) {
   678             return classDeclarationLocation;
   679         }
   680 
   681         for (AlternativeLocation alt : classDeclarationLocation.getAlternativeLocations()) {
   682             if (methodLocation.getFileObject().equals(alt.getLocation().getFileObject())) {
   683                 return alt.getLocation();
   684             }
   685         }
   686 
   687         return classDeclarationLocation;
   688     }
   689 
   690     static DeclarationLocation getLocation(Set<? extends IndexedElement> elements) {
   691         DeclarationLocation loc = DeclarationLocation.NONE;
   692         for (IndexedElement element : elements) {
   693             FileObject fo = element.getFileObject();
   694             if (fo == null) {
   695                 continue;
   696             }
   697             if (loc == DeclarationLocation.NONE) {
   698                 int offset = -1;
   699                 Node node = AstUtilities.getForeignNode(element);
   700                 if (node != null) {
   701                     offset = AstUtilities.getRange(node).getStart();
   702                 }
   703                 loc = new DeclarationLocation(fo, offset, element);
   704                 loc.addAlternative(new RubyAltLocation(element, false));
   705             } else {
   706                 AlternativeLocation alternate = new RubyAltLocation(element, false);
   707                 loc.addAlternative(alternate);
   708             }
   709         }
   710         return loc;
   711     }
   712 
   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
   719 
   720                 boolean template = type.indexOf(TEMPLATE) != -1;
   721                 FileObject dir;
   722                 String name;
   723                 int slashIndex = target.name.lastIndexOf('/');
   724                 if (slashIndex != -1) {
   725                     
   726                     FileObject app = RubyUtils.getAppDir(RubyUtils.getFileObject(info));
   727                     if (app == null) {
   728                         return DeclarationLocation.NONE;
   729                     }
   730                     
   731                     String relativePath = target.name.substring(0, slashIndex);
   732                     dir = app.getFileObject("views/" + relativePath); // NOI18N
   733                     if (dir == null) {
   734                         return DeclarationLocation.NONE;
   735                     }
   736                     name = target.name.substring(slashIndex+1); // NOI18N
   737                     
   738                 } else {
   739                     dir = RubyUtils.getFileObject(info).getParent();
   740                     name = target.name; // NOI18N
   741                 }
   742 
   743                 if (!template) {
   744                     name = "_" + name;
   745                 }
   746                 
   747                 DeclarationLocation partialLocation = findPartial(name, dir);
   748                 if (partialLocation != DeclarationLocation.NONE) {
   749                     return partialLocation;
   750                 }
   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();
   756 
   757                 String action = null;
   758                 String fileName = file.getName();
   759                 boolean isController = type.indexOf(CONTROLLER) != -1; // NOI18N
   760                 String path = ""; // NOI18N
   761                 if (isController) {
   762                     path = target.name;
   763                 } else {
   764                     if (!fileName.startsWith("_")) { // NOI18N
   765                                                      // For partials like "_foo", just use the surrounding view
   766                         path = fileName;
   767                         action = RubyUtils.getFileObject(info).getName();
   768                     }
   769                 }
   770                 
   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];
   782                 }
   783                 if (controllerAction[1] != null) {
   784                     action = controllerAction[1];
   785                 }
   786 
   787                 if (!fromView) {
   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;
   797                     } else {
   798                         controllerName = RubyUtils.getFileObject(info).getName();
   799                     }
   800                     return findActionLocation(asControllerClass(controllerName), action, info);
   801                 }
   802 
   803                 // Find app dir, and build up a relative path to the view file in the process
   804                 FileObject app = file.getParent();
   805 
   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();
   810 
   811                         break;
   812                     }
   813 
   814                     path = app.getNameExt() + "/" + path; // NOI18N
   815                     app = app.getParent();
   816                 }
   817 
   818                 if (app != null) {
   819                     FileObject controllerFile = app.getFileObject("controllers/" + path + "_controller.rb"); // NOI18N
   820                     if (controllerFile != null) {
   821                         int offset = 0;
   822                         if (action != null) {
   823                             offset = AstUtilities.findOffset(controllerFile, action);
   824                             if (offset < 0) {
   825                                 offset = 0;
   826                             }
   827                         }
   828                         
   829                         return new DeclarationLocation(controllerFile, offset);
   830                     }
   831                 }
   832             }
   833         }
   834         
   835         return DeclarationLocation.NONE;
   836     }
   837 
   838     private static String asControllerClass(String controllerName) {
   839         String suffix = controllerName.endsWith("_controller") ? "" : "_controller";//NOI18N
   840         return RubyUtils.underlinedNameToCamel(controllerName + suffix);
   841     }
   842 
   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);
   847     }
   848 
   849     /**
   850      * Finds the location of the partial matching the given <code>name</code> in the
   851      * given <code>dir</code>.
   852      * 
   853      * @param name
   854      * @param dir
   855      * @return
   856      */
   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);
   862         }
   863         // try extensions
   864         for (String ext : RubyUtils.RUBY_VIEW_EXTS) {
   865             partial = dir.getFileObject(name + ext);
   866             if (partial != null) {
   867                 return new DeclarationLocation(partial, 0);
   868 
   869             }
   870         }
   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);
   875             }
   876         }
   877 
   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);
   885                 }
   886             }
   887         }
   888         return DeclarationLocation.NONE;
   889 
   890     }
   891     /** Locate the :action and :controller strings in the hash list that is under the
   892      * given offsets
   893      * @return A string[2] where string[0] is the controller or null, and string[1] is the
   894      *   action or null
   895      */
   896     private String[] findControllerAction(ParserResult info, int lexOffset, int astOffset) {
   897         String[] result = new String[2];
   898         
   899         Node root = AstUtilities.getRoot(info);
   900         if (root == null) {
   901             return result;
   902         }
   903         AstPath path = new AstPath(root, astOffset);
   904         Iterator<Node> it = path.leafToRoot();
   905         Node prev = null;
   906         while (it.hasNext()) {
   907             Node n = it.next();
   908             
   909             if (n instanceof HashNode) {
   910                 if (prev instanceof ListNode) { // uhm... why am I going back to prev?
   911                     List<Node> hashItems = prev.childNodes();
   912 
   913                     Iterator<Node> hi = hashItems.iterator();
   914                     while (hi.hasNext()) {
   915                         String from = null;
   916                         String to = null;
   917                         
   918                         Node f = hi.next();
   919                         if (f instanceof SymbolNode) {
   920                             from = ((SymbolNode)f).getName();
   921                         }
   922                         
   923                         if (hi.hasNext()) {
   924                             Node t = hi.next();
   925                             if (t instanceof StrNode) {
   926                                 to = ((StrNode)t).getValue().toString();
   927                             }
   928                         }
   929                         
   930                         if ("controller".equals(from)) { // NOI18N
   931                             result[0] = to;
   932                         } else if ("action".equals(from)) { // NOI18N
   933                             result[1] = to;
   934                         }
   935                     }
   936                     
   937                     break;
   938                 }
   939             }
   940             
   941             prev = n;
   942         }
   943         return result;
   944     }
   945 
   946     /** A result from findRailsTarget which computes sections that have special
   947      * hyperlink semantics - like link_to, render :partial, render :action, :controller etc.
   948      */
   949     private static class RailsTarget {
   950         RailsTarget(String type, String name, OffsetRange range) {
   951             this.type = type;
   952             this.range = range;
   953             this.name = name;
   954         }
   955         
   956         @Override
   957         public String toString() {
   958             return "RailsTarget(" + type + ", " + name + ", " + range + ")";
   959         }
   960 
   961         String name;
   962         OffsetRange range;
   963         String type;
   964     }
   965 
   966     private boolean fastCheckIsRailsTarget(String s) {
   967         for (String targetName : RAILS_TARGET_RAW_NAMES) {
   968             if (s.indexOf(targetName) != -1) {
   969                 return true;
   970             }
   971         }
   972         return false;
   973 
   974     }
   975 
   976     private RailsTarget findRailsTarget(BaseDocument doc, TokenHierarchy<Document> th, int lexOffset) {
   977         try {
   978             doc.readLock();
   979             // TODO - limit this to RHTML files only?
   980             int begin = Utilities.getRowStart(doc, lexOffset);
   981             if (begin != -1) {
   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)) {
   985                     return null;
   986                 }
   987                 for (String target : RAILS_TARGETS) {
   988                     int index = s.indexOf(target);
   989                     if (index != -1) {
   990                         // Find string
   991                         int nameOffset = begin+index+target.length();
   992                         TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(th, nameOffset);
   993                         if (ts == null) {
   994                             return null;
   995                         }
   996 
   997                         ts.move(nameOffset);
   998 
   999                         StringBuilder sb = new StringBuilder();
  1000                         boolean started = false;
  1001                         while (ts.moveNext() && ts.offset() < end) {
  1002                             started = true;
  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());
  1007                             }
  1008 
  1009                             if (!"string".equals(id.primaryCategory())) {
  1010                                 break;
  1011                             }
  1012                         }
  1013                         if (!started) {
  1014                             return null;
  1015                         }
  1016 
  1017                         int rangeEnd = ts.offset();
  1018 
  1019                         String name = sb.toString();
  1020 
  1021                         if (lexOffset <= rangeEnd && lexOffset >= begin+index) {
  1022                             OffsetRange range = new OffsetRange(begin+index, rangeEnd);
  1023                             return new RailsTarget(target, name, range);
  1024                         }
  1025                     }
  1026                 }
  1027             }
  1028         } catch (BadLocationException ble) {
  1029             Exceptions.printStackTrace(ble);
  1030         } finally {
  1031             doc.readUnlock();
  1032         }
  1033 
  1034         return null;
  1035     }
  1036     
  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);
  1040 
  1041         int astOffset = caretOffset;
  1042         DeclarationLocation l = getMethodDeclaration(info, name, methods, 
  1043              path, closest, index, astOffset, lexOffset);
  1044 
  1045         return l;
  1046     }
  1047 
  1048     private Set<IndexedMethod> getApplicableMethods(String name, String possibleFqn, 
  1049             RubyType type, Call call, RubyIndex index) {
  1050         Set<IndexedMethod> methods = new HashSet<IndexedMethod>();
  1051         String fqn;
  1052         if (!type.isKnown() && possibleFqn != null && call.getLhs() == null && call != Call.UNKNOWN) {
  1053             fqn = possibleFqn;
  1054 
  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);
  1061 
  1062                 int f = fqn.lastIndexOf("::");
  1063 
  1064                 if (f == -1) {
  1065                     break;
  1066                 } else {
  1067                     fqn = fqn.substring(0, f);
  1068                 }
  1069             }
  1070         }
  1071 
  1072         if (type.isKnown() && methods.isEmpty()) {
  1073             fqn = possibleFqn;
  1074 
  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));
  1082                 }
  1083 
  1084                 int f = fqn.lastIndexOf("::");
  1085 
  1086                 if (f == -1) {
  1087                     break;
  1088                 } else {
  1089                     fqn = fqn.substring(0, f);
  1090                 }
  1091             }
  1092 
  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));
  1097                 }
  1098                 
  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();
  1109                                 if (f != null) {
  1110                                     fqns.add(f);
  1111                                 }
  1112                             }
  1113                             for (String f : fqns) {
  1114                                 if (!f.equals(realType)) {
  1115                                     methods.addAll(index.getInheritedMethods(f, name, QuerySupport.Kind.EXACT));
  1116                                 }
  1117                             }
  1118                         }
  1119                     }
  1120                 }
  1121             }
  1122             
  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));
  1127             }
  1128         }
  1129 
  1130         if (    methods.isEmpty()) {
  1131             if (!type.isKnown()) {
  1132                 methods.addAll(index.getMethods(name, QuerySupport.Kind.EXACT));
  1133             } else {
  1134                 methods.addAll(index.getMethods(name, type.getRealTypes(), QuerySupport.Kind.EXACT));
  1135             }
  1136             if (methods.isEmpty() && type.isKnown()) {
  1137                 methods = index.getMethods(name, QuerySupport.Kind.EXACT);
  1138             }
  1139         }
  1140         
  1141         return methods;
  1142     }
  1143 
  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);
  1147         if (doc == null) {
  1148             return DeclarationLocation.NONE;
  1149         }
  1150 
  1151         IndexedMethod candidate =
  1152             findBestMethodMatch(name, methods, doc,
  1153                 astOffset, lexOffset, path, closest, index);
  1154 
  1155         if (candidate != null) {
  1156             FileObject fileObject = candidate.getFileObject();
  1157             if (fileObject == null) {
  1158                 return DeclarationLocation.NONE;
  1159             }
  1160 
  1161             Node node = AstUtilities.getForeignNode(candidate);
  1162             int nodeOffset = 0;
  1163             if (node != null) {
  1164                 nodeOffset = node.getPosition().getStartOffset();
  1165                 if (node.getNodeType() == NodeType.ALIASNODE) {
  1166                     nodeOffset += 6; // 6 = lenght of 'alias '
  1167                 }
  1168             }
  1169 
  1170             DeclarationLocation loc = new DeclarationLocation(
  1171                 fileObject, nodeOffset, candidate);
  1172 
  1173             if (!CHOOSE_ONE_DECLARATION && methods.size() > 1) {
  1174                 // Could the :nodoc: alternatives: if there is only one nodoc'ed alternative
  1175                 // don't ask user!
  1176                 int not_nodoced = 0;
  1177                 for (final IndexedMethod mtd : methods) {
  1178                     if (!mtd.isNoDoc()) {
  1179                         not_nodoced++;
  1180                     }
  1181                 }
  1182                 if (not_nodoced >= 2) {
  1183                     for (final IndexedMethod mtd : methods) {
  1184                         loc.addAlternative(new RubyAltLocation(mtd, mtd == candidate));
  1185                     }
  1186                 }
  1187             }
  1188 
  1189             return loc;
  1190         }
  1191      
  1192         return DeclarationLocation.NONE;
  1193     }
  1194     
  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();
  1199 
  1200         // Is this a require-statement? If so, jump to the required file
  1201         try {
  1202             Document doc = RubyUtils.getDocument(parserResult);
  1203             if (doc == null) {
  1204                 return null;
  1205             }
  1206 
  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) {
  1211                 return null;
  1212             }
  1213             OffsetRange range = getReferenceSpan(doc, lexOffset);
  1214 
  1215             if (range == OffsetRange.NONE) {
  1216                 return null;
  1217             }
  1218 
  1219             boolean leftSide = range.getEnd() <= astOffset;
  1220 
  1221             Node root = AstUtilities.getRoot(parserResult);
  1222 
  1223             RubyIndex index = RubyIndex.get(parserResult);
  1224             if (root == null) {
  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());
  1228 
  1229 
  1230                 if ((index == null) || (text.length() == 0)) {
  1231                     return null;
  1232                 }
  1233 
  1234                 if (Character.isUpperCase(text.charAt(0))) {
  1235                     // A Class or Constant?
  1236                     // Not a method call
  1237                     return null;
  1238                 } else {
  1239                     // A method?
  1240                     Set<IndexedMethod> methods = index.getMethods(text, QuerySupport.Kind.EXACT);
  1241 
  1242                     BaseDocument bdoc = (BaseDocument)doc;
  1243                     IndexedMethod candidate =
  1244                         findBestMethodMatch(text, methods, bdoc,
  1245                             astOffset, lexOffset, null, null, index);
  1246 
  1247                     return candidate;
  1248                 } // TODO: @ - field?
  1249             }
  1250 
  1251             TokenHierarchy<Document> th = TokenHierarchy.get(doc);
  1252 
  1253             int tokenOffset = astOffset;
  1254 
  1255             if (leftSide && (tokenOffset > 0)) {
  1256                 tokenOffset--;
  1257             }
  1258 
  1259             // A method call
  1260             String name = ((INameNode)callNode).getName();
  1261             String fqn = AstUtilities.getFqnName(path);
  1262 
  1263             if ((fqn == null) || (fqn.length() == 0)) {
  1264                 fqn = "Object"; // NOI18N
  1265             }
  1266             
  1267             Call call = Call.getCallType((BaseDocument)doc, th, lexOffset);
  1268             boolean skipPrivate = true;
  1269             boolean done = call.isMethodExpected();
  1270             boolean skipInstanceMethods = call.isStatic();
  1271 
  1272             RubyType type = call.getType();
  1273             String lhs = call.getLhs();
  1274             QuerySupport.Kind kind = QuerySupport.Kind.EXACT;
  1275 
  1276             Node node = callNode;
  1277             if ((!type.isKnown()) && (lhs != null) && (node != null) && call.isSimpleIdentifier()) {
  1278                 Node method = AstUtilities.findLocalScope(node, path);
  1279 
  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);
  1287                 }
  1288             }
  1289 
  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;
  1298 
  1299                     IndexedClass sc = index.getSuperclass(fqn);
  1300 
  1301                     if (sc != null) {
  1302                         type = RubyType.create(sc.getFqn());
  1303                     } else {
  1304                         ClassNode cls = AstUtilities.findClass(path);
  1305 
  1306                         if (cls != null) {
  1307                             type = RubyType.create(AstUtilities.getSuperclass(cls));
  1308                         }
  1309                     }
  1310 
  1311                     if (!type.isKnown()) {
  1312                         type = RubyType.OBJECT; // NOI18N
  1313                     }
  1314                 }
  1315             }
  1316             if (call == Call.LOCAL && fqn != null && fqn.length() == 0) {
  1317                 fqn = "Object";
  1318             }
  1319 
  1320             Set<IndexedMethod> methods = getApplicableMethods(name, fqn, type, call, index);
  1321             
  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);
  1326             }
  1327 
  1328             IndexedMethod candidate =
  1329                 findBestMethodMatch(name, methods, (BaseDocument)doc,
  1330                     astOffset, lexOffset, path, callNode, index);
  1331 
  1332             if (alternativesHolder != null) {
  1333                 alternativesHolder[0] = methods;
  1334             }
  1335             return candidate;
  1336         } catch (BadLocationException ble) {
  1337             // do nothing - see #154991
  1338         }
  1339 
  1340         return null;
  1341     }
  1342 
  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);
  1347 
  1348         TokenSequence<?> ts = LexUtilities.getRubyTokenSequence((BaseDocument)doc, lexOffset);
  1349         if (ts == null) return DeclarationLocation.NONE;
  1350 
  1351         ts.move(lexOffset);
  1352 
  1353         if (!ts.moveNext() && !ts.movePrevious()) return DeclarationLocation.NONE;
  1354 
  1355         Token<?> token = ts.token();
  1356         TokenSequence<?> embedded = ts.embedded();
  1357 
  1358         if (embedded != null) {
  1359             embedded.move(lexOffset);
  1360 
  1361             if (!embedded.moveNext() && !embedded.movePrevious()) return DeclarationLocation.NONE;
  1362 
  1363             token = embedded.token();
  1364         }
  1365 
  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();
  1370 
  1371             if (method.startsWith("#")) {
  1372                 method = method.substring(1);
  1373 
  1374                 DeclarationLocation loc = findMethod(info, root, method, Arity.UNKNOWN);
  1375 
  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);
  1379             } else {
  1380                 // A URL such as http://netbeans.org - try to open it in a browser!
  1381                 try {
  1382                     return new DeclarationLocation(new URL(method));
  1383                 } catch (MalformedURLException mue) {
  1384                     // URL is from user source... don't complain with exception dialogs etc.
  1385                     ;
  1386                 }
  1387             }
  1388             
  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);
  1394 
  1395                 return findMethod(method, null, RubyType.create(clz), Call.UNKNOWN,
  1396                         info, astOffset, lexOffset, path, closest, index);
  1397             }
  1398         }
  1399 
  1400         return DeclarationLocation.NONE;
  1401     }
  1402     
  1403     @SuppressWarnings("empty-statement")
  1404     DeclarationLocation findLinkedMethod(ParserResult info, String method) {
  1405         Node root = AstUtilities.getRoot(info);
  1406         AstPath path = new AstPath();
  1407         path.descend(root);
  1408         Node closest = root;
  1409         int astOffset = 0;
  1410         int lexOffset = 0;
  1411         RubyIndex index = getIndex(info);
  1412 
  1413         if (root == null) {
  1414             return DeclarationLocation.NONE;
  1415         }
  1416 
  1417         if (method.startsWith("#")) {
  1418             method = method.substring(1);
  1419 
  1420             DeclarationLocation loc = findMethod(info, root, method, Arity.UNKNOWN);
  1421 
  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);
  1427             }
  1428 
  1429             return loc;
  1430         } else {
  1431             // A URL such as http://netbeans.org - try to open it in a browser!
  1432             try {
  1433                 URL url = new URL(method);
  1434 
  1435                 return new DeclarationLocation(url);
  1436             } catch (MalformedURLException mue) {
  1437                 // URL is from user source... don't complain with exception dialogs etc.
  1438                 ;
  1439             }
  1440         }
  1441 
  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);
  1447 
  1448             return findMethod(method, null, RubyType.create(clz), Call.UNKNOWN, info, astOffset, lexOffset, path, closest, index);
  1449         }
  1450         
  1451         return DeclarationLocation.NONE;
  1452     }
  1453 
  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
  1457         // and parse tree
  1458 
  1459         Set<IndexedMethod> methods = new HashSet<IndexedMethod>(methodSet);
  1460         
  1461         while (!methods.isEmpty()) {
  1462             IndexedMethod method =
  1463                 findBestMethodMatchHelper(name, methods, doc, astOffset, lexOffset, path, call, index);
  1464             Node node = AstUtilities.getForeignNode(method);
  1465 
  1466             if (node != null) {
  1467                 return method;
  1468             }
  1469 
  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());
  1474             } else {
  1475                 methods.remove(method);
  1476             }
  1477         }
  1478         
  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();
  1482         }
  1483 
  1484         return null;
  1485     }
  1486 
  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>();
  1490 
  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
  1494         // best candidate
  1495         if (callNode instanceof CallNode) {
  1496             Node node = ((CallNode)callNode).getReceiver();
  1497             String fqn = null;
  1498 
  1499             if (node instanceof Colon2Node) {
  1500                 fqn = AstUtilities.getFqn((Colon2Node)node);
  1501             } else if (node instanceof ConstNode) {
  1502                 fqn = ((ConstNode)node).getName();
  1503             }
  1504 
  1505             if (fqn != null) {
  1506                 while ((fqn != null) && (fqn.length() > 0)) {
  1507                     for (IndexedMethod method : methods) {
  1508                         if (fqn.equals(method.getClz())) {
  1509                             candidates.add(method);
  1510                         }
  1511                     }
  1512 
  1513                     // Check inherited methods; for example, if we've determined
  1514                     // that you're looking for Integer::foo, I should happily match
  1515                     // Numeric::foo.
  1516                     IndexedClass superClass = index.getSuperclass(fqn);
  1517 
  1518                     if (superClass != null) {
  1519                         fqn = superClass.getSignature();
  1520                     } else {
  1521                         break;
  1522                     }
  1523                 }
  1524             }
  1525         }
  1526 
  1527         if (candidates.size() == 1) {
  1528             return candidates.iterator().next();
  1529         } else if (!candidates.isEmpty()) {
  1530             methods = candidates;
  1531         }
  1532 
  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);
  1538 
  1539         Call call = Call.getCallType(doc, th, lexOffset);
  1540         boolean skipPrivate = true;
  1541 
  1542         if ((path != null) && (callNode != null) && (call != Call.LOCAL) && (call != Call.NONE)) {
  1543             boolean skipInstanceMethods = call.isStatic();
  1544 
  1545             candidates = new HashSet<IndexedMethod>();
  1546 
  1547             RubyType type = call.getType();
  1548 
  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();
  1553 
  1554                 String fqn = AstUtilities.getFqnName(path);
  1555 
  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;
  1564 
  1565                     IndexedClass sc = index.getSuperclass(fqn);
  1566 
  1567                     if (sc != null) {
  1568                         type = RubyType.create(sc.getFqn());
  1569                     } else {
  1570                         ClassNode cls = AstUtilities.findClass(path);
  1571 
  1572                         if (cls != null) {
  1573                             type = RubyType.create(AstUtilities.getSuperclass(cls));
  1574                         }
  1575                     }
  1576                 }
  1577 
  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);
  1586 
  1587                         int f = fqn.lastIndexOf("::");
  1588 
  1589                         if (f == -1) {
  1590                             break;
  1591                         } else {
  1592                             fqn = fqn.substring(0, f);
  1593                         }
  1594                     }
  1595 
  1596                     // Add methods in the class (without an FQN)
  1597                     if (candidates.isEmpty()) {
  1598                         candidates = index.getInheritedMethods(type, name, QuerySupport.Kind.EXACT);
  1599                     }
  1600                 }
  1601             }
  1602 
  1603             if (skipPrivate || skipInstanceMethods) {
  1604                 Set<IndexedMethod> m = new HashSet<IndexedMethod>();
  1605 
  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
  1611                         continue;
  1612                     }
  1613 
  1614                     // We can only call static methods
  1615                     if (skipInstanceMethods && !method.isStatic()) {
  1616                         continue;
  1617                     }
  1618 
  1619                     m.add(method);
  1620                 }
  1621 
  1622                 candidates = m;
  1623             }
  1624 
  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>();
  1628 
  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())) {
  1633                     cs.add(m);
  1634                 }
  1635             }
  1636 
  1637             if (cs.size() < candidates.size()) candidates = cs;
  1638         }
  1639 
  1640         if (candidates.size() == 1) {
  1641             return candidates.iterator().next();
  1642         } else if (!candidates.isEmpty()) {
  1643             methods = candidates;
  1644         }
  1645         
  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>();
  1652 
  1653         for (IndexedMethod method : methods) {
  1654             String attributes = method.getEncodedAttributes();
  1655             if (attributes != null && attributes.length() > 3) {
  1656                 candidates.add(method);
  1657             }
  1658         }
  1659 
  1660         if (candidates.size() == 1) {
  1661             return candidates.iterator().next();
  1662         } else if (!candidates.isEmpty()) {
  1663             methods = candidates;
  1664         }
  1665 
  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
  1671 
  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;
  1675 
  1676         if (path != null) {
  1677             candidates = new HashSet<IndexedMethod>();
  1678 
  1679             requires = AstUtilities.getRequires(path.root());
  1680 
  1681             for (IndexedMethod method : methods) {
  1682                 String require = method.getRequire();
  1683 
  1684                 if (requires.contains(require)) {
  1685                     candidates.add(method);
  1686                 }
  1687             }
  1688 
  1689             if (candidates.size() == 1) {
  1690                 return candidates.iterator().next();
  1691             } else if (!candidates.isEmpty()) {
  1692                 methods = candidates;
  1693             }
  1694         }
  1695 
  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>();
  1699 
  1700         for (IndexedMethod method : methods) {
  1701             String url = method.getFileUrl();
  1702 
  1703             if (RubyUtils.isRubyStubsURL(url)) {
  1704                 candidates.add(method);
  1705             }
  1706         }
  1707 
  1708         if (candidates.size() == 1) {
  1709             return candidates.iterator().next();
  1710         } else if (!candidates.isEmpty()) {
  1711             methods = candidates;
  1712         }
  1713 
  1714         // 4. See which methods are documented, and prefer those over undocumented methods
  1715         candidates = new HashSet<IndexedMethod>();
  1716 
  1717         int longestDocLength = 0;
  1718 
  1719         for (IndexedMethod method : methods) {
  1720             int length = method.getDocumentationLength();
  1721 
  1722             if (length > longestDocLength) {
  1723                 candidates.clear();
  1724                 candidates.add(method);
  1725                 longestDocLength = length;
  1726             } else if ((length > 0) && (length == longestDocLength)) {
  1727                 candidates.add(method);
  1728             }
  1729         }
  1730 
  1731         if (candidates.size() == 1) {
  1732             return candidates.iterator().next();
  1733         } else if (!candidates.isEmpty()) {
  1734             methods = candidates;
  1735         }
  1736 
  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>();
  1741 
  1742             Set<String> allRequires = index.getRequiresTransitively(requires);
  1743 
  1744             for (IndexedMethod method : methods) {
  1745                 String require = method.getRequire();
  1746 
  1747                 if (allRequires.contains(require)) {
  1748                     candidates.add(method);
  1749                 }
  1750             }
  1751 
  1752             if (candidates.size() == 1) {
  1753                 return candidates.iterator().next();
  1754             } else if (!candidates.isEmpty()) {
  1755                 methods = candidates;
  1756             }
  1757         }
  1758 
  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
  1763 
  1764         // 7. Look at superclasses and consider -their- requires to figure out
  1765         //   which class we're looking for methods in
  1766         // TODO
  1767 
  1768         // Pick one arbitrarily
  1769         if (methods.size() > 0) {
  1770             return methods.iterator().next();
  1771         } else {
  1772             return null;
  1773         }
  1774     }
  1775 
  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());
  1781 
  1782             if (name.equals(newName)) return getLocation(info, node);
  1783         } else if (node instanceof IParameterScope) {
  1784             ILocalVariable parameter = ((IParameterScope) node).getParameterNamed(name);
  1785             
  1786             return parameter != null ? getLocation(info, (Node) parameter) : DeclarationLocation.NONE;
  1787         }
  1788 
  1789         List<Node> list = node.childNodes();
  1790 
  1791         for (Node child : list) {
  1792             DeclarationLocation location = findLocal(info, child, name);
  1793 
  1794             if (location != DeclarationLocation.NONE) return location;
  1795         }
  1796 
  1797         return DeclarationLocation.NONE;
  1798     }
  1799 
  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);
  1805         }
  1806 
  1807         for (Node child : node.childNodes()) {
  1808             DeclarationLocation location = findDynamic(info, child, name);
  1809             
  1810             if (location != DeclarationLocation.NONE) return location;
  1811         }
  1812 
  1813         return DeclarationLocation.NONE;
  1814     }
  1815 
  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);
  1820             }
  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);
  1825             }
  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);
  1830 
  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) {
  1838                         return location;
  1839                     }
  1840                     return getLocation(info, symbols[i]);
  1841                 }
  1842             }
  1843         }
  1844 
  1845         for (Node child : node.childNodes()) {
  1846             DeclarationLocation location = findInstance(info, child, name, index);
  1847 
  1848             if (location != DeclarationLocation.NONE) return location;
  1849         }
  1850 
  1851         return DeclarationLocation.NONE;
  1852     }
  1853 
  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);
  1858             }
  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);
  1863             }
  1864 
  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);
  1870             //
  1871             //            for (int i = 0; i < symbols.length; i++) {
  1872             //                if (name.equals("@" + symbols[i].getName())) {
  1873             //                    return getLocation(info, symbols[i]);
  1874             //                }
  1875             //            }
  1876         }
  1877 
  1878         for (Node child : node.childNodes()) {
  1879             DeclarationLocation location = findClassVar(info, child, name);
  1880 
  1881             if (location != DeclarationLocation.NONE) return location;
  1882         }
  1883 
  1884         return DeclarationLocation.NONE;
  1885     }
  1886 
  1887     private DeclarationLocation findInstanceFromIndex(ParserResult info, String name, AstPath path, RubyIndex index, boolean inherited) {
  1888         String fqn = AstUtilities.getFqnName(path);
  1889 
  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
  1896             
  1897             Node node = AstUtilities.getForeignNode(field);
  1898 
  1899             if (node != null) {
  1900                 return new DeclarationLocation(field.getFileObject(),
  1901                     node.getPosition().getStartOffset(), field);
  1902             }
  1903         }
  1904 
  1905         return DeclarationLocation.NONE;
  1906     }
  1907 
  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);
  1912     }
  1913 
  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);
  1918             }
  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);
  1923             }
  1924         }
  1925 
  1926         for (Node child : node.childNodes()) {
  1927             DeclarationLocation location = findGlobal(info, child, name);
  1928 
  1929             if (location != DeclarationLocation.NONE) return location;
  1930         }
  1931 
  1932         return DeclarationLocation.NONE;
  1933     }
  1934 
  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);
  1941             }
  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);
  1947             }
  1948         }
  1949 
  1950         for (Node child : node.childNodes()) {
  1951             DeclarationLocation location = findMethod(info, child, name, arity);
  1952 
  1953             if (location != DeclarationLocation.NONE) return location;
  1954         }
  1955 
  1956         return DeclarationLocation.NONE;
  1957     }
  1958 
  1959 }