/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 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]"
*
* 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.
*
* Contributor(s):
*
* Portions Copyrighted 2010 Sun Microsystems, Inc.
*/
package org.netbeans.modules.ruby;
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 org.jrubyparser.ast.CallNode;
import org.jrubyparser.ast.FCallNode;
import org.jrubyparser.ast.INameNode;
import org.jrubyparser.ast.ListNode;
import org.jrubyparser.ast.Node;
import org.jrubyparser.ast.StrNode;
import org.netbeans.modules.csl.api.Modifier;
import org.netbeans.modules.parsing.spi.indexing.support.IndexDocument;
import org.netbeans.modules.ruby.elements.IndexedElement;
import org.openide.filesystems.FileObject;
import static org.netbeans.modules.ruby.RubyIndexer.*;
/**
* A helper class for doing Rails specific indexing.
*
* <i>Refactored out from RubyIndexer</i>.
*/
final class RailsIndexer {
private final ContextKnowledge knowledge;
private final RubyIndexer.TreeAnalyzer analyzer;
private static final String INCLUDE = "include";
private static final String REQUIRE = "require";
private static final String EXTEND = "extend";
RailsIndexer(ContextKnowledge knowledge, TreeAnalyzer analyzer) {
this.knowledge = knowledge;
this.analyzer = analyzer;
}
private static Map<String, Set<String>> createResultMap() {
Map<String, Set<String>> result = new HashMap<String, Set<String>>();
result.put(INCLUDE, new HashSet<String>());
result.put(REQUIRE, new HashSet<String>());
result.put(EXTEND, new HashSet<String>());
return result;
}
/**
* Performs rails specific indexing.
*
* @return true if normal indexing should also be performed; false otherwise.
*/
boolean index() {
String fileName = analyzer.getFile().getNameExt();
String path = analyzer.getFile().getPath();
// Rails special case
// in case of 2.3.2 fall through to do normal indexing as well, these special cases
// are needed for rails < 2.3.2, normal indexing handles 2.3.2 classes.
// if rails is in vendor/rails, we can't tell the version from
// the path, so playing safe and falling through to do also normal
// indexing in that case
boolean fallThrough = RubyUtils.isRails23OrHigher(analyzer.getFile().getPath())
|| analyzer.getFile().getPath().contains("vendor/rails"); //NOI18N
if ("action_controller.rb".equals(fileName)) { // NOI18N
// Locate "ActionController::Base.class_eval do"
// and take those include statements and stick them into ActionController::Base
handleRailsBase("ActionController"); // NOI18N
if (!fallThrough) {
return false;
}
} else if ("active_record.rb".equals(fileName) || path.endsWith("active_record/base.rb")) { // NOI18N
handleRailsBase("ActiveRecord"); // NOI18N
// handleRailsClass("ActiveRecord", "ActiveRecord" + "::Migration", "Migration", "migration");
// HACK
analyzer.getMigrationIndexer().handleMigrations();
if (!fallThrough) {
return false;
}
} else if ("action_mailer.rb".equals(fileName)) { // NOI18N
handleRailsBase("ActionMailer"); // NOI18N
if (!fallThrough) {
return false;
}
} else if ("action_view.rb".equals(fileName)) { // NOI18N
handleRailsBase("ActionView"); // NOI18N
// HACK
handleActionViewHelpers();
if (!fallThrough) {
return false;
}
//} else if ("action_web_service.rb".equals(fileName)) { // NOI18N
// Uh oh - we have two different kinds of class eval here - one for ActionWebService, one for ActionController!
// Gotta make this shiznit smarter!
//handleRailsBase("ActionWebService::Base", "Base", "ActionWebService"); // NOI18N
//handleRailsBase("ActionController:Base", "Base", "ActionController"); // NOI18N
} else if (fileName.equals("assertions.rb") && analyzer.getUrl().endsWith("lib/action_controller/assertions.rb")) { // NOI18N
handleRailsClass("Test::Unit", "Test::Unit::TestCase", "TestCase", "TestCase"); // NOI18N
if (!fallThrough) {
return false;
}
} else if (fileName.equals("schema_definitions.rb")) {
handleSchemaDefinitions();
// Fall through - also do normal indexing on the file
}
return true;
}
private void handleRailsBase(String classIn) {
handleRailsClass(classIn, classIn + "::Base", "Base", "base"); // NOI18N
}
/** There's some pretty complicated dynamic behavior in Rails in how
* the ActionController::Base class is decorated with module mixins;
* my code cannot handle this directly, but it's a really important
* special case to handle such that code completion works in the very
* key controller classes edited by users. (This logic is replicated
* in several other classes too - ActiveRecord etc.)
*/
private void handleRailsClass(String classIn, String classFqn, String clz, String clzNoCase) {
Node root = knowledge.getRoot();
IndexDocument document = analyzer.getSupport().createDocument(analyzer.getIndexable());
analyzer.getDocuments().add(document);
Map<String, Set<String>> result = createResultMap();
scan(root, result);
addResults(document, result);
// TODO:
//addIncluded(indexed);
int flags = 0;
document.addPair(FIELD_CLASS_ATTRS, IndexedElement.flagToString(flags), false, true);
document.addPair(FIELD_FQN_NAME, classFqn, true, true);
document.addPair(FIELD_CASE_INSENSITIVE_CLASS_NAME, clzNoCase, true, true);
document.addPair(FIELD_CLASS_NAME, clz, true, true);
document.addPair(FIELD_IN, classIn, false, true);
}
/**
* Action view has some special loading behavior of helpers - see actionview's
* "load_helpers" method.
* @todo Make sure that the Partials loading is working too
*/
private void handleActionViewHelpers() {
if (analyzer.getFile() == null || analyzer.getFile().getParent() == null) {
return;
}
assert analyzer.getFile().getName().equals("action_view");
FileObject helpers = analyzer.getFile().getParent().getFileObject("action_view/helpers"); // NOI18N
if (helpers == null) {
return;
}
StringBuilder include = new StringBuilder();
for (FileObject helper : helpers.getChildren()) {
String name = helper.getName();
if (name.endsWith("_helper")) { // NOI18N
String className = RubyUtils.underlinedNameToCamel(name);
String fqn = "ActionView::Helpers::" + className; // NOI18N
if (include.length() > 0) {
include.append(",");
}
include.append(fqn);
}
}
if (include.length() > 0) {
int flags = 0;
analyzer.addClassIncludes("Base", "ActionView::Base", "ActionView", flags,
include.toString());
}
}
private boolean scan(Node node, Map<String, Set<String>> result) {
boolean found = false;
if (node instanceof FCallNode) {
String name = ((INameNode) node).getName();
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) {
result.get(REQUIRE).add(require);
}
}
}
}
} else if (name.equals(INCLUDE) || name.equals(EXTEND)) {
final String key = name.equals(INCLUDE) ? INCLUDE : EXTEND;
Node argsNode = ((FCallNode) node).getArgsNode();
if (argsNode instanceof ListNode) {
result.get(key).addAll(AstUtilities.getValuesAsFqn((ListNode) argsNode));
}
}
} else if (node instanceof CallNode) {
// Look for ActionController::Base.class_eval do block to make
// sure we have the right special case
CallNode call = (CallNode) node;
if (call.getName().equals("class_eval")) { // NOI18N
Node receiver = call.getReceiverNode();
if ("Base".equals(AstUtilities.safeGetName(receiver))) {
found = true;
}
}
}
List<Node> list = node.childNodes();
for (Node child : list) {
if (child.isInvisible()) {
continue;
}
if (scan(child, result)) {
found = true;
}
}
return found;
}
private void handleSchemaDefinitions() {
// Make sure we're in Rails 2.0...
if (analyzer.getUrl().indexOf("activerecord-2") == -1) { // NOI18N
return;
}
Node root = AstUtilities.getRoot(analyzer.getResult());
if (root == null) {
return;
}
Map<String, Set<String>> result = createResultMap();
scan(root, result);
IndexDocument document = analyzer.getSupport().createDocument(analyzer.getIndexable());
analyzer.getDocuments().add(document);
addResults(document, result);
// TODO:
int flags = 0;
document.addPair(FIELD_CLASS_ATTRS, IndexedElement.flagToString(flags), false, true);
String clz = "TableDefinition";
String classIn = "ActiveRecord::ConnectionAdapters";
String classFqn = classIn + "::" + clz;
String clzNoCase = clz.toLowerCase();
document.addPair(FIELD_FQN_NAME, classFqn, true, true);
document.addPair(FIELD_CASE_INSENSITIVE_CLASS_NAME, clzNoCase, true, true);
document.addPair(FIELD_CLASS_NAME, clz, true, true);
document.addPair(FIELD_IN, classIn, false, true);
// Insert methods:
for (String type : new String[]{"string", "text", "integer", "float", "decimal", "datetime", "timestamp", "time", "date", "binary", "boolean"}) { // NOI18N
Set<Modifier> modifiers = EnumSet.of(Modifier.PUBLIC);
int mflags = getModifiersFlag(modifiers);
StringBuilder sb = new StringBuilder();
sb.append(type);
sb.append("(names,options);"); // NOI18N
sb.append(IndexedElement.flagToFirstChar(mflags));
sb.append(IndexedElement.flagToSecondChar(mflags));
sb.append(";;;options(=>limit|default:nil|null:bool|precision|scale)"); // NOI18N
String signature = sb.toString();
document.addPair(FIELD_METHOD_NAME, signature, true, true);
}
}
private void addResults(IndexDocument document, Map<String, Set<String>> result) {
String r = analyzer.getRequireString(result.get(REQUIRE));
if (r != null) {
document.addPair(FIELD_REQUIRES, r, false, true);
}
analyzer.addRequire(document);
String includes = analyzer.getIncludedString(result.get(INCLUDE));
if (includes != null) {
document.addPair(FIELD_INCLUDES, includes, false, true);
}
String extendz = analyzer.getIncludedString(result.get(EXTEND));
if (extendz != null) {
document.addPair(FIELD_EXTEND_WITH, extendz, false, true);
}
}
}