/*
* 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.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jrubyparser.ast.ArrayNode;
import org.jrubyparser.ast.CallNode;
import org.jrubyparser.ast.ClassNode;
import org.jrubyparser.ast.Colon2Node;
import org.jrubyparser.ast.FCallNode;
import org.jrubyparser.ast.ListNode;
import org.jrubyparser.ast.MethodDefNode;
import org.jrubyparser.ast.Node;
import org.jrubyparser.ast.NodeType;
import org.jrubyparser.ast.SClassNode;
import org.jrubyparser.ast.SelfNode;
import org.jrubyparser.ast.StrNode;
import org.jrubyparser.ast.INameNode;
import org.jrubyparser.ast.NewlineNode;
import org.netbeans.api.ruby.platform.RubyPlatform;
import org.netbeans.modules.csl.api.ElementKind;
import org.netbeans.modules.csl.api.Modifier;
import org.netbeans.modules.parsing.api.Snapshot;
import org.netbeans.modules.parsing.spi.Parser.Result;
import org.netbeans.modules.parsing.spi.indexing.Context;
import org.netbeans.modules.parsing.spi.indexing.EmbeddingIndexer;
import org.netbeans.modules.parsing.spi.indexing.EmbeddingIndexerFactory;
import org.netbeans.modules.parsing.spi.indexing.Indexable;
import org.netbeans.modules.parsing.spi.indexing.support.IndexDocument;
import org.netbeans.modules.parsing.spi.indexing.support.IndexingSupport;
import org.netbeans.modules.ruby.RubyStructureAnalyzer.AnalysisResult;
import org.netbeans.modules.ruby.elements.AstAttributeElement;
import org.netbeans.modules.ruby.elements.AstElement;
import org.netbeans.modules.ruby.elements.ClassElement;
import org.netbeans.modules.ruby.elements.Element;
import org.netbeans.modules.ruby.elements.IndexedClass;
import org.netbeans.modules.ruby.elements.IndexedElement;
import org.netbeans.modules.ruby.elements.IndexedMethod;
import org.netbeans.modules.ruby.elements.ModuleElement;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;
/**
* @todo Index global variables
* @todo Think about searching for modules; I will now hit EVERY class that refers to it
* (as their parent) whereas I really only want to hit individual module entries, right?
* @todo Do I index anything outside of module or classnodes? How do these get deleted?
* @todo Index migration and model files specially to extract database information that
* I can use to build dynamic model object attributes
* @todo Index require_gem separately?
* @todo Remove the FILENAME url since it's maintained elsewhere (in infrastructure) now!
*
* @author Tor Norbye
*/
public class RubyIndexer extends EmbeddingIndexer {
private static final Logger LOG = Logger.getLogger(RubyIndexer.class.getName());
//private static final boolean INDEX_UNDOCUMENTED = Boolean.getBoolean("ruby.index.undocumented");
private static final boolean INDEX_UNDOCUMENTED = true;
/**
* For unit tests, makes the indexer behave as when operating with
* user's sources in a project.
*/
static boolean userSourcesTest = false;
// Class/Module Document
static final String FIELD_EXTENDS_NAME = "extends"; //NOI18N
static final String FIELD_FQN_NAME = "fqn"; //NOI18N
static final String FIELD_IN = "in"; //NOI18N
static final String FIELD_CLASS_NAME = "class"; //NOI18N
static final String FIELD_CASE_INSENSITIVE_CLASS_NAME = "class-ig"; //NOI18N
static final String FIELD_REQUIRE = "require"; //NOI18N
static final String FIELD_REQUIRES = "requires"; //NOI18N
static final String FIELD_INCLUDES = "includes"; //NOI18N
static final String FIELD_EXTEND_WITH = "extendWith"; //NOI18N
// Rails 2.0 shorthand migrations; see http://dev.rubyonrails.org/changeset/6667?new_path=trunk
final static String[] FIXED_COLUMN_TYPES = new String[] {
// MUST BE SORTED - this array is binary searched!
"binary", "boolean", "date", "datetime", "decimal", "float", "integer", // NOI18N
"string", "text", "time", "timestamp" }; // NOI18N
/**
* A method definition. This is all compressed into a single line to make
* Lucene searching faster (I initially tried having a separate document
* for each method with separate attributes for the methods).
* <p>
* <pre>
* Format: methodname+args+;+modifiers+;+blockargs+;+returntypes;hashnames
* </pre>
* Only methodname is mandatory, but all previous elements must be
* present if a later element is specified.
* <p>
*
* The methodname can be any valid Ruby method name - including operator
* names like those identified by {@link RubyUtils#isOperator}. The
* method name is mandatory.
* optional: (arg1,arg2,arg3="def",arg4=null,&block)
* <p>
*
* The args should be of the format (arg1,arg2,...argn,optional1=val1,..,&blockarg)
* In particular, the parens should be there, and elements should be separated by
* comma.
* <p>
*
* The method modifiers is two characters in hex which reflects the bit map
* managed by IndexedElement. It records the following attributes (roughly)
* -> private
* -> protected
* -> static/classvar
* -> documented
* -> top level (implicit Object) ,
* -> database column
* -> This method takes a block
* -> This method MAY take a block (optional)
* <p>
* The block portion lists the block names to be used if this method
* takes a block. This may be empty if the block names
* are unknown or if the block doesn't take any parameters.
* <p>
* The returntypes should be a comma separated list of types this method
* is known to return.
* <p>
* The hashnames portion lists the possible values for each of the
* parameters in the earlier signature. Each argument is listed, along
* with a set of possible hashkeys allowed for that parameter. Optionally,
* each hashkey is augmented with a "type" which indicates what kinds of
* values are expected for the key.
* The format of this list is as follows:
* <pre>
* argname(key1|key2:keytype|key3...)
* </pre>
* Here, the :keytype part is optional, and keytype can be one of a number
* of predefined types. If the first letter is uppercase, it denotes a
* (possibly fully qualified) class name such as String. If it's
* lowercase, some possibilities include "action", "controller"
* (for Rails), "bool" (true or false), etc.
* <p>
* Examples:
* TODO
*/
static final String FIELD_METHOD_NAME = "method"; //NOI18N
/** Attributes: "i" -> private, "o" -> protected, ", "s" - static/notinstance, "d" - documented */
static final String FIELD_FIELD_NAME = "field"; //NOI18N
static final String FIELD_GLOBAL_NAME = "global"; //NOI18N
static final String FIELD_ATTRIBUTE_NAME = "attribute"; //NOI18N
static final String FIELD_CONSTANT_NAME = "constant"; //NOI18N
/** Attributes: hh;nnnn where hh is a hex representing flags in IndexedClass, and nnnn is the documentation length */
static final String FIELD_CLASS_ATTRS = "attrs"; //NOI18N
// TODO: Add class info to tell whether methods are static
static final String FIELD_DB_TABLE = "dbtable"; //NOI18N
/**
* Explicitly specfied table name in an AR model class (using set_table_name).
*/
static final String FIELD_EXPLICIT_DB_TABLE = "explicit-dbtable"; //NOI18N
static final String FIELD_DB_VERSION = "dbversion"; //NOI18N
static final String FIELD_DB_COLUMN = "dbcolumn"; //NOI18N
/** Special version the schema.rb is marked with (rather than a migration number) */
static final String SCHEMA_INDEX_VERSION = "schema"; // NOI18N
/**
* The name of the method that sets the table name for an AR model class.
*/
private static final String SET_TABLE_NAME = "set_table_name"; //NOII8N
/**
* The fields required to be loaded when constructing {@code IndexedClass}es.
* Excludes methods/fields etc.
*/
static final String[] CLASS_FIELDS = {
FIELD_EXTENDS_NAME, FIELD_FQN_NAME, FIELD_IN, FIELD_CLASS_NAME, FIELD_CLASS_ATTRS,
FIELD_CASE_INSENSITIVE_CLASS_NAME, FIELD_REQUIRE, FIELD_INCLUDES,
FIELD_EXTEND_WITH
};
// Method Document
//static final String FIELD_PARAMS = "params"; //NOI18N
//static final String FIELD_RDOC = "rdoc"; //NOI18N
//
/** Attributes: "i" -> private, "o" -> protected, ", "s" - static/notinstance, "D" - documented */
//static final String FIELD_METHOD_ATTRS = "mattrs"; //NOI18N
// TODO: Return types
// No point doing case insensitive indexing of method names since in Ruby
// they all tend to be fully lowercase anyway
//static final String FIELD_CASE_INSENSITIVE_METHOD_NAME = "method-ig"; //NOI18N
public RubyIndexer() {
}
// public String getPersistentUrl(File file) {
// String url;
// try {
// url = file.toURI().toURL().toExternalForm();
// // Make relative URLs for urls in the libraries
// return RubyIndex.getPreindexUrl(url);
// } catch (MalformedURLException ex) {
// Exceptions.printStackTrace(ex);
// return file.getPath();
// }
//
// }
@Override
protected void index(Indexable indexable, Result parserResult, Context context) {
Node root = AstUtilities.getRoot(parserResult);
RubyParseResult r = AstUtilities.getParseResult(parserResult);
if (root == null) {
return;
}
// I used to suppress indexing files that have had automatic cleanup to
// remove in-process editing. However, that makes code completion not
// work for local classes etc. that are being queried. I used to handle
// that by doing local AST searches but this had a lot of problems
// (not handling scoping and inheritance well etc.) so now I'm using the
// index for everything.
// if (r.getSanitizedRange() != OffsetRange.NONE) {
// return;
// }
IndexingSupport support;
try {
support = IndexingSupport.getInstance(context);
} catch (IOException ioe) {
LOG.log(Level.WARNING, null, ioe);
return;
}
TreeAnalyzer analyzer =
new TreeAnalyzer(r, support, indexable, new ContextKnowledge(null, root, r));
analyzer.analyze();
for (IndexDocument doc : analyzer.getDocuments()) {
support.addDocument(doc);
}
}
static int getModifiersFlag(Set<Modifier> modifiers) {
int flags = modifiers.contains(Modifier.STATIC) ? IndexedMethod.STATIC : 0;
if (modifiers.contains(Modifier.PRIVATE)) {
flags |= IndexedMethod.PRIVATE;
} else if (modifiers.contains(Modifier.PROTECTED)) {
flags |= IndexedMethod.PROTECTED;
}
return flags;
}
public static final class Factory extends EmbeddingIndexerFactory {
public static final String NAME = "ruby"; // NOI18N
public static final int VERSION = 9;
@Override
public EmbeddingIndexer createIndexer(Indexable indexable, Snapshot snapshot) {
if (isIndexable(indexable, snapshot)) {
return new RubyIndexer();
} else {
return null;
}
}
@Override
public int getIndexVersion() {
return VERSION;
}
@Override
public String getIndexerName() {
return NAME;
}
private boolean isIndexable(Indexable indexable, Snapshot snapshot) {
String extension = snapshot.getSource().getFileObject().getExt();
if (extension.equals("rb")) { // NOI18N
return true;
}
return false;
}
@Override
public void filesDeleted(Iterable<? extends Indexable> deleted, Context context) {
try {
IndexingSupport support = IndexingSupport.getInstance(context);
for (Indexable indexable : deleted) {
support.removeDocuments(indexable);
}
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
@Override
public void rootsRemoved(final Iterable<? extends URL> removedRoots) {
}
@Override
public void filesDirty(Iterable<? extends Indexable> dirty, Context context) {
try {
IndexingSupport is = IndexingSupport.getInstance(context);
for(Indexable i : dirty) {
is.markDirtyDocuments(i);
}
} catch (IOException ioe) {
LOG.log(Level.WARNING, null, ioe);
}
}
}
static final class TreeAnalyzer {
private final FileObject file;
private final IndexingSupport support;
private final Indexable indexable;
private String requires;
private final RubyParseResult result;
private int docMode;
private final List<IndexDocument> documents;
private String url;
private final boolean platform;
private final ContextKnowledge knowledge;
private final MigrationIndexer migrationIndexer;
private final RailsIndexer railsIndexer;
private TreeAnalyzer(RubyParseResult result,
IndexingSupport support,
Indexable indexable,
ContextKnowledge knowledge) {
this.result = result;
this.file = RubyUtils.getFileObject(result);
this.support = support;
this.indexable = indexable;
this.documents = new ArrayList<IndexDocument>();
this.platform = RubyUtils.isPlatformFile(file);
this.knowledge = knowledge;
this.migrationIndexer = new MigrationIndexer(knowledge, this);
this.railsIndexer = new RailsIndexer(knowledge, this);
}
FileObject getFile() {
return file;
}
IndexingSupport getSupport() {
return support;
}
Indexable getIndexable() {
return indexable;
}
String getUrl() {
return url;
}
RubyParseResult getResult() {
return result;
}
MigrationIndexer getMigrationIndexer() {
return migrationIndexer;
}
String getRequireString(Set<String> requireSet) {
return asCommaSeparatedString(requireSet);
}
String getIncludedString(Set<String> includes) {
return asCommaSeparatedString(includes);
}
private String asCommaSeparatedString(Set<String> strings) {
if (strings != null && strings.size() > 0) {
StringBuilder sb = new StringBuilder(20 * strings.size());
for (String each : strings) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(each);
}
return sb.toString();
}
return null;
}
List<IndexDocument> getDocuments() {
return documents;
}
public void analyze() {
try {
url = file.getURL().toExternalForm();
// Make relative URLs for urls in the libraries
url = RubyIndex.getPreindexUrl(url);
} catch (IOException ioe) {
Exceptions.printStackTrace(ioe);
}
String fileName = file.getNameExt();
// DB migration?
if (Character.isDigit(fileName.charAt(0)) &&
(fileName.matches("^\\d\\d\\d_.*") || fileName.matches("^\\d\\d\\d\\d\\d\\d\\d\\d\\d\\d\\d\\d\\d\\d_.*"))) { // NOI18N
if (file != null && file.getParent() != null && file.getParent().getName().equals("migrate")) { // NOI18N
migrationIndexer.handleMigration();
// Don't exit here - proceed to also index the class as Ruby code
}
} else if ("schema.rb".equals(fileName)) { //NOI18N
if (file != null && file.getParent() != null && file.getParent().getName().equals("db")) { // NOI18N
migrationIndexer.handleMigration();
// Don't exit here - proceed to also index the class as Ruby code
}
}
//Node root = file.getRootNode();
// Compute the requires for this file first such that
// each class or module recorded in the index for this
// file can reference their includes
AnalysisResult ar = result.getStructure();
requires = getRequireString(ar.getRequires());
List<?extends AstElement> structure = ar.getElements();
// rails special cases
if (!railsIndexer.index()) {
return;
}
if ((structure == null) || (structure.size() == 0)) {
if (requires != null) {
IndexDocument document = support.createDocument(indexable);
documents.add(document);
if (requires != null) {
document.addPair(FIELD_REQUIRES, requires, false, true);
}
addRequire(document);
}
return;
}
analyze(structure);
}
/** Add an entry for a class which provides the given includes */
void addClassIncludes(String className, String fqn, String in, int flags, String includes) {
IndexDocument document = support.createDocument(indexable);
documents.add(document);
if (includes != null) {
document.addPair(FIELD_INCLUDES, includes, false, true);
}
document.addPair(FIELD_CLASS_ATTRS, IndexedElement.flagToString(flags), false, true);
document.addPair(FIELD_FQN_NAME, fqn, true, true);
document.addPair(FIELD_CASE_INSENSITIVE_CLASS_NAME, className.toLowerCase(), true, true);
document.addPair(FIELD_CLASS_NAME, className, true, true);
if (in != null) {
document.addPair(FIELD_IN, in, false, true);
}
}
private void analyze(List<?extends AstElement> structure) {
List<AstElement> topLevelMethods = null;
IndexDocument globalDoc = null;
for (Element o : structure) {
// Todo: Iterate over the structure and index them
// fields, classes, etc.
AstElement element = (AstElement)o;
switch (element.getKind()) {
case MODULE:
case CLASS:
IndexDocument _doc = analyzeClassOrModule(element);
if (globalDoc == null) {
globalDoc = _doc;
}
break;
case METHOD:
// Method defined outside of an explicit class: added to Object.
// I only track these in the user's own classes - and skip tests.
if (shouldIndexTopLevel()) {
if (topLevelMethods == null) {
topLevelMethods = new ArrayList<AstElement>();
}
topLevelMethods.add(element);
}
break;
case GLOBAL: {
if (globalDoc == null) {
globalDoc = support.createDocument(indexable);
documents.add(globalDoc);
}
indexGlobal(element, globalDoc/*, nodoc*/);
break;
}
case CONSTRUCTOR:
case FIELD:
case ATTRIBUTE:
case CONSTANT:
// Methods, fields, attributes or constants outside of an explicit
// class or module: Added to Object/Kernel
// TODO - index us!
break;
}
}
if (topLevelMethods != null) {
analyzeTopLevelMethods(topLevelMethods);
}
}
private boolean shouldIndexTopLevel() {
// Don't index top level methods in the libraries
if (!platform || userSourcesTest) {
String name = file.getNameExt();
// Don't index spec methods or test methods
if (!name.endsWith("_spec.rb") && !name.endsWith("_test.rb")) {
// Don't index stuff in the vendor directory
if (url == null || url.indexOf("/vendor/") == -1) {
return true;
}
}
}
return false;
}
private IndexDocument analyzeClassOrModule(AstElement element) {
int previousDocMode = docMode;
IndexDocument document = null;
try {
int flags = 0;
boolean nodoc = false;
if (platform) {
// Should we skip this class? This is true for :nodoc: marked
// classes for example. We do NOT want to skip all children;
// in ActiveRecord for example we have this:
// module ActiveRecord
// module ConnectionAdapters # :nodoc:
// module SchemaStatements
// and we definitely WANT to index SchemaStatements even though
// ConnectionAdapters is not there
int newDocMode = RubyIndexerHelper.isNodocClass(element, result.getSnapshot());
if (newDocMode == RubyIndexerHelper.DOC) {
docMode = RubyIndexerHelper.DEFAULT_DOC;
} else if (newDocMode == RubyIndexerHelper.NODOC_ALL) {
flags |= IndexedElement.NODOC;
nodoc = true;
docMode = RubyIndexerHelper.NODOC_ALL;
} else if (newDocMode == RubyIndexerHelper.NODOC || docMode == RubyIndexerHelper.NODOC_ALL) {
flags |= IndexedElement.NODOC;
nodoc = true;
}
}
document = support.createDocument(indexable);
String fqn;
Node node = element.getNode();
if (element.getKind() == ElementKind.CLASS) {
ClassElement classElement = (ClassElement)element;
if (classElement.isVirtual()) {
flags |= IndexedElement.VIRTUAL;
}
fqn = classElement.getFqn();
if (node instanceof SClassNode) {
Node receiver = ((SClassNode)node).getReceiverNode();
if (receiver instanceof Colon2Node) {
fqn = AstUtilities.getFqn((Colon2Node)receiver);
} else if (receiver instanceof INameNode) {
// TODO - do I need to prefix the old fqn here?
fqn = ((INameNode)receiver).getName();
} else {
// Some other weird class def, like class << myvariable
// - I won't index those.
return document;
}
} else if (node instanceof ClassNode) {
ClassNode clz = (ClassNode)node;
Node superNode = clz.getSuperNode();
String superClass = null;
if (superNode != null) {
superClass = AstUtilities.getSuperclass(clz);
}
if (superClass != null) {
document.addPair(FIELD_EXTENDS_NAME, superClass, true, true);
//XXX: search for explicitly set table name only
// if one of the ancestors is ActiveRecord::Base
String tableName = getExplicitTableName(clz);
if (tableName != null) {
document.addPair(FIELD_EXPLICIT_DB_TABLE, tableName, true, true);
}
}
}
} else {
assert element.getKind() == ElementKind.MODULE;
ModuleElement moduleElement = (ModuleElement)element;
fqn = moduleElement.getFqn();
String extendWith = moduleElement.getExtendWith();
if (extendWith != null) {
document.addPair(FIELD_EXTEND_WITH, extendWith, false, true);
}
flags |= IndexedClass.MODULE;
}
String includes = getIncludedString(((ClassElement) element).getIncludes());
if (includes != null) {
document.addPair(FIELD_INCLUDES, includes, false, true);
}
String name = element.getName();
String in;
int classIndex = fqn.lastIndexOf("::");
if (classIndex != -1) {
in = fqn.substring(0, classIndex);
} else {
in = null;
}
boolean isDocumented = isDocumented(node);
int documentSize = getDocumentSize(node);
if (documentSize > 0) {
flags |= IndexedElement.DOCUMENTED;
}
StringBuilder attributes = new StringBuilder();
attributes.append(IndexedElement.flagToFirstChar(flags));
attributes.append(IndexedElement.flagToSecondChar(flags));
if (documentSize > 0) {
attributes.append(";");
attributes.append(Integer.toString(documentSize));
}
document.addPair(FIELD_CLASS_ATTRS, attributes.toString(), false, true);
/* Don't prune modules without documentation because
* this may be an existing module that we're defining
* new (documented) classes for*/
if (/*file.isPlatform() && */(element.getKind() == ElementKind.CLASS) &&
!INDEX_UNDOCUMENTED && !isDocumented) {
// XXX No, I might still want to recurse into the children -
// I may have classes with documentation in an undocumented
// module!!
return document;
}
document.addPair(FIELD_FQN_NAME, fqn, true, true);
document.addPair(FIELD_CASE_INSENSITIVE_CLASS_NAME, name.toLowerCase(), true, true);
document.addPair(FIELD_CLASS_NAME, name, true, true);
if (in != null) {
document.addPair(FIELD_IN, in, false, true);
}
addRequire(document);
// TODO:
//addIncluded(indexed);
if (requires != null) {
document.addPair(FIELD_REQUIRES, requires, false, true);
}
// Add the fields, etc.. Recursively add the children classes or modules if any
for (AstElement child : element.getChildren()) {
switch (child.getKind()) {
case CLASS:
case MODULE: {
if (child.getNode() instanceof SClassNode &&
((SClassNode)child.getNode()).getReceiverNode() instanceof SelfNode) {
// This is a class << self entry; I want to attach all these methods
// to the current class.
for (AstElement grandChild : child.getChildren()) {
switch (grandChild.getKind()) {
case CONSTRUCTOR:
case METHOD: {
indexMethod(grandChild, document, false, nodoc);
break;
}
case CLASS:
case MODULE: {
analyzeClassOrModule(grandChild);
break;
}
case FIELD: {
indexField(grandChild, document, nodoc);
break;
}
case GLOBAL: {
indexGlobal(grandChild, document/*, nodoc*/);
break;
}
case ATTRIBUTE: {
indexAttribute(grandChild, document, nodoc);
break;
}
case CONSTANT: {
indexConstant(grandChild, document, nodoc);
break;
}
}
}
} else {
analyzeClassOrModule(child);
}
break;
}
case CONSTRUCTOR:
case METHOD: {
indexMethod(child, document, false, nodoc);
break;
}
case FIELD: {
indexField(child, document, nodoc);
break;
}
case GLOBAL: {
indexGlobal(child, document/*, nodoc*/);
break;
}
case ATTRIBUTE: {
indexAttribute(child, document, nodoc);
break;
}
case CONSTANT: {
indexConstant(child, document, nodoc);
break;
}
}
}
documents.add(document);
} finally {
docMode = previousDocMode;
}
return document;
}
private void analyzeTopLevelMethods(List<? extends AstElement> children) {
IndexDocument document = support.createDocument(indexable); // TODO Measure
documents.add(document);
String name = "Object";
String in = null;
String fqn = "Object";
int flags = 0;
document.addPair(FIELD_CLASS_ATTRS, IndexedElement.flagToString(flags), false, true);
document.addPair(FIELD_FQN_NAME, fqn, true, true);
document.addPair(FIELD_CASE_INSENSITIVE_CLASS_NAME, name.toLowerCase(), true, true);
document.addPair(FIELD_CLASS_NAME, name, true, true);
addRequire(document);
if (requires != null) {
document.addPair(FIELD_REQUIRES, requires, false, true);
}
// TODO - find a way to combine all these methods (from this file) into a single item
// Add the fields, etc.. Recursively add the children classes or modules if any
for (AstElement child : children) {
assert child.getKind() == ElementKind.CONSTRUCTOR || child.getKind() == ElementKind.METHOD;
indexMethod(child, document, true, false);
// XXX what about fields, constants, attributes?
}
}
private void indexMethod(AstElement child, IndexDocument document, boolean topLevel, boolean nodoc) {
String signature = null;
Node childNode = child.getNode();
// dynamic methods are handled as method elemements as there is no separate
// element for them (probably such an element should be added to CSL?).
// checking the type here is hence required as dyn methods don't have a method def node
if (childNode instanceof MethodDefNode) {
signature = AstUtilities.getDefSignature((MethodDefNode) childNode);
} else {
signature = child.getName();
}
Set<Modifier> modifiers = child.getModifiers();
int flags = getModifiersFlag(modifiers);
if (nodoc) {
flags |= IndexedElement.NODOC;
}
if (topLevel) {
flags |= IndexedElement.TOPLEVEL;
}
boolean methodIsDocumented = isDocumented(childNode);
if (methodIsDocumented) {
flags |= IndexedElement.DOCUMENTED;
}
if (flags != 0) {
StringBuilder sb = new StringBuilder(signature);
sb.append(';');
sb.append(IndexedElement.flagToFirstChar(flags));
sb.append(IndexedElement.flagToSecondChar(flags));
signature = sb.toString();
}
//XXX: this will skip TI for tests as it did in GSF where
// platform was always false.
if (platform && !userSourcesTest) {
signature = RubyIndexerHelper.getMethodSignature(
child, flags, signature, file, knowledge);
if (signature == null) {
return;
}
} else {
if (!userSourcesTest) {
signature = RubyIndexerHelper.replaceAttributes(signature, flags);
}
signature = RubyIndexerHelper.getMethodSignatureForUserSources(child, signature, flags, knowledge);
}
document.addPair(FIELD_METHOD_NAME, signature, true, true);
// Storing a lowercase method name is kinda pointless in
// Ruby because the convention is to use all lowercase characters
// (using _ to separate words rather than camel case) so we're
// bloating the database for very little practical use here...
//ru.put(FIELD_CASE_INSENSITIVE_METHOD_NAME, name.toLowerCase());
if (child.getName().equals("initialize")) {
// Create static method alias "new"; rdoc also seems to do this
// Change signature
// TODO - don't do this for methods annotated :notnew:
signature = signature.replaceFirst("initialize", "new"); // NOI18N
// Make it static
if ((flags & IndexedElement.STATIC) == 0) {
// Add in static flag
flags |= IndexedElement.STATIC;
char first = IndexedElement.flagToFirstChar(flags);
char second = IndexedElement.flagToSecondChar(flags);
int attributeIndex = signature.indexOf(';');
if (attributeIndex == -1) {
signature = ((signature+ ";") + first) + second;
} else {
signature = (signature.substring(0, attributeIndex+1) + first) + second + signature.substring(attributeIndex+3);
}
}
document.addPair(FIELD_METHOD_NAME, signature, true, true);
}
}
private void indexAttribute(AstElement child, IndexDocument document, boolean nodoc) {
AstAttributeElement attributeElement = (AstAttributeElement) child;
String attribute = attributeElement.getName();
int flags = getModifiersFlag(child.getModifiers());
boolean isDocumented = isDocumented(attributeElement.getNode());
if(isDocumented) {
flags |= IndexedMethod.DOCUMENTED;
}
if (nodoc) {
flags |= IndexedElement.NODOC;
}
if (flags != 0) {
char first = IndexedElement.flagToFirstChar(flags);
char second = IndexedElement.flagToSecondChar(flags);
attribute = attribute + (";" + first) + second;
}
RubyType type = child.getType();
if (type.isKnown()) {
attribute += ";;" + type.asIndexedString() + ";";
}
document.addPair(FIELD_ATTRIBUTE_NAME, attribute, true, true);
}
private void indexConstant(AstElement child, IndexDocument document, boolean nodoc) {
// int flags = 0; // TODO
// if (nodoc) {
// flags |= IndexedElement.NODOC;
// }
RubyType type = child.getType();
StringBuilder signature = new StringBuilder(child.getName() + ';');
if (type.isKnown()) {
signature.append(type.asIndexedString());
}
document.addPair(FIELD_CONSTANT_NAME, signature.toString(), true, true);
}
private void indexField(AstElement child, IndexDocument document, boolean nodoc) {
StringBuilder signature = new StringBuilder(child.getName());
int flags = getModifiersFlag(child.getModifiers());
if (nodoc) {
flags |= IndexedElement.NODOC;
}
if (flags != 0) {
signature.append(';');
signature.append(IndexedElement.flagToFirstChar(flags));
signature.append(IndexedElement.flagToSecondChar(flags));
}
RubyType type = child.getType();
if (type.isKnown()) {
signature.append(";;" + type.asIndexedString() + ";");
}
// TODO - gather documentation on fields? naeh
document.addPair(FIELD_FIELD_NAME, signature.toString(), true, true);
}
private void indexGlobal(AstElement child, IndexDocument document/*, boolean nodoc*/) {
// Don't index globals in the libraries
if (!platform || userSourcesTest) {
String signature = child.getName();
// int flags = getModifiersFlag(child.getModifiers());
// if (nodoc) {
// flags |= IndexedElement.NODOC;
// }
//
// if (flags != 0) {
// StringBuilder sb = new StringBuilder(signature);
// sb.append(';');
// sb.append(IndexedElement.flagToFirstChar(flags));
// sb.append(IndexedElement.flagToSecondChar(flags));
// signature = sb.toString();
// }
// TODO - gather documentation on globals? naeh
document.addPair(FIELD_GLOBAL_NAME, signature, true, true);
}
}
private int getDocumentSize(Node node) {
List<String> comments = AstUtilities.gatherDocumentation(result.getSnapshot(), node);
if ((comments != null) && (comments.size() > 0)) {
int size = 0;
for (String line : comments) {
size += line.length();
}
return size;
}
return 0;
}
private boolean isDocumented(Node node) {
List<String> comments = AstUtilities.gatherDocumentation(result.getSnapshot(), node);
if ((comments != null) && (comments.size() > 0)) {
return true;
}
return false;
}
void addRequire(IndexDocument document) {
// Don't generate "require" clauses for anything in generated ruby;
// these classes are all built in and do not require any includes
// (besides, the file names are bogus - they are just derived from
// the class name by the stub generator)
String folder = (file.getParent() != null) && file.getParent().getParent() != null ?
file.getParent().getParent().getNameExt() : "";
if (folder.equals(RubyPlatform.RUBYSTUBS) && file.getName().startsWith("stub_")) {
return;
}
// Index for require-completion
String relative = indexable.getRelativePath();
if (relative != null) {
if (relative.endsWith(".rb")) { // NOI18N
relative = relative.substring(0, relative.length() - 3);
document.addPair(FIELD_REQUIRE, relative, true, true);
}
}
}
/**
* Gets the table name explicitly specified for the class.
*
* @param node
* @return
*/
private String getExplicitTableName(Node node) {
for (Node child : node.childNodes()) {
if (child instanceof FCallNode
&& SET_TABLE_NAME.equals(AstUtilities.getName(child))) {
Node arg = ((FCallNode) child).getArgsNode();
if (arg != null && arg instanceof ArrayNode) {
ArrayNode value = (ArrayNode) arg;
if (value.size() > 0) {
return AstUtilities.getNameOrValue(value.get(0));
}
}
// no point in continuing
return null;
}
String tableName = getExplicitTableName(child);
if (tableName != null) {
return tableName;
}
}
return null;
}
}
// no preindexing in parsing API
//
// private static FileObject preindexedDb;
// /** For testing only */
// public static void setPreindexedDb(FileObject preindexedDb) {
// RubyIndexer.preindexedDb = preindexedDb;
// }
//
// public FileObject getPreindexedDb() {
// if (preindexedDb == null) {
// File preindexed = InstalledFileLocator.getDefault().locate(
// "preindexed", "org.netbeans.modules.ruby", false); // NOI18N
// if (preindexed == null || !preindexed.isDirectory()) {
// throw new RuntimeException("Can't locate preindexed directory. Installation might be damaged"); // NOI18N
// }
// preindexedDb = FileUtil.toFileObject(preindexed);
// }
// return preindexedDb;
// }
//
// static boolean isPreindexing() {
// // part of a platform
// return false;
// }
}