/* * 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.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.jrubyparser.ast.ArrayNode; import org.jrubyparser.ast.DotNode; import org.jrubyparser.ast.ForNode; import org.jrubyparser.ast.INameNode; import org.jrubyparser.ast.IfNode; import org.jrubyparser.ast.ListNode; import org.jrubyparser.ast.LocalAsgnNode; import org.jrubyparser.ast.MultipleAsgnNode; import org.jrubyparser.ast.NilImplicitNode; import org.jrubyparser.ast.Node; import org.jrubyparser.ast.NodeType; import org.jrubyparser.ast.ToAryNode; import org.openide.filesystems.FileObject; /** * Perform type analysis on a given AST tree, attempting to provide a type * associated with each variable, field etc. * * @todo Track boolean types for simple operators; e.g. * cc_no_width = letter == '[' && !width * etc. The operators here let me conclude cc_no_width is of type boolean! * @todo Handle find* method in Rails to indicate object types * @todo A reference to "foo." in a method is an alias to "@foo" if the method * has not been defined explicitly. Attributes are especially clear, but an * index lookup from type analyzer may be too expensive. * @todo The structure analyzer already tracks field declarations for the current class; * I should use that to track down the types * @todo Use some statistical results to improve this; .to_s => String, .to_f => float, * etc. * @todo In create_table :posts do |t| * I need to realize the type of "t" is ActiveRecord::ConnectionAdapters::TableDefinition from schema_definitions.rb * @todo Methods whose names end with "?" probably return TrueClass or FalseClass * so I can handle those expressions without actual return value lookup * @todo Possible conventions - http://www.alexandria.ucsb.edu/~gjanee/archive/2005/python-type-checking.html * @todo http://www.codecommit.com/blog/ruby/adding-type-checking-to-ruby * * @author Tor Norbye */ final class RubyTypeAnalyzer { private final ContextKnowledge knowledge; private boolean analyzed; private boolean targetReached; /** * The names of the methods that have been analyzed. Needed to keep track on * when types of instance/class vars should be overridden - we don't want to * override the types when an inst var is assigned in different methods, e.g.: * <pre> * def foo * @baz = 1 * end * def bar * @baz = "str" * end * def whats_my_return_type * @baz * end * </pre> * * In the above <code>@baz</code> should be inferred both as <code>Fixnum</code> * and <code>String</code>. */ private final Set<String> analyzedMethods = new HashSet<String>(); private final RubyTypeInferencer typeInferencer; /** * Creates a new instance of RubyTypeAnalyzer for a given position. The * {@link #inferType} method will do the rest. */ RubyTypeAnalyzer(final ContextKnowledge knowledge, RubyTypeInferencer typeInferencer) { this.knowledge = knowledge; this.typeInferencer = typeInferencer; } void analyze() { if (!analyzed) { knowledge.setAnalyzed(true); RubyTypeAnalyzer.initFileTypeVars(knowledge); RDocAnalyzer.collectTypeAssertions(knowledge); analyze(knowledge.getRoot(), knowledge.getTypesForSymbols(), true, null); analyzed = true; } } /** * Collects the variables initialized in the given <code>multipleAsgnNode</code>. * @param multipleAsgnNode * @param typeInferencer * * @return a map containing the variable nodes and types of the variables in the given <code>multipleAsgnNode</code>. */ static void collectMultipleAsgnVars(MultipleAsgnNode multipleAsgnNode, RubyTypeInferencer typeInferencer, Map<Node, RubyType> result) { ListNode head = multipleAsgnNode.getHeadNode(); Node value = multipleAsgnNode.getValueNode(); if (head == null || value == null) { return; } // special case if (value.getNodeType() == NodeType.TOARYNODE) { value = ((ToAryNode) value).getValue(); } if (value.childNodes().size() != head.childNodes().size()) { return; } for (int i = 0; i < head.childNodes().size(); i++) { Node var = head.childNodes().get(i); collectTypes(var, value.childNodes().get(i), typeInferencer, result); } } private static void collectTypes(Node head, Node value, RubyTypeInferencer typeInferencer, Map<Node, RubyType> result) { if (head == null || value == null) { return; } if (value.getNodeType() == NodeType.TOARYNODE) { value = ((ToAryNode) value).getValue(); } // nested multiple asgn // if we have a multiple asgn of form (a,(b,c))=[1,[2,3]] the nested multipleAsgnNode don't // contain the correct value node, we need to get the value from the "parent" multipleAsgnNode if (head.getNodeType() == NodeType.MULTIPLEASGNNODE) { MultipleAsgnNode multipleAsgnNode = (MultipleAsgnNode) head; ListNode headNode = multipleAsgnNode.getHeadNode(); if (headNode != null && headNode.childNodes().size() == value.childNodes().size()) { for (int i = 0; i < multipleAsgnNode.getHeadNode().childNodes().size(); i++) { Node var = multipleAsgnNode.getHeadNode().childNodes().get(i); collectTypes(var, value.childNodes().get(i), typeInferencer, result); } } } else if (head.getNodeType() == NodeType.ARRAYNODE && value.getNodeType() == NodeType.ARRAYNODE) { ArrayNode headArray = (ArrayNode) head; ArrayNode valueArray = (ArrayNode) value; if (headArray.size() == valueArray.size()) { for (int i = 0; i < headArray.size(); i++) { collectTypes(headArray.get(i), valueArray.get(i), typeInferencer, result); } } } else { result.put(head, typeInferencer.inferType(value)); } } private static String getCurrentMethod(Node node, String currentMethod) { if (node.getNodeType() == NodeType.DEFNNODE || node.getNodeType() == NodeType.DEFSNODE) { return AstUtilities.getName(node); } if (node.getNodeType() == NodeType.MODULENODE || node.getNodeType() == NodeType.CLASSNODE || node.getNodeType() == NodeType.SCLASSNODE) { return ""; } return currentMethod; } /** * Analyze the given code block down to the given offset. The {@link * #inferType} method can then be used to read out the symbol type if any at * that point. * * @param currentMethod the method we're currently analyzing (may be null). */ private void analyze( final Node node, final Map<String, RubyType> typesForSymbols, final boolean override, String currentMethod) { // the method in which we are currently; helps in optimizing performance currentMethod = getCurrentMethod(node, currentMethod); // Avoid including definitions appearing later in the context than the // caret. (This only works for local variable analysis; for fields it // could be complicated by code earlier than the caret calling code // later than the caret which initializes the field... if (node == knowledge.getTarget()) { targetReached = true; } if (targetReached && node.getPosition().getStartOffset() > knowledge.getAstOffset()) { return; } // Algorithm: walk AST and look for assignments and such. // Attempt to compute the type of each expression and switch (node.getNodeType()) { case MULTIPLEASGNNODE: { MultipleAsgnNode multipleAsgnNode = (MultipleAsgnNode) node; Map<Node, RubyType> vars = new HashMap<Node, RubyType>(); collectMultipleAsgnVars(multipleAsgnNode, typeInferencer, vars); for (Node each : vars.keySet()) { if (each instanceof INameNode) { String name = AstUtilities.getName(each); maybePutTypeForSymbol(typesForSymbols, name, vars.get(each), override, currentMethod); } } return; } case LOCALASGNNODE: { String symbol = RubyTypeInferencer.getLocalVarPath(knowledge.getRoot(), node, currentMethod); LocalAsgnNode localAsgnNode = (LocalAsgnNode) node; // see if it is a loop var if (localAsgnNode.getValueNode() instanceof NilImplicitNode) { AstPath path = new AstPath(knowledge.getRoot(), node); Node leafParent = path.leafParent(); if (leafParent instanceof ForNode) { ForNode forNode = (ForNode) leafParent; Node iterNode = forNode.getIterNode(); if (iterNode instanceof DotNode) { DotNode dotNode = (DotNode) iterNode; RubyType type = typeInferencer.inferType(dotNode.getBeginNode()); maybePutTypeForSymbol(typesForSymbols, symbol, type, override, currentMethod); break; } } } RubyType type = typeInferencer.inferTypesOfRHS(node, currentMethod); maybePutTypeForSymbol(typesForSymbols, symbol, type, override, currentMethod); break; } case CONSTDECLNODE: { RubyType type = typeInferencer.inferTypesOfRHS(node, currentMethod); String fqn = AstUtilities.getFqnName(knowledge.getRoot(), node); maybePutTypeForSymbol(typesForSymbols, fqn, type, override, currentMethod); break; } case INSTASGNNODE: case GLOBALASGNNODE: case CLASSVARASGNNODE: case CLASSVARDECLNODE: case DASGNNODE: { RubyType type = typeInferencer.inferTypesOfRHS(node, currentMethod); // null element in types set means that we are not able to infer // the expression String symbol = AstUtilities.getName(node); maybePutTypeForSymbol(typesForSymbols, symbol, type, override, currentMethod); break; } // case ITERNODE: { // // A block. See if I know the LHS expression types, and if so // // I can propagate the type into the block variables. // } // case CALLNODE: { // // Look for known calls whose return types we can guess // String name = AstUtilities.getName(node); // if (name.startsWith("find")) { // } // } } if (node.getNodeType() == NodeType.IFNODE) { analyzeIfNode((IfNode) node, typesForSymbols, currentMethod); } else { for (Node child : node.childNodes()) { if (child.isInvisible()) { continue; } analyze(child, typesForSymbols, override, currentMethod); } } } private void analyzeIfNode(final IfNode ifNode, final Map<String, RubyType> typesForSymbols, String currentMethod) { Node thenBody = ifNode.getThenBody(); Map<String, RubyType> ifTypesAccu = new HashMap<String, RubyType>(); if (thenBody != null) { // might happen with e.g. 'unless' analyze(thenBody, ifTypesAccu, true, currentMethod); } Node elseBody = ifNode.getElseBody(); Map<String, RubyType> elseTypesAccu = new HashMap<String, RubyType>(); if (elseBody != null) { analyze(elseBody, elseTypesAccu, true, currentMethod); } Map<String, RubyType> allTypesForSymbols = new HashMap<String, RubyType>(); // accumulate 'then' and 'else' bodies into one collection so they will // not override each other for (Map.Entry<String, RubyType> entry : elseTypesAccu.entrySet()) { maybePutTypeForSymbol(allTypesForSymbols, entry.getKey(), entry.getValue(), false, currentMethod); } for (Map.Entry<String, RubyType> entry : ifTypesAccu.entrySet()) { maybePutTypeForSymbol(allTypesForSymbols, entry.getKey(), entry.getValue(), false, currentMethod); } // if there is no 'then' or 'else' body do not override assignment in // parent scope(s) for (Map.Entry<String, RubyType> entry : allTypesForSymbols.entrySet()) { String var = entry.getKey(); boolean override = ifTypesAccu.containsKey(var) && elseTypesAccu.containsKey(var); maybePutTypeForSymbol(typesForSymbols, var, entry.getValue(), override, currentMethod); } } /** * This is a bit of a trick. I really know the types of the builtin fields * here - @action_name, @assigns, @cookies,. However, this usage is * deprecated; people should be using the corresponding accessors methods. * Since I don't yet correctly do type analysis of attribute to method * mappings (because that would require consulting the index to make sure * the given method has not been overridden), I'll just simulate this by * pretending that there are -local- variables of the given name * corresponding to the return value of these methods. */ private static final String[] RAILS_CONTROLLER_VARS = new String[]{ "action_name", "String", // NOI18N "assigns", "Hash", // NOI18N "cookies", "ActionController::CookieJar", // NOI18N "flash", "ActionController::Flash::FlashHash", // NOI18N "headers", "Hash", // NOI18N "params", "Hash", // NOI18N "request", "ActionController::CgiRequest", // NOI18N "session", "CGI::Session", // NOI18N "url", "ActionController::UrlRewriter", // NOI18N }; /** * Look at the file name and file extension and see if we know about some * known variables. Does not perform real type analysis. Should be * deprecated once we have real type analysis for this kind. See also {@link * #RAILS_CONTROLLER_VARS}. */ private static void initFileTypeVars(final ContextKnowledge knowledge) { FileObject fo = RubyUtils.getFileObject(knowledge.getParserResult()); if (fo == null) { return; } String ext = fo.getExt(); if (ext.equals("rb")) { String name = fo.getName(); if (name.endsWith("_controller")) { // NOI18N // request, params, etc. for (int i = 0; i < RAILS_CONTROLLER_VARS.length; i += 2) { String var = RAILS_CONTROLLER_VARS[i]; String type = RAILS_CONTROLLER_VARS[i+1]; knowledge.maybePutTypeForSymbol(var, type, true); } } // test files //if (name.endsWith("_controller_test")) { // For test files in Rails, get testing context (#105043). In particular, actionpack's // ActionController::Assertions needs to be pulled in. This happens in action_controller/assertions.rb. } else if (ext.equals("rhtml") || ext.equals("erb")) { // NOI18N //Insert fields etc. as documented in actionpack's lib/action_view/base.rb (#105095) // Insert request, params, etc. for (int i = 0; i < RAILS_CONTROLLER_VARS.length; i += 2) { String var = RAILS_CONTROLLER_VARS[i]; String type = RAILS_CONTROLLER_VARS[i+1]; knowledge.maybePutTypeForSymbol(var, type, true); } } else if (ext.equals("rjs")) { // #105088 knowledge.maybePutTypeForSymbol("page", "ActionView::Helpers::PrototypeHelper::JavaScriptGenerator::GeneratorMethods", true); // NOI18N } else if (ext.equals("builder") || ext.equals("rxml")) { // NOI18N knowledge.maybePutTypeForSymbol("xml", "Builder::XmlMarkup", true); // NOI18N } } private void maybePutTypeForSymbol( final Map<String, RubyType> typesForSymbols, final String symbol, final RubyType newType, boolean override, final String currentMethod) { RubyType mapType = typesForSymbols.get(symbol); if (symbol.startsWith("@") && currentMethod != null && !analyzedMethods.contains(currentMethod)) { analyzedMethods.add(currentMethod); override = false; } if (mapType == null || override) { mapType = new RubyType(); typesForSymbols.put(symbol, mapType); } mapType.append(newType); } }