/* * 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-2008 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.lexer; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import org.netbeans.api.annotations.common.NonNull; import org.netbeans.api.lexer.Token; import org.netbeans.api.lexer.TokenHierarchy; import org.netbeans.api.lexer.TokenId; import org.netbeans.api.lexer.TokenSequence; import org.netbeans.editor.BaseDocument; import org.netbeans.editor.Utilities; import org.netbeans.modules.ruby.RubyPredefinedVariable; import org.netbeans.modules.ruby.RubyType; import org.netbeans.modules.ruby.RubyUtils; /** * Class which represents a Call in the source */ public class Call { /** * Pattern for recognizing constructor calls. */ private static final Pattern CALL_TO_NEW = Pattern.compile(".+(\\.new(\\z|\\(.*\\)))"); //NOI18N /** * Pattern for recognizing whether a method invocation chain contains a call to new */ private static final Pattern CALL_TO_NEW_IN_CHAIN = Pattern.compile(".+(\\.new(\\z|\\(.*\\)\\.?\\w*|\\.\\w?.*))"); //NOI18N public static final Call LOCAL = new Call(RubyType.unknown(), null, false, false); public static final Call NONE = new Call(RubyType.unknown(), null, false, false); public static final Call UNKNOWN = new Call(RubyType.unknown(), null, false, false); private final RubyType type; private final String lhs; private final boolean isStatic; private final boolean methodExpected; private final boolean constantExpected; private boolean isLHSConstant; /** * A token to use as LHS when we know the type already as a null * LHS indicates the code completer to add all the local variables / methods * from the current class, which we don't want since we already know the type, * and it would be incorrect to include items from the current * class. For example in the following example all instance vars from Foo would * be listed in CC if LHS is null: * <pre> * class Foo * .. instance vars * def bar * "some string".| * end * end * </pre> * Null was previously used in <code>tryLiteral</code> which caused the kind of * problems described above. * * Note that this is toked is never returned from <code>getLHS</code>. * * TODO: Need to improve the infrastucture to have a better way of doing this. * */ private static final String EMPTY_LHS = "EMPTY_LHS"; private Call(RubyType type, String lhs, boolean isStatic, boolean methodExpected) { this(type, lhs, isStatic, methodExpected, false); } private Call(RubyType type, String lhs, boolean isStatic, boolean methodExpected, boolean constantExpected) { super(); this.type = type; this.lhs = lhs; this.methodExpected = methodExpected; if (lhs == null && type.isKnown()) { assert type.isSingleton() : "should be singleton, was: " + type; lhs = type.first(); } this.isStatic = isStatic; this.constantExpected = constantExpected; } private void setLHSConstant(boolean isLHSConstant) { this.isLHSConstant = isLHSConstant; } public RubyType getType() { return type; } public String getLhs() { if (EMPTY_LHS.equals(lhs)) { return ""; } return lhs; } public boolean isStatic() { return isStatic; } public boolean isLHSConstant() { return isLHSConstant; } public boolean isSimpleIdentifier() { if (lhs == null || lhs.equals(EMPTY_LHS)) { return false; } // TODO - replace with the new RubyUtil validations for (int i = 0, n = lhs.length(); i < n; i++) { char c = lhs.charAt(i); if (Character.isJavaIdentifierPart(c)) { continue; } if ((c == '@') || (c == '$')) { continue; } return false; } return true; } public boolean isConstantExpected() { return constantExpected; } /** foo.| or foo.b| -> we're expecting a method call. For Foo:: we don't know. */ public boolean isMethodExpected() { return methodExpected; } @Override public String toString() { if (this == LOCAL) { return "LOCAL"; } else if (this == NONE) { return "NONE"; } else if (this == UNKNOWN) { return "UNKNOWN"; } else { return "Call(type: " + type + ", lhs: " + lhs + ", isStatic: " + isStatic + ", isLHSConstant: " + isLHSConstant + ')'; } } /** * Determine whether the given offset corresponds to a method call on * another object. This would happen in these cases: * * <pre> * Foo::|, Foo::Bar::|, Foo.|, Foo.x|, foo.|, foo.x| * </pre> * * and not here: * * <pre> * |, Foo|, foo| * </pre> * The method returns the left hand side token, if any, such as "Foo", Foo::Bar", * and "foo". If not, it will return null.<br> * Note that "self" and "super" are possible return values for the lhs, which mean * that you don't have a call on another object. Clients of this method should * handle that return value properly (I could return null here, but clients probably * want to distinguish self and super in this case so it's useful to return the info.) * <p> * This method will also try to be smart such that if you have a block or array * call, it will return the relevant classnames (e.g. for [1,2].x| it returns "Array"). */ @SuppressWarnings("unchecked") @NonNull public static Call getCallType(BaseDocument doc, TokenHierarchy<Document> th, int offset) { TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(th, offset); if (ts == null) { return Call.NONE; } ts.move(offset); boolean methodExpected = false; boolean constantExpected = false; if (!ts.moveNext() && !ts.movePrevious()) { return Call.NONE; } if (ts.offset() == offset) { // We're looking at the offset to the RIGHT of the caret // position, which could be whitespace, e.g. // "foo.x| " <-- looking at the whitespace ts.movePrevious(); } Token<?extends RubyTokenId> token = ts.token(); if (token != null) { TokenId id = token.id(); if (id == RubyTokenId.WHITESPACE) { return Call.LOCAL; } // We're within a String that has embedded Ruby. Drop into the // embedded language and iterate the ruby tokens there. if (id == RubyTokenId.EMBEDDED_RUBY) { ts = (TokenSequence)ts.embedded(); assert ts != null; ts.move(offset); if (!ts.moveNext() && !ts.movePrevious()) { return Call.NONE; } token = ts.token(); id = token.id(); } // See if we're in the identifier - "x" in "foo.x" // I could also be a keyword in case the prefix happens to currently // match a keyword, such as "next" // However, if we're at the end of the document, x. will lex . as an // identifier of text ".", so handle this case specially if ((id == RubyTokenId.IDENTIFIER) || (id == RubyTokenId.CONSTANT) || id.primaryCategory().equals("keyword")) { String tokenText = token.text().toString(); if (".".equals(tokenText)) { // Special case - continue - we'll handle this part next methodExpected = true; } else if ("::".equals(tokenText)) { // Special case - continue - we'll handle this part next } else { methodExpected = true; if (Character.isUpperCase(tokenText.charAt(0))) { methodExpected = false; } if (!ts.movePrevious()) { return Call.LOCAL; } } token = ts.token(); id = token.id(); } // If we're not in the identifier we need to be in the dot (in "foo.x"). // I can't just check for tokens DOT and COLON3 because for unparseable source // (like "File.|") the lexer will return the "." as an identifier. if (id == RubyTokenId.DOT) { methodExpected = true; } else if (id == RubyTokenId.COLON3) { } else if (id == RubyTokenId.IDENTIFIER) { String t = token.text().toString(); if (t.equals(".")) { methodExpected = true; } else if (t.equals("::")) { constantExpected = true; } else { return Call.LOCAL; } } else { return Call.LOCAL; } int lastSeparatorOffset = ts.offset(); int beginOffset = lastSeparatorOffset; int lineStart = 0; try { if (offset > doc.getLength()) { offset = doc.getLength(); } lineStart = Utilities.getRowStart(doc, offset); } catch (BadLocationException ble) { // do nothing - see #154991 } boolean dotted = false; // is this dotted expression? (e.g. foo.boo.) int inParens = 0; // how many unmatched ')' did we encounter // Find the beginning of the expression. We'll go past keywords, identifiers // and dots or double-colons while (ts.movePrevious()) { // If we get to the previous line we're done if (ts.offset() < lineStart) { break; } token = ts.token(); id = token.id(); if ((id == RubyTokenId.LPAREN)) { if (inParens > 0) { inParens--; continue; } else { break; // unmatched left parenthesis? } } else if ((id == RubyTokenId.RPAREN)) { inParens++; continue; } if (inParens > 0) { // ignore content inside of bracked continue; } String tokenText = null; if (id == RubyTokenId.ANY_KEYWORD) { tokenText = token.text().toString(); } if (id == RubyTokenId.WHITESPACE) { break; } // do not evaluate e.g. '1.even?.' expression to Fixnum type if (!dotted) { Call call = tryLiteral(id, methodExpected, tokenText); if (call != null) { return call; } } if (((id == RubyTokenId.GLOBAL_VAR) || (id == RubyTokenId.INSTANCE_VAR) || (id == RubyTokenId.CLASS_VAR) || (id == RubyTokenId.IDENTIFIER)) || id.primaryCategory().equals("keyword") || (id == RubyTokenId.DOT) || (id == RubyTokenId.COLON3) || (id == RubyTokenId.CONSTANT) || (id == RubyTokenId.SUPER) || (id == RubyTokenId.SELF) || (isLiteral(id))) { // We're building up a potential expression such as "Test::Unit" so continue looking beginOffset = ts.offset(); if (id == RubyTokenId.DOT) { dotted = true; } } else if ((id == RubyTokenId.LBRACE) || (id == RubyTokenId.LBRACKET)) { // It's an expression for example within a parenthesis, e.g. // yield(^File.join()) // in this case we can do top level completion // TODO: There are probably more valid contexts here break; } else { // Something else - such as "getFoo().x|" - at this point we don't know the type // so we'll just return unknown return Call.UNKNOWN; } } if (beginOffset < lastSeparatorOffset) { try { String lhs = doc.getText(beginOffset, lastSeparatorOffset - beginOffset); if (lhs.equals("super") || lhs.equals("self")) { // NOI18N return new Call(RubyType.create(lhs), lhs, false, true); } else if (Character.isUpperCase(lhs.charAt(0))) { // Detect constructor calls of the form String.new.^ int constructorCallLength = isCallToNew(lhs); if (constructorCallLength != -1) { // See if it looks like a type prior to that String type = lhs.substring(0, lhs.length() - constructorCallLength); if (RubyUtils.isValidConstantFQN(type)) { return new Call(RubyType.create(type), lhs, false, methodExpected); } } RubyType type = RubyPredefinedVariable.getType(lhs); // predefined vars are instances // also if it was a call to a constructor, the call is not static boolean isStatic = (type == null && !containsCallToNew(lhs) && !isChainedCall(lhs)) || constantExpected; boolean isLHSConstant = RubyUtils.isValidConstantFQN(lhs); if (type == null /* not predef. var */ && isLHSConstant) { type = RubyType.create(lhs); } RubyType rubyType = type == null ? RubyType.unknown() : type; Call call = new Call(rubyType, lhs, isStatic, methodExpected, constantExpected); call.setLHSConstant(isLHSConstant); return call; } else { // try __FILE__ or __LINE__ RubyType typeS = RubyPredefinedVariable.getType(lhs); RubyType type = typeS == null ? RubyType.unknown() : typeS; return new Call(type, lhs, false, methodExpected, constantExpected); } } catch (BadLocationException ble) { // do nothing - see #154991 } } else { return Call.UNKNOWN; } } return Call.LOCAL; } /** * Checks whether the given lhs represents a constructor invocation. * * @param lhs * @return the length of the contructor call or <code>-1</code> if * the given <code>lhs</code> did not represent a constructor call. */ private static int isCallToNew(String lhs) { Matcher matcher = CALL_TO_NEW.matcher(lhs); if (!matcher.matches()) { return -1; } return matcher.group(1).length(); } private static boolean containsCallToNew(String lhs) { return CALL_TO_NEW_IN_CHAIN.matcher(lhs).matches(); } private static boolean isChainedCall(String lhs) { return lhs.indexOf('.') > 0; //NOI18N } private static Call tryLiteral(final TokenId id, final boolean methodExpected, final String tokenText) { if (id == RubyTokenId.RBRACKET) { // Looks like we're operating on an array, e.g. // [1,2,3].each| return new Call(RubyType.ARRAY, EMPTY_LHS, false, methodExpected); } else if (id == RubyTokenId.RBRACE) { // XXX uh oh, what about blocks? {|x|printx}.| ? type="Proc" // Looks like we're operating on a hash, e.g. // {1=>foo,2=>bar}.each| return new Call(RubyType.HASH, EMPTY_LHS, false, methodExpected); } else if ((id == RubyTokenId.STRING_END) || (id == RubyTokenId.QUOTED_STRING_END)) { return new Call(RubyType.STRING, EMPTY_LHS, false, methodExpected); } else if (id == RubyTokenId.REGEXP_END) { return new Call(RubyType.REGEXP, EMPTY_LHS, false, methodExpected); } else if (id == RubyTokenId.INT_LITERAL) { return new Call(RubyType.FIXNUM, EMPTY_LHS, false, methodExpected); // Or Bignum? } else if (id == RubyTokenId.FLOAT_LITERAL) { return new Call(RubyType.FLOAT, EMPTY_LHS, false, methodExpected); } else if (id == RubyTokenId.TYPE_SYMBOL) { return new Call(RubyType.SYMBOL, EMPTY_LHS, false, methodExpected); } else if (id == RubyTokenId.RANGE) { return new Call(RubyType.RANGE, EMPTY_LHS, false, methodExpected); } else if ((id == RubyTokenId.ANY_KEYWORD) && "nil".equals(tokenText)) { // NOI18N return new Call(RubyType.NIL_CLASS, EMPTY_LHS, false, methodExpected); } else if ((id == RubyTokenId.ANY_KEYWORD) && "true".equals(tokenText)) { // NOI18N return new Call(RubyType.TRUE_CLASS, EMPTY_LHS, false, methodExpected); } else if ((id == RubyTokenId.ANY_KEYWORD) && "false".equals(tokenText)) { // NOI18N return new Call(RubyType.FALSE_CLASS, EMPTY_LHS, false, methodExpected); } else { return null; } } private static boolean isLiteral(final TokenId id) { return id == RubyTokenId.RBRACKET || id == RubyTokenId.RBRACE || id == RubyTokenId.STRING_END || id == RubyTokenId.REGEXP_END || id == RubyTokenId.INT_LITERAL || id == RubyTokenId.FLOAT_LITERAL || id == RubyTokenId.TYPE_SYMBOL || id == RubyTokenId.RANGE || id == RubyTokenId.ANY_KEYWORD || id == RubyTokenId.ANY_KEYWORD || id == RubyTokenId.ANY_KEYWORD; } }