/*
* Copyright 2009-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.codehaus.groovy.eclipse.codeassist.processors;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.ImportNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Variable;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.eclipse.codeassist.GroovyContentAssist;
import org.codehaus.groovy.eclipse.codeassist.completions.GroovyExtendedCompletionContext;
import org.codehaus.groovy.eclipse.codeassist.creators.AbstractProposalCreator;
import org.codehaus.groovy.eclipse.codeassist.creators.CategoryProposalCreator;
import org.codehaus.groovy.eclipse.codeassist.creators.FieldProposalCreator;
import org.codehaus.groovy.eclipse.codeassist.creators.IProposalCreator;
import org.codehaus.groovy.eclipse.codeassist.creators.MethodProposalCreator;
import org.codehaus.groovy.eclipse.codeassist.proposals.IGroovyProposal;
import org.codehaus.groovy.eclipse.codeassist.requestor.ContentAssistContext;
import org.codehaus.groovy.eclipse.codeassist.requestor.ContentAssistLocation;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.core.CompletionContext;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.ISourceReference;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.groovy.core.util.ReflectionUtils;
import org.eclipse.jdt.groovy.search.AccessorSupport;
import org.eclipse.jdt.groovy.search.ITypeRequestor;
import org.eclipse.jdt.groovy.search.TypeInferencingVisitorFactory;
import org.eclipse.jdt.groovy.search.TypeInferencingVisitorWithRequestor;
import org.eclipse.jdt.groovy.search.TypeLookupResult;
import org.eclipse.jdt.groovy.search.TypeLookupResult.TypeConfidence;
import org.eclipse.jdt.groovy.search.VariableScope;
import org.eclipse.jdt.groovy.search.VariableScope.VariableInfo;
import org.eclipse.jdt.internal.codeassist.InternalCompletionContext;
import org.eclipse.jdt.internal.core.SearchableEnvironment;
import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal;
import org.eclipse.jdt.ui.text.java.JavaContentAssistInvocationContext;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
public class StatementAndExpressionCompletionProcessor extends AbstractGroovyCompletionProcessor {
private class ExpressionCompletionRequestor implements ITypeRequestor {
/** number of array accesses that must be dereferenced */
private int derefCount;
private boolean isStatic;
private ClassNode lhsType;
private ClassNode resultingType;
private Set<ClassNode> categories;
private Expression arrayAccessLHS;
private VariableScope currentScope;
private boolean visitSuccessful;
private ExpressionCompletionRequestor() {
// remember the rightmost part of the LHS of a binary expression
ASTNode maybeLHS = getContext().getPerceivedCompletionNode();
while (maybeLHS != null) {
if (maybeLHS instanceof BinaryExpression) {
maybeLHS = arrayAccessLHS = ((BinaryExpression) maybeLHS).getLeftExpression();
derefCount += 1;
} else if (maybeLHS instanceof PropertyExpression) {
arrayAccessLHS = ((PropertyExpression) maybeLHS).getObjectExpression();
maybeLHS = ((PropertyExpression) maybeLHS).getProperty();
} else if (maybeLHS instanceof MethodCallExpression) {
arrayAccessLHS = ((MethodCallExpression) maybeLHS).getObjectExpression();
maybeLHS = ((MethodCallExpression) maybeLHS).getMethod();
} else {
if (maybeLHS instanceof Expression) {
arrayAccessLHS = (Expression) maybeLHS;
}
maybeLHS = null;
}
}
}
public VisitStatus acceptASTNode(ASTNode node, TypeLookupResult result, IJavaElement enclosingElement) {
// check to see if the enclosing element does not enclose the nodeToLookFor
if (!interestingElement(enclosingElement)) {
return VisitStatus.CANCEL_MEMBER;
}
if (node instanceof ClassNode) {
ClassNode clazz = (ClassNode) node;
if (clazz.redirect() == clazz && clazz.isScript()) {
return VisitStatus.CONTINUE;
}
} else if (node instanceof MethodNode) {
MethodNode run = (MethodNode) node;
if (run.getName().equals("run") &&
run.getDeclaringClass().isScript() &&
(run.getParameters() == null || run.getParameters().length == 0)) {
return VisitStatus.CONTINUE;
}
} else if (node == lhsNode) {
// NOTE: this should be mutually exclusive to maybeRememberTypeOfLHS()
// save the inferred type of the LHS node for ranking of the proposals
if (result.confidence != TypeConfidence.UNKNOWN) {
if (result.declaration instanceof MethodNode) {
MethodNode meth = (MethodNode) result.declaration;
if (AccessorSupport.SETTER.isAccessorKind(meth, false)) {
lhsType = meth.getParameters()[0].getType();
}
} else {
lhsType = result.type;
}
if (VariableScope.OBJECT_CLASS_NODE.equals(lhsType)) {
lhsType = null;
}
}
}
boolean derefList = false; // if true use the parameterized type of the list
boolean success = doTest(node);
if (!success) {
// maybe this is content assist after array access, i.e. foo[0]._
derefList = success = doTestForAfterArrayAccess(node);
}
if (success) {
maybeRememberTypeOfLHS(result);
categories = result.scope.getCategoryNames();
resultingType = findResultingType(result, derefList);
visitSuccessful = true;
isStatic = node instanceof StaticMethodCallExpression ||
// if we are completing on '.class' then never static context
(node instanceof ClassExpression && !resultingType.equals(VariableScope.CLASS_CLASS_NODE));
currentScope = result.scope;
return VisitStatus.STOP_VISIT;
}
return VisitStatus.CONTINUE;
}
private ClassNode findResultingType(TypeLookupResult result, boolean derefList) {
// if completing on a method call with an implicit 'this'.
ClassNode candidate = getContext().location == ContentAssistLocation.METHOD_CONTEXT ? result.declaringType : result.type;
if (derefList) {
for (int i = 0; i < derefCount; i += 1) {
// GRECLIPSE-742: does the LHS type have a 'getAt' method?
boolean getAtFound = false;
List<MethodNode> getAts = candidate.getMethods("getAt");
for (MethodNode getAt : getAts) {
if (getAt.getParameters() != null && getAt.getParameters().length == 1) {
candidate = getAt.getReturnType();
getAtFound = true;
}
}
if (!getAtFound) {
if (VariableScope.MAP_CLASS_NODE.equals(candidate)) {
// for maps, always use the type of value
candidate = candidate.getGenericsTypes()[1].getType();
} else {
for (int j = 0; j < derefCount; j++) {
candidate = VariableScope.extractElementType(candidate);
}
}
}
}
}
// now look at spread expressions
// might be part of a spread operation
boolean extractElementType = false; // for spread operations
ASTNode enclosing = result.scope.getEnclosingNode();
if (enclosing instanceof MethodCallExpression) {
extractElementType = ((MethodCallExpression) enclosing).isSpreadSafe();
} else if (enclosing instanceof PropertyExpression) {
extractElementType = ((PropertyExpression) enclosing).isSpreadSafe();
}
if (extractElementType) {
candidate = VariableScope.extractElementType(candidate);
}
if (ClassHelper.isPrimitiveType(candidate)) {
candidate = ClassHelper.getWrapper(candidate);
}
return candidate;
}
/**
* Determines if this is the lhs of an array access -- the 'foo' of 'foo[0]'.
*/
private boolean doTestForAfterArrayAccess(ASTNode node) {
return node == arrayAccessLHS;
}
private void maybeRememberTypeOfLHS(TypeLookupResult result) {
if (isAssignmentOfLHS(result.enclosingAssignment)) {
// check to see if this is the rhs of an assignment.
// if so, then attempt to use the type of the lhs for
// ordering of the proposals
if (lhsNode instanceof Variable) {
Variable variable = (Variable) lhsNode;
VariableInfo info = result.scope.lookupName(variable.getName());
lhsType = (info != null ? info.type : variable.getType());
}/* else if (lhsNode instanceof FieldExpression) {
lhsType = lhsNode.getType();
}*/ else if (lhsNode instanceof PropertyExpression) {
lhsType = ((PropertyExpression) lhsNode).getProperty().getType();
}
if (VariableScope.OBJECT_CLASS_NODE.equals(lhsType)) {
lhsType = null;
}
}
}
private boolean isAssignmentOfLHS(BinaryExpression node) {
if (node != null && lhsNode != null) {
Expression expression = node.getLeftExpression();
return expression == lhsNode || (expression.getClass() == lhsNode.getClass() &&
expression.getStart() == lhsNode.getStart() && expression.getEnd() == lhsNode.getEnd());
}
return false;
}
private boolean doTest(ASTNode node) {
if (node instanceof ArgumentListExpression) {
// || node instanceof PropertyExpression) {
// we never complete on a list of arguments, but rather one of the arguments itself
// also, never do a completion on the property expression, but rather on the
// propertyExpression.getProperty() node
return false;
} else if (node instanceof BinaryExpression) {
BinaryExpression bin = (BinaryExpression) node;
if (bin.getLeftExpression() == arrayAccessLHS) {
// don't return true here, but rather wait for the LHS to
// come through
// this way we can use the derefed value as the completion
// type
return false;
}
}
return isNotExpressionAndStatement(completionNode, node) && completionNode.getStart() == node.getStart() && completionNode.getEnd() == node.getEnd();
}
/**
* @return {@code true} iff enclosingElement's source location contains the source location of {@link #nodeToLookFor}
*/
private boolean interestingElement(IJavaElement enclosingElement) {
// the clinit is always interesting since the clinit contains static initializers
if (enclosingElement.getElementName().equals("<clinit>")) {
return true;
}
if (enclosingElement instanceof ISourceReference) {
try {
ISourceRange range = ((ISourceReference) enclosingElement).getSourceRange();
return range.getOffset() <= completionNode.getStart() &&
range.getOffset() + range.getLength() >= completionNode.getEnd();
} catch (JavaModelException e) {
GroovyContentAssist.logError(e);
}
}
return false;
}
private boolean isNotExpressionAndStatement(ASTNode thisNode, ASTNode otherNode) {
if (thisNode instanceof Expression) {
return !(otherNode instanceof Statement);
} else if (thisNode instanceof Statement) {
return !(otherNode instanceof Expression);
} else {
return true;
}
}
}
/**
* The ASTNode being completed.
*/
private final ASTNode completionNode;
/**
* The LHS of the assignment associated with this content assist invocation, or {@code null} if there is none.
*/
private final Expression lhsNode;
public StatementAndExpressionCompletionProcessor(ContentAssistContext context,
JavaContentAssistInvocationContext javaContext, SearchableEnvironment nameEnvironment) {
super(context, javaContext, nameEnvironment);
this.completionNode = context.getPerceivedCompletionNode();
this.lhsNode = context.lhsNode;
}
public List<ICompletionProposal> generateProposals(IProgressMonitor monitor) {
TypeInferencingVisitorFactory factory = new TypeInferencingVisitorFactory();
ContentAssistContext context = getContext();
TypeInferencingVisitorWithRequestor visitor = factory.createVisitor(context.unit);
ExpressionCompletionRequestor requestor = new ExpressionCompletionRequestor();
// if completion node is null, then it is likely because of a syntax error
if (completionNode != null) {
visitor.visitCompilationUnit(requestor);
}
ClassNode completionType;
boolean isStatic;
List<IGroovyProposal> groovyProposals = new LinkedList<IGroovyProposal>();
if (requestor.visitSuccessful) {
isStatic = isStatic() || requestor.isStatic;
completionType = getCompletionType(requestor);
if (completionType == null) {
if (context.containingDeclaration instanceof ClassNode) {
completionType = (ClassNode) context.containingDeclaration;
} else {
completionType = context.unit.getModuleNode().getScriptClassDummy();
}
}
IProposalCreator[] creators = chooseProposalCreators(isStatic);
if (context.completionNode instanceof ClassExpression) {
if ("java.lang.Class".equals(completionType.getName())) {
// add proposals for static members
ClassNode type = ((ClassExpression) context.completionNode).getType();
proposalCreatorLoop(context, requestor, type, isStatic, groovyProposals, creators, false);
}
}
proposalCreatorLoop(context, requestor, completionType, isStatic, groovyProposals, creators, false);
if (context.location == ContentAssistLocation.STATEMENT) {
ClassNode closureThis = requestor.currentScope.getThis();
if (closureThis != null && !closureThis.equals(completionType)) {
// inside of a closure; must also add content assist for this (previously did the delegate)
proposalCreatorLoop(context, requestor, closureThis, isStatic, groovyProposals, creators, true);
}
}
} else {
// we are at the statement location of a script
// return the category proposals only
AnnotatedNode node = context.containingDeclaration;
ClassNode containingClass;
if (node instanceof ClassNode) {
containingClass = (ClassNode) node;
} else if (node instanceof MethodNode) {
containingClass = ((MethodNode) node).getDeclaringClass();
} else {
containingClass = null;
}
if (containingClass != null) {
groovyProposals.addAll(new CategoryProposalCreator().findAllProposals(containingClass,
VariableScope.ALL_DEFAULT_CATEGORIES, context.getPerceivedCompletionExpression(), false,
ContentAssistLocation.STATEMENT == context.location));
} else if (node instanceof ImportNode) {
ImportNode importNode = (ImportNode) node;
if (importNode.isStatic()) {
containingClass = importNode.getType();
groovyProposals.addAll(new FieldProposalCreator().findAllProposals(containingClass,
VariableScope.ALL_DEFAULT_CATEGORIES, context.getPerceivedCompletionExpression(), true,
ContentAssistLocation.STATEMENT == context.location));
groovyProposals.addAll(new MethodProposalCreator().findAllProposals(containingClass,
VariableScope.ALL_DEFAULT_CATEGORIES, context.getPerceivedCompletionExpression(), true,
ContentAssistLocation.STATEMENT == context.location));
}
}
completionType = context.containingDeclaration instanceof ClassNode ? (ClassNode) context.containingDeclaration
: context.unit.getModuleNode().getScriptClassDummy();
isStatic = false;
}
// get proposals from providers
try {
context.currentScope = requestor.currentScope != null ? requestor.currentScope : createTopLevelScope(completionType);
List<IProposalProvider> providers = ProposalProviderRegistry.getRegistry().getProvidersFor(context.unit);
for (IProposalProvider provider : providers) {
try {
List<IGroovyProposal> otherProposals = provider.getStatementAndExpressionProposals(context, completionType, isStatic, requestor.categories);
if (otherProposals != null) {
groovyProposals.addAll(otherProposals);
}
} catch (Exception e) {
GroovyContentAssist.logError("Exception when using third party proposal provider: " + provider.getClass().getCanonicalName(), e);
}
}
} catch (CoreException e) {
GroovyContentAssist.logError("Exception accessing proposal provider registry", e);
}
fillInExtendedContext(requestor);
// extra filtering and sorting provided by third parties
try {
List<IProposalFilter> filters = ProposalProviderRegistry.getRegistry().getFiltersFor(context.unit);
for (IProposalFilter filter : filters) {
try {
List<IGroovyProposal> newProposals = filter.filterProposals(groovyProposals, context, getJavaContext());
groovyProposals = newProposals == null ? groovyProposals : newProposals;
} catch (Exception e) {
GroovyContentAssist.logError("Exception when using third party proposal filter: " + filter.getClass().getCanonicalName(), e);
}
}
} catch (CoreException e) {
GroovyContentAssist.logError("Exception accessing proposal provider registry", e);
}
List<ICompletionProposal> javaProposals = new ArrayList<ICompletionProposal>(groovyProposals.size());
JavaContentAssistInvocationContext javaContext = getJavaContext();
for (IGroovyProposal groovyProposal : groovyProposals) {
try {
IJavaCompletionProposal javaProposal = groovyProposal.createJavaProposal(context, javaContext);
if (javaProposal != null) {
javaProposals.add(javaProposal);
}
} catch (Exception e) {
GroovyContentAssist.logError("Exception when creating groovy completion proposal", e);
}
}
return javaProposals;
}
private void proposalCreatorLoop(ContentAssistContext context, ExpressionCompletionRequestor requestor,
ClassNode completionType, boolean isStatic, List<IGroovyProposal> groovyProposals, IProposalCreator[] creators, boolean isClosureThis) {
for (IProposalCreator creator : creators) {
if (isClosureThis && !creator.redoForLoopClosure()) {
// avoid duplicate DGMs by not proposing category proposals twice
continue;
}
if (creator instanceof AbstractProposalCreator) {
((AbstractProposalCreator) creator).setLhsType(requestor.lhsType);
((AbstractProposalCreator) creator).setCurrentScope(requestor.currentScope);
((AbstractProposalCreator) creator).setFavoriteStaticMembers(context.getFavoriteStaticMembers());
}
groovyProposals.addAll(creator.findAllProposals(
completionType,
requestor.categories,
context.getPerceivedCompletionExpression(),
isStatic,
ContentAssistLocation.STATEMENT == context.location));
}
}
private void fillInExtendedContext(ExpressionCompletionRequestor requestor) {
JavaContentAssistInvocationContext javaContext = getJavaContext();
CompletionContext coreContext = javaContext.getCoreContext();
if (coreContext != null && !coreContext.isExtended()) {
// must use reflection to set the fields
ReflectionUtils.setPrivateField(InternalCompletionContext.class, "isExtended", coreContext, true);
ReflectionUtils.setPrivateField(InternalCompletionContext.class, "extendedContext", coreContext,
new GroovyExtendedCompletionContext(getContext(), requestor.currentScope));
}
}
protected VariableScope createTopLevelScope(ClassNode completionType) {
VariableScope scope = new VariableScope(null, completionType, false);
return scope;
}
/**
* When completing an expression, use the completion type found by the requestor.
* Otherwise, use the current type.
*/
private ClassNode getCompletionType(ExpressionCompletionRequestor requestor) {
if (getContext().location == ContentAssistLocation.EXPRESSION) {
return requestor.resultingType;
} else if (getContext().location == ContentAssistLocation.METHOD_CONTEXT) {
// if we are completing on a variable expression here, that means
// we have something like this:
// myMethodCall _
// so, we want to look at the type of 'this' to complete on
return completionNode instanceof VariableExpression ? requestor.currentScope.getDelegateOrThis() : requestor.resultingType;
} else {
// use the current 'this' type so that closure types are correct
ClassNode type = requestor.currentScope.getDelegateOrThis();
if (type != null) {
return type;
} else {
// will only happen if in top level scope
return requestor.resultingType;
}
}
}
/**
* When completing a expression, static context exists only if a ClassExpression
* When completing a statement, static context exists if in a static method or field
*/
private boolean isStatic() {
if (getContext().location == ContentAssistLocation.STATEMENT) {
AnnotatedNode annotated = getContext().containingDeclaration;
if (annotated instanceof FieldNode) {
return ((FieldNode) annotated).isStatic();
} else if (annotated instanceof MethodNode) {
return ((MethodNode) annotated).isStatic();
}
}
return false;
}
private IProposalCreator[] chooseProposalCreators(boolean isStatic) {
String completionExpression = getContext().fullCompletionExpression;
if (completionExpression == null) completionExpression = "";
if (completionExpression.matches(".+\\.@\\w*")) {
if (!isStatic) {
return new IProposalCreator[] {new FieldProposalCreator()};
}
return new IProposalCreator[0];
}
if (completionExpression.matches(".+\\.&\\w*")) {
return new IProposalCreator[] {new MethodProposalCreator()};
// TODO: Completions should not insert parens and arguments.
// TODO: Don't want "class" suggested. Static case is not well handled.
}
return getAllProposalCreators();
}
}