/*
* 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-2009 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.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.ImageIcon;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.jrubyparser.ast.CallNode;
import org.jrubyparser.ast.ClassNode;
import org.jrubyparser.ast.Colon2Node;
import org.jrubyparser.ast.CommentNode;
import org.jrubyparser.ast.ConstDeclNode;
import org.jrubyparser.ast.ConstNode;
import org.jrubyparser.ast.DefnNode;
import org.jrubyparser.ast.DefsNode;
import org.jrubyparser.ast.FCallNode;
import org.jrubyparser.ast.GlobalAsgnNode;
import org.jrubyparser.ast.InstAsgnNode;
import org.jrubyparser.ast.ListNode;
import org.jrubyparser.ast.LocalAsgnNode;
import org.jrubyparser.ast.MethodDefNode;
import org.jrubyparser.ast.ModuleNode;
import org.jrubyparser.ast.Node;
import org.jrubyparser.ast.SClassNode;
import org.jrubyparser.ast.StrNode;
import org.jrubyparser.ast.SymbolNode;
import org.jrubyparser.ast.INameNode;
import org.jrubyparser.SourcePosition;
import org.jrubyparser.ast.AliasNode;
import org.jrubyparser.ast.MultipleAsgnNode;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenId;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.api.lexer.TokenUtilities;
import org.netbeans.editor.BaseDocument;
import org.netbeans.modules.csl.api.ElementHandle;
import org.netbeans.modules.csl.api.ElementKind;
import org.netbeans.modules.csl.api.HtmlFormatter;
import org.netbeans.modules.csl.api.Modifier;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.csl.api.StructureItem;
import org.netbeans.modules.csl.api.StructureScanner.Configuration;
import org.netbeans.modules.csl.api.StructureScanner;
import org.netbeans.modules.csl.spi.ParserResult;
import org.netbeans.modules.ruby.elements.AstAttributeElement;
import org.netbeans.modules.ruby.elements.AstClassElement;
import org.netbeans.modules.ruby.elements.AstDynamicMethodElement;
import org.netbeans.modules.ruby.elements.AstElement;
import org.netbeans.modules.ruby.elements.AstFieldElement;
import org.netbeans.modules.ruby.elements.AstMethodElement;
import org.netbeans.modules.ruby.elements.AstModuleElement;
import org.netbeans.modules.ruby.elements.AstNameElement;
import org.netbeans.modules.ruby.elements.Element;
import org.netbeans.modules.ruby.lexer.RubyTokenId;
import org.netbeans.modules.ruby.platform.gems.Gems;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;
import org.openide.util.ImageUtilities;
import org.openide.util.NbBundle;
/**
* @todo Rewrite various other helper classes to use the scanned structure
* for the file instead of searching from scratch. For example, the code
* completion scanner should rely on the structure view to add local
* classes, fields and globals - it should only scan the current method
* for local variables. Similarly, the declaration finder should use it
* to locate local classes, method definitions and such. And obviously,
* the semantic analyzer should use it to find private methods.
*
* @author Tor Norbye
*/
public class RubyStructureAnalyzer implements StructureScanner {
private static final String ACTIVE_RECORD_NAMED_SCOPE = "ActiveRecord::NamedScope::Scope";//NOI18N
/** Name of 'ClassMethods' modules; need special handling */
private static final String CLASSMETHODS = "ClassMethods"; //NOI18N
private Set<AstClassElement> haveAccessModifiers;
private List<AstElement> structure;
private Map<AstClassElement, Set<InstAsgnNode>> fields;
private Map<String, GlobalAsgnNode> globals;
private Set<String> requires;
private List<AstMethodElement> methods;
private Map<AstClassElement, Set<AstAttributeElement>> attributes;
private RubyParseResult result;
private RubyIndex index;
private RubyTypeInferencer typeInferencer;
private boolean isTestFile;
private static final String RUBY_KEYWORD = "org/netbeans/modules/ruby/jruby.png"; //NOI18N
private static ImageIcon keywordIcon;
private static final String ALIAS_METHOD = "alias_method"; //NOI18N
private static final String DEFINE_METHOD = "define_method"; //NOI18N
/**
* Holds the last class/module encountered.
*/
private AstElement lastClassElement;
static final String[] DYNAMIC_METHODS = {ALIAS_METHOD, DEFINE_METHOD};
public RubyStructureAnalyzer() {
}
public List<?extends StructureItem> scan(final ParserResult result) {
if (RubyUtils.isRhtmlOrYamlFile(RubyUtils.getFileObject(result))) {
return scanRhtml(result);
}
this.result = AstUtilities.getParseResult(result);
if (this.result == null) {
return Collections.<StructureItem>emptyList();
}
AnalysisResult ar = this.result.getStructure();
List<?extends AstElement> elements = ar.getElements();
List<StructureItem> itemList = new ArrayList<StructureItem>(elements.size());
for (AstElement e : elements) {
if (!e.isHidden()) {
itemList.add(new RubyStructureItem(e, result));
}
}
return itemList;
}
public static class AnalysisResult {
private List<?extends AstElement> elements;
private Map<AstClassElement, Set<AstAttributeElement>> attributes;
private Set<String> requires;
private AnalysisResult() {
}
public AstElement getElementFor(Node node) {
for (AstElement element : getElements()) {
AstElement result = findElement(element, node);
if (result != null) {
return result;
}
}
return null;
}
public AstElement findElement(AstElement element, Node node) {
if (element.getNode() == node) {
return element;
}
for (AstElement child : element.getChildren()) {
if (child.getNode() == node) {
return child;
}
AstElement result = findElement(child, node);
if (result != null) {
return result;
}
}
return null;
}
public Set<String> getRequires() {
return requires;
}
public void setRequires(Set<String> requires) {
this.requires = requires;
}
private void setElements(List<?extends AstElement> elements) {
this.elements = elements;
}
private void setAttributes(Map<AstClassElement, Set<AstAttributeElement>> attributes) {
this.attributes = attributes;
}
public Map<AstClassElement, Set<AstAttributeElement>> getAttributes() {
return attributes;
}
public List<?extends AstElement> getElements() {
if (elements == null) {
return Collections.emptyList();
}
return elements;
}
}
private AnalysisResult scan(final RubyParseResult result) {
AnalysisResult analysisResult = new AnalysisResult();
Node root = AstUtilities.getRoot(result);
if (root == null) {
return analysisResult;
}
isTestFile = false;
String name = RubyUtils.getFileObject(result).getNameExt();
int dot = name.lastIndexOf('.');
if (dot != -1) {
name = name.substring(0, dot);
}
if (name.startsWith("test_") || // NOI18N
name.endsWith("_test") || // NOI18N
name.endsWith("_spec")) { // NOI18N
isTestFile = true;
}
structure = new ArrayList<AstElement>();
fields = new HashMap<AstClassElement, Set<InstAsgnNode>>();
attributes = new HashMap<AstClassElement, Set<AstAttributeElement>>();
requires = new HashSet<String>();
methods = new ArrayList<AstMethodElement>();
haveAccessModifiers = new HashSet<AstClassElement>();
AstPath path = new AstPath();
path.descend(root);
ContextKnowledge knowledge = new ContextKnowledge(index, root, result);
knowledge.setAnalyzedMethods(methods);
this.typeInferencer = RubyTypeInferencer.create(knowledge);
// TODO: I should pass in a "default" context here to stash methods etc. outside of modules and classes
scan(root, path, null, null, null);
path.ascend();
// Process fields
Map<String, InstAsgnNode> names = new HashMap<String, InstAsgnNode>();
for (AstClassElement clz : fields.keySet()) {
Set<InstAsgnNode> assignments = fields.get(clz);
// Find unique variables
if (assignments != null) {
for (InstAsgnNode assignment : assignments) {
names.put(assignment.getName(), assignment);
}
// Add unique fields
for (InstAsgnNode field : names.values()) {
AstFieldElement co = new AstFieldElement(result, field);
//co.setIn(AstUtilities.getClassOrModuleName(clz));
co.setIn(clz.getFqn());
// Make sure I don't already have an entry for this field as an
// attr_accessor or writer
String fieldName = field.getName();
co.setType(knowledge.getType(fieldName));
if (fieldName.startsWith("@@")) {
fieldName = fieldName.substring(2);
} else if (fieldName.startsWith("@")) {
fieldName = fieldName.substring(1);
}
boolean found = false;
for (AstElement member : clz.getChildren()) {
if ((member.getKind() == ElementKind.ATTRIBUTE) &&
member.getName().equals(fieldName)) {
member.setType(co.getType());
found = true;
break;
}
}
// hide from the navigator view if there was attr_accessor for this field
if (found) {
co.setHidden(true);
}
clz.addChild(co);
}
names.clear();
}
}
// Globals
if (globals != null) {
List<String> sortedNames = new ArrayList<String>(globals.keySet());
Collections.sort(sortedNames);
for (String globalName : sortedNames) {
GlobalAsgnNode global = globals.get(globalName);
AstElement co = new AstNameElement(result, global, globalName,
ElementKind.GLOBAL);
structure.add(co);
}
names.clear();
}
// Process access modifiers
for (AstClassElement clz : haveAccessModifiers) {
// There are "public", "protected" or "private" modifiers in the
// document; we should scan it more carefully for these and
// annotate them properly
Set<Node> protectedMethods = new HashSet<Node>();
Set<Node> privateMethods = new HashSet<Node>();
AstUtilities.findPrivateMethods(clz.getNode(), protectedMethods, privateMethods);
if (privateMethods.size() > 0) {
// TODO: Annotate my structure elements appropriately
for (Element o : methods) {
if (o instanceof AstMethodElement) {
AstMethodElement jn = (AstMethodElement)o;
if (privateMethods.contains(jn.getNode())) {
jn.setAccess(Modifier.PRIVATE);
}
}
}
// TODO: Private fields!
}
if (protectedMethods.size() > 0) {
// TODO: Annotate my structure elements appropriately
for (Element o : methods) {
if (o instanceof AstMethodElement) {
AstMethodElement jn = (AstMethodElement)o;
if (protectedMethods.contains(jn.getNode())) {
jn.setAccess(Modifier.PROTECTED);
}
}
}
// TODO: Protected fields!
}
}
analysisResult.setElements(structure);
analysisResult.setAttributes(attributes);
analysisResult.setRequires(requires);
return analysisResult;
}
/**
* @return true if the file being analyzed appears to be
* within a rails 3.x gem.
*/
private boolean isRails3File() {
FileObject file = RubyUtils.getFileObject(result);
for (String railsGem : Gems.getRailsGems()) {
if (file.getPath().indexOf(railsGem + "-3.") > 0) {
return true;
}
}
return false;
}
private AnalysisResult getCachedAnalysis(final RubyParseResult result) {
// TODO: implement together with #cacheAnalysis
return null;
// File file = result.getFile().getFile();
// AnalysisResult cachedRestul = cache.get(file);
// if (cachedRestul == null) {
// return null;
// } else {
// return cachedRestul;
// }
}
private void cacheAnalysis(RubyParseResult result, AnalysisResult scan) {
// TODO: store in the cache, prune old result and/or time-out old results, ...
// File file = result.getFile().getFile();
// cache.put(file, scan);
}
public Map<String, List<OffsetRange>> folds(final ParserResult result) {
if (RubyUtils.isRhtmlFile(RubyUtils.getFileObject(result))) {
return Collections.emptyMap();
}
Node root = AstUtilities.getRoot(result);
if (root == null) {
return Collections.emptyMap();
}
RubyParseResult rpr = AstUtilities.getParseResult(result);
AnalysisResult analysisResult = rpr.getStructure();
Map<String,List<OffsetRange>> folds = new HashMap<String,List<OffsetRange>>();
List<OffsetRange> codefolds = new ArrayList<OffsetRange>();
folds.put("codeblocks", codefolds); // NOI18N
try {
BaseDocument doc = RubyUtils.getDocument(result);
if (doc != null) {
try {
doc.readLock(); // For Utilities.getRowStart access
addFolds(doc, analysisResult.getElements(), folds, codefolds);
} finally {
doc.readUnlock();
}
}
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
}
return folds;
}
private void addFolds(
final BaseDocument doc,
final List<? extends AstElement> elements,
final Map<String,List<OffsetRange>> folds,
final List<OffsetRange> codeblocks) throws BadLocationException {
for (AstElement element : elements) {
ElementKind kind = element.getKind();
switch (kind) {
case METHOD:
case CONSTRUCTOR:
case CLASS:
case MODULE: {
Node node = element.getNode();
OffsetRange range = AstUtilities.getRange(node);
// note: unlike for java, allowing also folding of
// top level classes and modules - in ruby it's fairly common
// to have several top level classes in one file (#140247)
int start = range.getStart();
// Start the fold at the END of the line
start = org.netbeans.editor.Utilities.getRowEnd(doc, start);
int end = range.getEnd();
if (start != (-1) && end != (-1) && start < end && end <= doc.getLength()) {
range = new OffsetRange(start, end);
codeblocks.add(range);
}
break;
}
case TEST: {
Node node = element.getNode();
OffsetRange range = AstUtilities.getRange(node);
int start = range.getStart();
// Start the fold at the END of the line
start = org.netbeans.editor.Utilities.getRowEnd(doc, start);
int end = range.getEnd();
if (start != (-1) && end != (-1) && start < end && end <= doc.getLength()) {
range = new OffsetRange(start, end);
codeblocks.add(range);
}
break;
}
}
List<? extends AstElement> children = element.getChildren();
if (children != null && children.size() > 0) {
addFolds(doc, children, folds, codeblocks);
}
}
}
private void addToParent(AstElement parent, AstElement child) {
if (parent != null) {
parent.addChild(child);
} else {
structure.add(child);
}
if (child.getKind() == ElementKind.CLASS
|| child.getKind() == ElementKind.MODULE) {
lastClassElement = child;
}
}
private void scan(
final Node node,
final AstPath path,
String in,
Set<String> includes,
AstElement parent) {
// Recursively search for methods or method calls that match the name and arity
switch (node.getNodeType()) {
case CLASSNODE: {
AstClassElement co = new AstClassElement(result, node);
co.setIn(in);
String fqn = AstUtilities.getFqnName(path);
co.setFqn(fqn);
// Pass on to children
in = AstUtilities.getClassOrModuleName((ClassNode)node);
includes = new HashSet<String>();
co.setIncludes(includes);
addToParent(parent, co);
parent = co;
break;
}
case MODULENODE: {
AstModuleElement co = new AstModuleElement(result, node);
co.setIn(in);
String moduleFqn = AstUtilities.getFqnName(path);
co.setFqn(moduleFqn);
in = AstUtilities.getClassOrModuleName((ModuleNode)node);
// special case handling for ClassMethods in Rails 3 - the indexer can't handle
// the way append_features is used in ActiveSupport::Concern. This should be
// quite safe as all 'ClassMethods' modules
// in Rails 3 appear to get the same treatment.
if (CLASSMETHODS.equals(in) && parent instanceof AstModuleElement && isRails3File()) {
((AstModuleElement) parent).setExtendWith(moduleFqn);
}
addToParent(parent, co);
parent = co;
includes = new HashSet<String>();
co.setIncludes(includes);
break;
}
case SCLASSNODE: {
// Singleton class, e.g. class << self, or class << File, etc.
AstClassElement co = new AstClassElement(result, node);
co.setIn(in);
co.setFqn(AstUtilities.getFqnName(path));
// Pass on to children
Node receiver = ((SClassNode)node).getReceiverNode();
if (receiver instanceof INameNode) {
in = AstUtilities.getName(receiver);
} else {
in = null;
}
includes = new HashSet<String>();
co.setIncludes(includes);
addToParent(parent, co);
parent = co;
break;
}
case DEFNNODE:
case DEFSNODE: {
AstMethodElement co = new AstMethodElement(result, node);
methods.add(co);
String clzFqn = AstUtilities.getFqnName(path);
co.setIn(clzFqn);
// "initialize" methods are private
if ((node instanceof DefnNode) && "initialize".equals(AstUtilities.getName(node))) {
co.setAccess(Modifier.PRIVATE);
} else if ((parent != null) && parent.getNode() instanceof SClassNode) {
// What about public/protected/private access?
co.setModifiers(EnumSet.of(Modifier.STATIC));
}
// A module inclusion callback? These often (at least in Rails) call
// extend() to insert the instance methods into the class
if (node instanceof DefsNode &&
parent instanceof AstModuleElement &&
"included".equals(AstUtilities.getName(node))) { // NOI18N
// Analyze the given method to see if it's doing a simple
// base.extend(Whatever) in the included method.
String extendWith = getExtendWith((DefsNode)node);
if (extendWith != null) {
if (extendWith.indexOf(':') == -1) {
String fqn = AstUtilities.getFqnName(path);
extendWith = fqn + "::" + extendWith; // NOI18N
}
((AstModuleElement)parent).setExtendWith(extendWith);
}
}
if (node instanceof DefnNode || node instanceof DefsNode) {
RubyType type = new RubyType();
type.append(typeInferencer.inferType(node));
co.setType(type);
}
// TODO - don't add this to the top level! Make a nested list
addToParent(parent, co);
break;
}
case CONSTDECLNODE: {
ConstDeclNode constNode = (ConstDeclNode) node;
AstElement co = new AstNameElement(result, node, AstUtilities.getName(node),
ElementKind.CONSTANT);
co.setType(typeInferencer.inferTypesOfRHS(constNode));
co.setIn(in);
addToParent(parent, co);
break;
}
case CLASSVARDECLNODE: {
AstFieldElement co = new AstFieldElement(result, node);
co.setIn(in);
addToParent(parent, co);
break;
}
case GLOBALASGNNODE: {
// We don't have unique declarations, only assignments (possibly many)
// so stash these in a map and extract unique fields when we're done
if (globals == null) {
globals = new HashMap<String, GlobalAsgnNode>();
}
GlobalAsgnNode global = (GlobalAsgnNode)node;
globals.put(global.getName(), global);
break;
}
case INSTASGNNODE: {
AstClassElement classParent = findClassParent(parent);
if (classParent != null) {
// We don't have unique declarations, only assignments (possibly many)
// so stash these in a map and extract unique fields when we're done
Set<InstAsgnNode> assignments = fields.get(classParent);
if (assignments == null) {
assignments = new HashSet<InstAsgnNode>();
fields.put(classParent, assignments);
}
assignments.add((InstAsgnNode)node);
}
break;
}
case VCALLNODE: {
String name = AstUtilities.getName(node);
if (("private".equals(name) || "protected".equals(name)) &&
parent instanceof AstClassElement) { // NOI18N
haveAccessModifiers.add((AstClassElement)parent);
}
break;
}
case MULTIPLEASGNNODE: {
Map<Node, RubyType> vars = new HashMap<Node, RubyType>();
RubyTypeAnalyzer.collectMultipleAsgnVars((MultipleAsgnNode) node, typeInferencer, vars);
for (Node each : vars.keySet()) {
switch (each.getNodeType()) {
case LOCALASGNNODE: {
if (parent == null && AstUtilities.findMethod(path) == null) {
String name = AstUtilities.getName(each);
if (findExistingVariable(name) == null) {
AstElement co = new AstNameElement(result, each, name, ElementKind.VARIABLE);
co.setType(vars.get(each));
co.setIn(in);
structure.add(co);
}
}
break;
}
case INSTASGNNODE: {
AstClassElement classParent = findClassParent(parent);
if (classParent != null) {
Set<InstAsgnNode> assignments = fields.get(classParent);
if (assignments == null) {
assignments = new HashSet<InstAsgnNode>();
fields.put(classParent, assignments);
}
assignments.add((InstAsgnNode) each);
}
break;
}
}
}
break;
}
case LOCALASGNNODE: {
// Only include variables at the top level
if (parent == null && AstUtilities.findMethod(path) == null) {
// Make sure we're not inside a method
// TODO - avoid duplicates?
String name = AstUtilities.getName(node);
boolean found = findExistingVariable(name) != null;
if (!found) {
AstElement co = new AstNameElement(result, node, name,
ElementKind.VARIABLE);
assert node instanceof LocalAsgnNode : "LocalAsgnNode expected";
co.setType(typeInferencer.inferTypesOfRHS(node));
co.setIn(in);
structure.add(co);
}
}
break;
}
case ALIASNODE: {
AliasNode aliasNode = (AliasNode) node;
String aliasedMethodName = AstUtilities.getNameOrValue(aliasNode.getOldName());
if (aliasedMethodName != null) {
addAliasedMethod(aliasedMethodName, aliasNode, parent, in);
}
break;
}
case FCALLNODE: {
String name = AstUtilities.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 (ALIAS_METHOD.equals(name)) {
List<Node> values = AstUtilities.getChildValues(node);
if (values.size() == 2) {
Node newMethod = values.get(0);
String aliasedMethodName = AstUtilities.getNameOrValue(values.get(1));
addAliasedMethod(aliasedMethodName, newMethod, parent, in);
}
} else if (DEFINE_METHOD.equals(name)) {
List<Node> values = AstUtilities.getChildValues(node);
if (!values.isEmpty()) {
Node newMethod = values.get(0);
String newMethodName = AstUtilities.getNameOrValue(newMethod);
if (newMethodName != null) {
AstDynamicMethodElement co = new AstDynamicMethodElement(result, newMethod);
co.setIn(in);
// try inferring type only if define_method(sym, method)
// was invoked w/o the method param
if (values.size() == 1) {
Node iter = ((FCallNode) node).getIterNode();
if (iter != null) {
co.setType(typeInferencer.inferType(iter));
}
}
co.setHidden(true);
addToParent(parent, co);
}
}
} else if (AstUtilities.isNamedScope(node)) {
SymbolNode[] symbols = AstUtilities.getSymbols(node);
if (symbols.length > 0) {
SymbolNode method = symbols[0];
AstDynamicMethodElement co = new AstDynamicMethodElement(result, method);
co.setIn(in);
co.setModifiers(EnumSet.of(Modifier.PUBLIC, Modifier.STATIC));
// the return type of named scopes is a proxy
if (in != null && in.length() > 0 && Character.isUpperCase(in.charAt(0))) {
co.setType(new RubyType(ACTIVE_RECORD_NAMED_SCOPE, in)); //NOI18N
}
co.setHidden(true);
addToParent(parent, co);
}
} else if (AstUtilities.isActiveRecordAssociation(node)) {
SymbolNode[] symbols = AstUtilities.getSymbols(node);
if (symbols.length > 0) {
SymbolNode method = symbols[0];
method.getName();
AstDynamicMethodElement co = new AstDynamicMethodElement(result, method);
co.setIn(in);
co.setModifiers(EnumSet.of(Modifier.PUBLIC));
String type = ActiveRecordAssociationFinder.getClassNameFor(node, method);
if (type != null && type.length() > 0) {
co.setType(RubyType.create(type)); //NOI18N
}
co.setHidden(true);
addToParent(parent, co);
}
} else if ((includes != null) && name.equals("include")) {
Node argsNode = ((FCallNode)node).getArgsNode();
if (argsNode instanceof ListNode) {
includes.addAll(AstUtilities.getValuesAsFqn((ListNode)argsNode));
}
} else if (("private".equals(name) || "protected".equals(name)) &&
parent instanceof AstClassElement) { // NOI18N
haveAccessModifiers.add((AstClassElement)parent);
} else if (AstUtilities.isAttr(node)) {
// TODO: Compute the symbols and check for equality
// attr_reader, attr_accessor, attr_writer
SymbolNode[] symbols = AstUtilities.getAttrSymbols(node);
if ((symbols != null) && (symbols.length > 0)) {
for (SymbolNode s : symbols) {
AstAttributeElement co = new AstAttributeElement(result, s, node);
if (parent instanceof AstClassElement) {
Set<AstAttributeElement> attrsInClass = attributes.get(parent);
if (attrsInClass == null) {
attrsInClass = new HashSet<AstAttributeElement>();
attributes.put((AstClassElement)parent, attrsInClass);
}
// hide duplicates from navigator, e.g. in case when there are both
// attr_reader and attr_writer for a field
for (AstAttributeElement attr : attrsInClass) {
if (attr.getName().equals(s.getName())) {
co.setHidden(true);
}
}
attrsInClass.add(co);
}
addToParent(parent, co);
}
}
} else if (name.equals("module_function")) { // NOI18N
// TODO: module_function without arguments will make all the following methods
// module function - is this common?
Node argsNode = ((FCallNode)node).getArgsNode();
if (argsNode instanceof ListNode) {
ListNode args = (ListNode)argsNode;
for (int j = 0, m = args.size(); j < m; j++) {
Node n = args.get(j);
if (n instanceof SymbolNode) {
String func = AstUtilities.getName(n);
if ((func != null) && (func.length() > 0)) {
// Find existing method
AstMethodElement method = null;
for (Element o : methods) {
if (o instanceof AstMethodElement) {
AstMethodElement jn = (AstMethodElement)o;
if (func.equals(jn.getName())) {
// TODO - some kind of arity comparison?
method = jn;
break;
}
}
}
if (method != null) {
// Make a new static version of the named function
Node dupeNode = method.getNode();
AstMethodElement co = new AstMethodElement(result, dupeNode);
co.setIn(in);
// "initialize" methods are private
if ((dupeNode instanceof DefnNode) &&
"initialize".equals(AstUtilities.getName(dupeNode))) { // NOI18N
co.setAccess(Modifier.PRIVATE);
}
// module_functions are static
// What about public/protected/private access?
co.setModifiers(EnumSet.of(Modifier.STATIC));
// TODO - don't add this to the top level! Make a nested list
addToParent(parent, co);
}
}
}
}
}
} else if (isTestFile && parent == null && TestNameResolver.isRspecDescribe(name)) {
// creates a fake class for rspec files to be able index instance variables etc in it
String className = RubyUtils.underlinedNameToCamel(RubyUtils.getFileObject(result).getName());
AstClassElement co = new AstClassElement(result, node);
co.setIn(in);
co.setFqn(className);
co.setName(className);
co.setHidden(false);
co.setVirtual(true);
in = className;
addToParent(parent, co);
parent = co;
}
if (isTestFile && TestNameResolver.isTestMethodName(name)) {
String desc = name;
FCallNode fc = (FCallNode) node;
if (fc.getIterNode() != null || "it".equals(name)) { // NOI18N // "it" without do/end: pending
Node argsNode = fc.getArgsNode();
if (argsNode instanceof ListNode) {
ListNode args = (ListNode) argsNode;
for (String descBl : AstUtilities.getNamesOrValues(args)) {
// TODO handle
// describe ThingsController, "GET #index" do
// e.g. where the desc string is not first
// No truncation? See 138259
//desc = RubyUtils.truncate(descBl.toString(), MAX_RUBY_LABEL_LENGTH);
desc = descBl;
if (TestNameResolver.isShouldaMethod(name)) {
String shouldaMethodName = TestNameResolver.getTestName(path);
AstElement co = new AstDynamicMethodElement(result, node, shouldaMethodName);
co.setHidden(true);
co.setIn(in);
// need to add as a child to the class,
// not to the parent which is here typically "context"
addToParent(lastClassElement, co);
}
// Prepend the function type (unless it's test - see 138260
if (!name.equals("test")) { // NOI18N
desc = name + ": " + desc; // NOI18N
}
break;
}
}
AstElement co = new AstNameElement(result, node, desc,
ElementKind.TEST);
addToParent(parent, co);
parent = co;
}
}
}
break;
}
List<Node> list = node.childNodes();
for (Node child : list) {
if (child.isInvisible()) {
continue;
}
path.descend(child);
scan(child, path, in, includes, parent);
path.ascend();
}
}
private AstClassElement findClassParent(AstElement candidate) {
if (candidate instanceof AstClassElement) {
return (AstClassElement) candidate;
}
if (isTestFile && lastClassElement instanceof AstClassElement) {
return (AstClassElement) lastClassElement;
}
return null;
}
private void addAliasedMethod(String aliasedMethodName, Node newMethod, AstElement parent, String in) {
if (aliasedMethodName != null && aliasedMethodName.trim().length() > 0) {
AstMethodElement aliased = findExistingMethod(aliasedMethodName);
if (aliased != null) {
AstDynamicMethodElement co = new AstDynamicMethodElement(result, newMethod);
co.setModifiers(aliased.getModifiers());
co.setParameters(aliased.getParameters());
co.setIn(in);
co.setType(aliased.getType());
co.setHidden(true);
addToParent(parent, co);
}
}
}
private AstElement findExistingVariable(String name) {
for (AstElement child : structure) {
if (child.getKind() == ElementKind.VARIABLE && name.equals(child.getName())) {
return child;
}
}
return null;
}
private AstMethodElement findExistingMethod(String name) {
for (AstMethodElement each : methods) {
if (each.getClass().equals(AstMethodElement.class) // don't include AstDynamicMethodElement
&& each.getName().equals(name)) {
return each;
}
}
return null;
}
/** Analyze the given method and see if it looks like the following
* common pattern (at least in Rails) :
* <pre>
* def self.included(base)
* base.extend(ClassMethods)
* end
* </pre>
* If it does, return the class whose methods is added - e.g. "ClassMethods"
* in the above example.
*/
private String getExtendWith(final MethodDefNode node) {
// TODO Check that we have a single parameter,
// and that the same parameter is the name of a single method
// call; a CallNode, whose name is extend and whose single
// argument is a LocalVarNode (the parameter node).
// The parameter list should be an ArrayNode containing just
// a ConstNode (look for FQNs here, could be a Colon2Node).
List<String> argList = AstUtilities.getDefArgs(node, true);
if (argList == null || argList.size() != 1) {
return null;
}
String param = argList.get(0);
CallNode call = findExtendCall(node);
if (call == null) {
return null;
}
Node receiver = call.getReceiverNode();
if (receiver == null || !(receiver instanceof INameNode)) {
return null;
}
String receiverName = AstUtilities.getName(receiver);
if (!param.equals(receiverName)) {
return null;
}
Node argsNode = call.getArgsNode();
if (argsNode instanceof ListNode) {
ListNode args = (ListNode)argsNode;
if (args.size() == 1) {
Node n = args.get(0);
String rn = null;
if (n instanceof Colon2Node) {
// TODO - check to see if we qualify
rn = AstUtilities.getName(n);
} else if (n instanceof ConstNode) {
rn = AstUtilities.getName(n);
}
return rn;
}
}
return null;
}
private CallNode findExtendCall(final Node node) {
if (node instanceof CallNode) {
CallNode call = (CallNode)node;
if ("extend".equals(call.getName())) { // NOI18N
return call;
}
}
List<Node> list = node.childNodes();
for (Node child : list) {
if (child.isInvisible()) {
continue;
}
CallNode call = findExtendCall(child);
if (call != null) {
return call;
}
}
return null;
}
private static Set<FileObject> currentlyAnalyzingWithIndex = new HashSet<FileObject>();
AnalysisResult analyze(final RubyParseResult result) {
AnalysisResult scan = getCachedAnalysis(result);
if (scan != null) {
return scan;
}
boolean addedWithIndex = false; // prevent stack-overflow
FileObject toAnalyze = RubyUtils.getFileObject(result);
try {
addedWithIndex = currentlyAnalyzingWithIndex.add(toAnalyze);
if (addedWithIndex && result != null) {
this.index = RubyIndex.get(result);
}
this.result = result;
scan = scan(result);
result.setStructure(scan);
cacheAnalysis(result, scan);
return scan;
} finally {
if (addedWithIndex) {
boolean removed = currentlyAnalyzingWithIndex.remove(toAnalyze);
assert removed : "consistent state";
}
}
}
/** Look through the comment nodes and associate them with the AST nodes */
public void addComments(final RubyParseResult result) {
Node root = result.getRootNode();
if (root == null) {
return;
}
org.jrubyparser.parser.ParserResult r = result.getJRubyResult();
// REALLY slow implementation
List<CommentNode> comments = r.getCommentNodes();
for (CommentNode comment : comments) {
SourcePosition pos = comment.getPosition();
int start = pos.getStartOffset();
int end = pos.getEndOffset();
Node node = findClosest(root, start, end);
assert node != null;
node.addComment(comment);
}
}
private Node findClosest(final Node node, final int start, final int end) {
List<Node> list = node.childNodes();
SourcePosition pos = node.getPosition();
if (end < pos.getStartOffset()) {
return node;
}
if (start > pos.getEndOffset()) {
return null;
}
for (Node child : list) {
if (child.isInvisible()) {
continue;
}
Node closest = findClosest(child, start, end);
if (closest != null) {
return closest;
}
}
return null;
}
private class RubyStructureItem implements StructureItem {
AstElement node;
ElementKind kind;
ParserResult result;
private RubyStructureItem(AstElement node, ParserResult result) {
this.node = node;
this.result = result;
kind = node.getKind();
}
public String getName() {
return node.getName();
}
public String getHtml(HtmlFormatter formatter) {
formatter.reset();
formatter.appendText(node.getName());
if ((kind == ElementKind.METHOD) || (kind == ElementKind.CONSTRUCTOR)) {
// Append parameters
AstMethodElement jn = (AstMethodElement)node;
Collection<String> parameters = jn.getParameters();
if ((parameters != null) && (parameters.size() > 0)) {
formatter.appendHtml("(");
formatter.parameters(true);
for (Iterator<String> it = parameters.iterator(); it.hasNext();) {
String ve = it.next();
// TODO - if I know types, list the type here instead. For now, just use the parameter name instead
formatter.appendText(ve);
if (it.hasNext()) {
formatter.appendHtml(", ");
}
}
formatter.parameters(false);
formatter.appendHtml(")");
}
}
RubyType type = node.getType();
if (type.isKnown()) {
formatter.appendHtml("<font color='#777777'>"); // NOI18N
formatter.appendHtml(" : "); // NOI18N
formatter.appendText(typeAsString(type));
formatter.appendHtml("</font>"); // NOI18N
}
return formatter.getText();
}
private String typeAsString(final RubyType type) {
String types = type.asString(", "); // NOI18N
// TODO, should we really show the information about the type we are
// not able to fully infer? Does it bother users or do they like it?
if (type.hasUnknownMember()) {
NbBundle.getMessage(RubyStructureAnalyzer.class, "RubyUnknownType");
types += ", " + NbBundle.getMessage(RubyStructureAnalyzer.class, "RubyUnknownType");
}
return types;
}
public ElementHandle getElementHandle() {
return node;
}
public ElementKind getKind() {
return kind;
}
public Set<Modifier> getModifiers() {
return node.getModifiers();
}
public boolean isLeaf() {
switch (kind) {
case ATTRIBUTE:
case CONSTANT:
case CONSTRUCTOR:
case METHOD:
case FIELD:
case KEYWORD:
case VARIABLE:
case GLOBAL:
case OTHER:
return true;
case MODULE:
case CLASS:
return false;
case TEST: {
List<AstElement> nested = node.getChildren();
return nested == null || nested.size() == 0;
}
default:
throw new RuntimeException("Unhandled kind: " + kind);
}
}
public List<? extends StructureItem> getNestedItems() {
List<AstElement> nested = node.getChildren();
if ((nested != null) && (nested.size() > 0)) {
List<RubyStructureItem> children = new ArrayList<RubyStructureItem>(nested.size());
for (AstElement co : nested) {
if (!co.isHidden()) {
children.add(new RubyStructureItem(co, result));
}
}
return children;
} else {
return Collections.emptyList();
}
}
public long getPosition() {
return node.getNode().getPosition().getStartOffset();
}
public long getEndPosition() {
return node.getNode().getPosition().getEndOffset();
}
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
if (!(o instanceof RubyStructureItem)) {
// System.out.println("- not a desc");
return false;
}
RubyStructureItem d = (RubyStructureItem)o;
if (kind != d.kind) {
// System.out.println("- kind");
return false;
}
if (!getName().equals(d.getName())) {
// System.out.println("- name");
return false;
}
if ((kind == ElementKind.METHOD) || (kind == ElementKind.CONSTRUCTOR)) {
// consider also arity (#131134)
Arity arity = Arity.getDefArity(node.getNode());
Arity darity = Arity.getDefArity(d.node.getNode());
if (!arity.equals(darity)) {
return false;
}
if (!getModifiers().equals(d.getModifiers())) {
return false;
}
// consider parameters names and thus their arity (issue 101508)
List<String> parameters = ((AstMethodElement) node).getParameters();
List<String> dparameters = ((AstMethodElement) d.node).getParameters();
if (parameters == null) {
return dparameters == null;
} else {
return parameters.equals(dparameters);
}
}
// if ( !this.elementHandle.signatureEquals(d.elementHandle) ) {
// return false;
// }
/*
if ( !modifiers.equals(d.modifiers)) {
// E.println("- modifiers");
return false;
}
*/
// System.out.println("Equals called");
return true;
}
@Override
public int hashCode() {
int hash = 7;
hash = (29 * hash) + ((this.getName() != null) ? this.getName().hashCode() : 0);
hash = (29 * hash) + ((this.kind != null) ? this.kind.hashCode() : 0);
if ((kind == ElementKind.METHOD) || (kind == ElementKind.CONSTRUCTOR)) {
// consider also arity
Arity arity = Arity.getDefArity(node.getNode());
hash = 37 * hash + arity.hashCode();
}
// hash = 29 * hash + (this.modifiers != null ? this.modifiers.hashCode() : 0);
return hash;
}
@Override
public String toString() {
return getName() + " (kind: " + kind + ')';
}
public ImageIcon getCustomIcon() {
if (kind == ElementKind.TEST) {
// see #138409 -- use the same icon for all kind of test/unit tests
Node astNode = node.getNode();
if (astNode instanceof INameNode && "test".equals(AstUtilities.getName(astNode))) {//NOI18N
// there's no api for getting csl icons
return ImageUtilities.loadImageIcon("org/netbeans/modules/csl/source/resources/icons/methodPublic.png", false);//NOI18N
}
}
return null;
}
public String getSortText() {
return getName();
}
}
private List<? extends StructureItem> scanRhtml(ParserResult result) {
List<RhtmlStructureItem> items = new ArrayList<RhtmlStructureItem>();
AbstractDocument doc = RubyUtils.getDocument(result);
if (doc == null) {
return Collections.emptyList();
}
doc.readLock ();
try {
TokenHierarchy th = TokenHierarchy.get(doc);
TokenSequence ts = th.tokenSequence();
if (ts == null) {
return items;
}
ts.moveStart();
while (ts.moveNext()) {
TokenId id = ts.token().id();
if (id.name().equals("DELIMITER")) {
int start = ts.offset();
if (ts.moveNext()) {
Token token = ts.token();
int end = ts.offset() + token.length();
if (!token.id().name().equals("DELIMITER")) {
while (ts.moveNext()) {
if (ts.token().id().name().equals("DELIMITER")) {
end = ts.offset() + token.length();
break;
}
}
}
String name = navigatorName((Document)doc, th, start);
items.add(new RhtmlStructureItem(name, start, end));
}
}
}
} finally {
doc.readUnlock ();
}
return items;
}
public static String navigatorName(Document doc, TokenHierarchy th, int offset) {
TokenSequence ts = th.tokenSequence();
ts.move(offset);
if (ts.moveNext()) {
TokenId id = ts.token().id();
if (id.name().equals("DELIMITER")) {
if (ts.moveNext()) {
id = ts.token().id();
if (id.name().startsWith("RUBY")) {
TokenSequence t = ts.embedded();
if (t != null) {
t.moveStart();
if (!t.moveNext()) {
return DEFAULT_LABEL;
}
while (t.token().id() == RubyTokenId.WHITESPACE) {
if (!t.moveNext()) {
break;
}
}
int begin = t.offset();
id = t.token().id();
if (id == RubyTokenId.WHITESPACE) {
// Empty tag
return DEFAULT_LABEL;
}
// Treat <%h specially!
if (id == RubyTokenId.IDENTIFIER && TokenUtilities.equals(t.token().text(), "h")) { // NOI18N
if (!t.moveNext()) {
// Just a <%h%>
int end = t.offset() + t.token().length();
return createName(doc, begin, end);
}
// Skip any whitespace after this one
while (t.token().id() == RubyTokenId.WHITESPACE) {
if (!t.moveNext()) {
break;
}
}
id = t.token().id();
}
if (id == RubyTokenId.STRING_BEGIN || id == RubyTokenId.QUOTED_STRING_BEGIN || id == RubyTokenId.REGEXP_BEGIN) {
while (t.moveNext()) {
id = t.token().id();
if (id == RubyTokenId.STRING_END || id == RubyTokenId.QUOTED_STRING_END || id == RubyTokenId.REGEXP_END) {
int end = t.offset() + t.token().length();
return createName(doc, begin, end);
}
}
}
int end = t.offset() + t.token().length();
// See if this is a "foo.bar" expression and if so, include ".bar"
if (t.moveNext()) {
TokenId newId = t.token().id();
if (newId == RubyTokenId.DOT || id == RubyTokenId.LPAREN) { // Also handle (expr)
if (t.moveNext()) {
end = t.offset() + t.token().length();
}
}
}
return createName(doc, begin, end);
}
}
}
}
}
//
// // Fallback mechanism - just pull text out of the document
// String content = createName(doc, offset, offset + leaf.getLength());
// if (content.startsWith("<%= ")) { // NOI18N
// // NOI18N
// if (content.startsWith("<%= ")) { // NOI18N
// content = content.substring(4);
// } else {
// content = content.substring(3);
// }
// } else if (content.startsWith("<%")) { // NOI18N
// // NOI18N
// if (content.startsWith("<% ")) { // NOI18N
// content = content.substring(3);
// } else {
// content = content.substring(2);
// }
// }
// if (content.endsWith("-%>")) { // NOI18N
// content = content.substring(0, content.length() - 3);
// } else if (content.endsWith("%>")) { // NOI18N
// content = content.substring(0, content.length() - 2);
// }
// return content;
//// }
return DEFAULT_LABEL;
}
/** Create label for a navigator item */
private static String createName(Document doc, int begin, int end) {
try {
boolean truncated = false;
int length = end - begin;
if (begin + length > doc.getLength()) {
length = doc.getLength() - begin;
truncated = true;
}
if (length > MAX_RUBY_LABEL_LENGTH) {
length = MAX_RUBY_LABEL_LENGTH;
truncated = true;
}
String content = doc.getText(begin, length);
int newline = content.indexOf('\n');
if (newline != -1) {
if (content.startsWith("<%\n") || content.startsWith("<%#\n")) {
content = content.substring(newline+1);
newline = content.indexOf('\n');
if (newline != -1) {
content = content.substring(0, newline);
}
} else {
boolean startsWithNewline = true;
for (int i = 0; i < newline; i++) {
if (!Character.isWhitespace((content.charAt(i)))) {
startsWithNewline = false;
break;
}
}
if (startsWithNewline) {
content = content.substring(newline+1);
} else {
content = content.substring(0, newline);
}
}
}
if (truncated) {
return content + "..."; // NOI18N
} else {
return content;
}
} catch (BadLocationException ble) {
// do nothing - see #154991
}
return DEFAULT_LABEL;
}
public Configuration getConfiguration() {
return null;
}
private static class RhtmlStructureItem implements StructureItem {
private final String name;
private final int start;
private final int end;
private final ElementHandle handle;
public RhtmlStructureItem(String name, int start, int end) {
this.name = name;
this.start = start;
this.end = end;
this.handle = new RhtmlElementHandle(name);
}
@Override
public String getName() {
return name;
}
@Override
public String getHtml(HtmlFormatter formatter) {
formatter.appendText(name);
return formatter.getText();
}
@Override
public ElementHandle getElementHandle() {
return handle;
}
@Override
public ElementKind getKind() {
return ElementKind.OTHER;
}
@Override
public Set<Modifier> getModifiers() {
return Collections.emptySet();
}
@Override
public boolean isLeaf() {
return true;
}
@Override
public List<? extends StructureItem> getNestedItems() {
return Collections.emptyList();
}
@Override
public long getPosition() {
return start;
}
@Override
public long getEndPosition() {
return end;
}
@Override
public int hashCode() {
int hash = 7;
hash = (29 * hash) + ((this.getName() != null) ? this.getName().hashCode() : 0);
// hash = 29 * hash + (this.modifiers != null ? this.modifiers.hashCode() : 0);
return hash;
}
@Override
public String toString() {
return getName();
}
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
if (!(o instanceof RubyStructureItem)) {
// System.out.println("- not a desc");
return false;
}
RubyStructureItem d = (RubyStructureItem)o;
if (!getName().equals(d.getName())) {
// System.out.println("- name");
return false;
}
return true;
}
public ImageIcon getCustomIcon() {
if (keywordIcon == null) {
keywordIcon = ImageUtilities.loadImageIcon(RUBY_KEYWORD, false);
}
return keywordIcon;
}
public String getSortText() {
return Integer.toHexString(10000+(int)getPosition());
}
}
/** A dummy ElementHandle impl for RhtmlStructureItem that need to return a non-null
* value for {@code StructureItem#getElementHandle}.
*/
private static class RhtmlElementHandle implements ElementHandle {
private final String name;
public RhtmlElementHandle(String name) {
this.name = name;
}
@Override
public FileObject getFileObject() {
return null;
}
@Override
public String getMimeType() {
return null;
}
@Override
public String getName() {
return name;
}
@Override
public String getIn() {
return null;
}
@Override
public ElementKind getKind() {
return ElementKind.OTHER;
}
@Override
public Set<Modifier> getModifiers() {
return Collections.emptySet();
}
@Override
public boolean signatureEquals(ElementHandle handle) {
return this.name.equals(handle.getName());
}
@Override
public OffsetRange getOffsetRange(ParserResult result) {
return OffsetRange.NONE;
}
}
/** Number of characters to display from the Ruby fragments in the navigator */
private static final int MAX_RUBY_LABEL_LENGTH = 30;
/** Default label to use on navigator items where we don't have more accurate
* information */
private static final String DEFAULT_LABEL = "<% %>"; // NOI18N
}