ruby/src/org/netbeans/modules/ruby/AstUtilities.java
author enebo@netbeans.org
Sun, 08 Dec 2013 12:20:16 -0600
changeset 4555 3773928e70d0
parent 4554 07958c1ff402
permissions -rw-r--r--
Let jruby-parser figure out occurrences and delete netbeans logic for this
     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.util.ArrayList;
    47 import java.util.Arrays;
    48 import java.util.Collection;
    49 import java.util.Collections;
    50 import java.util.HashMap;
    51 import java.util.HashSet;
    52 import java.util.Iterator;
    53 import java.util.LinkedList;
    54 import java.util.List;
    55 import java.util.Map;
    56 import java.util.Set;
    57 import java.util.logging.Level;
    58 import java.util.logging.Logger;
    59 import javax.swing.text.BadLocationException;
    60 import javax.swing.text.Document;
    61 import org.jrubyparser.ast.AliasNode;
    62 import org.jrubyparser.ast.ArgsCatNode;
    63 import org.jrubyparser.ast.ArgsNode;
    64 import org.jrubyparser.ast.ArgumentNode;
    65 import org.jrubyparser.ast.AssignableNode;
    66 import org.jrubyparser.ast.CallNode;
    67 import org.jrubyparser.ast.ClassNode;
    68 import org.jrubyparser.ast.Colon2Node;
    69 import org.jrubyparser.ast.Colon3Node;
    70 import org.jrubyparser.ast.ConstNode;
    71 import org.jrubyparser.ast.FCallNode;
    72 import org.jrubyparser.ast.IScopingNode;
    73 import org.jrubyparser.ast.ListNode;
    74 import org.jrubyparser.ast.MethodDefNode;
    75 import org.jrubyparser.ast.ModuleNode;
    76 import org.jrubyparser.ast.Node;
    77 import org.jrubyparser.ast.NodeType;
    78 import org.jrubyparser.ast.SClassNode;
    79 import org.jrubyparser.ast.StrNode;
    80 import org.jrubyparser.ast.SymbolNode;
    81 import org.jrubyparser.ast.VCallNode;
    82 import org.jrubyparser.ast.INameNode;
    83 import org.jrubyparser.SourcePosition;
    84 import org.jrubyparser.ast.AndNode;
    85 import org.jrubyparser.ast.AttrAssignNode;
    86 import org.jrubyparser.ast.CaseNode;
    87 import org.jrubyparser.ast.DSymbolNode;
    88 import org.jrubyparser.ast.DefinedNode;
    89 import org.jrubyparser.ast.DotNode;
    90 import org.jrubyparser.ast.HashNode;
    91 import org.jrubyparser.ast.IArgumentNode;
    92 import org.jrubyparser.ast.ILiteralNode;
    93 import org.jrubyparser.ast.ILocalScope;
    94 import org.jrubyparser.ast.IfNode;
    95 import org.jrubyparser.ast.IterNode;
    96 import org.jrubyparser.ast.LiteralNode;
    97 import org.jrubyparser.ast.NewlineNode;
    98 import org.jrubyparser.ast.NilNode;
    99 import org.jrubyparser.ast.NotNode;
   100 import org.jrubyparser.ast.OrNode;
   101 import org.jrubyparser.ast.RescueNode;
   102 import org.jrubyparser.ast.ReturnNode;
   103 import org.jrubyparser.ast.UntilNode;
   104 import org.jrubyparser.ast.WhenNode;
   105 import org.netbeans.editor.BaseDocument;
   106 import org.netbeans.editor.Utilities;
   107 import org.netbeans.modules.csl.api.Modifier;
   108 import org.netbeans.modules.csl.api.OffsetRange;
   109 import org.netbeans.modules.csl.spi.GsfUtilities;
   110 import org.netbeans.modules.csl.spi.ParserResult;
   111 import org.netbeans.modules.parsing.api.ParserManager;
   112 import org.netbeans.modules.parsing.api.ResultIterator;
   113 import org.netbeans.modules.parsing.api.Snapshot;
   114 import org.netbeans.modules.parsing.api.Source;
   115 import org.netbeans.modules.parsing.api.UserTask;
   116 import org.netbeans.modules.parsing.spi.ParseException;
   117 import org.netbeans.modules.parsing.spi.Parser;
   118 import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport;
   119 import org.netbeans.modules.ruby.elements.IndexedElement;
   120 import org.netbeans.modules.ruby.elements.IndexedField;
   121 import org.netbeans.modules.ruby.elements.IndexedMethod;
   122 import org.openide.filesystems.FileObject;
   123 import org.openide.util.Exceptions;
   124 
   125 /**
   126  * Various utilities for operating on the JRuby ASTs that are used
   127  * elsewhere.
   128  * 
   129  * @todo Rewrite many of the custom recursion routines to simply 
   130  *  call {@link addNodesByType} and then iterate (without recursion) over
   131  *  the result set.
   132  *
   133  * @author Tor Norbye
   134  */
   135 public class AstUtilities {
   136     private static final Logger LOGGER = Logger.getLogger(AstUtilities.class.getName());
   137 
   138     private static final String[] ATTR_ACCESSORS = {"attr", "attr_reader", "attr_accessor", "attr_writer",
   139         "attr_internal", "attr_internal_accessor", "attr_internal_reader", "attr_internal_writer"};
   140 
   141     /** ActiveSupport extensions */
   142     private static final String[] CATTR_ACCESSORS = {"cattr_reader", "cattr_accessor", "cattr_writer"};
   143 
   144     /** The names of AR scope methods - named_scope in rails 2.x, rails 3.x deprecates it
   145      in favor of 'scope' */
   146     private static final String[] NAMED_SCOPE = {"named_scope", "scope"};
   147     /**
   148      * Tries to cast the given <code>result</code> to <code>RubyParseResult</code> 
   149      * and returns it. Returns <code>null</code> if it wasn't an instance of <code>RubyParseResult</code>.
   150      * 
   151      * @param result
   152      * @return
   153      */
   154     public static RubyParseResult getParseResult(Parser.Result result) {
   155         if (!(result instanceof RubyParseResult)) {
   156             LOGGER.log(Level.WARNING, "Expected RubyParseResult, but have {0}", result);
   157             return null;
   158         }
   159         return (RubyParseResult) result;
   160     }
   161 
   162     public static int getAstOffset(Parser.Result info, int lexOffset) {
   163         RubyParseResult result = getParseResult(info);
   164         if (result == null) return lexOffset;
   165 
   166         return result.getSnapshot().getEmbeddedOffset(lexOffset);
   167     }
   168 
   169     public static OffsetRange getAstOffsets(Parser.Result info, OffsetRange lexicalRange) {
   170         RubyParseResult result = getParseResult(info);
   171         if (result == null) return lexicalRange;
   172         
   173         int rangeStart = lexicalRange.getStart();
   174         int start = result.getSnapshot().getEmbeddedOffset(rangeStart);
   175         if (start == rangeStart) return lexicalRange;
   176         if (start == -1) return OffsetRange.NONE;
   177 
   178         // Assumes the translated range maintains size
   179         return new OffsetRange(start, start + lexicalRange.getLength());
   180     }
   181 
   182     /** This is a utility class only, not instantiatiable */
   183     private AstUtilities() {
   184     }
   185 
   186     /**
   187      * Get the rdoc documentation associated with the given node in the given document.
   188      * The node must have position information that matches the source in the document.
   189      */
   190     public static List<String> gatherDocumentation(Snapshot baseDoc, Node node) {
   191         LinkedList<String> comments = new LinkedList<String>();
   192         int elementBegin = node.getPosition().getStartOffset();
   193 
   194         try {
   195             if (elementBegin < 0 || elementBegin >= baseDoc.getText().length()) return null;
   196 
   197             // Search to previous lines, locate comments. Once we have a non-whitespace line that isn't
   198             // a comment, we're done
   199 
   200             int offset = GsfUtilities.getRowStart(baseDoc.getText(), elementBegin);
   201             offset--;
   202 
   203             // Skip empty and whitespace lines
   204             while (offset >= 0) {
   205                 // Find beginning of line
   206                 offset = GsfUtilities.getRowStart(baseDoc.getText(), offset);
   207 
   208                 if (!GsfUtilities.isRowEmpty(baseDoc.getText(), offset) &&
   209                         !GsfUtilities.isRowWhite(baseDoc.getText(), offset)) {
   210                     break;
   211                 }
   212 
   213                 offset--;
   214             }
   215 
   216             if (offset < 0) {
   217                 return null;
   218             }
   219 
   220             while (offset >= 0) {
   221                 // Find beginning of line
   222                 offset = GsfUtilities.getRowStart(baseDoc.getText(), offset);
   223 
   224                 if (GsfUtilities.isRowEmpty(baseDoc.getText(), offset) || GsfUtilities.isRowWhite(baseDoc.getText(), offset)) {
   225                     // Empty lines not allowed within an rdoc
   226                     break;
   227                 }
   228 
   229                 // This is a comment line we should include
   230                 int lineBegin = GsfUtilities.getRowFirstNonWhite(baseDoc.getText(), offset);
   231                 int lineEnd = GsfUtilities.getRowLastNonWhite(baseDoc.getText(), offset) + 1;
   232                 String line = baseDoc.getText().subSequence(lineBegin, lineEnd).toString();
   233 
   234                 // Tolerate "public", "private" and "protected" here --
   235                 // Test::Unit::Assertions likes to put these in front of each
   236                 // method.
   237                 if (line.startsWith("#")) {
   238                     comments.addFirst(line);
   239                 } else if ((comments.size() == 0) && line.startsWith("=end") &&
   240                         (lineBegin == GsfUtilities.getRowStart(baseDoc.getText(), offset))) {
   241                     // It could be a =begin,=end document - see scanf.rb in Ruby lib for example. Treat this differently.
   242                     gatherInlineDocumentation(comments, baseDoc, offset);
   243 
   244                     return comments;
   245                 } else if (line.equals("public") || line.equals("private") ||
   246                         line.equals("protected")) { // NOI18N
   247                                                     // Skip newlines back up to the comment
   248                     offset--;
   249 
   250                     while (offset >= 0) {
   251                         // Find beginning of line
   252                         offset = GsfUtilities.getRowStart(baseDoc.getText(), offset);
   253 
   254                         if (!GsfUtilities.isRowEmpty(baseDoc.getText(), offset) &&
   255                                 !GsfUtilities.isRowWhite(baseDoc.getText(), offset)) {
   256                             break;
   257                         }
   258 
   259                         offset--;
   260                     }
   261 
   262                     continue;
   263                 } else {
   264                     // No longer in a comment
   265                     break;
   266                 }
   267 
   268                 // Previous line
   269                 offset--;
   270             }
   271         } catch (BadLocationException ble) {
   272             // do nothing - see #154991
   273         }
   274 
   275         return comments;
   276     }
   277 
   278     private static void gatherInlineDocumentation(LinkedList<String> comments,
   279         Snapshot baseDoc, int offset) throws BadLocationException {
   280         // offset points to a line containing =end
   281         // Skip the =end list
   282         offset = GsfUtilities.getRowStart(baseDoc.getText(), offset);
   283         offset--;
   284 
   285         // Search backwards in the document for the =begin (if any) and add all lines in reverse
   286         // order in between.
   287         while (offset >= 0) {
   288             // Find beginning of line
   289             offset = GsfUtilities.getRowStart(baseDoc.getText(), offset);
   290 
   291             // This is a comment line we should include
   292             int lineBegin = offset;
   293             int lineEnd = GsfUtilities.getRowEnd(baseDoc.getText(), offset);
   294             String line = baseDoc.getText().subSequence(lineBegin, lineEnd).toString();
   295 
   296             if (line.startsWith("=begin")) return;  // We're done!
   297 
   298             comments.addFirst(line);
   299 
   300             // Previous line
   301             offset--;
   302         }
   303     }
   304 
   305     /**
   306      * <strong>This method may block for a long time; use with caution.</strong>.
   307      */
   308     public static Node getForeignNode(final IndexedElement elem) {
   309         return getForeignNode(elem, null);
   310     }
   311 
   312     /**
   313      * <strong>This method may block for a long time; use with caution.</strong>.
   314      * @param elem
   315      * @param foreignInfoHolder
   316      * @return
   317      */
   318     public static Node getForeignNode(final IndexedElement elem, final Parser.Result[] foreignInfoHolder) {
   319         FileObject fo = elem.getFileObject();
   320         if (fo == null) {
   321             return null;
   322         }
   323 
   324         Source source = Source.create(fo);
   325         final Node[] nodeHolder = new Node[1];
   326         try {
   327             ParserManager.parse(Collections.singleton(source), new UserTask() {
   328                 @Override
   329                 public void run(ResultIterator resultIterator) throws Exception {
   330                     Parser.Result result = resultIterator.getParserResult();
   331                     if (foreignInfoHolder != null) {
   332                         assert foreignInfoHolder.length == 1;
   333                         foreignInfoHolder[0] = result;
   334                     }
   335 
   336                     Node root = AstUtilities.getRoot(result);
   337                     if (root != null) {
   338                         String signature = elem.getSignature();
   339 
   340                         if (signature != null) {
   341                             Node node = AstUtilities.findBySignature(root, signature);
   342 
   343                             // Special handling for "new" - these are synthesized from "initialize" methods
   344                             if ((node == null) && "new".equals(elem.getName())) { // NOI18N
   345                                 if (signature.indexOf("#new") != -1) {
   346                                     signature = signature.replaceFirst("#new", "#initialize"); //NOI18N
   347                                 } else {
   348                                     signature = signature.replaceFirst("new", "initialize"); //NOI18N
   349                                 }
   350                                 node = AstUtilities.findBySignature(root, signature);
   351                             }
   352 
   353                             nodeHolder[0] = node;
   354                         }
   355                     }
   356                 }
   357             });
   358         } catch (ParseException ex) {
   359             Exceptions.printStackTrace(ex);
   360             return null;
   361         }
   362 
   363 
   364         return nodeHolder[0];
   365     }
   366 
   367 //    public static Node getForeignNode(final IndexedElement o) {
   368 //        FileObject fo = o.getFileObject();
   369 //        if (fo == null) {
   370 //            return null;
   371 //        }
   372 //
   373 //        if (file == null) {
   374 //            return null;
   375 //        }
   376 //
   377 //        List<ParserFile> files = Collections.singletonList(file);
   378 //        SourceFileReader reader =
   379 //            new SourceFileReader() {
   380 //                public CharSequence read(ParserFile file)
   381 //                    throws IOException {
   382 //                    Document doc = o.getDocument();
   383 //
   384 //                    if (doc == null) {
   385 //                        return "";
   386 //                    }
   387 //
   388 //                    try {
   389 //                        return doc.getText(0, doc.getLength());
   390 //                    } catch (BadLocationException ble) {
   391 //                        IOException ioe = new IOException();
   392 //                        ioe.initCause(ble);
   393 //                        throw ioe;
   394 //                    }
   395 //                }
   396 //
   397 //                public int getCaretOffset(ParserFile fileObject) {
   398 //                    return -1;
   399 //                }
   400 //            };
   401 //
   402 //        DefaultParseListener listener = new DefaultParseListener();
   403 //        // TODO - embedding model?
   404 //        TranslatedSource translatedSource = null; // TODO - determine this here?
   405 //        Parser.Job job = new Parser.Job(files, listener, reader, translatedSource);
   406 //        new RubyParser().parseFiles(job);
   407 //
   408 //        ParserResult result = listener.getParserResult();
   409 //
   410 //        if (result == null) {
   411 //            return null;
   412 //        }
   413 //
   414 //        Node root = AstUtilities.getRoot(result);
   415 //
   416 //        if (root == null) {
   417 //            return null;
   418 //        }
   419 //
   420 //        String signature = o.getSignature();
   421 //
   422 //        if (signature == null) {
   423 //            return null;
   424 //        }
   425 //
   426 //        Node node = AstUtilities.findBySignature(root, signature);
   427 //
   428 //        // Special handling for "new" - these are synthesized from "initialize" methods
   429 //        if ((node == null) && "new".equals(o.getName())) { // NOI18N
   430 //            signature = signature.replaceFirst("new", "initialize"); //NOI18N
   431 //            node = AstUtilities.findBySignature(root, signature);
   432 //        }
   433 //
   434 //        return node;
   435 //    }
   436 
   437     public static int boundCaretOffset(ParserResult result, int caretOffset) {
   438         Document doc = RubyUtils.getDocument(result);
   439         if (doc != null) {
   440             // If you invoke code completion while indexing is in progress, the
   441             // completion job (which stores the caret offset) will be delayed until
   442             // indexing is complete - potentially minutes later. When the job
   443             // is finally run we need to make sure the caret position is still valid.
   444             int length = doc.getLength();
   445 
   446             if (caretOffset > length) caretOffset = length;
   447         }
   448 
   449         return caretOffset;
   450     }
   451 
   452     /**
   453      * Return the set of requires that are defined in this AST
   454      * (no transitive closure though).
   455      */
   456     public static Set<String> getRequires(Node root) {
   457         Set<String> requires = new HashSet<String>();
   458         addRequires(root, requires);
   459 
   460         return requires;
   461     }
   462     
   463     private static void addRequires(Node node, Set<String> requires) {
   464         if (node.getNodeType() == NodeType.FCALLNODE) {
   465             FCallNode fcall = (FCallNode) node;
   466 
   467             if ("require".equals(fcall.getName()) && fcall.getArgs() instanceof ListNode) {
   468                 ListNode args = (ListNode) fcall.getArgs(); // FIXME: Can this ever not be fcall?
   469 
   470                 if (args.size() > 0) {
   471                     Node n = args.get(0);
   472 
   473                     // For dyn-strings, we have n instances DStrNode butwe can't handle these
   474                     if (n instanceof StrNode) {
   475                         String require = ((StrNode)n).getValue();
   476 
   477                         if (require != null && require.length() > 0) requires.add(require.toString());
   478                     }
   479                 }
   480             }
   481         } else if (node.getNodeType() == NodeType.MODULENODE || node.getNodeType() == NodeType.CLASSNODE ||
   482                 node.getNodeType() == NodeType.DEFNNODE || node.getNodeType() == NodeType.DEFSNODE) {
   483             return; // Only look for require statements at the top level
   484         }
   485 
   486         for (Node child : node.childNodes()) {
   487             addRequires(child, requires);
   488         }
   489     }
   490 
   491     /** Locate the method of the given name and arity */
   492     public static MethodDefNode findMethod(Node node, String name, Arity arity) {
   493         // Recursively search for methods or method calls that match the name and arity
   494         if (node instanceof MethodDefNode && (((INameNode) node).getName()).equals(name) && 
   495                 Arity.matches(arity, Arity.getArity((MethodDefNode) node))) return (MethodDefNode) node;
   496 
   497         for (Node child : node.childNodes()) {
   498             MethodDefNode match = findMethod(child, name, arity);
   499             
   500             if (match != null) return match;
   501         }
   502 
   503         return null;
   504     }
   505 
   506     /**
   507      * Gets the closest node at the given offset.
   508      * 
   509      * @param root
   510      * @param offset
   511      * @return the closest node or <code>null</code>.
   512      */
   513     public static Node findNodeAtOffset(Node root, int offset) {
   514         return root == null ? null : root.getNodeAt(offset);
   515     }
   516 
   517     public static MethodDefNode findMethodAtOffset(Node root, int offset) {
   518         Node child = findNodeAtOffset(root, offset);
   519         
   520         return child == null ? null : child.getMethodFor();
   521     }
   522     
   523     public static Node findNodeAtOffset(ParserResult info, int caretOffset) {
   524         Node root = AstUtilities.getRoot(info);
   525         if (root == null) return null;
   526 
   527         int astOffset = AstUtilities.getAstOffset(info, caretOffset);
   528         if (astOffset == -1) return null;
   529 
   530         Node closest = root.getNodeAt(astOffset);
   531         if (closest == null) return null;
   532         
   533         return closest;
   534     }
   535 
   536     // FIXME: Replace this with something which returns nearest module...Do we really specifically need class?
   537     public static ClassNode findClassAtOffset(Node root, int offset) {
   538         for (Node node: new AstPath(root, offset)) {
   539             if (node instanceof ClassNode) return (ClassNode) node;
   540         }
   541 
   542         return null;
   543     }
   544 
   545     public static Node findLocalScope(Node node, AstPath path) {
   546         Node method = findMethod(path);
   547         if (method != null) return method;
   548         if (path.root() != null) return path.root();
   549 
   550         method = findBlock(path);
   551         if (method != null) return method;
   552 
   553         method = path.leafParent();
   554 
   555         if (method.getNodeType() == NodeType.NEWLINENODE) method = path.leafGrandParent();
   556         if (method == null) method = node;
   557 
   558         return method;
   559     }
   560 
   561     public static Node findDynamicScope(Node node, AstPath path) {
   562         Node block = findBlock(path);
   563 
   564         if (block == null) {
   565             block = path.leafParent(); // Use parent
   566 
   567             if (block == null) block = node;
   568         }
   569 
   570         return block;
   571     }
   572 
   573     // Enebo: This method is not what the name or comment says it is?  findOuterMostBlockScope?
   574     public static Node findBlock(AstPath path) {
   575         Node candidate = null;
   576         for (Node curr : path) {
   577             if (curr instanceof IterNode) candidate = curr;
   578             if (curr instanceof ILocalScope) return candidate;
   579         }
   580 
   581         return candidate;
   582     }
   583 
   584     /** Find the closest method enclosing the given node */
   585     public static MethodDefNode findMethod(AstPath path) {
   586         for (Node curr : path) {
   587             if (curr instanceof MethodDefNode) return (MethodDefNode) curr;
   588             if (curr instanceof ILocalScope) break; // No def/defs so bump off early
   589         }
   590 
   591         return null;
   592     }
   593 
   594     // XXX Shouldn't this go in the REVERSE direction? I might find
   595     // a superclass here!
   596     // XXX What about SClassNode?
   597     public static ClassNode findClass(AstPath path) {
   598         // Find the closest block node enclosing the given node
   599         for (Node curr : path) {
   600             if (curr instanceof ClassNode) {
   601                 return (ClassNode)curr;
   602             }
   603         }
   604 
   605         return null;
   606     }
   607 
   608     public static IScopingNode findClassOrModule(AstPath path) {
   609         // Find the closest block node enclosing the given node
   610         for (Node curr : path) {
   611             // XXX What about SClassNodes?
   612             if (curr.getNodeType() == NodeType.CLASSNODE || curr.getNodeType() == NodeType.MODULENODE) {
   613                 return (IScopingNode)curr;
   614             }
   615         }
   616 
   617         return null;
   618     }
   619 
   620     public static boolean isCall(Node node) {
   621         return node.getNodeType() == NodeType.FCALLNODE ||
   622                 node.getNodeType() == NodeType.VCALLNODE ||
   623                 node.getNodeType() == NodeType.CALLNODE;
   624     }
   625 
   626     static boolean isRaiseCall(Node node) {
   627         return isCall(node) && "raise".equals(((INameNode) node).getName());
   628     }
   629 
   630     static Node findNextNonNewLineNode(Node target) {
   631         if (target.getNodeType() != NodeType.NEWLINENODE) {
   632             return target;
   633         }
   634         NewlineNode newlineNode = (NewlineNode) target;
   635         return findNextNonNewLineNode(newlineNode.getNextNode());
   636     }
   637     
   638     public static String getCallName(Node node) {
   639         assert isCall(node);
   640 
   641         if (node instanceof INameNode) return ((INameNode) node).getName();
   642 
   643         return null;
   644     }
   645 
   646     public static String getDefName(Node node) {
   647         if (node instanceof MethodDefNode) return ((MethodDefNode) node).getName();
   648 
   649         assert false : node;
   650 
   651         return null;
   652     }
   653     
   654     public static boolean isConstructorMethod(MethodDefNode node) {
   655         String name = node.getName();
   656 
   657         return name.equals("new") || name.equals("initialize"); // NOI18N
   658     }
   659 
   660     /**
   661      * Look for the caret offset in the parameter list; return the
   662      * index of the parameter that contains it.
   663      */
   664     public static int findArgumentIndex(Node node, int offset) {
   665         switch (node.getNodeType()) {
   666         case FCALLNODE: case CALLNODE: {
   667             Node argsNode = ((IArgumentNode)node).getArgs();
   668             
   669             return argsNode == null ? -1 : findArgumentIndex(argsNode, offset);
   670         }
   671         case ARGSCATNODE: {
   672             ArgsCatNode acn = (ArgsCatNode)node;
   673 
   674             int index = findArgumentIndex(acn.getFirst(), offset);
   675             if (index != -1) return index;
   676 
   677             index = findArgumentIndex(acn.getSecond(), offset);
   678             if (index != -1) return getConstantArgs(acn) + index; // Add in arg count on the left
   679                 
   680             SourcePosition pos = node.getPosition();
   681             if (offset >= pos.getStartOffset() && offset <= pos.getEndOffset())return getConstantArgs(acn);
   682             
   683             return -1;
   684         }
   685         case HASHNODE: 
   686             // Everything gets glommed into the same hash parameter offset
   687             return offset;
   688         default:
   689             if (node instanceof ListNode) {
   690                 List<Node> children = node.childNodes();
   691 
   692                 int prevEnd = Integer.MAX_VALUE;
   693 
   694                 for (int index = 0; index < children.size(); index++) {
   695                     Node child = children.get(index);
   696                     SourcePosition pos = child.getPosition();
   697 
   698                     if  (offset <= pos.getEndOffset() && (offset >= prevEnd || offset >= pos.getStartOffset())) return index;
   699 
   700                     prevEnd = pos.getEndOffset();
   701                 }
   702 
   703                 // Caret -inside- empty parentheses?
   704                 SourcePosition pos = node.getPosition();
   705                 if (offset > pos.getStartOffset() && offset < pos.getEndOffset()) return 0;
   706             } else {
   707                 SourcePosition pos = node.getPosition();
   708                 if (offset >= pos.getStartOffset() && offset <= pos.getEndOffset()) return 0;
   709             }
   710 
   711             return -1;
   712         }
   713     }
   714 
   715     /** Utility method used by findArgumentIndex: count the constant number of
   716      * arguments in a parameter list before the argscatnode */
   717     private static int getConstantArgs(ArgsCatNode acn) {
   718         Node node = acn.getFirst();
   719 
   720         if (node instanceof ListNode) {
   721             List<Node> children = node.childNodes();
   722 // TODO - if one of the children is Node.INVALID_POSITION perhaps I need to reduce the count            
   723 
   724             return children.size();
   725         } 
   726 
   727         return 1;
   728     }
   729 
   730     /**
   731      * Return true iff the given call note can be considered a valid call of the given method.
   732      */
   733     public static boolean isCallFor(Node call, Arity callArity, Node method) {
   734         assert isCall(call);
   735         assert method instanceof MethodDefNode;
   736 
   737         // Simple call today...
   738         return getDefName(method).equals(getCallName(call)) &&
   739         Arity.matches(callArity, Arity.getArity((MethodDefNode) method));
   740     }
   741 
   742     // TODO: use the structure analyzer data for more accurate traversal?
   743     /** For the given signature, locating the corresponding Node within the tree that
   744      * it corresponds to */
   745     public static Node findBySignature(Node root, String signature) {
   746         String originalSig = signature;
   747 
   748         //String name = signature.split("(::)")
   749         // Find next name we're looking for
   750         boolean[] lookingForMethod = new boolean[1];
   751         String name = getNextSigComponent(signature, lookingForMethod);
   752         signature = signature.substring(name.length());
   753 
   754         Node node = findBySignature(root, root, signature, name, lookingForMethod);
   755         
   756         // Handle top level methods
   757         if (node == null && originalSig.startsWith("Object#")) {
   758             // Just look for top level method definitions instead
   759             originalSig = originalSig.substring(originalSig.indexOf('#')+1);
   760             name = getNextSigComponent(signature, lookingForMethod);
   761             signature = originalSig.substring(name.length());
   762             lookingForMethod[0] = true;
   763             
   764             node = findBySignature(root, root, signature, name, lookingForMethod);
   765         }
   766 
   767         return node;
   768     }
   769 
   770     // For a signature of the form Foo::Bar#baz(arg1,arg2,...)
   771     // pull out the next component; in the above, successively return
   772     // "Foo", "Bar", "baz", etc.
   773     private static String getNextSigComponent(String signature, boolean[] lookingForMethod) {
   774         StringBuilder sb = new StringBuilder();
   775         int i = 0;
   776         int n = signature.length();
   777 
   778         // Skip leading separators
   779         for (; i < n; i++) {
   780             char c = signature.charAt(i);
   781 
   782             if (c == '#') {
   783                 lookingForMethod[0] = true;
   784                 continue;
   785             } else if ((c == ':') || (c == '(')) {
   786                 continue;
   787             }
   788 
   789             break;
   790         }
   791 
   792         // Add the name
   793         for (; i < n; i++) {
   794             char c = signature.charAt(i);
   795 
   796             if ((c == '#') || (c == ':') || (c == '(')) {
   797                 break;
   798             }
   799 
   800             sb.append(c);
   801         }
   802 
   803         return sb.toString();
   804     }
   805 
   806     private static Node findBySignature(Node root, Node node, String signature, String name, boolean[] lookingForMethod) {
   807         switch (node.getNodeType()) {
   808         case INSTASGNNODE:
   809             if (name.charAt(0) == '@' && name.equals(((INameNode) node).getLexicalName())) return node;
   810             break;
   811         case CLASSVARDECLNODE: case CLASSVARASGNNODE:
   812             if (name.startsWith("@@") && name.equals(((INameNode) node).getLexicalName())) return node;
   813             break;
   814 
   815         case DEFNNODE:
   816         case DEFSNODE: {
   817             MethodDefNode def = (MethodDefNode) node;
   818             if (lookingForMethod[0] && name.equals(def.getName())) {
   819                 // See if the parameter list matches
   820                 // XXX TODO
   821                 List<String> parameters = def.getArgs().getNormativeParameterNameList(false);
   822 
   823                 if (signature.length() == 0 && (parameters == null || parameters.isEmpty())) {
   824                     return def; // No args
   825                 } else if (signature.length() != 0) {
   826                     assert signature.charAt(0) == '(' : signature;
   827 
   828                     String argList = signature.substring(1, signature.length() - 1);
   829                     String[] args = argList.split(",");
   830 
   831                     if (args.length == parameters.size()) {
   832                         // Should I enforce equality here?
   833                         boolean equal = true;
   834 
   835                         for (int i = 0; i < args.length; i++) {
   836                             if (!args[i].equals(parameters.get(i))) {
   837                                 equal = false;
   838 
   839                                 break;
   840                             }
   841                         }
   842 
   843                         if (equal) return def;
   844                     }
   845                 }
   846             } else if (isAttr(def)) {
   847                 for (SymbolNode sym : getAttrSymbols(def)) {
   848                     if (name.equals(sym.getName())) return node;
   849                 }
   850             }
   851             break;
   852         }
   853         case FCALLNODE:
   854             if (isAttr(node) || isNamedScope(node) || isActiveRecordAssociation(node) || 
   855                     isNodeNameIn((INameNode) node, RubyStructureAnalyzer.DYNAMIC_METHODS)) {
   856                 for (Node each : getChildValues(node)) {
   857                     if (name.equals(getNameOrValue(each))) return each;
   858                 }
   859             } else if (TestNameResolver.isShouldaMethod(((INameNode) node).getName())) {
   860                 String shoulda = TestNameResolver.getTestName(new AstPath(root, node));
   861             
   862                 if (name.equals(shoulda)) return node;
   863             }
   864 
   865             break;
   866         case ALIASNODE:
   867             AliasNode aliasNode = (AliasNode) node;
   868             if (name.equals(getNameOrValue(aliasNode.getNewName())) || name.equals(getNameOrValue(aliasNode.getOldName()))) {
   869                 return aliasNode;
   870             }
   871             break;
   872         case CONSTDECLNODE:
   873             if (name.equals(getName(node))) return node;
   874         break;
   875         case CLASSNODE:
   876         case MODULENODE: {
   877                 Colon3Node c3n = ((IScopingNode)node).getCPath();
   878 
   879                 if (c3n instanceof Colon2Node) {
   880                     String fqn = getFqn((Colon2Node)c3n);
   881 
   882                     if (fqn.startsWith(name) && signature.startsWith(fqn.substring(name.length()))) {
   883                         signature = signature.substring(fqn.substring(name.length()).length());
   884                         name = getNextSigComponent(signature, lookingForMethod);
   885 
   886                         if (name.length() == 0) return node; // Signature is a class/module already.
   887 
   888                         int index = signature.indexOf(name);
   889                         assert index != -1;
   890                         signature = signature.substring(index + name.length());
   891                     }
   892                 } else if (name.equals(AstUtilities.getClassOrModuleName(((IScopingNode)node)))) {
   893                     name = getNextSigComponent(signature, lookingForMethod);
   894 
   895                     if (name.length() == 0) return node; // Signature is a class/module already.
   896 
   897                     int index = signature.indexOf(name);
   898                     assert index != -1;
   899                     signature = signature.substring(index + name.length());
   900                 }
   901             break;
   902         }
   903         case SCLASSNODE:
   904             Node receiver = ((SClassNode)node).getReceiver();
   905             String rn = null;
   906 
   907             if (receiver instanceof Colon2Node) {
   908                 // TODO - check to see if we qualify
   909                 rn = getName(receiver);
   910             } else if (receiver instanceof ConstNode) {
   911                 rn = getName(receiver);
   912             } // else: some other type of singleton class definition, like class << foo
   913 
   914             if (rn != null) {
   915                 if (name.equals(rn)) {
   916                     name = getNextSigComponent(signature, lookingForMethod);
   917 
   918                     if (name.length() == 0) return node; // Signature is a class/module already.
   919 
   920                     int index = signature.indexOf(name);
   921                     assert index != -1;
   922                     signature = signature.substring(index + name.length());
   923                 }
   924             }
   925             break;
   926         }
   927 
   928         boolean old = lookingForMethod[0];
   929 
   930         for (Node child : node.childNodes()) {
   931             Node match = findBySignature(root, child, signature, name, lookingForMethod);
   932 
   933             if (match != null) return match;
   934         }
   935         lookingForMethod[0] = old;
   936 
   937         return null;
   938     }
   939 
   940     /** Return true iff the given node contains the given offset */
   941     public static boolean containsOffset(Node node, int offset) {
   942         SourcePosition pos = node.getPosition();
   943 
   944         return ((offset >= pos.getStartOffset()) && (offset <= pos.getEndOffset()));
   945     }
   946 
   947     /**
   948      * Like {@link #getRange(org.jrubyparser.ast.Node) }, but returns a position
   949      * for <code>NilNode</code>s too (but <strong>not</strong> for 
   950      * <code>NilImplicitNode</code>s!!). This is needed for highlightning 
   951      * exit points, <code>nil</code> is a common return value. Possibly 
   952      * {@link #getRange(org.jrubyparser.ast.Node) } itself should return a range
   953      * for <code>NilNode</code>s, but I'm too afraid to change it now as it 
   954      * is used in a lot of places.
   955      * @param node
   956      * @return
   957      */
   958     static OffsetRange getRangeIncludeNil(Node node) {
   959         if (node != null && node.getClass().equals(NilNode.class)) {
   960             SourcePosition pos = node.getPosition();
   961             try {
   962                 return new OffsetRange(pos.getStartOffset(), pos.getEndOffset());
   963             } catch (Throwable t) {
   964                 // ...because there are some problems -- see AstUtilities.testStress
   965                 Exceptions.printStackTrace(t);
   966                 return OffsetRange.NONE;
   967             }
   968         }
   969         return getRange(node);
   970     }
   971     /**
   972      * Return a range that matches the given node's source buffer range
   973      */
   974     public static OffsetRange getRange(Node node) {
   975         if (node.getNodeType() == NodeType.NILNODE) return OffsetRange.NONE;
   976 
   977         SourcePosition pos = node.getPosition();
   978         try {
   979             return new OffsetRange(pos.getStartOffset(), pos.getEndOffset());
   980         } catch (Throwable t) {
   981             // ...because there are some problems -- see AstUtilities.testStress
   982             Exceptions.printStackTrace(t);
   983             return OffsetRange.NONE;
   984         }
   985     }
   986     
   987     public static OffsetRange offsetRangeFor(SourcePosition position) {
   988         return new OffsetRange(position.getStartOffset(), position.getEndOffset());
   989     }
   990 
   991     public static OffsetRange getNameRange(Node node) {
   992         if (node instanceof AssignableNode) {
   993             return offsetRangeFor(((AssignableNode)node).getLeftHandSidePosition());
   994         } else if (node instanceof MethodDefNode) {
   995             return offsetRangeFor(((INameNode) node).getLexicalNamePosition());
   996         } else if (isCall(node)) {
   997             return getCallRange(node);
   998         } else if (node instanceof ClassNode) {
   999             // TODO - try to pull out the constnode or colon2node holding the class name,
  1000             // and return it!
  1001             Colon3Node c3n = ((ClassNode)node).getCPath();
  1002             if (c3n != null) {
  1003                 return getRange(c3n);
  1004             } else {
  1005                 return getRange(node);
  1006             }
  1007         } else if (node instanceof ModuleNode) {
  1008             // TODO - try to pull out the constnode or colon2node holding the class name,
  1009             // and return it!
  1010             Colon3Node c3n = ((ModuleNode)node).getCPath();
  1011             if (c3n != null) {
  1012                 return getRange(c3n);
  1013             } else {
  1014                 return getRange(node);
  1015             }
  1016 //        } else if (node instanceof SClassNode) {
  1017 //            // TODO - try to pull out the constnode or colon2node holding the class name,
  1018 //            // and return it!
  1019 //            Colon3Node c3n = ((SClassNode)node).getCPath();
  1020 //            if (c3n != null) {
  1021 //                return getRange(c3n);
  1022 //            } else {
  1023 //                return getRange(node);
  1024 //            }
  1025         } else {
  1026             return getRange(node);
  1027         }
  1028     }
  1029     
  1030     /** For CallNodes, the offset range for the AST node includes the entire parameter list.
  1031      *  We want ONLY the actual call/operator name. So compute that on our own.
  1032      */
  1033     public static OffsetRange getCallRange(Node node) {
  1034         SourcePosition pos = node.getPosition();
  1035         int start = pos.getStartOffset();
  1036         
  1037         assert isCall(node);
  1038         assert node instanceof INameNode;
  1039 
  1040         if (node instanceof CallNode) {
  1041             // A call of the form Foo.bar. "bar" is the CallNode, "Foo" is the ReceiverNode.
  1042             // Here I'm only handling named nodes; there may be others
  1043             Node receiver = ((CallNode)node).getReceiver();
  1044 
  1045             // end of "Foo::bar" + "."
  1046             if (receiver != null) start = receiver.getPosition().getEndOffset() + 1; 
  1047         }
  1048 
  1049         int end = node instanceof INameNode ? start + getName(node).length() : pos.getEndOffset();
  1050 
  1051         return new OffsetRange(start, end);
  1052     }
  1053 
  1054     /*
  1055     public static OffsetRange getFunctionNameRange(Node node) {
  1056         // TODO - enforce MethodDefNode and call getNameNode on it!
  1057         for (Node child : node.childNodes()) {
  1058             if (child instanceof ArgumentNode) {
  1059                 OffsetRange range = AstUtilities.getRange(child);
  1060 
  1061                 return range;
  1062             }
  1063         }
  1064 
  1065         if (node instanceof MethodDefNode) {
  1066             for (Node child : node.childNodes()) {
  1067                 if (child instanceof ConstNode) {
  1068                     SourcePosition pos = child.getPosition();
  1069                     int end = pos.getEndOffset();
  1070                     int start;
  1071 
  1072                     if (INCLUDE_DEFS_PREFIX) {
  1073                         start = pos.getStartOffset();
  1074                     } else {
  1075                         start = end + 1;
  1076                     }
  1077 
  1078                     // TODO - look at the source buffer and tweak offset if it's wrong
  1079                     // This assumes we have a single constant node, followed by a dot, followed by the name
  1080                     end = end + 1 + AstUtilities.getDefName(node).length(); // +1: "."
  1081 
  1082                     OffsetRange range = new OffsetRange(start, end);
  1083 
  1084                     return range;
  1085                 }
  1086             }
  1087         }
  1088 
  1089         return OffsetRange.NONE;
  1090     }*/
  1091 
  1092     /**
  1093      * Return the OffsetRange for an AliasNode that represents the new name portion.
  1094      */
  1095     public static OffsetRange getAliasNewRange(AliasNode node) {
  1096         // XXX I don't know where the old and new names are since the user COULD
  1097         // have used more than one whitespace character for separation. For now I'll
  1098         // just have to assume it's the normal case with one space:  alias new old.
  1099         // I -could- use the getPosition.getEndOffset() to see if this looks like it's
  1100         // the case (e.g. node length != "alias ".length + old.length+new.length+1).
  1101         // In this case I could go peeking in the source buffer to see where the
  1102         // spaces are - between alias and the first word or between old and new. XXX.
  1103         SourcePosition pos = node.getPosition();
  1104 
  1105         int newStart = pos.getStartOffset() + 6; // 6: "alias ".length()
  1106 
  1107         String name = getNameOrValue(node.getNewName());
  1108         int length = name != null ? name.length() : 0;
  1109         return new OffsetRange(newStart, newStart + length);
  1110     }
  1111 
  1112     /**
  1113      * Return the OffsetRange for an AliasNode that represents the old name portion.
  1114      */
  1115     public static OffsetRange getAliasOldRange(AliasNode node) {
  1116         // XXX I don't know where the old and new names are since the user COULD
  1117         // have used more than one whitespace character for separation. For now I'll
  1118         // just have to assume it's the normal case with one space:  alias new old.
  1119         // I -could- use the getPosition.getEndOffset() to see if this looks like it's
  1120         // the case (e.g. node length != "alias ".length + old.length+new.length+1).
  1121         // In this case I could go peeking in the source buffer to see where the
  1122         // spaces are - between alias and the first word or between old and new. XXX.
  1123         SourcePosition pos = node.getPosition();
  1124 
  1125         String newName = getNameOrValue(node.getNewName());
  1126         int newLength = newName != null ? newName.length() : 0;
  1127 
  1128         String oldName = getNameOrValue(node.getOldName());
  1129         int oldLength = oldName != null ? oldName.length() : 0;
  1130 
  1131         int oldStart = pos.getStartOffset() + 6 + newLength + 1; // 6: "alias ".length; 1: " ".length
  1132 
  1133         return new OffsetRange(oldStart, oldStart + oldLength);
  1134     }
  1135 
  1136     public static String getClassOrModuleName(IScopingNode node) {
  1137         return getName(node.getCPath());
  1138     }
  1139 
  1140     public static List<ClassNode> getClasses(Node root) {
  1141         // I would like to use a visitor for this, but it's not
  1142         // working - I get NPE's within DefaultIteratorVisitor
  1143         // on valid ASTs, and I see it's not used heavily in JRuby,
  1144         // so I'm not doing it this way for now.
  1145         //final List<ClassNode> classes = new ArrayList<ClassNode>();
  1146         //// There could be multiple Class definitions for this
  1147         //// same class, and (empirically) rdoc shows the documentation
  1148         //// for the last declaration.
  1149         //NodeVisitor findClasses = new AbstractVisitor() {
  1150         //    public Instruction visitClassNode(ClassNode node) {
  1151         //        classes.add(node);
  1152         //        return visitNode(node);
  1153         //    }
  1154         //
  1155         //    protected Instruction visitNode(Node iVisited) {
  1156         //        return null;
  1157         //    }
  1158         //};
  1159         //new DefaultIteratorVisitor(findClasses).visitRootNode((RootNode)parseResult.getRootNode());
  1160         List<ClassNode> classes = new ArrayList<ClassNode>();
  1161         addClasses(root, classes);
  1162 
  1163         return classes;
  1164     }
  1165 
  1166     private static void addClasses(Node node, List<ClassNode> classes) {
  1167         if (node instanceof ClassNode) classes.add((ClassNode)node);
  1168 
  1169         for (Node child : node.childNodes()) {
  1170             addClasses(child, classes);
  1171         }
  1172     }
  1173 
  1174     private static void addAncestorParents(Node node, StringBuilder sb) {
  1175         if (node instanceof Colon2Node) {
  1176             Colon2Node c2n = (Colon2Node)node;
  1177             addAncestorParents(c2n.getLeftNode(), sb);
  1178 
  1179             if ((sb.length() > 0) && (sb.charAt(sb.length() - 1) != ':')) {
  1180                 sb.append("::");
  1181             }
  1182 
  1183             sb.append(c2n.getName());
  1184         } else if (node instanceof INameNode) {
  1185             if ((sb.length() > 0) && (sb.charAt(sb.length() - 1) != ':')) {
  1186                 sb.append("::");
  1187             }
  1188 
  1189             sb.append(getName(node));
  1190         }
  1191     }
  1192 
  1193     public static String getFqn(Colon2Node c2n) {
  1194         StringBuilder sb = new StringBuilder();
  1195 
  1196         addAncestorParents(c2n, sb);
  1197 
  1198         return sb.toString();
  1199     }
  1200 
  1201     public static String getSuperclass(ClassNode clz) {
  1202         if (clz.getSuper() == null) return null;
  1203         
  1204         StringBuilder sb = new StringBuilder();
  1205         
  1206         addAncestorParents(clz.getSuper(), sb);
  1207 
  1208         return sb.toString();
  1209     }
  1210 
  1211     static String getFqnName(Node root, Node target) {
  1212         String fqn = getFqnName(new AstPath(root, target));
  1213         if (fqn.length() > 0) {
  1214             return fqn + "::" + getName(target);
  1215         }
  1216         return getName(target);
  1217     }
  1218 
  1219     static String getFqnName(AstPath path, String simpleName) {
  1220         String fqn = getFqnName(path);
  1221         if (fqn.length() > 0) {
  1222             return fqn + "::" + simpleName;
  1223         }
  1224         return simpleName;
  1225     }
  1226 
  1227     /** Compute the module/class name for the given node path */
  1228     public static String getFqnName(AstPath path) {
  1229         StringBuilder sb = new StringBuilder();
  1230 
  1231         Iterator<Node> it = path.rootToLeaf();
  1232 
  1233         while (it.hasNext()) {
  1234             Node node = it.next();
  1235 
  1236             if (node instanceof ModuleNode || node instanceof ClassNode) {
  1237                 Colon3Node cpath = ((IScopingNode)node).getCPath();
  1238                 if (cpath == null) continue;
  1239 
  1240                 if (sb.length() > 0) sb.append("::"); // NOI18N
  1241 
  1242                 if (cpath instanceof Colon2Node) {
  1243                     sb.append(getFqn((Colon2Node)cpath));
  1244                 } else {
  1245                     sb.append(cpath.getName());
  1246                 }
  1247             }
  1248         }
  1249 
  1250         return sb.toString();
  1251     }
  1252 
  1253     public static boolean isAttr(Node node) {
  1254         return isCallNode(node) && (isNodeNameIn((INameNode) node, ATTR_ACCESSORS) || isNodeNameIn((INameNode) node, CATTR_ACCESSORS));
  1255     }
  1256 
  1257     public static boolean isCAttr(Node node) {
  1258         return isCallNode(node) && isNodeNameIn((INameNode) node, CATTR_ACCESSORS);
  1259     }
  1260 
  1261     static boolean isNamedScope(Node node) {
  1262         return isCallNode(node) && isNodeNameIn((INameNode) node, NAMED_SCOPE);
  1263     }
  1264 
  1265     public static boolean isActiveRecordAssociation(Node node) {
  1266         return isCallNode(node) && isNodeNameIn((INameNode) node, ActiveRecordAssociationFinder.AR_ASSOCIATIONS);
  1267     }
  1268 
  1269     static boolean isNodeNameIn(INameNode node, String... names) {
  1270         String name = node.getName();
  1271         for (String each : names) {
  1272             if (each.equals(name)) return true;
  1273         }
  1274         return false;
  1275     }
  1276 
  1277     private static boolean isCallNode(Node node) {
  1278         return node instanceof FCallNode || node instanceof VCallNode;
  1279     }
  1280 
  1281     /**
  1282      * Returns the names or values of the nodes in the given {@code listNode}; 
  1283      * not including null and not empty values.
  1284      *
  1285      * @param listNode
  1286      * @return the names/values; the result does not contain {@code null}s or empty strings.
  1287      */
  1288     static List<String> getNamesOrValues(ListNode listNode) {
  1289         List<String> result = new ArrayList<String>(listNode.size());
  1290         for (int i = 0, max = listNode.size(); i < max; i++) {
  1291             Node n = listNode.get(i);
  1292 
  1293             // For dynamically computed strings, we have n instanceof DStrNode
  1294             // but I can't handle these anyway
  1295             if (n instanceof StrNode || n instanceof SymbolNode || n instanceof ConstNode) {
  1296                 String name = getNameOrValue(n);
  1297                 if (name != null && !name.isEmpty()) {
  1298                     result.add(name);
  1299                 }
  1300             } 
  1301         }
  1302         return result;
  1303 
  1304     }
  1305     /**
  1306      * Gets the name or the value of given node, depending on its type.
  1307      * 
  1308      * @param node the node whose value to get.
  1309      * @return the name or value of the given node or <code>null</code>.
  1310      */
  1311     public static String getNameOrValue(Node node) {
  1312         if (node instanceof StrNode) return ((StrNode) node).getValue();
  1313         if (node instanceof INameNode) return getName(node);
  1314 
  1315         if (node instanceof DSymbolNode) {
  1316             if (!node.childNodes().isEmpty()) {
  1317                 Node child = node.childNodes().get(0);
  1318                 return getNameOrValue(child);
  1319             }
  1320         }
  1321         if (node instanceof LiteralNode) {
  1322             return ((LiteralNode) node).getName();
  1323         }
  1324         return null;
  1325     }
  1326 
  1327     static SymbolNode[] getSymbols(Node node) {
  1328         List<SymbolNode> symbolList = new ArrayList<SymbolNode>();
  1329         for (Node child : getChildValues(node)) {
  1330             if (child instanceof SymbolNode) {
  1331                 symbolList.add((SymbolNode) child);
  1332             }
  1333 
  1334         }
  1335         return symbolList.toArray(new SymbolNode[symbolList.size()]);
  1336     }
  1337 
  1338     static List<Node> getChildValues(Node node) {
  1339         List<Node> result = new ArrayList<Node>();
  1340         for (Node child : node.childNodes()) {
  1341             if (child instanceof ListNode) {
  1342                 List<Node> values = child.childNodes();
  1343                 result.addAll(values);
  1344             }
  1345         }
  1346         return result;
  1347 
  1348     }
  1349 
  1350     public static SymbolNode[] getAttrSymbols(Node node) {
  1351         assert isAttr(node);
  1352         return getSymbols(node);
  1353     }
  1354 
  1355     public static Node getRoot(final FileObject sourceFO) {
  1356         Source source = Source.create(sourceFO);
  1357         if (source == null) return null;
  1358 
  1359         final Node[] rootHolder = new Node[1];
  1360         try {
  1361             ParserManager.parse(Collections.singleton(source), new UserTask() {
  1362                 @Override
  1363                 public void run(ResultIterator ri) throws Exception {
  1364                     Parser.Result result = ri.getParserResult();
  1365                     rootHolder[0] = getRoot(result);
  1366                 }
  1367             });
  1368         } catch (ParseException ex) {
  1369             Exceptions.printStackTrace(ex);
  1370         }
  1371         return rootHolder[0];
  1372     }
  1373 
  1374     /**
  1375      * Gets the root node from the given <code>parserResult</code>. May return
  1376      * <code>null</code> if <code>parserResult</code> was not a <code>RubyParserResult</code> or
  1377      * did not have a root node. For example for parts of .erb files the parserResult might
  1378      * (validly) be RhtmlParser$FakeParserResult, in which case this method returns <code>null</code>.
  1379      * 
  1380      * @param parserResult 
  1381      * @return the root node or <code>null</code>.
  1382      */
  1383     public static Node getRoot(Parser.Result parserResult) {
  1384         if (parserResult == null) return null;
  1385         
  1386         if (!(parserResult instanceof RubyParseResult)) {
  1387             if (LOGGER.isLoggable(Level.FINE)) {
  1388                 String msg = "Expected RubyParseResult, but got " + parserResult; //NOI18N
  1389                 // log an exception too see the stack trace
  1390                 LOGGER.log(Level.FINE, msg, new Exception(msg));
  1391             }
  1392             return null;
  1393         }
  1394 
  1395         return ((RubyParseResult) parserResult).getRootNode();
  1396     }
  1397 
  1398     /**
  1399      * Get the private and protected methods in the given class
  1400      */
  1401     public static void findPrivateMethods(Node clz, Set<Node> protectedMethods,
  1402         Set<Node> privateMethods) {
  1403         Set<String> publicMethodSymbols = new HashSet<String>();
  1404         Set<String> protectedMethodSymbols = new HashSet<String>();
  1405         Set<String> privateMethodSymbols = new HashSet<String>();
  1406         Set<Node> publicMethods = new HashSet<Node>();
  1407 
  1408         List<Node> list = clz.childNodes();
  1409 
  1410         Modifier access = Modifier.PUBLIC;
  1411 
  1412         for (Node child : list) {
  1413             access = getMethodAccess(child, access, publicMethodSymbols, protectedMethodSymbols,
  1414                     privateMethodSymbols, publicMethods, protectedMethods, privateMethods);
  1415         }
  1416 
  1417         // Can't just return private methods directly, since sometimes you can
  1418         // specify that a particular method is public before we know about it,
  1419         // so I can't just remove it from the known private list when I see the
  1420         // access modifier
  1421         privateMethodSymbols.removeAll(publicMethodSymbols);
  1422         protectedMethodSymbols.removeAll(publicMethodSymbols);
  1423 
  1424         // Should I worry about private foo;  protected :foo ?
  1425         // Seems unlikely somebody would do that... I guess
  1426         // I could do privateMethodSymbols.removeAll(protectedMethodSymbols) etc.
  1427         //privateMethodSymbols.removeAll(protectedMethodSymbols);
  1428         //protectedMethodSymbols.removeAll(privateMethodSymbols);
  1429 
  1430         // Add all methods known to be private into the private node set
  1431         for (String name : privateMethodSymbols) {
  1432             for (Node n : publicMethods) {
  1433                 if (name.equals(AstUtilities.getDefName(n))) {
  1434                     privateMethods.add(n);
  1435                 }
  1436             }
  1437         }
  1438 
  1439         for (String name : protectedMethodSymbols) {
  1440             for (Node n : publicMethods) {
  1441                 if (name.equals(AstUtilities.getDefName(n))) {
  1442                     protectedMethods.add(n);
  1443                 }
  1444             }
  1445         }
  1446     }
  1447 
  1448     /**
  1449      * @todo Should I really recurse into classes? If I have nested classes private
  1450      *  methods ther shouldn't be included for the parent!
  1451      *
  1452      * @param access The "current" known access level (PUBLIC, PROTECTED or PRIVATE)
  1453      * @return the access level to continue with at this syntactic level
  1454      */
  1455     private static Modifier getMethodAccess(Node node, Modifier access,
  1456         Set<String> publicMethodSymbols, Set<String> protectedMethodSymbols,
  1457         Set<String> privateMethodSymbols, Set<Node> publicMethods, Set<Node> protectedMethods,
  1458         Set<Node> privateMethods) {
  1459         if (node instanceof MethodDefNode) {
  1460             if (access == Modifier.PRIVATE) {
  1461                 privateMethods.add(node);
  1462             } else if (access == Modifier.PUBLIC) {
  1463                 publicMethods.add(node);
  1464             } else if (access == Modifier.PROTECTED) {
  1465                 protectedMethods.add(node);
  1466             }
  1467 
  1468             // XXX Can I have nested method definitions? If so I may have to continue here
  1469             return access;
  1470         } else if (node instanceof VCallNode || node instanceof FCallNode) {
  1471             String name = getName(node);
  1472 
  1473             if ("private".equals(name)) {
  1474                 // TODO - see if it has arguments, if it does - it's just a single
  1475                 // method defined to be private
  1476                 // Iterate over arguments and add symbols...
  1477                 if (Arity.callHasArguments(node)) {
  1478                     List<Node> params = node.childNodes();
  1479 
  1480                     for (Node param : params) {
  1481                         if (param instanceof ListNode) {
  1482                             List<Node> params2 = param.childNodes();
  1483 
  1484                             for (Node param2 : params2) {
  1485                                 if (param2 instanceof SymbolNode) {
  1486                                     String symbol = getName(param2);
  1487                                     privateMethodSymbols.add(symbol);
  1488                                 }
  1489                             }
  1490                         }
  1491                     }
  1492                 } else {
  1493                     access = Modifier.PRIVATE;
  1494                 }
  1495 
  1496                 return access;
  1497             } else if ("protected".equals(name)) {
  1498                 // TODO - see if it has arguments, if it does - it's just a single
  1499                 // method defined to be private
  1500                 // Iterate over arguments and add symbols...
  1501                 if (Arity.callHasArguments(node)) {
  1502                     List<Node> params = node.childNodes();
  1503 
  1504                     for (Node param : params) {
  1505                         if (param instanceof ListNode) {
  1506                             List<Node> params2 = param.childNodes();
  1507 
  1508                             for (Node param2 : params2) {
  1509                                 if (param2 instanceof SymbolNode) {
  1510                                     String symbol = getName(param2);
  1511                                     protectedMethodSymbols.add(symbol);
  1512                                 }
  1513                             }
  1514                         }
  1515                     }
  1516                 } else {
  1517                     access = Modifier.PROTECTED;
  1518                 }
  1519 
  1520                 return access;
  1521             } else if ("public".equals(name)) {
  1522                 if (!Arity.callHasArguments(node)) {
  1523                     access = Modifier.PUBLIC;
  1524 
  1525                     return access;
  1526                 } else {
  1527                     List<Node> params = node.childNodes();
  1528 
  1529                     for (Node param : params) {
  1530                         if (param instanceof ListNode) {
  1531                             List<Node> params2 = param.childNodes();
  1532 
  1533                             for (Node param2 : params2) {
  1534                                 if (param2 instanceof SymbolNode) {
  1535                                     String symbol = getName(param2);
  1536                                     publicMethodSymbols.add(symbol);
  1537                                 }
  1538                             }
  1539                         }
  1540                     }
  1541                 }
  1542             }
  1543 
  1544             return access;
  1545         } else if (node instanceof ClassNode || node instanceof ModuleNode) {
  1546             return access;
  1547         }
  1548 
  1549         for (Node child : node.childNodes()) {
  1550             access = getMethodAccess(child, access, publicMethodSymbols, protectedMethodSymbols,
  1551                     privateMethodSymbols, publicMethods, protectedMethods, privateMethods);
  1552         }
  1553 
  1554         return access;
  1555     }
  1556     
  1557     /**
  1558      * Get the method name for the given offset - or null if it cannot be found. This
  1559      * will initiate a new parse job if necessary.
  1560      */
  1561     public static String getMethodName(FileObject fo, final int lexOffset) {
  1562         Source source = Source.create(fo);
  1563 
  1564         if (source == null) {
  1565             return null;
  1566         }
  1567 
  1568         final String[] methodName = new String[1];
  1569         try {
  1570             ParserManager.parse(Collections.singleton(source), new UserTask() {
  1571                 @Override
  1572                 public void run(ResultIterator resultIterator) throws Exception {
  1573                     Parser.Result result = resultIterator.getParserResult();
  1574                     Node root = AstUtilities.getRoot(result);
  1575                     if (root == null) {
  1576                         return;
  1577                     }
  1578                     int astOffset = AstUtilities.getAstOffset(result, lexOffset);
  1579                     if (astOffset == -1) {
  1580                         return;
  1581                     }
  1582                     org.jrubyparser.ast.MethodDefNode method = AstUtilities.findMethodAtOffset(root, astOffset);
  1583                     if (method == null) {
  1584                         // It's possible the user had the caret on a line
  1585                         // that includes a method that isn't actually inside
  1586                         // the method block - such as the beginning of the
  1587                         // "def" line, or the end of a line after "end".
  1588                         // The latter isn't very likely, but the former can
  1589                         // happen, so let's check the method bodies at the
  1590                         // end of the current line
  1591                         BaseDocument doc = RubyUtils.getDocument(result);
  1592                         if (doc != null) {
  1593                             try {
  1594                                 int endOffset = Utilities.getRowEnd(doc, lexOffset);
  1595                                 if (endOffset != lexOffset) {
  1596                                     astOffset = AstUtilities.getAstOffset(result, endOffset);
  1597                                     if (astOffset == -1) {
  1598                                         return;
  1599                                     }
  1600                                     method = AstUtilities.findMethodAtOffset(root, astOffset);
  1601                                 }
  1602                             } catch (BadLocationException ble) {
  1603                                 // do nothing - see #154991
  1604                             }
  1605                         }
  1606                     }
  1607                     if (method != null) {
  1608                         methodName[0] = method.getName();
  1609                     }
  1610                 }
  1611             });
  1612         } catch (ParseException ex) {
  1613             Exceptions.printStackTrace(ex);
  1614         }
  1615 
  1616         return methodName[0];
  1617     }
  1618 
  1619     /**
  1620      * Get the test name surrounding the given offset - or null if it cannot be found.
  1621      * NOTE: This will initiate a new parse job if necessary. 
  1622      */
  1623     public static String getTestName(FileObject fo, final int caretOffset) {
  1624         Source source = Source.create(fo);
  1625 
  1626         if (source == null) {
  1627             return null;
  1628         }
  1629         
  1630         final String[] testName = new String[1];
  1631 
  1632         try {
  1633             ParserManager.parse(Collections.singleton(source), new UserTask() {
  1634 
  1635                 @Override
  1636                 public void run(ResultIterator resultIterator) throws Exception {
  1637                     try {
  1638                         Parser.Result result = resultIterator.getParserResult();
  1639                         Node root = AstUtilities.getRoot(result);
  1640                         if (root == null) {
  1641                             return;
  1642                         }
  1643                         // Make sure the offset isn't at the beginning of a line
  1644                         BaseDocument doc = RubyUtils.getDocument(result, true);
  1645                         if (doc == null) {
  1646                             return;
  1647                         }
  1648                         int lexOffset = caretOffset;
  1649                         int rowStart = Utilities.getRowFirstNonWhite(doc, lexOffset);
  1650                         if (rowStart != -1 && lexOffset <= rowStart) {
  1651                             lexOffset = rowStart + 1;
  1652                         }
  1653                         int astOffset = AstUtilities.getAstOffset(result, lexOffset);
  1654                         if (astOffset == -1) {
  1655                             return;
  1656                         }
  1657                         AstPath path = new AstPath(root, astOffset);
  1658                         
  1659                         testName[0] = TestNameResolver.getTestName(path);
  1660                         
  1661                     } catch (BadLocationException ex) {
  1662                         // do nothing - see #154991
  1663                     }
  1664                 }
  1665             });
  1666         } catch (ParseException ex) {
  1667             Exceptions.printStackTrace(ex);
  1668         }
  1669 
  1670         return testName[0];
  1671     }
  1672 
  1673     public static int findOffset(FileObject fo, final String methodName) {
  1674         Source source = Source.create(fo);
  1675 
  1676         if (source == null) {
  1677             return -1;
  1678         }
  1679         
  1680         final int[] offset = new int[1];
  1681         offset[0] = -1;
  1682 
  1683         try {
  1684             ParserManager.parse(Collections.singleton(source), new UserTask() {
  1685                 @Override
  1686                 public void run(ResultIterator resultIterator) throws Exception {
  1687                     Parser.Result result = resultIterator.getParserResult();
  1688                     Node root = AstUtilities.getRoot(result);
  1689                     if (root == null) {
  1690                         return;
  1691                     }
  1692 
  1693                     org.jrubyparser.ast.Node method =
  1694                             AstUtilities.findMethod(root, methodName, Arity.UNKNOWN);
  1695 
  1696                     if (method != null) {
  1697                         int startOffset = method.getPosition().getStartOffset();
  1698                         offset[0] = startOffset;
  1699                     }
  1700                 }
  1701             });
  1702         } catch (ParseException ex) {
  1703             Exceptions.printStackTrace(ex);
  1704         }
  1705 
  1706         return offset[0];
  1707     }
  1708     
  1709     /** Collect nodes of the given types (node.getNodeType()==NodeType.x) under the given root */
  1710     public static void addNodesByType(Node root, NodeType[] nodeIds, List<Node> result) {
  1711         for (NodeType nodeId : nodeIds) {
  1712             if (root.getNodeType() == nodeId) {
  1713                 result.add(root);
  1714                 break;
  1715             }
  1716         }
  1717 
  1718         for (Node child : root.childNodes()) {
  1719             addNodesByType(child, nodeIds, result);
  1720         }
  1721     }
  1722     
  1723     public static List<Node> getOutermostToInnerMostBlocks(Node leaf) {
  1724         Node scope = leaf.getInnermostIter();
  1725         
  1726         if (scope == null) return Collections.emptyList();
  1727         
  1728         List<Node> result = new ArrayList<Node>();
  1729         while (scope != null) {
  1730             result.add(scope);
  1731             
  1732             scope = scope.getInnermostIter();
  1733         }
  1734         
  1735         Collections.reverse(result);
  1736         
  1737         return result;
  1738     }
  1739     
  1740     /** Return all the blocknodes that apply to the given node. The outermost block
  1741      * is returned first.
  1742      */
  1743     public static List<Node> getApplicableBlocks(AstPath path, boolean includeNested) {
  1744         Node block = AstUtilities.findBlock(path);
  1745 
  1746         if (block == null) { // Use parent
  1747             block = path.leafParent();
  1748 
  1749             if (block == null) return Collections.emptyList();
  1750         }
  1751         
  1752         List<Node> result = new ArrayList<Node>();
  1753         Iterator<Node> it = path.leafToRoot();
  1754         
  1755         // Skip the leaf node, we're going to add it unconditionally afterwards
  1756         if (includeNested) {
  1757             if (it.hasNext()) it.next();
  1758         }
  1759 
  1760         Node leaf = path.root();
  1761 
  1762         while_loop:
  1763         while (it.hasNext()) {
  1764             Node n = it.next();
  1765             switch (n.getNodeType()) {
  1766                 case ITERNODE:
  1767                     leaf = n;
  1768                     result.add(n);
  1769                     break;
  1770                 case DEFNNODE:
  1771                 case DEFSNODE:
  1772                 case CLASSNODE:
  1773                 case SCLASSNODE:
  1774                 case MODULENODE:
  1775                     leaf = n;
  1776                     break while_loop;
  1777             }
  1778         }
  1779 
  1780         if (includeNested) {
  1781             addNodesByType(leaf, new NodeType[] { NodeType.ITERNODE }, result);
  1782         }
  1783         
  1784       return result;
  1785     }
  1786     
  1787     public static String guessName(Parser.Result result, OffsetRange lexRange, OffsetRange astRange) {
  1788         String guessedName = "";
  1789         
  1790         // Try to guess the name - see if it's in a method and if so name it after the parameter
  1791         IndexedMethod[] methodHolder = new IndexedMethod[1];
  1792         @SuppressWarnings("unchecked") Set<IndexedMethod>[] alternatesHolder = new Set[1];
  1793         int[] paramIndexHolder = new int[1];
  1794         int[] anchorOffsetHolder = new int[1];
  1795         if (!RubyMethodCompleter.computeMethodCall(result, lexRange.getStart(), astRange.getStart(),
  1796                 methodHolder, paramIndexHolder, anchorOffsetHolder, alternatesHolder, QuerySupport.Kind.PREFIX)) {
  1797 
  1798             return guessedName;
  1799         }
  1800 
  1801         IndexedMethod targetMethod = methodHolder[0];
  1802         int index = paramIndexHolder[0];
  1803 
  1804         List<String> params = targetMethod.getParameters();
  1805         if (params == null || params.size() <= index) {
  1806             return guessedName;
  1807         }
  1808         
  1809         String s = params.get(index);
  1810         if (s.startsWith("*") || s.startsWith("&")) { // Don't include * or & in variable name
  1811             s = s.substring(1);
  1812         }
  1813         return s;
  1814     }
  1815     
  1816     public static Set<String> getUsedFields(RubyIndex index, AstPath path) {
  1817         String fqn = AstUtilities.getFqnName(path);
  1818         if (fqn == null || fqn.length() == 0) {
  1819             return Collections.emptySet();
  1820         }
  1821         Set<IndexedField> fields = index.getInheritedFields(fqn, "", QuerySupport.Kind.PREFIX, false);
  1822         Set<String> fieldNames = new HashSet<String>();
  1823         for (IndexedField f : fields) {
  1824             fieldNames.add(f.getName());
  1825         }
  1826         
  1827         return fieldNames;
  1828     }
  1829     
  1830     public static Set<String> getUsedMethods(RubyIndex index, AstPath path) {
  1831         String fqn = AstUtilities.getFqnName(path);
  1832         if (fqn == null || fqn.length() == 0) {
  1833             return Collections.emptySet();
  1834         }
  1835         Set<IndexedMethod> methods = index.getInheritedMethods(fqn, "", QuerySupport.Kind.PREFIX);
  1836         Set<String> methodNames = new HashSet<String>();
  1837         for (IndexedMethod m : methods) {
  1838             methodNames.add(m.getName());
  1839         }
  1840         
  1841         return methodNames;
  1842     }
  1843     
  1844     /** @todo Implement properly */
  1845     public static Set<String> getUsedConstants(RubyIndex index, AstPath path) {
  1846         //String fqn = AstUtilities.getFqnName(path);
  1847         //if (fqn == null || fqn.length() == 0) {
  1848             return Collections.emptySet();
  1849         //}
  1850         //Set<IndexedConstant> constants = index.getInheritedConstants(fqn, "", QuerySupport.Kind.PREFIX);
  1851         //Set<String> constantNames = new HashSet<String>();
  1852         //for (IndexedConstant m : constants) {
  1853         //    constantNames.add(m.getName());
  1854         //}
  1855         //
  1856         //return constantNames;
  1857     }
  1858     
  1859     public static Set<String> getUsedLocalNames(AstPath path, Node closest) {
  1860         Node method = AstUtilities.findLocalScope(closest, path);
  1861         Map<String, Node> variables = new HashMap<String, Node>();
  1862         // Add locals
  1863         RubyCodeCompleter.addLocals(method, variables);
  1864 
  1865         List<Node> applicableBlocks = AstUtilities.getApplicableBlocks(path, false);
  1866         for (Node block : applicableBlocks) {
  1867             RubyCodeCompleter.addDynamic(block, variables);
  1868         }
  1869         
  1870         return variables.keySet();
  1871     }
  1872 
  1873     /**
  1874      * Throws {@link ClassCastException} if the given node is not instance of
  1875      * {@link INameNode} or {@link LiteralNode}.
  1876      *
  1877      * @param node instance of {@link INameNode} or {@link LiteralNode}.
  1878      * @return node's name
  1879      */
  1880     public static String getName(final Node node) {
  1881         if (node instanceof LiteralNode) return ((LiteralNode) node).getName();
  1882 
  1883         return ((INameNode) node).getName();
  1884     }
  1885 
  1886     /**
  1887      * Like {@link #getName(org.jrubyparser.ast.Node) }, but instead of throwing
  1888      * a CCE returns <code>null</code> if the given <code>node</code> wasn't an
  1889      * instance of {@ INameNode}.
  1890      *
  1891      * @param node
  1892      * @return the name or <code>null</code>.
  1893      */
  1894     static String safeGetName(final Node node) {
  1895         if (node instanceof INameNode) return ((INameNode) node).getName();
  1896         if (node instanceof LiteralNode) return ((LiteralNode) node).getName();
  1897         return null;
  1898     }
  1899 
  1900     /**
  1901      * Finds exit points of a method definition for the given node.
  1902      *
  1903      * @param defNode {@link MethodDefNode method definition node}
  1904      * @param exits accumulator for found exit points
  1905      */
  1906     public static void findExitPoints(final MethodDefNode defNode, final Collection<? super Node> exits) {
  1907         Node body = defNode.getBody();
  1908 
  1909         if (body != null) findExitPoints(body, exits); // method with empty body
  1910     }
  1911 
  1912     static void findExitPoints(final Node body, final Collection<? super Node> exits) {
  1913         findNonLastExitPoints(body, exits);
  1914         findLastNodes(body, exits);
  1915     }
  1916 
  1917     /**
  1918      * Gets the values of the given {@code args}, as fully qualified 
  1919      * names in case of {@link Colon2Node}s.
  1920      * @param args
  1921      * @return
  1922      */
  1923     static List<String> getValuesAsFqn(ListNode args) {
  1924         if (args.size() == 0) return Collections.emptyList();
  1925 
  1926         List<String> result = new ArrayList<String>(args.size());
  1927         for (Node n : args.childNodes()) {
  1928             if (n instanceof Colon2Node) {
  1929                 result.add(getFqn((Colon2Node) n));
  1930             } else if (n instanceof INameNode) {
  1931                 result.add(getName(n));
  1932             }
  1933         }
  1934         return result;
  1935     }
  1936 
  1937     private static void findLastNodes(final Node node, Collection<? super Node> result) {
  1938         if (node == null) return;
  1939 
  1940         List<Node> children = findExitChidren(node);
  1941         if (children.isEmpty()) {
  1942             result.add(node);
  1943             return;
  1944         }
  1945         // AttrAssgn does not put arguments into an ArgsNode unfortunately.
  1946         if (!(node instanceof AttrAssignNode)) {
  1947             for (Node child : children) {
  1948                 if (child instanceof ArgsNode || child instanceof ArgumentNode) {
  1949                     // Done - no valid statement
  1950                     result.add(node);
  1951                     return;
  1952                 }
  1953                 findLastNodes(child, result);
  1954             }
  1955         }
  1956     }
  1957 
  1958     private static List<Node> findExitChidren(Node node) {
  1959         if (node instanceof IfNode) {
  1960             IfNode ifNode = (IfNode) node;
  1961             return Arrays.asList(ifNode.getThenBody(), ifNode.getElseBody());
  1962         }
  1963         if (node instanceof CaseNode) {
  1964             CaseNode caseNode = (CaseNode) node;
  1965             ListNode cases = caseNode.getCases();
  1966             List<Node> result = new ArrayList<Node>(cases.childNodes());
  1967             result.add(caseNode.getElse());
  1968             return result;
  1969         }
  1970         if (node instanceof WhenNode) {
  1971             WhenNode whenNode = (WhenNode) node;
  1972             return Collections.singletonList(whenNode.getBody());
  1973         }
  1974         if (node instanceof OrNode) return node.childNodes();
  1975         if (node instanceof AndNode) return Collections.singletonList(((AndNode) node).getSecond());
  1976         if (node instanceof ReturnNode || isCall(node) || node instanceof ILiteralNode || node instanceof HashNode
  1977                 || node instanceof DotNode || node instanceof NotNode || node instanceof UntilNode
  1978                 || node instanceof DefinedNode) {
  1979             return Collections.emptyList();
  1980         }
  1981         if (node instanceof RescueNode) return node.childNodes();
  1982 
  1983         List<Node> children = node.childNodes();
  1984         if (!children.isEmpty()) {
  1985             Node lastChild = children.get(children.size() -1);
  1986             
  1987             return Collections.singletonList(lastChild);
  1988         }
  1989         return children;
  1990     }
  1991 
  1992 
  1993     /** Helper for {@link #findExitPoints}. */
  1994     private static void findNonLastExitPoints(final Node node, final Collection<? super Node> exits) {
  1995         switch (node.getNodeType()) {
  1996             case RETURNNODE: case YIELDNODE:
  1997                 exits.add(node);
  1998                 break;
  1999             case CLASSNODE: case SCLASSNODE: case MODULENODE:
  2000                 return; // Don't go into sub methods, classes, etc
  2001             case FCALLNODE:
  2002                 String name = ((INameNode) node).getName();
  2003                 if ("fail".equals(name) || "raise".equals(name)) exits.add(node); // NOI18N
  2004                 break;
  2005         }
  2006         if (node instanceof MethodDefNode) return;  // Don't go into sub methods, classes, etc
  2007 
  2008         for (Node child : node.childNodes()) {
  2009             findNonLastExitPoints(child, exits);
  2010         }
  2011     }
  2012 }