/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
*
* Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common
* Development and Distribution License("CDDL") (collectively, the
* "License"). You may not use this file except in compliance with the
* License. You can obtain a copy of the License at
* http://www.netbeans.org/cddl-gplv2.html
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
* specific language governing permissions and limitations under the
* License. When distributing the software, include this License Header
* Notice in each file and include the License file at
* nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the
* License Header, with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* Contributor(s):
*
* The Original Software is NetBeans. The Initial Developer of the Original
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun
* Microsystems, Inc. All Rights Reserved.
*
* If you wish your version of this file to be governed by only the CDDL
* or only the GPL Version 2, indicate your decision by adding
* "[Contributor] elects to include this software in this distribution
* under the [CDDL or GPL Version 2] license." If you do not indicate a
* single choice of license, a recipient has the option to distribute
* your version of this file under either the CDDL, the GPL Version 2 or
* to extend the choice of license to its licensees as provided above.
* However, if you add GPL Version 2 code and therefore, elected the GPL
* Version 2 license, then the option applies only if the new code is
* made subject to such option by the copyright holder.
*/
package org.netbeans.modules.ruby;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.jrubyparser.ast.AliasNode;
import org.jrubyparser.ast.ArgsCatNode;
import org.jrubyparser.ast.ArgsNode;
import org.jrubyparser.ast.ArgumentNode;
import org.jrubyparser.ast.AssignableNode;
import org.jrubyparser.ast.CallNode;
import org.jrubyparser.ast.ClassNode;
import org.jrubyparser.ast.Colon2Node;
import org.jrubyparser.ast.Colon3Node;
import org.jrubyparser.ast.ConstNode;
import org.jrubyparser.ast.FCallNode;
import org.jrubyparser.ast.IScopingNode;
import org.jrubyparser.ast.ListNode;
import org.jrubyparser.ast.LocalAsgnNode;
import org.jrubyparser.ast.MethodDefNode;
import org.jrubyparser.ast.ModuleNode;
import org.jrubyparser.ast.MultipleAsgnNode;
import org.jrubyparser.ast.Node;
import org.jrubyparser.ast.NodeType;
import org.jrubyparser.ast.SClassNode;
import org.jrubyparser.ast.StrNode;
import org.jrubyparser.ast.SymbolNode;
import org.jrubyparser.ast.VCallNode;
import org.jrubyparser.ast.INameNode;
import org.jrubyparser.SourcePosition;
import org.jrubyparser.ast.AndNode;
import org.jrubyparser.ast.CaseNode;
import org.jrubyparser.ast.DSymbolNode;
import org.jrubyparser.ast.DefinedNode;
import org.jrubyparser.ast.DotNode;
import org.jrubyparser.ast.HashNode;
import org.jrubyparser.ast.ILiteralNode;
import org.jrubyparser.ast.IfNode;
import org.jrubyparser.ast.LiteralNode;
import org.jrubyparser.ast.NewlineNode;
import org.jrubyparser.ast.NilNode;
import org.jrubyparser.ast.NotNode;
import org.jrubyparser.ast.OrNode;
import org.jrubyparser.ast.RescueNode;
import org.jrubyparser.ast.ReturnNode;
import org.jrubyparser.ast.UntilNode;
import org.jrubyparser.ast.WhenNode;
import org.netbeans.editor.BaseDocument;
import org.netbeans.editor.Utilities;
import org.netbeans.modules.csl.api.Modifier;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.csl.spi.GsfUtilities;
import org.netbeans.modules.csl.spi.ParserResult;
import org.netbeans.modules.parsing.api.ParserManager;
import org.netbeans.modules.parsing.api.ResultIterator;
import org.netbeans.modules.parsing.api.Snapshot;
import org.netbeans.modules.parsing.api.Source;
import org.netbeans.modules.parsing.api.UserTask;
import org.netbeans.modules.parsing.spi.ParseException;
import org.netbeans.modules.parsing.spi.Parser;
import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport;
import org.netbeans.modules.ruby.elements.IndexedElement;
import org.netbeans.modules.ruby.elements.IndexedField;
import org.netbeans.modules.ruby.elements.IndexedMethod;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;
/**
* Various utilities for operating on the JRuby ASTs that are used
* elsewhere.
*
* @todo Rewrite many of the custom recursion routines to simply
* call {@link addNodesByType} and then iterate (without recursion) over
* the result set.
*
* @author Tor Norbye
*/
public class AstUtilities {
private static final Logger LOGGER = Logger.getLogger(AstUtilities.class.getName());
/**
* Whether or not the prefixes for defs should be highlighted, e.g. in def
* HTTP.foo should "HTTP." be highlighted, or just the foo portion?
*/
private static final boolean INCLUDE_DEFS_PREFIX = false;
private static final String[] ATTR_ACCESSORS = {"attr", "attr_reader", "attr_accessor", "attr_writer",
"attr_internal", "attr_internal_accessor", "attr_internal_reader", "attr_internal_writer"};
/** ActiveSupport extensions */
private static final String[] CATTR_ACCESSORS = {"cattr_reader", "cattr_accessor", "cattr_writer"};
/** The names of AR scope methods - named_scope in rails 2.x, rails 3.x deprecates it
in favor of 'scope' */
private static final String[] NAMED_SCOPE = {"named_scope", "scope"};
/**
* Tries to cast the given <code>result</code> to <code>RubyParseResult</code>
* and returns it. Returns <code>null</code> if it wasn't an instance of <code>RubyParseResult</code>.
*
* @param result
* @return
*/
public static RubyParseResult getParseResult(Parser.Result result) {
if (!(result instanceof RubyParseResult)) {
LOGGER.log(Level.WARNING, "Expected RubyParseResult, but have {0}", result);
return null;
}
return (RubyParseResult) result;
}
public static int getAstOffset(Parser.Result info, int lexOffset) {
RubyParseResult result = getParseResult(info);
if (result != null) {
return result.getSnapshot().getEmbeddedOffset(lexOffset);
}
return lexOffset;
}
public static OffsetRange getAstOffsets(Parser.Result info, OffsetRange lexicalRange) {
RubyParseResult result = getParseResult(info);
if (result != null) {
int rangeStart = lexicalRange.getStart();
int start = result.getSnapshot().getEmbeddedOffset(rangeStart);
if (start == rangeStart) {
return lexicalRange;
} else if (start == -1) {
return OffsetRange.NONE;
} else {
// Assumes the translated range maintains size
return new OffsetRange(start, start + lexicalRange.getLength());
}
}
return lexicalRange;
}
/** This is a utility class only, not instantiatiable */
private AstUtilities() {
}
/**
* Get the rdoc documentation associated with the given node in the given document.
* The node must have position information that matches the source in the document.
*/
public static List<String> gatherDocumentation(Snapshot baseDoc, Node node) {
LinkedList<String> comments = new LinkedList<String>();
int elementBegin = node.getPosition().getStartOffset();
try {
if (elementBegin < 0 || elementBegin >= baseDoc.getText().length()) {
return null;
}
// Search to previous lines, locate comments. Once we have a non-whitespace line that isn't
// a comment, we're done
int offset = GsfUtilities.getRowStart(baseDoc.getText(), elementBegin);
offset--;
// Skip empty and whitespace lines
while (offset >= 0) {
// Find beginning of line
offset = GsfUtilities.getRowStart(baseDoc.getText(), offset);
if (!GsfUtilities.isRowEmpty(baseDoc.getText(), offset) &&
!GsfUtilities.isRowWhite(baseDoc.getText(), offset)) {
break;
}
offset--;
}
if (offset < 0) {
return null;
}
while (offset >= 0) {
// Find beginning of line
offset = GsfUtilities.getRowStart(baseDoc.getText(), offset);
if (GsfUtilities.isRowEmpty(baseDoc.getText(), offset) || GsfUtilities.isRowWhite(baseDoc.getText(), offset)) {
// Empty lines not allowed within an rdoc
break;
}
// This is a comment line we should include
int lineBegin = GsfUtilities.getRowFirstNonWhite(baseDoc.getText(), offset);
int lineEnd = GsfUtilities.getRowLastNonWhite(baseDoc.getText(), offset) + 1;
String line = baseDoc.getText().subSequence(lineBegin, lineEnd).toString();
// Tolerate "public", "private" and "protected" here --
// Test::Unit::Assertions likes to put these in front of each
// method.
if (line.startsWith("#")) {
comments.addFirst(line);
} else if ((comments.size() == 0) && line.startsWith("=end") &&
(lineBegin == GsfUtilities.getRowStart(baseDoc.getText(), offset))) {
// It could be a =begin,=end document - see scanf.rb in Ruby lib for example. Treat this differently.
gatherInlineDocumentation(comments, baseDoc, offset);
return comments;
} else if (line.equals("public") || line.equals("private") ||
line.equals("protected")) { // NOI18N
// Skip newlines back up to the comment
offset--;
while (offset >= 0) {
// Find beginning of line
offset = GsfUtilities.getRowStart(baseDoc.getText(), offset);
if (!GsfUtilities.isRowEmpty(baseDoc.getText(), offset) &&
!GsfUtilities.isRowWhite(baseDoc.getText(), offset)) {
break;
}
offset--;
}
continue;
} else {
// No longer in a comment
break;
}
// Previous line
offset--;
}
} catch (BadLocationException ble) {
// do nothing - see #154991
}
return comments;
}
private static void gatherInlineDocumentation(LinkedList<String> comments,
Snapshot baseDoc, int offset) throws BadLocationException {
// offset points to a line containing =end
// Skip the =end list
offset = GsfUtilities.getRowStart(baseDoc.getText(), offset);
offset--;
// Search backwards in the document for the =begin (if any) and add all lines in reverse
// order in between.
while (offset >= 0) {
// Find beginning of line
offset = GsfUtilities.getRowStart(baseDoc.getText(), offset);
// This is a comment line we should include
int lineBegin = offset;
int lineEnd = GsfUtilities.getRowEnd(baseDoc.getText(), offset);
String line = baseDoc.getText().subSequence(lineBegin, lineEnd).toString();
if (line.startsWith("=begin")) {
// We're done!
return;
}
comments.addFirst(line);
// Previous line
offset--;
}
}
/**
* <strong>This method may block for a long time; use with caution.</strong>.
*/
public static Node getForeignNode(final IndexedElement elem) {
return getForeignNode(elem, null);
}
/**
* <strong>This method may block for a long time; use with caution.</strong>.
* @param elem
* @param foreignInfoHolder
* @return
*/
public static Node getForeignNode(final IndexedElement elem, final Parser.Result[] foreignInfoHolder) {
FileObject fo = elem.getFileObject();
if (fo == null) {
return null;
}
Source source = Source.create(fo);
final Node[] nodeHolder = new Node[1];
try {
ParserManager.parse(Collections.singleton(source), new UserTask() {
@Override
public void run(ResultIterator resultIterator) throws Exception {
Parser.Result result = resultIterator.getParserResult();
if (foreignInfoHolder != null) {
assert foreignInfoHolder.length == 1;
foreignInfoHolder[0] = result;
}
Node root = AstUtilities.getRoot(result);
if (root != null) {
String signature = elem.getSignature();
if (signature != null) {
Node node = AstUtilities.findBySignature(root, signature);
// Special handling for "new" - these are synthesized from "initialize" methods
if ((node == null) && "new".equals(elem.getName())) { // NOI18N
if (signature.indexOf("#new") != -1) {
signature = signature.replaceFirst("#new", "#initialize"); //NOI18N
} else {
signature = signature.replaceFirst("new", "initialize"); //NOI18N
}
node = AstUtilities.findBySignature(root, signature);
}
nodeHolder[0] = node;
}
}
}
});
} catch (ParseException ex) {
Exceptions.printStackTrace(ex);
return null;
}
return nodeHolder[0];
}
// public static Node getForeignNode(final IndexedElement o) {
// FileObject fo = o.getFileObject();
// if (fo == null) {
// return null;
// }
//
// if (file == null) {
// return null;
// }
//
// List<ParserFile> files = Collections.singletonList(file);
// SourceFileReader reader =
// new SourceFileReader() {
// public CharSequence read(ParserFile file)
// throws IOException {
// Document doc = o.getDocument();
//
// if (doc == null) {
// return "";
// }
//
// try {
// return doc.getText(0, doc.getLength());
// } catch (BadLocationException ble) {
// IOException ioe = new IOException();
// ioe.initCause(ble);
// throw ioe;
// }
// }
//
// public int getCaretOffset(ParserFile fileObject) {
// return -1;
// }
// };
//
// DefaultParseListener listener = new DefaultParseListener();
// // TODO - embedding model?
// TranslatedSource translatedSource = null; // TODO - determine this here?
// Parser.Job job = new Parser.Job(files, listener, reader, translatedSource);
// new RubyParser().parseFiles(job);
//
// ParserResult result = listener.getParserResult();
//
// if (result == null) {
// return null;
// }
//
// Node root = AstUtilities.getRoot(result);
//
// if (root == null) {
// return null;
// }
//
// String signature = o.getSignature();
//
// if (signature == null) {
// return null;
// }
//
// Node node = AstUtilities.findBySignature(root, signature);
//
// // Special handling for "new" - these are synthesized from "initialize" methods
// if ((node == null) && "new".equals(o.getName())) { // NOI18N
// signature = signature.replaceFirst("new", "initialize"); //NOI18N
// node = AstUtilities.findBySignature(root, signature);
// }
//
// return node;
// }
public static int boundCaretOffset(ParserResult result, int caretOffset) {
Document doc = RubyUtils.getDocument(result);
if (doc != null) {
// If you invoke code completion while indexing is in progress, the
// completion job (which stores the caret offset) will be delayed until
// indexing is complete - potentially minutes later. When the job
// is finally run we need to make sure the caret position is still valid.
int length = doc.getLength();
if (caretOffset > length) {
caretOffset = length;
}
}
return caretOffset;
}
/**
* Return the set of requires that are defined in this AST
* (no transitive closure though).
*/
public static Set<String> getRequires(Node root) {
Set<String> requires = new HashSet<String>();
addRequires(root, requires);
return requires;
}
private static void addRequires(Node node, Set<String> requires) {
if (node.getNodeType() == NodeType.FCALLNODE) {
// A method call
String name = getName(node);
if (name.equals("require")) { // XXX Load too?
Node argsNode = ((FCallNode)node).getArgsNode();
if (argsNode instanceof ListNode) {
ListNode args = (ListNode)argsNode;
if (args.size() > 0) {
Node n = args.get(0);
// For dynamically computed strings, we have n instanceof DStrNode
// but I can't handle these anyway
if (n instanceof StrNode) {
String require = ((StrNode)n).getValue();
if ((require != null) && (require.length() > 0)) {
requires.add(require.toString());
}
}
}
}
}
} else if (node.getNodeType() == NodeType.MODULENODE || node.getNodeType() == NodeType.CLASSNODE ||
node.getNodeType() == NodeType.DEFNNODE || node.getNodeType() == NodeType.DEFSNODE) {
// Only look for require statements at the top level
return;
}
List<Node> list = node.childNodes();
for (Node child : list) {
if (child.isInvisible()) {
continue;
}
addRequires(child, requires);
}
}
/** Locate the method of the given name and arity */
public static MethodDefNode findMethod(Node node, String name, Arity arity) {
// Recursively search for methods or method calls that match the name and arity
if ((node.getNodeType() == NodeType.DEFNNODE || node.getNodeType() == NodeType.DEFSNODE) &&
getName(node).equals(name)) {
Arity defArity = Arity.getDefArity(node);
if (Arity.matches(arity, defArity)) {
return (MethodDefNode)node;
}
}
List<Node> list = node.childNodes();
for (Node child : list) {
if (child.isInvisible()) {
continue;
}
MethodDefNode match = findMethod(child, name, arity);
if (match != null) {
return match;
}
}
return null;
}
/**
* Gets the closest node at the given offset.
*
* @param root
* @param offset
* @return the closest node or <code>null</code>.
*/
public static Node findNodeAtOffset(Node root, int offset) {
AstPath path = new AstPath(root, offset);
Iterator<Node> it = path.leafToRoot();
return it.hasNext() ? it.next() : null;
}
public static MethodDefNode findMethodAtOffset(Node root, int offset) {
AstPath path = new AstPath(root, offset);
Iterator<Node> it = path.leafToRoot();
while (it.hasNext()) {
Node node = it.next();
if (node.getNodeType() == NodeType.DEFNNODE || node.getNodeType() == NodeType.DEFSNODE) {
return (MethodDefNode)node;
}
}
return null;
}
public static ClassNode findClassAtOffset(Node root, int offset) {
AstPath path = new AstPath(root, offset);
Iterator<Node> it = path.leafToRoot();
while (it.hasNext()) {
Node node = it.next();
if (node instanceof ClassNode) {
return (ClassNode)node;
}
}
return null;
}
public static Node findLocalScope(Node node, AstPath path) {
Node method = findMethod(path);
if (method == null) {
Iterator<Node> it = path.leafToRoot();
while (it.hasNext()) {
Node n = it.next();
switch (n.getNodeType()) {
case DEFNNODE:
case DEFSNODE:
case CLASSNODE:
case SCLASSNODE:
case MODULENODE:
return n;
}
}
if (path.root() != null) {
return path.root();
}
method = findBlock(path);
}
if (method == null) {
method = path.leafParent();
if (method.getNodeType() == NodeType.NEWLINENODE) {
method = path.leafGrandParent();
}
if (method == null) {
method = node;
}
}
return method;
}
public static Node findDynamicScope(Node node, AstPath path) {
Node block = findBlock(path);
if (block == null) {
// Use parent
block = path.leafParent();
if (block == null) {
block = node;
}
}
return block;
}
public static Node findBlock(AstPath path) {
// Find the most distant block node enclosing the given node (within
// the current method/class/module
Node candidate = null;
for (Node curr : path) {
switch (curr.getNodeType()) {
//case BLOCKNODE:
case ITERNODE:
candidate = curr;
break;
case DEFNNODE:
case DEFSNODE:
case CLASSNODE:
case SCLASSNODE:
case MODULENODE:
return candidate;
}
}
return candidate;
}
public static MethodDefNode findMethod(AstPath path) {
// Find the closest block node enclosing the given node
for (Node curr : path) {
if (curr.getNodeType() == NodeType.DEFNNODE || curr.getNodeType() == NodeType.DEFSNODE) {
return (MethodDefNode)curr;
}
if (curr.getNodeType() == NodeType.CLASSNODE || curr.getNodeType() == NodeType.SCLASSNODE ||
curr.getNodeType() == NodeType.MODULENODE) {
break;
}
}
return null;
}
// XXX Shouldn't this go in the REVERSE direction? I might find
// a superclass here!
// XXX What about SClassNode?
public static ClassNode findClass(AstPath path) {
// Find the closest block node enclosing the given node
for (Node curr : path) {
if (curr instanceof ClassNode) {
return (ClassNode)curr;
}
}
return null;
}
public static IScopingNode findClassOrModule(AstPath path) {
// Find the closest block node enclosing the given node
for (Node curr : path) {
// XXX What about SClassNodes?
if (curr.getNodeType() == NodeType.CLASSNODE || curr.getNodeType() == NodeType.MODULENODE) {
return (IScopingNode)curr;
}
}
return null;
}
public static boolean isCall(Node node) {
return node.getNodeType() == NodeType.FCALLNODE ||
node.getNodeType() == NodeType.VCALLNODE ||
node.getNodeType() == NodeType.CALLNODE;
}
static boolean isRaiseCall(Node node) {
if (isCall(node)) {
return "raise".equals(getName(node)); //NOI18N
}
return false;
}
static boolean isAssignmentNode(Node node) {
return node.getNodeType() == NodeType.INSTASGNNODE ||
node.getNodeType() == NodeType.LOCALASGNNODE ||
node.getNodeType() == NodeType.CLASSVARASGNNODE ||
node.getNodeType() == NodeType.GLOBALASGNNODE ||
node.getNodeType() == NodeType.ATTRASSIGNNODE ||
node.getNodeType() == NodeType.MULTIPLEASGNNODE;
}
static Node findNextNonNewLineNode(Node target) {
if (target.getNodeType() != NodeType.NEWLINENODE) {
return target;
}
NewlineNode newlineNode = (NewlineNode) target;
return findNextNonNewLineNode(newlineNode.getNextNode());
}
public static String getCallName(Node node) {
assert isCall(node);
if (node instanceof INameNode) {
return getName(node);
}
assert false : node;
return null;
}
public static String getDefName(Node node) {
if (node instanceof MethodDefNode) {
return getName(node);
}
assert false : node;
return null;
}
public static ArgumentNode getDefNameNode(MethodDefNode node) {
return node.getNameNode();
}
public static boolean isConstructorMethod(MethodDefNode node) {
String name = node.getName();
if (name.equals("new") || name.equals("initialize")) { // NOI18N
return true;
}
return false;
}
/** Find the direct child which is an ArgsNode, and pick out the argument names
* @param node The method definition node
* @param namesOnly If true, return only the parameter names for rest args and
* blocks. If false, include "*" and "&".
*/
public static List<String> getDefArgs(MethodDefNode node, boolean namesOnly) {
// TODO - do anything special about (&), blocks, argument lists (*), etc?
List<Node> nodes = node.childNodes();
// TODO - use AstElement.getParameters?
for (Node c : nodes) {
if (c instanceof ArgsNode) {
ArgsNode an = (ArgsNode)c;
List<Node> args = an.childNodes();
List<String> parameters = new ArrayList<String>();
for (Node arg : args) {
if (arg instanceof ListNode) {
List<Node> args2 = arg.childNodes();
for (Node arg2 : args2) {
if (arg2 instanceof ArgumentNode || arg2 instanceof LocalAsgnNode) {
String name = getName(arg2);
parameters.add(name);
}
}
}
}
// Rest args
if (an.getRest() != null) {
String name = an.getRest().getName();
if (!namesOnly) {
name = "*" + name;
}
parameters.add(name);
}
// Block args
if (an.getBlock() != null) {
String name = an.getBlock().getName();
if (!namesOnly) {
name = "&" + name;
}
parameters.add(name);
}
return parameters;
}
}
return null;
}
public static String getDefSignature(MethodDefNode node) {
StringBuilder sb = new StringBuilder();
sb.append(getDefName(node));
List<String> args = getDefArgs(node, false);
if ((args != null) && (args.size() > 0)) {
sb.append('(');
Iterator<String> it = args.iterator();
sb.append(it.next());
while (it.hasNext()) {
sb.append(',');
sb.append(it.next());
}
sb.append(')');
}
return sb.toString();
}
/**
* Look for the caret offset in the parameter list; return the
* index of the parameter that contains it.
*/
public static int findArgumentIndex(Node node, int offset) {
switch (node.getNodeType()) {
case FCALLNODE: {
Node argsNode = ((FCallNode)node).getArgsNode();
if (argsNode == null) {
return -1;
}
return findArgumentIndex(argsNode, offset);
}
case CALLNODE: {
Node argsNode = ((CallNode)node).getArgsNode();
if (argsNode == null) {
return -1;
}
return findArgumentIndex(argsNode, offset);
}
case ARGSCATNODE: {
ArgsCatNode acn = (ArgsCatNode)node;
int index = findArgumentIndex(acn.getFirstNode(), offset);
if (index != -1) {
return index;
}
index = findArgumentIndex(acn.getSecondNode(), offset);
if (index != -1) {
// Add in arg count on the left
return getConstantArgs(acn) + index;
}
SourcePosition pos = node.getPosition();
if ((offset >= pos.getStartOffset()) && (offset <= pos.getEndOffset())) {
return getConstantArgs(acn);
}
return -1;
}
case HASHNODE:
// Everything gets glommed into the same hash parameter offset
return offset;
default:
if (node instanceof ListNode) {
List<Node> children = node.childNodes();
int prevEnd = Integer.MAX_VALUE;
for (int index = 0; index < children.size(); index++) {
Node child = children.get(index);
if (child.isInvisible()) {
continue;
}
if (child.getNodeType() == NodeType.HASHNODE) {
// Invalid offsets - the hashnode often has the wrong offset
OffsetRange range = AstUtilities.getRange(child);
if ((offset <= range.getEnd()) &&
((offset >= prevEnd) || (offset >= range.getStart()))) {
return index;
}
prevEnd = range.getEnd();
} else {
SourcePosition pos = child.getPosition();
if ((offset <= pos.getEndOffset()) &&
((offset >= prevEnd) || (offset >= pos.getStartOffset()))) {
return index;
}
prevEnd = pos.getEndOffset();
}
}
// Caret -inside- empty parentheses?
SourcePosition pos = node.getPosition();
if ((offset > pos.getStartOffset()) && (offset < pos.getEndOffset())) {
return 0;
}
} else {
SourcePosition pos = node.getPosition();
if ((offset >= pos.getStartOffset()) && (offset <= pos.getEndOffset())) {
return 0;
}
}
return -1;
}
}
/** Utility method used by findArgumentIndex: count the constant number of
* arguments in a parameter list before the argscatnode */
private static int getConstantArgs(ArgsCatNode acn) {
Node node = acn.getFirstNode();
if (node instanceof ListNode) {
List children = node.childNodes();
// TODO - if one of the children is Node.INVALID_POSITION perhaps I need to reduce the count
return children.size();
} else {
return 1;
}
}
/**
* Return true iff the given call note can be considered a valid call of the given method.
*/
public static boolean isCallFor(Node call, Arity callArity, Node method) {
assert isCall(call);
assert method instanceof MethodDefNode;
// Simple call today...
return getDefName(method).equals(getCallName(call)) &&
Arity.matches(callArity, Arity.getDefArity(method));
}
// TODO: use the structure analyzer data for more accurate traversal?
/** For the given signature, locating the corresponding Node within the tree that
* it corresponds to */
public static Node findBySignature(Node root, String signature) {
String originalSig = signature;
//String name = signature.split("(::)")
// Find next name we're looking for
boolean[] lookingForMethod = new boolean[1];
String name = getNextSigComponent(signature, lookingForMethod);
signature = signature.substring(name.length());
Node node = findBySignature(root, root, signature, name, lookingForMethod);
// Handle top level methods
if (node == null && originalSig.startsWith("Object#")) {
// Just look for top level method definitions instead
originalSig = originalSig.substring(originalSig.indexOf('#')+1);
name = getNextSigComponent(signature, lookingForMethod);
signature = originalSig.substring(name.length());
lookingForMethod[0] = true;
node = findBySignature(root, root, signature, name, lookingForMethod);
}
return node;
}
// For a signature of the form Foo::Bar#baz(arg1,arg2,...)
// pull out the next component; in the above, successively return
// "Foo", "Bar", "baz", etc.
private static String getNextSigComponent(String signature, boolean[] lookingForMethod) {
StringBuilder sb = new StringBuilder();
int i = 0;
int n = signature.length();
// Skip leading separators
for (; i < n; i++) {
char c = signature.charAt(i);
if (c == '#') {
lookingForMethod[0] = true;
continue;
} else if ((c == ':') || (c == '(')) {
continue;
}
break;
}
// Add the name
for (; i < n; i++) {
char c = signature.charAt(i);
if ((c == '#') || (c == ':') || (c == '(')) {
break;
}
sb.append(c);
}
return sb.toString();
}
private static Node findBySignature(Node root, Node node, String signature, String name, boolean[] lookingForMethod) {
switch (node.getNodeType()) {
case INSTASGNNODE:
if (name.charAt(0) == '@') {
String n = getName(node);
//if (name.regionMatches(1, n, 0, n.length())) {
if (name.equals(n)) {
return node;
}
}
break;
case CLASSVARDECLNODE:
case CLASSVARASGNNODE:
if (name.startsWith("@@")) {
String n = getName(node);
//if (name.regionMatches(2, n, 0, n.length())) {
if (name.equals(n)) {
return node;
}
}
break;
case DEFNNODE:
case DEFSNODE:
if (lookingForMethod[0] && name.equals(AstUtilities.getDefName(node))) {
// See if the parameter list matches
// XXX TODO
List<String> parameters = getDefArgs((MethodDefNode)node, false);
if ((signature.length() == 0) &&
((parameters == null) || (parameters.size() == 0))) {
// No args
return node;
} else if (signature.length() != 0) {
assert signature.charAt(0) == '(' : signature;
String argList = signature.substring(1, signature.length() - 1);
String[] args = argList.split(",");
if (args.length == parameters.size()) {
// Should I enforce equality here?
boolean equal = true;
for (int i = 0; i < args.length; i++) {
if (!args[i].equals(parameters.get(i))) {
equal = false;
break;
}
}
if (equal) {
return node;
}
}
}
} else if (isAttr(node)) {
SymbolNode[] symbols = getAttrSymbols(node);
for (SymbolNode sym : symbols) {
if (name.equals(sym.getName())) {
return node;
}
}
}
break;
case FCALLNODE:
if (isAttr(node) || isNamedScope(node)
|| isActiveRecordAssociation(node) || isNodeNameIn(node, RubyStructureAnalyzer.DYNAMIC_METHODS)) { //NOI18N
List<Node> values = getChildValues(node);
for (Node each : values) {
if (name.equals(getNameOrValue(each))) {
return each;
}
}
} else if (TestNameResolver.isShouldaMethod(getName(node))) {
String shoulda = TestNameResolver.getTestName(new AstPath(root, node));
if (name.equals(shoulda)) {
return node;
}
}
break;
case ALIASNODE:
AliasNode aliasNode = (AliasNode) node;
if (name.equals(getNameOrValue(aliasNode.getNewName())) || name.equals(getNameOrValue(aliasNode.getOldName()))) {
return aliasNode;
}
break;
case CONSTDECLNODE:
if (name.equals(getName(node))) {
return node;
}
break;
case CLASSNODE:
case MODULENODE: {
Colon3Node c3n = ((IScopingNode)node).getCPath();
if (c3n instanceof Colon2Node) {
String fqn = getFqn((Colon2Node)c3n);
if (fqn.startsWith(name) && signature.startsWith(fqn.substring(name.length()))) {
signature = signature.substring(fqn.substring(name.length()).length());
name = getNextSigComponent(signature, lookingForMethod);
if (name.length() == 0) {
// The signature points to a class (or module) - just return it
return node;
}
int index = signature.indexOf(name);
assert index != -1;
signature = signature.substring(index + name.length());
}
} else if (name.equals(AstUtilities.getClassOrModuleName(((IScopingNode)node)))) {
name = getNextSigComponent(signature, lookingForMethod);
if (name.length() == 0) {
// The signature points to a class (or module) - just return it
return node;
}
int index = signature.indexOf(name);
assert index != -1;
signature = signature.substring(index + name.length());
}
break;
}
case SCLASSNODE:
Node receiver = ((SClassNode)node).getReceiverNode();
String rn = null;
if (receiver instanceof Colon2Node) {
// TODO - check to see if we qualify
rn = getName(receiver);
} else if (receiver instanceof ConstNode) {
rn = getName(receiver);
} // else: some other type of singleton class definition, like class << foo
if (rn != null) {
if (name.equals(rn)) {
name = getNextSigComponent(signature, lookingForMethod);
if (name.length() == 0) {
// The signature points to a class (or module) - just return it
return node;
}
int index = signature.indexOf(name);
assert index != -1;
signature = signature.substring(index + name.length());
}
}
break;
}
List<Node> list = node.childNodes();
boolean old = lookingForMethod[0];
for (Node child : list) {
if (child.isInvisible()) {
continue;
}
Node match = findBySignature(root, child, signature, name, lookingForMethod);
if (match != null) {
return match;
}
}
lookingForMethod[0] = old;
return null;
}
/** Return true iff the given node contains the given offset */
public static boolean containsOffset(Node node, int offset) {
SourcePosition pos = node.getPosition();
return ((offset >= pos.getStartOffset()) && (offset <= pos.getEndOffset()));
}
/**
* Like {@link #getRange(org.jrubyparser.ast.Node) }, but returns a position
* for <code>NilNode</code>s too (but <strong>not</strong> for
* <code>NilImplicitNode</code>s!!). This is needed for highlightning
* exit points, <code>nil</code> is a common return value. Possibly
* {@link #getRange(org.jrubyparser.ast.Node) } itself should return a range
* for <code>NilNode</code>s, but I'm too afraid to change it now as it
* is used in a lot of places.
* @param node
* @return
*/
static OffsetRange getRangeIncludeNil(Node node) {
if (node != null && node.getClass().equals(NilNode.class)) {
SourcePosition pos = node.getPosition();
try {
return new OffsetRange(pos.getStartOffset(), pos.getEndOffset());
} catch (Throwable t) {
// ...because there are some problems -- see AstUtilities.testStress
Exceptions.printStackTrace(t);
return OffsetRange.NONE;
}
}
return getRange(node);
}
/**
* Return a range that matches the given node's source buffer range
*/
public static OffsetRange getRange(Node node) {
if (node.isInvisible()) {
return OffsetRange.NONE;
}
if (node.getNodeType() == NodeType.NOTNODE) {
SourcePosition pos = node.getPosition();
// "unless !(x < 5)" gives a not-node with wrong offsets - starts
// with ! but doesn't include the closing )
List<Node> list = node.childNodes();
if (list != null && list.size() > 0) {
Node first = list.get(0);
if (first.getNodeType() == NodeType.NEWLINENODE) {
OffsetRange range = getRange(first);
return new OffsetRange(pos.getStartOffset(), range.getEnd());
}
}
return new OffsetRange(pos.getStartOffset(), pos.getEndOffset());
} else if (node.getNodeType() == NodeType.HASHNODE) {
// Workaround for incorrect JRuby AST offsets for hashnodes :
// render :action => 'list'
// has wrong argument offsets, which we want to correct.
// Just adopt the start offset of its first child (if any) and
// the end offset of its last child (if any)
List<Node> list = node.childNodes();
if (list != null && list.size() > 0) {
int start = list.get(0).getPosition().getStartOffset();
int end = list.get(list.size()-1).getPosition().getEndOffset();
return new OffsetRange(start, end);
} else {
SourcePosition pos = node.getPosition();
return new OffsetRange(pos.getStartOffset(), pos.getEndOffset());
}
} else if (node.getNodeType() == NodeType.NILNODE) {
return OffsetRange.NONE;
} else {
SourcePosition pos = node.getPosition();
try {
return new OffsetRange(pos.getStartOffset(), pos.getEndOffset());
} catch (Throwable t) {
// ...because there are some problems -- see AstUtilities.testStress
Exceptions.printStackTrace(t);
return OffsetRange.NONE;
}
}
}
/**
* Return a range that matches the lvalue for an assignment. The node must be namable.
*/
public static OffsetRange getLValueRange(AssignableNode node) {
if (node instanceof MultipleAsgnNode) {
MultipleAsgnNode man = (MultipleAsgnNode)node;
if (man.getHeadNode() != null) {
return getNameRange(man.getHeadNode());
} else {
return getRange(node);
}
}
assert node instanceof INameNode : node;
SourcePosition pos = node.getPosition();
OffsetRange range =
new OffsetRange(pos.getStartOffset(),
pos.getStartOffset() + getName(node).length());
return range;
}
public static OffsetRange getNameRange(Node node) {
if (node instanceof AssignableNode) {
return getLValueRange((AssignableNode)node);
} else if (node instanceof MethodDefNode) {
return getFunctionNameRange(node);
} else if (isCall(node)) {
return getCallRange(node);
} else if (node instanceof ClassNode) {
// TODO - try to pull out the constnode or colon2node holding the class name,
// and return it!
Colon3Node c3n = ((ClassNode)node).getCPath();
if (c3n != null) {
return getRange(c3n);
} else {
return getRange(node);
}
} else if (node instanceof ModuleNode) {
// TODO - try to pull out the constnode or colon2node holding the class name,
// and return it!
Colon3Node c3n = ((ModuleNode)node).getCPath();
if (c3n != null) {
return getRange(c3n);
} else {
return getRange(node);
}
// } else if (node instanceof SClassNode) {
// // TODO - try to pull out the constnode or colon2node holding the class name,
// // and return it!
// Colon3Node c3n = ((SClassNode)node).getCPath();
// if (c3n != null) {
// return getRange(c3n);
// } else {
// return getRange(node);
// }
} else {
return getRange(node);
}
}
/** For CallNodes, the offset range for the AST node includes the entire parameter list.
* We want ONLY the actual call/operator name. So compute that on our own.
*/
public static OffsetRange getCallRange(Node node) {
SourcePosition pos = node.getPosition();
int start = pos.getStartOffset();
int end = pos.getEndOffset();
assert isCall(node);
assert node instanceof INameNode;
if (node instanceof CallNode) {
// A call of the form Foo.bar. "bar" is the CallNode, "Foo" is the ReceiverNode.
// Here I'm only handling named nodes; there may be others
Node receiver = ((CallNode)node).getReceiverNode();
if (receiver != null && !receiver.isInvisible()) {
start = receiver.getPosition().getEndOffset() + 1; // end of "Foo::bar" + "."
}
}
if (node instanceof INameNode) {
end = start + getName(node).length();
}
return new OffsetRange(start, end);
}
public static OffsetRange getFunctionNameRange(Node node) {
// TODO - enforce MethodDefNode and call getNameNode on it!
for (Node child : node.childNodes()) {
if (child instanceof ArgumentNode) {
OffsetRange range = AstUtilities.getRange(child);
return range;
}
}
if (node instanceof MethodDefNode) {
for (Node child : node.childNodes()) {
if (child instanceof ConstNode) {
SourcePosition pos = child.getPosition();
int end = pos.getEndOffset();
int start;
if (INCLUDE_DEFS_PREFIX) {
start = pos.getStartOffset();
} else {
start = end + 1;
}
// TODO - look at the source buffer and tweak offset if it's wrong
// This assumes we have a single constant node, followed by a dot, followed by the name
end = end + 1 + AstUtilities.getDefName(node).length(); // +1: "."
OffsetRange range = new OffsetRange(start, end);
return range;
}
}
}
return OffsetRange.NONE;
}
/**
* Return the OffsetRange for an AliasNode that represents the new name portion.
*/
public static OffsetRange getAliasNewRange(AliasNode node) {
// XXX I don't know where the old and new names are since the user COULD
// have used more than one whitespace character for separation. For now I'll
// just have to assume it's the normal case with one space: alias new old.
// I -could- use the getPosition.getEndOffset() to see if this looks like it's
// the case (e.g. node length != "alias ".length + old.length+new.length+1).
// In this case I could go peeking in the source buffer to see where the
// spaces are - between alias and the first word or between old and new. XXX.
SourcePosition pos = node.getPosition();
int newStart = pos.getStartOffset() + 6; // 6: "alias ".length()
String name = getNameOrValue(node.getNewName());
int length = name != null ? name.length() : 0;
return new OffsetRange(newStart, newStart + length);
}
/**
* Return the OffsetRange for an AliasNode that represents the old name portion.
*/
public static OffsetRange getAliasOldRange(AliasNode node) {
// XXX I don't know where the old and new names are since the user COULD
// have used more than one whitespace character for separation. For now I'll
// just have to assume it's the normal case with one space: alias new old.
// I -could- use the getPosition.getEndOffset() to see if this looks like it's
// the case (e.g. node length != "alias ".length + old.length+new.length+1).
// In this case I could go peeking in the source buffer to see where the
// spaces are - between alias and the first word or between old and new. XXX.
SourcePosition pos = node.getPosition();
String newName = getNameOrValue(node.getNewName());
int newLength = newName != null ? newName.length() : 0;
String oldName = getNameOrValue(node.getOldName());
int oldLength = oldName != null ? oldName.length() : 0;
int oldStart = pos.getStartOffset() + 6 + newLength + 1; // 6: "alias ".length; 1: " ".length
return new OffsetRange(oldStart, oldStart + oldLength);
}
public static String getClassOrModuleName(IScopingNode node) {
return getName(node.getCPath());
}
public static List<ClassNode> getClasses(Node root) {
// I would like to use a visitor for this, but it's not
// working - I get NPE's within DefaultIteratorVisitor
// on valid ASTs, and I see it's not used heavily in JRuby,
// so I'm not doing it this way for now.
//final List<ClassNode> classes = new ArrayList<ClassNode>();
//// There could be multiple Class definitions for this
//// same class, and (empirically) rdoc shows the documentation
//// for the last declaration.
//NodeVisitor findClasses = new AbstractVisitor() {
// public Instruction visitClassNode(ClassNode node) {
// classes.add(node);
// return visitNode(node);
// }
//
// protected Instruction visitNode(Node iVisited) {
// return null;
// }
//};
//new DefaultIteratorVisitor(findClasses).visitRootNode((RootNode)parseResult.getRootNode());
List<ClassNode> classes = new ArrayList<ClassNode>();
addClasses(root, classes);
return classes;
}
private static void addClasses(Node node, List<ClassNode> classes) {
if (node instanceof ClassNode) {
classes.add((ClassNode)node);
}
List<Node> list = node.childNodes();
for (Node child : list) {
if (child.isInvisible()) {
continue;
}
addClasses(child, classes);
}
}
private static void addAncestorParents(Node node, StringBuilder sb) {
if (node instanceof Colon2Node) {
Colon2Node c2n = (Colon2Node)node;
addAncestorParents(c2n.getLeftNode(), sb);
if ((sb.length() > 0) && (sb.charAt(sb.length() - 1) != ':')) {
sb.append("::");
}
sb.append(c2n.getName());
} else if (node instanceof INameNode) {
if ((sb.length() > 0) && (sb.charAt(sb.length() - 1) != ':')) {
sb.append("::");
}
sb.append(getName(node));
}
}
public static String getFqn(Colon2Node c2n) {
StringBuilder sb = new StringBuilder();
addAncestorParents(c2n, sb);
return sb.toString();
}
public static String getSuperclass(ClassNode clz) {
StringBuilder sb = new StringBuilder();
if (clz.getSuperNode() != null) {
addAncestorParents(clz.getSuperNode(), sb);
return sb.toString();
}
return null;
}
static String getFqnName(Node root, Node target) {
String fqn = getFqnName(new AstPath(root, target));
if (fqn.length() > 0) {
return fqn + "::" + getName(target);
}
return getName(target);
}
static String getFqnName(AstPath path, String simpleName) {
String fqn = getFqnName(path);
if (fqn.length() > 0) {
return fqn + "::" + simpleName;
}
return simpleName;
}
/** Compute the module/class name for the given node path */
public static String getFqnName(AstPath path) {
StringBuilder sb = new StringBuilder();
Iterator<Node> it = path.rootToLeaf();
while (it.hasNext()) {
Node node = it.next();
if (node instanceof ModuleNode || node instanceof ClassNode) {
Colon3Node cpath = ((IScopingNode)node).getCPath();
if (cpath == null) {
continue;
}
if (sb.length() > 0) {
sb.append("::"); // NOI18N
}
if (cpath instanceof Colon2Node) {
sb.append(getFqn((Colon2Node)cpath));
} else {
sb.append(cpath.getName());
}
}
}
return sb.toString();
}
public static boolean isAttr(Node node) {
if (!isCallNode(node)) {
return false;
}
return isNodeNameIn(node, ATTR_ACCESSORS) || isNodeNameIn(node, CATTR_ACCESSORS);
}
public static boolean isCAttr(Node node) {
if (!isCallNode(node)) {
return false;
}
return isNodeNameIn(node, CATTR_ACCESSORS);
}
static boolean isNamedScope(Node node) {
if (!isCallNode(node)) {
return false;
}
return isNodeNameIn(node, NAMED_SCOPE);
}
public static boolean isActiveRecordAssociation(Node node) {
if (!isCallNode(node)) {
return false;
}
return isNodeNameIn(node, ActiveRecordAssociationFinder.AR_ASSOCIATIONS);
}
static boolean isNodeNameIn(Node node, String... names) {
String name = getName(node);
for (String each : names) {
if (each.equals(name)) {
return true;
}
}
return false;
}
private static boolean isCallNode(Node node) {
return node instanceof FCallNode || node instanceof VCallNode;
}
/**
* Returns the names or values of the nodes in the given {@code listNode};
* not including null and not empty values.
*
* @param listNode
* @return the names/values; the result does not contain {@code null}s or empty strings.
*/
static List<String> getNamesOrValues(ListNode listNode) {
List<String> result = new ArrayList<String>(listNode.size());
for (int i = 0, max = listNode.size(); i < max; i++) {
Node n = listNode.get(i);
// For dynamically computed strings, we have n instanceof DStrNode
// but I can't handle these anyway
if (n instanceof StrNode || n instanceof SymbolNode || n instanceof ConstNode) {
String name = getNameOrValue(n);
if (name != null && !name.isEmpty()) {
result.add(name);
}
}
}
return result;
}
/**
* Gets the name or the value of given node, depending on its type.
*
* @param node the node whose value to get.
* @return the name or value of the given node or <code>null</code>.
*/
public static String getNameOrValue(Node node) {
if (node instanceof SymbolNode) {
return ((SymbolNode) node).getName();
}
if (node instanceof StrNode) {
return ((StrNode) node).getValue();
}
if (node instanceof INameNode) {
return getName(node);
}
if (node instanceof DSymbolNode) {
if (!node.childNodes().isEmpty()) {
Node child = node.childNodes().get(0);
return getNameOrValue(child);
}
}
if (node instanceof LiteralNode) {
return ((LiteralNode) node).getName();
}
return null;
}
static SymbolNode[] getSymbols(Node node) {
List<SymbolNode> symbolList = new ArrayList<SymbolNode>();
for (Node child : getChildValues(node)) {
if (child instanceof SymbolNode) {
symbolList.add((SymbolNode) child);
}
}
return symbolList.toArray(new SymbolNode[symbolList.size()]);
}
static List<Node> getChildValues(Node node) {
List<Node> result = new ArrayList<Node>();
for (Node child : node.childNodes()) {
if (child instanceof ListNode) {
List<Node> values = child.childNodes();
result.addAll(values);
}
}
return result;
}
public static SymbolNode[] getAttrSymbols(Node node) {
assert isAttr(node);
return getSymbols(node);
}
public static Node getRoot(final FileObject sourceFO) {
Source source = Source.create(sourceFO);
if (source == null) {
return null;
}
final Node[] rootHolder = new Node[1];
try {
ParserManager.parse(Collections.singleton(source), new UserTask() {
@Override
public void run(ResultIterator ri) throws Exception {
Parser.Result result = ri.getParserResult();
rootHolder[0] = getRoot(result);
}
});
} catch (ParseException ex) {
Exceptions.printStackTrace(ex);
}
return rootHolder[0];
}
/**
* Gets the root node from the given <code>parserResult</code>. May return
* <code>null</code> if <code>parserResult</code> was not a <code>RubyParserResult</code> or
* did not have a root node. For example for parts of .erb files the parserResult might
* (validly) be RhtmlParser$FakeParserResult, in which case this method returns <code>null</code>.
*
* @param parserResult
* @return the root node or <code>null</code>.
*/
public static Node getRoot(Parser.Result parserResult) {
if (!(parserResult instanceof RubyParseResult)) {
if (LOGGER.isLoggable(Level.FINE)) {
String msg = "Expected RubyParseResult, but got " + parserResult; //NOI18N
// log an exception too see the stack trace
LOGGER.log(Level.FINE, msg, new Exception(msg));
}
return null;
}
RubyParseResult result = (RubyParseResult) parserResult;
return result.getRootNode();
}
/**
* Get the private and protected methods in the given class
*/
public static void findPrivateMethods(Node clz, Set<Node> protectedMethods,
Set<Node> privateMethods) {
Set<String> publicMethodSymbols = new HashSet<String>();
Set<String> protectedMethodSymbols = new HashSet<String>();
Set<String> privateMethodSymbols = new HashSet<String>();
Set<Node> publicMethods = new HashSet<Node>();
List<Node> list = clz.childNodes();
Modifier access = Modifier.PUBLIC;
for (Node child : list) {
if (child.isInvisible()) {
continue;
}
access = getMethodAccess(child, access, publicMethodSymbols, protectedMethodSymbols,
privateMethodSymbols, publicMethods, protectedMethods, privateMethods);
}
// Can't just return private methods directly, since sometimes you can
// specify that a particular method is public before we know about it,
// so I can't just remove it from the known private list when I see the
// access modifier
privateMethodSymbols.removeAll(publicMethodSymbols);
protectedMethodSymbols.removeAll(publicMethodSymbols);
// Should I worry about private foo; protected :foo ?
// Seems unlikely somebody would do that... I guess
// I could do privateMethodSymbols.removeAll(protectedMethodSymbols) etc.
//privateMethodSymbols.removeAll(protectedMethodSymbols);
//protectedMethodSymbols.removeAll(privateMethodSymbols);
// Add all methods known to be private into the private node set
for (String name : privateMethodSymbols) {
for (Node n : publicMethods) {
if (name.equals(AstUtilities.getDefName(n))) {
privateMethods.add(n);
}
}
}
for (String name : protectedMethodSymbols) {
for (Node n : publicMethods) {
if (name.equals(AstUtilities.getDefName(n))) {
protectedMethods.add(n);
}
}
}
}
/**
* @todo Should I really recurse into classes? If I have nested classes private
* methods ther shouldn't be included for the parent!
*
* @param access The "current" known access level (PUBLIC, PROTECTED or PRIVATE)
* @return the access level to continue with at this syntactic level
*/
private static Modifier getMethodAccess(Node node, Modifier access,
Set<String> publicMethodSymbols, Set<String> protectedMethodSymbols,
Set<String> privateMethodSymbols, Set<Node> publicMethods, Set<Node> protectedMethods,
Set<Node> privateMethods) {
if (node instanceof MethodDefNode) {
if (access == Modifier.PRIVATE) {
privateMethods.add(node);
} else if (access == Modifier.PUBLIC) {
publicMethods.add(node);
} else if (access == Modifier.PROTECTED) {
protectedMethods.add(node);
}
// XXX Can I have nested method definitions? If so I may have to continue here
return access;
} else if (node instanceof VCallNode || node instanceof FCallNode) {
String name = getName(node);
if ("private".equals(name)) {
// TODO - see if it has arguments, if it does - it's just a single
// method defined to be private
// Iterate over arguments and add symbols...
if (Arity.callHasArguments(node)) {
List<Node> params = node.childNodes();
for (Node param : params) {
if (param instanceof ListNode) {
List<Node> params2 = param.childNodes();
for (Node param2 : params2) {
if (param2 instanceof SymbolNode) {
String symbol = getName(param2);
privateMethodSymbols.add(symbol);
}
}
}
}
} else {
access = Modifier.PRIVATE;
}
return access;
} else if ("protected".equals(name)) {
// TODO - see if it has arguments, if it does - it's just a single
// method defined to be private
// Iterate over arguments and add symbols...
if (Arity.callHasArguments(node)) {
List<Node> params = node.childNodes();
for (Node param : params) {
if (param instanceof ListNode) {
List<Node> params2 = param.childNodes();
for (Node param2 : params2) {
if (param2 instanceof SymbolNode) {
String symbol = getName(param2);
protectedMethodSymbols.add(symbol);
}
}
}
}
} else {
access = Modifier.PROTECTED;
}
return access;
} else if ("public".equals(name)) {
if (!Arity.callHasArguments(node)) {
access = Modifier.PUBLIC;
return access;
} else {
List<Node> params = node.childNodes();
for (Node param : params) {
if (param instanceof ListNode) {
List<Node> params2 = param.childNodes();
for (Node param2 : params2) {
if (param2 instanceof SymbolNode) {
String symbol = getName(param2);
publicMethodSymbols.add(symbol);
}
}
}
}
}
}
return access;
} else if (node instanceof ClassNode || node instanceof ModuleNode) {
return access;
}
List<Node> list = node.childNodes();
for (Node child : list) {
if (child.isInvisible()) {
continue;
}
access = getMethodAccess(child, access, publicMethodSymbols, protectedMethodSymbols,
privateMethodSymbols, publicMethods, protectedMethods, privateMethods);
}
return access;
}
/**
* Get the method name for the given offset - or null if it cannot be found. This
* will initiate a new parse job if necessary.
*/
public static String getMethodName(FileObject fo, final int lexOffset) {
Source source = Source.create(fo);
if (source == null) {
return null;
}
final String[] methodName = new String[1];
try {
ParserManager.parse(Collections.singleton(source), new UserTask() {
@Override
public void run(ResultIterator resultIterator) throws Exception {
Parser.Result result = resultIterator.getParserResult();
Node root = AstUtilities.getRoot(result);
if (root == null) {
return;
}
int astOffset = AstUtilities.getAstOffset(result, lexOffset);
if (astOffset == -1) {
return;
}
org.jrubyparser.ast.MethodDefNode method = AstUtilities.findMethodAtOffset(root, astOffset);
if (method == null) {
// It's possible the user had the caret on a line
// that includes a method that isn't actually inside
// the method block - such as the beginning of the
// "def" line, or the end of a line after "end".
// The latter isn't very likely, but the former can
// happen, so let's check the method bodies at the
// end of the current line
BaseDocument doc = RubyUtils.getDocument(result);
if (doc != null) {
try {
int endOffset = Utilities.getRowEnd(doc, lexOffset);
if (endOffset != lexOffset) {
astOffset = AstUtilities.getAstOffset(result, endOffset);
if (astOffset == -1) {
return;
}
method = AstUtilities.findMethodAtOffset(root, astOffset);
}
} catch (BadLocationException ble) {
// do nothing - see #154991
}
}
}
if (method != null) {
methodName[0] = method.getName();
}
}
});
} catch (ParseException ex) {
Exceptions.printStackTrace(ex);
}
return methodName[0];
}
/**
* Get the test name surrounding the given offset - or null if it cannot be found.
* NOTE: This will initiate a new parse job if necessary.
*/
public static String getTestName(FileObject fo, final int caretOffset) {
Source source = Source.create(fo);
if (source == null) {
return null;
}
final String[] testName = new String[1];
try {
ParserManager.parse(Collections.singleton(source), new UserTask() {
@Override
public void run(ResultIterator resultIterator) throws Exception {
try {
Parser.Result result = resultIterator.getParserResult();
Node root = AstUtilities.getRoot(result);
if (root == null) {
return;
}
// Make sure the offset isn't at the beginning of a line
BaseDocument doc = RubyUtils.getDocument(result, true);
if (doc == null) {
return;
}
int lexOffset = caretOffset;
int rowStart = Utilities.getRowFirstNonWhite(doc, lexOffset);
if (rowStart != -1 && lexOffset <= rowStart) {
lexOffset = rowStart + 1;
}
int astOffset = AstUtilities.getAstOffset(result, lexOffset);
if (astOffset == -1) {
return;
}
AstPath path = new AstPath(root, astOffset);
testName[0] = TestNameResolver.getTestName(path);
} catch (BadLocationException ex) {
// do nothing - see #154991
}
}
});
} catch (ParseException ex) {
Exceptions.printStackTrace(ex);
}
return testName[0];
}
public static int findOffset(FileObject fo, final String methodName) {
Source source = Source.create(fo);
if (source == null) {
return -1;
}
final int[] offset = new int[1];
offset[0] = -1;
try {
ParserManager.parse(Collections.singleton(source), new UserTask() {
@Override
public void run(ResultIterator resultIterator) throws Exception {
Parser.Result result = resultIterator.getParserResult();
Node root = AstUtilities.getRoot(result);
if (root == null) {
return;
}
org.jrubyparser.ast.Node method =
AstUtilities.findMethod(root, methodName, Arity.UNKNOWN);
if (method != null) {
int startOffset = method.getPosition().getStartOffset();
offset[0] = startOffset;
}
}
});
} catch (ParseException ex) {
Exceptions.printStackTrace(ex);
}
return offset[0];
}
/** Collect nodes of the given types (node.getNodeType()==NodeType.x) under the given root */
public static void addNodesByType(Node root, NodeType[] nodeIds, List<Node> result) {
for (int i = 0; i < nodeIds.length; i++) {
if (root.getNodeType() == nodeIds[i]) {
result.add(root);
break;
}
}
List<Node> list = root.childNodes();
for (Node child : list) {
if (child.isInvisible()) {
continue;
}
addNodesByType(child, nodeIds, result);
}
}
/** Return all the blocknodes that apply to the given node. The outermost block
* is returned first.
*/
public static List<Node> getApplicableBlocks(AstPath path, boolean includeNested) {
Node block = AstUtilities.findBlock(path);
if (block == null) {
// Use parent
block = path.leafParent();
if (block == null) {
return Collections.emptyList();
}
}
List<Node> result = new ArrayList<Node>();
Iterator<Node> it = path.leafToRoot();
// Skip the leaf node, we're going to add it unconditionally afterwards
if (includeNested) {
if (it.hasNext()) {
it.next();
}
}
Node leaf = path.root();
while_loop:
while (it.hasNext()) {
Node n = it.next();
switch (n.getNodeType()) {
//case BLOCKNODE:
case ITERNODE:
leaf = n;
result.add(n);
break;
case DEFNNODE:
case DEFSNODE:
case CLASSNODE:
case SCLASSNODE:
case MODULENODE:
leaf = n;
break while_loop;
}
}
if (includeNested) {
addNodesByType(leaf, new NodeType[] { /*NodeType.BLOCKNODE,*/ NodeType.ITERNODE }, result);
}
return result;
}
public static String guessName(Parser.Result result, OffsetRange lexRange, OffsetRange astRange) {
String guessedName = "";
// Try to guess the name - see if it's in a method and if so name it after the parameter
IndexedMethod[] methodHolder = new IndexedMethod[1];
@SuppressWarnings("unchecked")
Set<IndexedMethod>[] alternatesHolder = new Set[1];
int[] paramIndexHolder = new int[1];
int[] anchorOffsetHolder = new int[1];
if (!RubyMethodCompleter.computeMethodCall(result, lexRange.getStart(), astRange.getStart(),
methodHolder, paramIndexHolder, anchorOffsetHolder, alternatesHolder, QuerySupport.Kind.PREFIX)) {
return guessedName;
}
IndexedMethod targetMethod = methodHolder[0];
int index = paramIndexHolder[0];
List<String> params = targetMethod.getParameters();
if (params == null || params.size() <= index) {
return guessedName;
}
String s = params.get(index);
if (s.startsWith("*") || s.startsWith("&")) { // Don't include * or & in variable name
s = s.substring(1);
}
return s;
}
public static Set<String> getUsedFields(RubyIndex index, AstPath path) {
String fqn = AstUtilities.getFqnName(path);
if (fqn == null || fqn.length() == 0) {
return Collections.emptySet();
}
Set<IndexedField> fields = index.getInheritedFields(fqn, "", QuerySupport.Kind.PREFIX, false);
Set<String> fieldNames = new HashSet<String>();
for (IndexedField f : fields) {
fieldNames.add(f.getName());
}
return fieldNames;
}
public static Set<String> getUsedMethods(RubyIndex index, AstPath path) {
String fqn = AstUtilities.getFqnName(path);
if (fqn == null || fqn.length() == 0) {
return Collections.emptySet();
}
Set<IndexedMethod> methods = index.getInheritedMethods(fqn, "", QuerySupport.Kind.PREFIX);
Set<String> methodNames = new HashSet<String>();
for (IndexedMethod m : methods) {
methodNames.add(m.getName());
}
return methodNames;
}
/** @todo Implement properly */
public static Set<String> getUsedConstants(RubyIndex index, AstPath path) {
//String fqn = AstUtilities.getFqnName(path);
//if (fqn == null || fqn.length() == 0) {
return Collections.emptySet();
//}
//Set<IndexedConstant> constants = index.getInheritedConstants(fqn, "", QuerySupport.Kind.PREFIX);
//Set<String> constantNames = new HashSet<String>();
//for (IndexedConstant m : constants) {
// constantNames.add(m.getName());
//}
//
//return constantNames;
}
public static Set<String> getUsedLocalNames(AstPath path, Node closest) {
Node method = AstUtilities.findLocalScope(closest, path);
Map<String, Node> variables = new HashMap<String, Node>();
// Add locals
RubyCodeCompleter.addLocals(method, variables);
List<Node> applicableBlocks = AstUtilities.getApplicableBlocks(path, false);
for (Node block : applicableBlocks) {
RubyCodeCompleter.addDynamic(block, variables);
}
return variables.keySet();
}
/**
* Throws {@link ClassCastException} if the given node is not instance of
* {@link INameNode} or {@link LiteralNode}.
*
* @param node instance of {@link INameNode} or {@link LiteralNode}.
* @return node's name
*/
public static String getName(final Node node) {
if (node instanceof LiteralNode) {
return ((LiteralNode) node).getName();
}
return ((INameNode) node).getName();
}
/**
* Like {@link #getName(org.jrubyparser.ast.Node) }, but instead of throwing
* a CCE returns <code>null</code> if the given <code>node</code> wasn't an
* instance of {@ INameNode}.
*
* @param node
* @return the name or <code>null</code>.
*/
static String safeGetName(final Node node) {
if (node instanceof INameNode || node instanceof LiteralNode) {
return getName(node);
}
return null;
}
/**
* Finds exit points of a method definition for the given node.
*
* @param defNode {@link MethodDefNode method definition node}
* @param exits accumulator for found exit points
*/
public static void findExitPoints(final MethodDefNode defNode, final Collection<? super Node> exits) {
Node body = defNode.getBodyNode();
if (body != null) { // method with empty body
findExitPoints(body, exits);
}
}
static void findExitPoints(final Node body, final Collection<? super Node> exits) {
findNonLastExitPoints(body, exits);
findLastNodes(body, exits);
}
/**
* Gets the values of the given {@code args}, as fully qualified
* names in case of {@link Colon2Node}s.
* @param args
* @return
*/
static List<String> getValuesAsFqn(ListNode args) {
if (args.size() == 0) {
return Collections.emptyList();
}
List<String> result = new ArrayList<String>(args.size());
for (Node n : args.childNodes()) {
if (n instanceof Colon2Node) {
result.add(getFqn((Colon2Node) n));
} else if (n instanceof INameNode) {
result.add(getName(n));
}
}
return result;
}
private static void findLastNodes(final Node node, Collection<? super Node> result) {
if (node == null) {
return;
}
List<Node> children = findExitChidren(node);
if (children.isEmpty()) {
result.add(node);
return;
}
for (Node child : children) {
if (child instanceof ArgsNode || child instanceof ArgumentNode) {
// Done - no valid statement
result.add(node);
return;
}
findLastNodes(child, result);
}
}
private static List<Node> findExitChidren(Node node) {
if (node instanceof IfNode) {
IfNode ifNode = (IfNode) node;
return Arrays.asList(ifNode.getThenBody(), ifNode.getElseBody());
}
if (node instanceof CaseNode) {
CaseNode caseNode = (CaseNode) node;
ListNode cases = caseNode.getCases();
List<Node> result = new ArrayList<Node>(cases.childNodes());
result.add(caseNode.getElseNode());
return result;
}
if (node instanceof WhenNode) {
WhenNode whenNode = (WhenNode) node;
return Collections.singletonList(whenNode.getBodyNode());
}
if (node instanceof OrNode) {
return node.childNodes();
}
if (node instanceof AndNode) {
return Collections.singletonList(((AndNode) node).getSecondNode());
}
if (node instanceof ReturnNode
|| isCall(node)
|| node instanceof ILiteralNode
|| node instanceof HashNode
|| node instanceof DotNode
|| node instanceof NotNode
|| node instanceof UntilNode
|| node instanceof DefinedNode) {
return Collections.emptyList();
}
if (node instanceof RescueNode) {
return node.childNodes();
}
List<Node> children = node.childNodes();
if (!children.isEmpty()) {
Node lastChild = children.get(children.size() -1);
return Collections.singletonList(lastChild);
}
return children;
}
/** Helper for {@link #findExitPoints}. */
private static void findNonLastExitPoints(final Node node, final Collection<? super Node> exits) {
switch (node.getNodeType()) {
case RETURNNODE:
case YIELDNODE:
exits.add(node);
break;
case CLASSNODE:
case SCLASSNODE:
case MODULENODE:
return; // Don't go into sub methods, classes, etc
case FCALLNODE:
FCallNode fc = (FCallNode) node;
if ("fail".equals(fc.getName()) || "raise".equals(fc.getName())) { // NOI18N
exits.add(node);
}
break;
}
if (node instanceof MethodDefNode) {
// Don't go into sub methods, classes, etc
return;
}
List<Node> children = node.childNodes();
for (Node child : children) {
if (child.isInvisible()) {
continue;
}
findNonLastExitPoints(child, exits);
}
}
}