/*
* 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.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.jrubyparser.ast.CallNode;
import org.jrubyparser.ast.ClassNode;
import org.jrubyparser.ast.FCallNode;
import org.jrubyparser.ast.IScopingNode;
import org.jrubyparser.ast.Node;
import org.jrubyparser.ast.NodeType;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.editor.BaseDocument;
import org.netbeans.editor.Utilities;
import org.netbeans.modules.csl.api.CompletionProposal;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.parsing.spi.Parser;
import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport;
import org.netbeans.modules.ruby.RubyCompletionItem.DbItem;
import org.netbeans.modules.ruby.RubyCompletionItem.MethodItem;
import org.netbeans.modules.ruby.RubyParser.Sanitize;
import org.netbeans.modules.ruby.elements.IndexedClass;
import org.netbeans.modules.ruby.elements.IndexedConstant;
import org.netbeans.modules.ruby.elements.IndexedMethod;
import org.netbeans.modules.ruby.lexer.Call;
import org.netbeans.modules.ruby.lexer.LexUtilities;
import org.netbeans.modules.ruby.lexer.RubyTokenId;
final class RubyMethodCompleter extends RubyBaseCompleter {
private static int callLineStart = -1;
private static IndexedMethod callMethod;
private final String fqn;
private final Call call;
static boolean complete(
final List<? super CompletionProposal> proposals,
final CompletionRequest request,
final String fqn,
final Call call,
final int anchor,
final boolean caseSensitive) {
RubyMethodCompleter rsc = new RubyMethodCompleter(proposals, request, fqn, call, anchor, caseSensitive);
return rsc.complete();
}
private RubyMethodCompleter(
final List<? super CompletionProposal> proposals,
final CompletionRequest request,
final String fqn,
final Call call,
final int anchor,
final boolean caseSensitive) {
super(proposals, request, anchor, caseSensitive);
this.fqn = fqn;
this.call = call;
}
/**
* Determine if we're trying to complete the name of a method on another
* object rather than an inherited or local one. These should list ALL known
* methods, unless of course we know the type of the method we're operating
* on (such as strings or regexps), or types inferred through data flow
* analysis
*
* @todo Look for self or this or super; these should be limited to
* inherited.
*/
private boolean complete() {
final String prefix = request.prefix;
final int lexOffset = request.lexOffset;
final TokenHierarchy<Document> th = request.th;
final AstPath path = request.path;
final QuerySupport.Kind kind = request.kind;
final Node target = request.target != null ? AstUtilities.findNextNonNewLineNode(request.target) : null;
TokenSequence<? extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(th, lexOffset);
// Look in the token stream for constructs of the type
// foo.x^
// or
// foo.^
// and if found, add all methods
// (no keywords etc. are possible matches)
if ((getIndex() == null) || (ts == null)) {
return false;
}
boolean skipPrivate = true;
if ((call == Call.LOCAL) || (call == Call.NONE)) {
return false;
}
// If we're not sure we're only looking for a method, don't abort after this
boolean done = call.isMethodExpected();
String lhs = call.getLhs();
boolean skipInstanceMethods = call.isStatic();
Set<IndexedMethod> methods = new HashSet<IndexedMethod>();
RubyType type = RubyType.unknown();
final RubyType callType = call.getType();
if (callType.isKnown() && !call.isLHSConstant()) {
type = callType;
}
// Target might be null somehow, since AST is not always available when
// the source is in the incosistent state
if (!type.isKnown() && lhs != null && target != null) {
if (call.isSimpleIdentifier() || call.isLHSConstant()) {
Node method = AstUtilities.findLocalScope(target, path);
String _lhs = lhs;
if (call.isLHSConstant()) {
// TODO: curently constants are class/module insensitive, cf. #154098
int lastColon2 = lhs.lastIndexOf("::"); // NOI18N
if (lastColon2 != -1) {
_lhs = lhs.substring(lastColon2 + 2);
}
}
if (method != null) {
// TODO - if the lhs is "foo.bar." I need to split this
// up and do it a bit more cleverly
type = getTypesForConstant(lhs);
if (!type.isKnown()) {
// try fqn
type = getTypesForConstant(AstUtilities.getFqnName(path, lhs));
}
if (!type.isKnown()) {
type = createTypeInferencer(request, method).inferType(_lhs);
}
if (type.isKnown() && call.isLHSConstant()) {
// lhs is not a class or module, is a constant for which we have
// type-inference. Clumsy -> polish infrastructure..
skipInstanceMethods = false;
}
}
if (!type.isKnown() && call.isLHSConstant() && callType != null) {
type = callType;
}
} else if (AstUtilities.isAssignmentNode(target)) {
if (!target.childNodes().isEmpty()) {
Node child = target.childNodes().get(0);
if (AstUtilities.isCall(child)) {
type = RubyTypeInferencer.create(request.createContextKnowledge(), false).inferType(child);
}
}
}
}
if (!type.isKnown() && target != null && AstUtilities.isCall(target)) {
type = getTypeForCall(target);
}
// I'm not doing any data flow analysis at this point, so
// I can't do anything with a LHS like "foo.". Only actual types.
if (type.isKnown()) {
if ("self".equals(lhs)) { // NOI18N
type = RubyType.create(fqn);
skipPrivate = true;
} else if ("super".equals(lhs)) { // NOI18N
skipPrivate = true;
IndexedClass sc = getIndex().getSuperclass(fqn);
if (sc != null) {
type = RubyType.create(sc.getFqn());
} else {
ClassNode cls = AstUtilities.findClass(path);
if (cls != null) {
type = RubyType.create(AstUtilities.getSuperclass(cls));
}
}
if (!type.isKnown()) {
type = RubyType.OBJECT;
}
}
if (type.isKnown()) {
// Possibly a class on the left hand side: try searching with the class as a qualifier.
// Try with the LHS + current FQN recursively. E.g. if we're in
// Test::Unit when there's a call to Foo.x, we'll try
// Test::Unit::Foo, and Test::Foo
String _fqn = fqn;
while (methods.isEmpty()) {
for (String realType : type.getRealTypes()) {
methods.addAll(getIndex().getInheritedMethods(_fqn + "::" + realType, prefix, kind));
}
int f = _fqn.lastIndexOf("::");
if (f == -1) {
break;
} else {
_fqn = _fqn.substring(0, f);
}
}
// Add methods in the class (without an FQN)
for (String realType : type.getRealTypes()) {
methods.addAll(getIndex().getInheritedMethods(realType, prefix, kind, true, true));
}
}
}
// Try just the method call (e.g. across all classes). This is ignoring the
// left hand side because we can't resolve it.
if (methods.isEmpty() || type.hasUnknownMember()) {
methods.addAll(getIndex().getMethods(prefix, kind));
}
for (IndexedMethod method : RubyDynamicFindersCompleter.proposeDynamicMethods(methods, proposals, request, anchor)) {
// Don't include private or protected methods on other objects
if (skipPrivate && (method.isPrivate() && !"new".equals(method.getName()))) {
// TODO - "initialize" removal here should not be necessary since they should
// be marked as private, but index doesn't contain that yet
continue;
}
// We can only call static methods. And module class is a special case (#110267)
if (skipInstanceMethods && !method.isStatic() && !method.doesBelongToModule()) {
continue;
}
// Do not offer instance methods of Module class as instance methods (issue #110267)
if (!skipInstanceMethods && method.doesBelongToModule()) {
continue;
}
// do not offer static methods for instances
if (!skipInstanceMethods && method.isStatic()) {
continue;
}
if (method.isNoDoc()) {
continue;
}
if (method.getMethodType() == IndexedMethod.MethodType.DBCOLUMN) {
DbItem item = new DbItem(method, method.getName(), method.getIn(), anchor, request);
propose(item);
continue;
}
MethodItem methodItem = new MethodItem(method, anchor, request);
// Exact matches
methodItem.setSmart(method.isSmart());
propose(methodItem);
}
return done;
}
private RubyType getTypeForCall(Node target) {
if ("".equals(request.prefix)) {
// we often have broken AST here, try to handle one commmon case
Node realTarget = findClosestMatchingNode(target);
if (realTarget != null) {
target = realTarget;
}
return RubyTypeInferencer.create(request.createContextKnowledge(), false).inferType(target);
} else {
if (target instanceof CallNode) {
Node receiver = ((CallNode) target).getReceiverNode();
return RubyTypeInferencer.create(request.createContextKnowledge(), false).inferType(receiver);
} else { // receiver is self
IScopingNode clazz = AstUtilities.findClassOrModule(request.path);
if (clazz != null) {
return RubyType.create(AstUtilities.getClassOrModuleName(clazz));
}
}
}
return RubyType.unknown();
}
private Node findClosestMatchingNode(Node target) {
// when we have e.g.
// a_method().anotherMethod.^ (<= invoke CC here)
// Foo.new
//
// the target is Foo and anotherMethod is its receiver (since the AST is broken)
// this method tries to find the real target based on the lhs.
String name = AstUtilities.getCallName(target);
String lhs = call.getLhs();
if (lhs == null) {
return target;
}
if (lhs.equals(name)) {
return target;
}
int lastDot = lhs.lastIndexOf(".");
if (lastDot != -1) {
lhs = lhs.substring(lastDot + 1, lhs.length());
int lastLeftParen = lhs.lastIndexOf("(");
if (lastLeftParen != -1) {
lhs = lhs.substring(lastLeftParen + 1, lhs.length());
}
}
if (name.equals(lhs)) {
return target;
}
for (Node child : target.childNodes()) {
if (AstUtilities.isCall(child) && lhs.equals(AstUtilities.getCallName(child))) {
return child;
}
}
return null;
}
/**
* Compute the current method call at the given offset. Returns false if
* we're not in a method call. The argument index is returned in
* parameterIndexHolder[0] and the method being called in methodHolder[0].
*/
static boolean computeMethodCall(Parser.Result parserResult, int lexOffset, int astOffset,
IndexedMethod[] methodHolder, int[] parameterIndexHolder, int[] anchorOffsetHolder,
Set<IndexedMethod>[] alternativesHolder, QuerySupport.Kind kind) {
try {
Node root = AstUtilities.getRoot(parserResult);
if (root == null) {
return false;
}
IndexedMethod targetMethod = null;
int index = -1;
AstPath path = null;
// Account for input sanitation
// TODO - also back up over whitespace, and if I hit the method
// I'm parameter number 0
int originalAstOffset = astOffset;
// Adjust offset to the left
BaseDocument doc = RubyUtils.getDocument(parserResult, true);
if (doc == null) {
return false;
}
int newLexOffset = LexUtilities.findSpaceBegin(doc, lexOffset);
if (newLexOffset < lexOffset) {
astOffset -= (lexOffset - newLexOffset);
}
RubyParseResult rpr = AstUtilities.getParseResult(parserResult);
OffsetRange range = rpr.getSanitizedRange();
if (range != OffsetRange.NONE && range.containsInclusive(astOffset)) {
if (astOffset != range.getStart()) {
astOffset = range.getStart() - 1;
if (astOffset < 0) {
astOffset = 0;
}
path = new AstPath(root, astOffset);
}
}
if (path == null) {
path = new AstPath(root, astOffset);
}
int currentLineStart = Utilities.getRowStart(doc, lexOffset);
if (callLineStart != -1 && currentLineStart == callLineStart) {
// We know the method call
targetMethod = callMethod;
// if (targetMethod != null) {
// Somehow figure out the argument index
// Perhaps I can keep the target tree around and look in it
// (This is all trying to deal with temporarily broken
// or ambiguous calls.
// }
}
// Compute the argument index
Node call = null;
int anchorOffset = -1;
if (targetMethod != null) {
Iterator<Node> it = path.leafToRoot();
String name = targetMethod.getName();
while (it.hasNext()) {
Node node = it.next();
if (AstUtilities.isCall(node) &&
name.equals(AstUtilities.getCallName(node))) {
if (node.getNodeType() == NodeType.CALLNODE) {
Node argsNode = ((CallNode) node).getArgsNode();
if (argsNode != null) {
index = AstUtilities.findArgumentIndex(argsNode, astOffset);
if (index == -1 && astOffset < originalAstOffset) {
index = AstUtilities.findArgumentIndex(argsNode, originalAstOffset);
}
if (index != -1) {
call = node;
anchorOffset = argsNode.getPosition().getStartOffset();
}
}
} else if (node.getNodeType() == NodeType.FCALLNODE) {
Node argsNode = ((FCallNode) node).getArgsNode();
if (argsNode != null) {
index = AstUtilities.findArgumentIndex(argsNode, astOffset);
if (index == -1 && astOffset < originalAstOffset) {
index = AstUtilities.findArgumentIndex(argsNode, originalAstOffset);
}
if (index != -1) {
call = node;
anchorOffset = argsNode.getPosition().getStartOffset();
}
}
} else if (node.getNodeType() == NodeType.VCALLNODE) {
// We might be completing at the end of a method call
// and we don't have parameters yet so it just looks like
// a vcall, e.g.
// create_table |
// This is okay as long as the caret is outside and to
// the right of this call. However
final OffsetRange callRange = AstUtilities.getCallRange(node);
AstUtilities.getCallName(node);
if (originalAstOffset > callRange.getEnd()) {
index = 0;
call = node;
anchorOffset = callRange.getEnd() + 1;
}
}
break;
}
}
}
boolean haveSanitizedComma = rpr.getSanitized() == Sanitize.EDITED_DOT ||
rpr.getSanitized() == Sanitize.ERROR_DOT;
if (haveSanitizedComma) {
// We only care about removed commas since that
// affects the parameter count
if (rpr.getSanitizedContents().indexOf(',') == -1) {
haveSanitizedComma = false;
}
}
if (call == null) {
// Find the call in around the caret. Beware of
// input sanitization which could have completely
// removed the current parameter (e.g. with just
// a comma, or something like ", @" or ", :")
// where we accidentally end up in the previous
// parameter.
ListIterator<Node> it = path.leafToRoot();
nodesearch:
while (it.hasNext()) {
Node node = it.next();
if (kind == QuerySupport.Kind.EXACT) {
// For documentation popups, don't go up through blocks
if (node.getNodeType() == NodeType.ITERNODE || node.getNodeType() == NodeType.DEFNNODE || node.getNodeType() == NodeType.DEFSNODE) {
// Don't consider calls outside the current block or method (149540)
break;
}
}
if (node.getNodeType() == NodeType.CALLNODE) {
final OffsetRange callRange = AstUtilities.getCallRange(node);
if (haveSanitizedComma && originalAstOffset > callRange.getEnd() && it.hasNext()) {
for (int i = 0; i < 3 && it.hasNext(); i++) {
// It's not really a peek in the sense
// that there's no reason to retry these
// nodes later
Node peek = it.next();
if (AstUtilities.isCall(peek) &&
Utilities.getRowStart(doc, LexUtilities.getLexerOffset(parserResult, peek.getPosition().getStartOffset())) ==
Utilities.getRowStart(doc, lexOffset)) {
// Use the outer method call instead
if (it.hasPrevious()) {
it.previous();
}
continue nodesearch;
}
}
}
Node argsNode = ((CallNode) node).getArgsNode();
if (argsNode != null) {
index = AstUtilities.findArgumentIndex(argsNode, astOffset);
if (index == -1 && astOffset < originalAstOffset) {
index = AstUtilities.findArgumentIndex(argsNode, originalAstOffset);
}
if (index != -1) {
call = node;
anchorOffset = argsNode.getPosition().getStartOffset();
break;
}
} else {
if (originalAstOffset > callRange.getEnd()) {
index = 0;
call = node;
anchorOffset = callRange.getEnd() + 1;
break;
}
}
} else if (node.getNodeType() == NodeType.FCALLNODE) {
final OffsetRange callRange = AstUtilities.getCallRange(node);
if (haveSanitizedComma && originalAstOffset > callRange.getEnd() && it.hasNext()) {
for (int i = 0; i < 3 && it.hasNext(); i++) {
// It's not really a peek in the sense
// that there's no reason to retry these
// nodes later
Node peek = it.next();
if (AstUtilities.isCall(peek) &&
Utilities.getRowStart(doc, LexUtilities.getLexerOffset(parserResult, peek.getPosition().getStartOffset())) ==
Utilities.getRowStart(doc, lexOffset)) {
// Use the outer method call instead
if (it.hasPrevious()) {
it.previous();
}
continue nodesearch;
}
}
}
Node argsNode = ((FCallNode) node).getArgsNode();
if (argsNode != null) {
index = AstUtilities.findArgumentIndex(argsNode, astOffset);
if (index == -1 && astOffset < originalAstOffset) {
index = AstUtilities.findArgumentIndex(argsNode, originalAstOffset);
}
if (index != -1) {
call = node;
anchorOffset = argsNode.getPosition().getStartOffset();
break;
}
}
} else if (node.getNodeType() == NodeType.VCALLNODE) {
// We might be completing at the end of a method call
// and we don't have parameters yet so it just looks like
// a vcall, e.g.
// create_table |
// This is okay as long as the caret is outside and to
// the right of this call.
final OffsetRange callRange = AstUtilities.getCallRange(node);
if (haveSanitizedComma && originalAstOffset > callRange.getEnd() && it.hasNext()) {
for (int i = 0; i < 3 && it.hasNext(); i++) {
// It's not really a peek in the sense
// that there's no reason to retry these
// nodes later
Node peek = it.next();
if (AstUtilities.isCall(peek) &&
Utilities.getRowStart(doc, LexUtilities.getLexerOffset(parserResult, peek.getPosition().getStartOffset())) ==
Utilities.getRowStart(doc, lexOffset)) {
// Use the outer method call instead
if (it.hasPrevious()) {
it.previous();
}
continue nodesearch;
}
}
}
if (originalAstOffset > callRange.getEnd()) {
index = 0;
call = node;
anchorOffset = callRange.getEnd() + 1;
break;
}
}
}
}
if (index != -1 && haveSanitizedComma && call != null) {
Node an = null;
if (call.getNodeType() == NodeType.FCALLNODE) {
an = ((FCallNode) call).getArgsNode();
} else if (call.getNodeType() == NodeType.CALLNODE) {
an = ((CallNode) call).getArgsNode();
}
if (an != null && index < an.childNodes().size() &&
an.childNodes().get(index).getNodeType() == NodeType.HASHNODE) {
// We should stay within the hashnode, so counteract the
// index++ which follows this if-block
index--;
}
// Adjust the index to account for our removed
// comma
index++;
}
if ((call == null) || (index == -1)) {
callLineStart = -1;
callMethod = null;
return false;
} else if (targetMethod == null) {
// Look up the
// See if we can find the method corresponding to this call
targetMethod = new RubyDeclarationFinder().findMethodDeclaration(parserResult, call, path,
alternativesHolder);
if (targetMethod == null) {
return false;
}
}
callLineStart = currentLineStart;
callMethod = targetMethod;
methodHolder[0] = callMethod;
parameterIndexHolder[0] = index;
// TODO - if you're in a splat target, I should be highlighting the splat target!!
if (anchorOffset == -1) {
anchorOffset = call.getPosition().getStartOffset(); // TODO - compute
}
anchorOffsetHolder[0] = anchorOffset;
} catch (BadLocationException ble) {
return false;
}
return true;
}
private static RubyTypeInferencer createTypeInferencer(final CompletionRequest request, final Node target) {
ContextKnowledge knowledge = request.createContextKnowledge();
request.target = target;
return RubyTypeInferencer.create(knowledge, false);
}
private RubyType getTypesForConstant(final String constantFqn) {
String module = RubyUtils.parseConstantName(constantFqn)[0];
Set<? extends IndexedConstant> constants = getIndex().getConstants(constantFqn);
for (IndexedConstant indexedConstant : constants) {
if (module.equals(indexedConstant.getFqn())) {
RubyType type = indexedConstant.getType();
if (type.isKnown()) {
return type;
}
}
}
return RubyType.unknown();
}
}