/* * 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 2009 Sun Microsystems, Inc. */ package org.netbeans.modules.ruby; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import org.jrubyparser.ast.Colon2Node; import org.jrubyparser.ast.IScopingNode; import org.jrubyparser.ast.IterNode; import org.jrubyparser.ast.MethodDefNode; import org.jrubyparser.ast.Node; import org.jrubyparser.ast.NodeType; import org.jrubyparser.ast.ReturnNode; import org.netbeans.modules.ruby.elements.IndexedMethod; import org.netbeans.modules.ruby.options.TypeInferenceSettings; public final class RubyTypeInferencer { private final ContextKnowledge knowledge; private RubyTypeAnalyzer analyzer; private final boolean fast; public static RubyTypeInferencer create(ContextKnowledge knowledge) { return new RubyTypeInferencer(knowledge, true); } /** * Creates a new type inferencer. * * @param knowledge * @param fast skip possibly long running queries (provides less * exact results). * @return */ public static RubyTypeInferencer create(ContextKnowledge knowledge, boolean fast) { return new RubyTypeInferencer(knowledge, fast); } private RubyTypeInferencer(final ContextKnowledge knowledge, boolean fast) { assert knowledge != null : "need ContextKnowledge for RubyTypeAnalyzer"; this.knowledge = knowledge; this.fast = fast; this.analyzer = new RubyTypeAnalyzer(knowledge, this); } /** * Tries to infer the type(s) of the given <code>symbol</code> within the * context, that is by walking and analyzing {@link #RubyTypeAnalyzer the * given block}. * * @param symbol symbol for which to infer the type * @return inferred {@link RubyType types}, never <code>null</code>; */ public RubyType inferType(final String symbol) { analyzer.analyze(); RubyType type = knowledge.getType(symbol); if (type == null || !type.isKnown()) { type = knowledge.getType(getLocalVarPath(new AstPath(knowledge.getRoot(), knowledge.getTarget()), symbol)); } // Special cases if (!type.isKnown()) { // Handle migrations. This needs better flow analysis of block // variables but do quickfix for 6.0 which will work in most // migrations files. if ("t".equals(symbol) && knowledge.getRoot().getNodeType() == NodeType.DEFSNODE) { // NOI18N String n = AstUtilities.getName(knowledge.getRoot()); if ("up".equals(n) || ("down".equals(n))) { // NOI18N return RubyType.create("ActiveRecord::ConnectionAdapters::TableDefinition"); // NOI18N } } } // We keep track of the types contained within Arrays // internally (and probably hashes as well, TODO) // such that we can do the right thing when you operate // on an Array. However, clients should only see the "raw" (and real) // type. if (type.isKnown()) { for (String realType : type.getRealTypes()) { if (realType.startsWith("Array<")) { // NOI18N return RubyType.ARRAY; } } } return type; } /** * Returns the name of the given local variable prefixed with the name * of the method where it is defined (if any), e.g. <code>"my_method/my_local_var"</code> or * just <code>"my_local_var"</code> if it was defined at top-level. * * @param root the root node. * @param target the local var node. * @param currentMethod the current method; may be {@code null} in which * case an {@code AstPath} is constructed to find out the current method. * * @return */ static String getLocalVarPath(Node root, Node target, String currentMethod) { String varName = AstUtilities.getName(target); if (currentMethod != null) { return currentMethod + "/" + varName; } return getLocalVarPath(new AstPath(root, target), varName); } private static String getLocalVarPath(AstPath path, String varName) { Node methodNode = AstUtilities.findMethod(path); if (methodNode != null) { return AstUtilities.getName(methodNode) + "/" + varName; } return varName; } /** Called on AsgnNodes to compute RHS. */ RubyType inferTypesOfRHS(final Node node) { return inferTypesOfRHS(node, null); } RubyType inferTypesOfRHS(final Node node, String currentMethod) { List<Node> children = node.childNodes(); if (children.size() != 1) { return RubyType.unknown(); } return inferType(children.get(0), currentMethod); } RubyType inferType(final Node node) { return inferType(node, null); } private RubyType inferType(final Node node, String currentMethod) { RubyType type = knowledge.getType(node); if (type != null) { return type; } if (!knowledge.wasAnalyzed()) { analyzer.analyze(); } switch (node.getNodeType()) { case LOCALVARNODE: // uses currentMethod if passed; while it is not 100% accurate (currentMethod gets assigned // only when a new method def is encountered), constructing an AstPath for every local variable // is too time consuming type = knowledge.getType(getLocalVarPath(knowledge.getRoot(), node, currentMethod)); break; case GLOBALVARNODE: String name = AstUtilities.getName(node); type = knowledge.getType(name); if (!type.isKnown()) { // try predefined RubyType preDefType = RubyPredefinedVariable.getType(name); if (preDefType != null) { type = preDefType; } } break; case DVARNODE: case INSTVARNODE: case CLASSVARNODE: type = knowledge.getType(AstUtilities.getName(node)); break; case CONSTNODE: String fqn = AstUtilities.getFqnName(knowledge.getRoot(), node); type = knowledge.getType(fqn); break; case COLON2NODE: type = knowledge.getType(AstUtilities.getFqn((Colon2Node) node)); break; case RETURNNODE: ReturnNode retNode = (ReturnNode) node; type = inferType(retNode.getValueNode(), currentMethod); break; case DEFNNODE: case DEFSNODE: MethodDefNode methodDefNode = (MethodDefNode) node; currentMethod = methodDefNode.getName(); type = inferMethodNode(methodDefNode); break; case ITERNODE: type = inferIterNode((IterNode) node, currentMethod); break; case SELFNODE: type = inferSelf(node); break; case SUPERNODE: case ZSUPERNODE: type = inferSuperNode(node); break; } if (type == null && AstUtilities.isCall(node)) { type = RubyMethodTypeInferencer.inferTypeFor(node, knowledge, fast); } if (type == null) { type = getTypeForLiteral(node); } // null element in types set means that we are not able to infer // the expression knowledge.setType(node, type); return type; } private RubyType inferSuperNode(Node node) { RubyType selfType = inferSelf(node); if (selfType == null || !selfType.isKnown()) { return RubyType.unknown(); } MethodDefNode methodDefNode = AstUtilities.findMethod(new AstPath(knowledge.getRoot(), node)); if (methodDefNode != null && TypeInferenceSettings.getDefault().getMethodTypeInference()) { RubyIndex index = knowledge.getIndex(); if (index != null) { RubyType resultType = new RubyType(); for (String each : selfType.getRealTypes()) { IndexedMethod method = index.getSuperMethod(each, methodDefNode.getName(), true); if (method != null) { resultType.append(method.getType()); } } return resultType; } } return RubyType.unknown(); } private RubyType inferSelf(Node node) { Node root = knowledge.getRoot(); AstPath path = new AstPath(root, node); IScopingNode clazz = AstUtilities.findClassOrModule(path); if (clazz == null) { return null; } return RubyType.create(AstUtilities.getClassOrModuleName(clazz)); } private RubyType inferIterNode(IterNode iterNode, String currentMethod) { Node body = iterNode.getBodyNode(); RubyType result = new RubyType(); if (body != null) { Set<Node> exits = new LinkedHashSet<Node>(); AstUtilities.findExitPoints(body, exits); for (Node exit : exits) { result.append(inferType(exit, currentMethod)); } } return result; } private RubyType inferMethodNode(MethodDefNode methodDefNode) { String name = methodDefNode.getName(); RubyType fastType = RubyMethodTypeInferencer.fastCheckType(name); if (fastType == null && RubyMethodTypeInferencer.returnsReceiver(name)) { fastType = inferSelf(methodDefNode); } if (fastType != null) { return fastType; } if (TypeInferenceSettings.getDefault().getRdocTypeInference()) { List<String> rdocs = AstUtilities.gatherDocumentation(knowledge.getParserResult().getSnapshot(), methodDefNode); if (rdocs != null) { RubyType type = RDocAnalyzer.collectTypesFromComment(rdocs); if (type != null && type.isKnown()) { return type; } } } // this can be very time consuming, return if TI is not enabled RubyType result = new RubyType(); Set<Node> exits = new LinkedHashSet<Node>(); AstUtilities.findExitPoints(methodDefNode, exits); for (Node exit : exits) { if (!AstUtilities.isRaiseCall(exit)) { result.append(inferType(exit, name)); } } return result; } /** * Returns type for Ruby built-in literal, like String, Array, Hash, Regexp, * etc. * * @param node node representing Ruby build-int literal, i.e. having {@link * Node#nodeId} e.g. {@link NodeType#STRNODE}, {@link * NodeType#ARRAYNODE}, {@link NodeType#HASHNODE}, {@link * NodeType#REGEXPNODE}, ... * @return Ruby type as used in Ruby code, e.g. <code>String</code>, * <code>Array</code>, <code>Hash</code>, <code>Regexp</code>, ... */ static RubyType getTypeForLiteral(final Node node) { switch (node.getNodeType()) { case ARRAYNODE: case ZARRAYNODE: return RubyType.ARRAY; // NOI18N case DEFINEDNODE: case STRNODE: case DSTRNODE: case XSTRNODE: case DXSTRNODE: return RubyType.STRING; // NOI18N case FIXNUMNODE: return RubyType.FIXNUM; // NOI18N case BIGNUMNODE: return RubyType.BIGNUM; // NOI18N case HASHNODE: return RubyType.HASH; // NOI18N case REGEXPNODE: case DREGEXPNODE: return RubyType.REGEXP; // NOI18N case SYMBOLNODE: case DSYMBOLNODE: return RubyType.SYMBOL; // NOI18N case FLOATNODE: return RubyType.FLOAT; // NOI18N case NILNODE: // NilImplicitNode - don't use it, the type is really unknown! if (!node.isInvisible()) { return RubyType.NIL_CLASS; // NOI18N } break; case SCLASSNODE: case UNDEFNODE: case UNTILNODE: return RubyType.NIL_CLASS; case NOTNODE: return RubyType.BOOLEAN; case TRUENODE: return RubyType.TRUE_CLASS; // NOI18N case FALSENODE: return RubyType.FALSE_CLASS; // NOI18N case DOTNODE: return RubyType.RANGE; case BACKREFNODE: case NTHREFNODE: return new RubyType(RubyType.STRING, RubyType.NIL_CLASS); } return RubyType.unknown(); } @Override public String toString() { return "RubyTypeAnalyzer[knowledge:" + knowledge + ']'; // NOI18N } }