/* * 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.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.jrubyparser.ast.MethodDefNode; import org.jrubyparser.ast.Node; import org.netbeans.modules.parsing.api.Source; /** * Currently serves (mainly) for analyzing RDoc of generated stubs of Ruby core * methods which are written in native language (Java, C). * * <p> * Might be just temporary solution until we utilize different approach, like * e.g. <em>base_types.rb</em> described in * <a href="http://www.cs.umd.edu/~jfoster/ruby.pdf">Static Type Inference for Ruby</a> * paper. */ final class RDocAnalyzer { private static final Logger LOGGER = Logger.getLogger(RDocAnalyzer.class.getName()); static final String PARAM_HINT_ARG = "#:arg:"; // NOI18N static final String PARAM_HINT_RETURN = "#:return:=>"; // NOI18N private static final List<TypeCommentAnalyzer> RAW_TYPE_COMMENT_ANALYZERS = initRawTypeCommentAnalyzers(); private static final List<TypeCommentAnalyzer> TYPE_COMMENT_ANALYZERS = initTypeCommentAnalyzers(); private static final Pattern PARAM_HINT_ARG_PATTERN = Pattern.compile(PARAM_HINT_ARG + "\\s*(\\S+)\\s*=>\\s*(.+)\\s*"); private final RubyType type; private RDocAnalyzer() { this.type = new RubyType(); } private static List<TypeCommentAnalyzer> initRawTypeCommentAnalyzers() { List<TypeCommentAnalyzer> result = new ArrayList<TypeCommentAnalyzer>(); result.add(new HashAnalyzer()); result.add(new ArrayAnalyzer()); return result; } private static List<TypeCommentAnalyzer> initTypeCommentAnalyzers() { List<TypeCommentAnalyzer> result = new ArrayList<TypeCommentAnalyzer>(); result.add(new ClassNameAnalyzer()); result.add(new CustomClassNameAnalyzer()); result.add(new NumericAnalyzer()); result.add(new TrueFalseAnalyzer()); result.add(new StringAnalyzer()); return result; } static RubyType collectTypesFromComment(final List<? extends String> comment) { RDocAnalyzer rda = new RDocAnalyzer(); for (String line : comment) { line = line.trim(); if (!inspect(line)) { // NOI18N break; // ignore other then the header } rda.parseTypeFromLine(line); } return rda.type; } private static boolean inspect(String line) { // ignore other then the header and type assertions return line.startsWith("# ") //NOI18N || line.startsWith(PARAM_HINT_ARG) || line.startsWith(PARAM_HINT_RETURN); } private void parseTypeFromLine(String line) { // type assertions first if (addTypes(returnTypeFromTypeAssertion(line))) { return; } // try '#=>' first since e.g. rdocs for hash use that to // indicate return values int typeIndex = line.indexOf(" #=> "); // NOI18N if (typeIndex == -1) { typeIndex = line.indexOf(" -> "); // NOI18N } if (typeIndex == -1) { typeIndex = line.indexOf(" => "); // NOI18N } if (typeIndex == -1) { return; } String rawCommentTypes = line.substring(typeIndex + 4).trim(); if (rawCommentTypes.length() == 0) { return; } addTypes(analyzeRawCommentType(rawCommentTypes)); if (type.isKnown()) { LOGGER.log(Level.FINE, "Could not resolve type for {0}", line); } } private boolean addTypes(List<String> types) { if (types.isEmpty()) { return false; } for (String each : types) { type.add(each); } return true; } private static List<String> analyzeRawCommentType(String rawCommentTypes) { List<String> result = new ArrayList<String>(); String[] rawCommentTypes2 = rawCommentTypes.split(" or "); // NOI18N for (String rawCommentType : rawCommentTypes2) { // first try whether we have an array or a hash for (TypeCommentAnalyzer analyzer : RAW_TYPE_COMMENT_ANALYZERS) { String realType = analyzer.getType(rawCommentType); if (realType != null) { // return, the type was already recognized as hash/array/etc (doesn't // make sense to split these with ',' result.add(realType); return result; } } String[] commentTypes = rawCommentType.split(","); // NOI18N for (String commentType : commentTypes) { commentType = commentType.trim(); if (commentType.length() > 0) { String type = addRealTypeForCommentType(commentType); if (type != null) { result.add(type); } } } } return result; } private static String addRealTypeForCommentType(final String commentType) { for (TypeCommentAnalyzer analyzer : TYPE_COMMENT_ANALYZERS) { String realType = analyzer.getType(commentType); if (realType != null) { return realType; } } return null; } static List<String> returnTypeFromTypeAssertion(String line) { int start = line.indexOf(PARAM_HINT_RETURN); if (start != -1) { String rawCommentTypes = line.substring(start + PARAM_HINT_RETURN.length()).trim(); if (rawCommentTypes.length() == 0) { return Collections.emptyList(); } return analyzeRawCommentType(rawCommentTypes); } return Collections.emptyList(); } static TypeForSymbol paramTypesFromTypeAssertion(String line) { Matcher matcher = PARAM_HINT_ARG_PATTERN.matcher(line); if (!matcher.matches()) { return null; } return new TypeForSymbol(matcher.group(1), analyzeRawCommentType(matcher.group(2).trim())); } /** Look at type assertions in the document and initialize name context. */ static void collectTypeAssertions(final ContextKnowledge knowledge) { Node root = knowledge.getRoot(); if (root instanceof MethodDefNode) { // Look for parameter hints List<String> rdoc = AstUtilities.gatherDocumentation(knowledge.getParserResult().getSnapshot(), root); if ((rdoc != null) && (rdoc.size() > 0)) { for (String line : rdoc) { List<String> returnTypes = returnTypeFromTypeAssertion(line); if (!returnTypes.isEmpty()) { knowledge.setType(root, new RubyType(returnTypes)); } else { TypeForSymbol tfs = paramTypesFromTypeAssertion(line); if (tfs != null) { knowledge.maybePutTypeForSymbol(tfs.getName(), new RubyType(tfs.getTypes()), true); } } } } } } // package private for unit tests static List<String> getStandardNameVariants(String baseName) { List<String> result = new ArrayList<String>(9); result.add(baseName); // ideally should analyze baseName and add just the appropriate article... result.add("a_" + baseName); result.add("an_" + baseName); String underlined = RubyUtils.camelToUnderlinedName(baseName); result.add(underlined); result.add("a_" + underlined); result.add("an_" + underlined); String camelCase = RubyUtils.underlinedNameToCamel(baseName); result.add(camelCase); result.add("a" + camelCase); result.add("an" + camelCase); return result; } private static String validName(String type) { if (RubyUtils.isValidConstantName(type)) { return type; } return null; } static String resolveType(String typeInComment) { if ("".equals(typeInComment.trim()) || !Character.isLetter(typeInComment.charAt(0))) { return null; } if (typeInComment.startsWith("an_")) { return validName(RubyUtils.underlinedNameToCamel(typeInComment.substring(3))); } if (typeInComment.startsWith("a_")) { return validName(RubyUtils.underlinedNameToCamel(typeInComment.substring(2))); } if (typeInComment.startsWith("an") && typeInComment.length() > 2 && Character.isUpperCase(typeInComment.charAt(2))) { return validName(typeInComment.substring(2)); } if (typeInComment.startsWith("a") && typeInComment.length() > 1 && Character.isUpperCase(typeInComment.charAt(1))) { return validName(typeInComment.substring(1)); } if (Character.isUpperCase(typeInComment.charAt(0))) { return validName(typeInComment); } return validName(RubyUtils.underlinedNameToCamel(typeInComment)); } private static abstract class TypeCommentAnalyzer { final String getType(String comment) { String result = doGetType(comment); if (result != null && LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest("Resolved type [" + result + "] for comment: [" + comment + "]. " + "Analyzer: [" + getClass().getSimpleName() + "]."); } return result; } protected abstract String doGetType(String comment); } private static final class ClassNameAnalyzer extends TypeCommentAnalyzer { private static final Map<String, String> COMMENT_TYPE_TO_REAL_TYPE = new HashMap<String, String>(); static { COMMENT_TYPE_TO_REAL_TYPE.put("!obj", "FalseClass"); // NOI18N putType("abs_file_name", "String"); // NOI18N putType("class", "Class"); // NOI18N putType("super_class", "Class"); // NOI18N putType("klass", "Class"); // NOI18N putType("dir", "Dir"); // NOI18N putType("dir_name", "String"); // NOI18N putType("fixnum", "Fixnum"); // NOI18N putType("hash", "Hash"); // NOI18N putType("hsh", "Hash"); // NOI18N putType("array", "Array"); // NOI18N putType("sub_array", "Array"); // NOI18N putType("ary", "Array"); // NOI18N putType("object", "Object"); // NOI18N putType("enum", "Enumeration"); // NOI18N putType("enumerat", "Enumeration"); // NOI18N putType("enumerator", "Enumeration"); // NOI18N putType("enumeration", "Enumeration"); // NOI18N putType("io", "IO"); // NOI18N putType("ios", "IO"); // NOI18N putType("proc", "Proc"); // NOI18N putType("str", "String"); // NOI18N putType("base_name", "String"); // NOI18N putType("big", "Bignum"); // NOI18N putType("bignum", "Bignum"); // NOI18N putType("boolean", "TrueClass"); // NOI18N putType("bool", "TrueClass"); // NOI18N putType("buffer", "String"); // NOI18N putType("binding", "Binding"); // NOI18N putType("exception", "Exception"); putType("no_method_error", "NoMethodError"); COMMENT_TYPE_TO_REAL_TYPE.put("false", "FalseClass"); // NOI18N putType("file", "File"); putType("fixnum", "Fixnum"); putType("float", "Float"); putType("flt", "Float"); COMMENT_TYPE_TO_REAL_TYPE.put(":foo", "Symbol"); // NOI18N putType("integer", "Integer"); putType("int", "Integer"); putType("matchdata", "MatchData"); putType("method", "Method"); putType("mod", "Module"); putType("name_error", "NameError"); // NOI18N putType("new_method", "UnboundMethod"); // NOI18N putType("new_regexp", "Regexp"); // NOI18N putType("new_str", "String"); // NOI18N putType("new_time", "Time"); // NOI18N putType("nil", "NilClass"); // NOI18N putType("number", "Numeric"); // NOI18N putType("numeric", "Numeric"); // NOI18N putType("numeric_result", "Numeric"); // NOI18N putType("num", "Numeric"); // NOI18N putType("obj", "Object"); // NOI18N putType("other_big", "Bignum"); // NOI18N putType("outbuf", "String"); // NOI18N putType("prc", "Proc"); // NOI18N putType("range", "Range"); // NOI18N putType("regexp", "Regexp"); // NOI18N putType("rng", "Range"); // NOI18N COMMENT_TYPE_TO_REAL_TYPE.put("stat", "File::Stat"); // NOI18N putType("string", "String"); // NOI18N putType("str", "String"); // NOI18N putType("system_exit", "SystemExit"); // NOI18N putType("struct", "Struct"); // NOI18N putType("struct_tms", "Struct::Tms"); // NOI18N putType("symbol", "Symbol"); // NOI18N putType("sym", "Symbol"); // NOI18N putType("thgrp", "ThreadGroup"); // NOI18N putType("thread", "Thread"); // NOI18N putType("thr", "Thread"); // NOI18N putType("time", "Time"); // NOI18N // TODO: should return both Class and Module COMMENT_TYPE_TO_REAL_TYPE.put("class_or_module", "Class"); // NOI18N COMMENT_TYPE_TO_REAL_TYPE.put("e", "Enumeration"); // NOI18N COMMENT_TYPE_TO_REAL_TYPE.put("old_seed", "Numeric"); // NOI18N COMMENT_TYPE_TO_REAL_TYPE.put("old_seed", "Numeric"); // NOI18N COMMENT_TYPE_TO_REAL_TYPE.put("true", "TrueClass"); // NOI18N COMMENT_TYPE_TO_REAL_TYPE.put("false", "FalseClass"); // NOI18N COMMENT_TYPE_TO_REAL_TYPE.put("path", "String"); // NOI18N COMMENT_TYPE_TO_REAL_TYPE.put("$_", "String"); // NOI18N putType("unbound_method", "UnboundMethod"); // NOI18N } private static void putType(String baseName, String type) { for (String each : getStandardNameVariants(baseName)) { COMMENT_TYPE_TO_REAL_TYPE.put(each, type); } } @Override protected String doGetType(String comment) { return COMMENT_TYPE_TO_REAL_TYPE.get(comment); } } private static final class CustomClassNameAnalyzer extends TypeCommentAnalyzer { /** * Exceptions for which we don't want to create a type. */ // TODO: create an own type for self that the method TI infrastructure could // use and return the receiver in these cases. private static final String[] EXCEPTIONS = {"Self", "Key", "Value", "Detail", "Result"}; protected String doGetType(String typeInComment) { String result = resolveType(typeInComment); for (String each : EXCEPTIONS) { if (each.equals(result)) { return null; } } return result; } } private static final class NumericAnalyzer extends TypeCommentAnalyzer { protected String doGetType(String typeInComment) { if (isFixnum(typeInComment)) { return "Fixnum"; } if (isFloat(typeInComment)) { return "Float"; } return null; } private boolean isFixnum(String str) { try { Integer.parseInt(str); return true; } catch (NumberFormatException nfe) { // check for +, otherwise e.g. +5 is not recognized correctly if (str.startsWith("+") && str.length() > 1) { return isFixnum(str.substring(1)); } return false; } } private boolean isFloat(String str) { try { Float.parseFloat(str); return true; } catch (NumberFormatException nfe) { return false; } } } private static final class StringAnalyzer extends TypeCommentAnalyzer { protected String doGetType(String typeInComment) { return typeInComment.startsWith("\"") && typeInComment.endsWith("\"") ? "String" : null; } } private static final class TrueFalseAnalyzer extends TypeCommentAnalyzer { private static final String[] TRUE_TYPES = {"true"}; //NOI18N private static final String[] FALSE_TYPES = {"false"}; //NOI18N protected String doGetType(String typeInComment) { for (String type : TRUE_TYPES) { if (type.equalsIgnoreCase(typeInComment)) { return "TrueClass"; //NOI18N } } for (String type : FALSE_TYPES) { if (type.equalsIgnoreCase(typeInComment)) { return "FalseClass"; //NOI18N } } return null; } } private static final class ArrayAnalyzer extends TypeCommentAnalyzer { protected String doGetType(String typeInComment) { String trimmed = typeInComment.trim(); if (trimmed.startsWith("[") && trimmed.endsWith("]")) { return "Array"; } return null; } } private static final class HashAnalyzer extends TypeCommentAnalyzer { protected String doGetType(String typeInComment) { String trimmed = typeInComment.trim(); if (trimmed.startsWith("{") && trimmed.endsWith("}")) { return "Hash"; } return null; } } static class TypeForSymbol { private final String name; private final List<String> types; public TypeForSymbol(String name, List<String> types) { this.name = name; this.types = types; } public String getName() { return name; } public List<String> getTypes() { return types; } } }