blob: c07295a769678d9cf3a771179f4bc096bd8b3de7 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2016 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* xored software, Inc. - initial API and Implementation (Andrei Sobolev)
* xored software, Inc. - RubyDocumentation display improvements (Alex Panchenko <alex@xored.com>)
*******************************************************************************/
package org.eclipse.dltk.ruby.internal.core.codeassist;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.dltk.ast.ASTNode;
import org.eclipse.dltk.ast.declarations.MethodDeclaration;
import org.eclipse.dltk.ast.declarations.ModuleDeclaration;
import org.eclipse.dltk.ast.declarations.TypeDeclaration;
import org.eclipse.dltk.ast.expressions.CallExpression;
import org.eclipse.dltk.ast.references.ConstantReference;
import org.eclipse.dltk.ast.references.VariableReference;
import org.eclipse.dltk.codeassist.ScriptSelectionEngine;
import org.eclipse.dltk.compiler.env.IModuleSource;
import org.eclipse.dltk.core.DLTKCore;
import org.eclipse.dltk.core.DLTKLanguageManager;
import org.eclipse.dltk.core.IField;
import org.eclipse.dltk.core.IMember;
import org.eclipse.dltk.core.IMethod;
import org.eclipse.dltk.core.IModelElement;
import org.eclipse.dltk.core.IScriptProject;
import org.eclipse.dltk.core.ISourceModule;
import org.eclipse.dltk.core.ISourceRange;
import org.eclipse.dltk.core.IType;
import org.eclipse.dltk.core.ModelException;
import org.eclipse.dltk.core.ScriptModelUtil;
import org.eclipse.dltk.core.mixin.IMixinElement;
import org.eclipse.dltk.core.search.IDLTKSearchConstants;
import org.eclipse.dltk.core.search.IDLTKSearchScope;
import org.eclipse.dltk.core.search.SearchEngine;
import org.eclipse.dltk.core.search.SearchMatch;
import org.eclipse.dltk.core.search.SearchParticipant;
import org.eclipse.dltk.core.search.SearchPattern;
import org.eclipse.dltk.core.search.SearchRequestor;
import org.eclipse.dltk.core.search.TypeNameMatch;
import org.eclipse.dltk.core.search.TypeNameMatchRequestor;
import org.eclipse.dltk.ruby.ast.RubyAssignment;
import org.eclipse.dltk.ruby.ast.RubyColonExpression;
import org.eclipse.dltk.ruby.ast.RubyForStatement2;
import org.eclipse.dltk.ruby.ast.RubyMethodArgument;
import org.eclipse.dltk.ruby.ast.RubySuperExpression;
import org.eclipse.dltk.ruby.core.RubyPlugin;
import org.eclipse.dltk.ruby.core.model.FakeField;
import org.eclipse.dltk.ruby.core.utils.RubySyntaxUtils;
import org.eclipse.dltk.ruby.internal.parser.mixin.RubyMixinClass;
import org.eclipse.dltk.ruby.internal.parser.mixin.RubyMixinElementInfo;
import org.eclipse.dltk.ruby.internal.parser.mixin.RubyMixinMethod;
import org.eclipse.dltk.ruby.internal.parser.mixin.RubyMixinModel;
import org.eclipse.dltk.ruby.internal.parsers.jruby.ASTUtils;
import org.eclipse.dltk.ruby.typeinference.RubyClassType;
import org.eclipse.dltk.ruby.typeinference.RubyModelUtils;
import org.eclipse.dltk.ruby.typeinference.RubyTypeInferencingUtils;
import org.eclipse.dltk.ruby.typeinference.evaluators.ColonExpressionEvaluator;
import org.eclipse.dltk.ruby.typeinference.evaluators.ConstantReferenceEvaluator;
import org.eclipse.dltk.ruby.typeinference.goals.NonTypeConstantTypeGoal;
import org.eclipse.dltk.ti.BasicContext;
import org.eclipse.dltk.ti.DLTKTypeInferenceEngine;
import org.eclipse.dltk.ti.GoalState;
import org.eclipse.dltk.ti.goals.AbstractTypeGoal;
import org.eclipse.dltk.ti.goals.ExpressionTypeGoal;
import org.eclipse.dltk.ti.goals.IGoal;
import org.eclipse.dltk.ti.types.IEvaluatedType;
public class RubySelectionEngine extends ScriptSelectionEngine {
public static final boolean DEBUG = DLTKCore.DEBUG_SELECTION;
protected int actualSelectionStart;
protected int actualSelectionEnd;
private Set<IModelElement> selectionElements = new HashSet<IModelElement>();
private RubySelectionParser parser = new RubySelectionParser();
private ISourceModule sourceModule;
private ASTNode[] wayToNode;
private DLTKTypeInferenceEngine inferencer;
private TypeDeclaration getEnclosingType(ASTNode node) {
return ASTUtils.getEnclosingType(wayToNode, node, true);
}
private CallExpression getEnclosingCallNode(ASTNode node) {
return ASTUtils.getEnclosingCallNode(wayToNode, node, true);
}
public RubySelectionEngine() {
inferencer = new DLTKTypeInferenceEngine();
}
private RubyMixinModel mixinModel;
@Override
public IModelElement[] select(IModuleSource sourceUnit,
int selectionSourceStart, int selectionSourceEnd) {
sourceModule = (ISourceModule) sourceUnit.getModelElement();
mixinModel = RubyMixinModel
.getInstance(sourceModule.getScriptProject());
String source = sourceUnit.getSourceContents();
if (DEBUG) {
System.out.print("SELECTION IN "); //$NON-NLS-1$
System.out.print(sourceUnit.getFileName());
System.out.print(" FROM "); //$NON-NLS-1$
System.out.print(selectionSourceStart);
System.out.print(" TO "); //$NON-NLS-1$
System.out.println(selectionSourceEnd);
System.out.println("SELECTION - Source :"); //$NON-NLS-1$
System.out.println(source);
}
if (!checkSelection(source, selectionSourceStart, selectionSourceEnd)) {
return new IModelElement[0];
}
actualSelectionEnd--; // inclusion fix
if (DEBUG) {
System.out.print("SELECTION - Checked : \""); //$NON-NLS-1$
System.out.print(source.substring(actualSelectionStart,
actualSelectionEnd + 1));
System.out.println('"');
}
try {
ModuleDeclaration parsedUnit = this.parser.parse(sourceUnit);
if (parsedUnit != null) {
if (DEBUG) {
System.out.println("SELECTION - AST :"); //$NON-NLS-1$
System.out.println(parsedUnit.toString());
}
ASTNode node = ASTUtils.findMinimalNode(parsedUnit,
actualSelectionStart, actualSelectionEnd);
if (node == null)
return new IModelElement[0];
this.wayToNode = ASTUtils.restoreWayToNode(parsedUnit, node);
if (node instanceof TypeDeclaration) {
selectionOnTypeDeclaration(parsedUnit,
(TypeDeclaration) node);
} else if (node instanceof MethodDeclaration) {
selectionOnMethodDeclaration(parsedUnit,
(MethodDeclaration) node);
} else if (node instanceof ConstantReference
|| node instanceof RubyColonExpression) {
selectTypes(parsedUnit, node);
} else if (node instanceof VariableReference) {
selectionOnVariable(parsedUnit, (VariableReference) node);
} else if (node instanceof RubyMethodArgument) {
selectOnMethodArgument(parsedUnit,
(RubyMethodArgument) node);
} else if (node instanceof RubySuperExpression) {
selectOnSuper(parsedUnit, (RubySuperExpression) node);
} else {
CallExpression parentCall = this.getEnclosingCallNode(node);
if (parentCall != null) {
selectOnMethod(parsedUnit, parentCall);
} else { // parentCall == null
}
}
}
} catch (IndexOutOfBoundsException e) { // work-around internal failure
RubyPlugin.log(e);
}
return selectionElements.toArray(new IModelElement[selectionElements.size()]);
}
private void selectOnSuper(ModuleDeclaration parsedUnit,
RubySuperExpression superExpr) {
RubyClassType selfClass = RubyTypeInferencingUtils.determineSelfClass(
mixinModel, sourceModule, parsedUnit, superExpr.sourceStart());
MethodDeclaration enclosingMethod = ASTUtils.getEnclosingMethod(
wayToNode, superExpr, false);
if (enclosingMethod != null) {
String name = enclosingMethod.getName();
RubyMixinClass rubyClass = mixinModel.createRubyClass(selfClass);
RubyMixinClass superclass = rubyClass.getSuperclass();
RubyMixinMethod method = superclass.getMethod(name);
if (method != null) {
IMethod[] sourceMethods = method.getSourceMethods();
addArrayToCollection(sourceMethods, selectionElements);
}
}
}
private void selectOnColonExpression(ModuleDeclaration parsedUnit,
RubyColonExpression node) {
BasicContext basicContext = new BasicContext(sourceModule, parsedUnit);
ColonExpressionEvaluator evaluator = new ColonExpressionEvaluator(
new ExpressionTypeGoal(basicContext, node));
IGoal[] init = evaluator.init();
if (init == null || init.length == 0) {
// should never be here
// System.err.println("Why did ColonExpressionEvaluator evaluated so
// fast?");
} else {
IEvaluatedType leftType = inferencer.evaluateType(
(AbstractTypeGoal) init[0], -1);
IGoal[] goals = evaluator.subGoalDone(init[0], leftType,
GoalState.DONE);
if (goals == null || goals.length == 0) { // good, we have
// type-constant
Object evaluatedType = evaluator.produceResult();
if (evaluatedType instanceof RubyClassType) {
RubyMixinClass mixinClass = mixinModel
.createRubyClass((RubyClassType) evaluatedType);
if (mixinClass != null)
addArrayToCollection(mixinClass.getSourceTypes(),
selectionElements);
}
} else {
if (goals[0] instanceof NonTypeConstantTypeGoal) {
processNonTypeConstant((NonTypeConstantTypeGoal) (goals[0]));
}
}
}
}
/**
* Uses goal info for selection on non-type goal
*
* @param ngoal
*/
private void processNonTypeConstant(NonTypeConstantTypeGoal ngoal) {
IMixinElement element = ngoal.getElement();
if (element != null) {
Object[] eObjects = element.getAllObjects();
for (int i = 0; i < eObjects.length; i++) {
if (eObjects[i] instanceof RubyMixinElementInfo) {
RubyMixinElementInfo info = (RubyMixinElementInfo) eObjects[i];
Object obj = info.getObject();
if (obj instanceof IModelElement) {
this.selectionElements.add((IModelElement) obj);
}
}
}
}
}
private void selectOnConstant(ModuleDeclaration parsedUnit,
ConstantReference node) {
BasicContext basicContext = new BasicContext(sourceModule, parsedUnit);
ConstantReferenceEvaluator evaluator = new ConstantReferenceEvaluator(
new ExpressionTypeGoal(basicContext, node));
IGoal[] init = evaluator.init();
if (init == null || init.length == 0) {
Object evaluatedType = evaluator.produceResult();
if (evaluatedType instanceof RubyClassType) {
RubyMixinClass mixinClass = mixinModel
.createRubyClass((RubyClassType) evaluatedType);
if (mixinClass != null)
addArrayToCollection(mixinClass.getSourceTypes(),
selectionElements);
}
} else if (init[0] instanceof NonTypeConstantTypeGoal) {
// it'a non-type constant
processNonTypeConstant((NonTypeConstantTypeGoal) init[0]);
}
}
/**
* Checks, whether giver selection is correct selection, or can be expanded
* to correct selection region. As result will set
* this.actualSelection(Start|End) properly. In case of incorrect selection,
* will return false.
*
* @param source
* @param start
* @param end
* @return
*/
protected boolean checkSelection(String source, int start, int end) {
if (start > end) {
int x = start;
start = end;
end = x;
}
if (start + 1 == end) {
ISourceRange range = RubySyntaxUtils.getEnclosingName(source, end);
if (range != null) {
this.actualSelectionStart = range.getOffset();
this.actualSelectionEnd = this.actualSelectionStart
+ range.getLength();
// return true;
}
ISourceRange range2 = RubySyntaxUtils.insideMethodOperator(source,
end);
if (range != null
&& (range2 == null || range2.getLength() < range
.getLength()))
return true;
if (range2 != null) {
this.actualSelectionStart = range2.getOffset();
this.actualSelectionEnd = this.actualSelectionStart
+ range2.getLength();
return true;
}
} else {
if (start >= 0 && end < source.length()) {
String str = source.substring(start, end + 1);
if (RubySyntaxUtils.isRubyName(str)) {
this.actualSelectionStart = start;
this.actualSelectionEnd = end + 1;
return true;
}
}
}
return false;
}
private void selectTypes(ModuleDeclaration parsedUnit, ASTNode node) {
if (node instanceof ConstantReference) {
selectOnConstant(parsedUnit, (ConstantReference) node);
} else if (node instanceof RubyColonExpression) {
selectOnColonExpression(parsedUnit, (RubyColonExpression) node);
}
if (selectionElements.isEmpty()) {
TypeNameMatchRequestor requestor = new TypeNameMatchRequestor() {
@Override
public void acceptTypeNameMatch(TypeNameMatch match) {
selectionElements.add(match.getType());
}
};
String unqualifiedName = null;
if (node instanceof RubyColonExpression) {
RubyColonExpression expr = (RubyColonExpression) node;
unqualifiedName = expr.getName();
} else if (node instanceof ConstantReference) {
ConstantReference expr = (ConstantReference) node;
unqualifiedName = expr.getName();
}
if (unqualifiedName != null) {
ScriptModelUtil.searchTypeDeclarations(sourceModule
.getScriptProject(), unqualifiedName, requestor);
}
}
}
private void selectOnMethodArgument(ModuleDeclaration parsedUnit,
RubyMethodArgument arg) {
selectionElements.add(createLocalVariable(arg.getName(), arg
.sourceStart(), arg.sourceEnd()));
}
private void selectionOnVariable(ModuleDeclaration parsedUnit,
VariableReference e) {
String name = e.getName();
if (name.startsWith("@")) { //$NON-NLS-1$
IField[] fields = RubyModelUtils.findFields(mixinModel,
sourceModule, parsedUnit, name, e.sourceStart());
addArrayToCollection(fields, selectionElements);
} else {
/*
* local vars (legacy, saved for speed reasons: we don't need to use
* mixin model for local vars)
*/
ASTNode parentScope = null;
for (int i = wayToNode.length; --i >= 0;) {
final ASTNode node = wayToNode[i];
if (node instanceof MethodDeclaration
|| node instanceof TypeDeclaration
|| node instanceof ModuleDeclaration
|| node instanceof RubyForStatement2) {
parentScope = node;
break;
}
}
if (parentScope != null) {
RubyAssignment[] assignments = RubyTypeInferencingUtils
.findLocalVariableAssignments(parentScope, e, name);
if (assignments.length > 0) {
final ASTNode left = assignments[0].getLeft();
selectionElements.add(createLocalVariable(name, left
.sourceStart(), left.sourceEnd()));
} else {
selectionElements.add(createLocalVariable(name, e
.sourceStart(), e.sourceEnd()));
}
}
}
}
private IField createLocalVariable(String name, int nameStart, int nameEnd) {
return new FakeField(sourceModule, name, nameStart, nameEnd - nameStart);
}
private IType[] getSourceTypesForClass(ModuleDeclaration parsedUnit,
ASTNode statement) {
ExpressionTypeGoal typeGoal = new ExpressionTypeGoal(new BasicContext(
sourceModule, parsedUnit), statement);
IEvaluatedType evaluatedType = this.inferencer.evaluateType(typeGoal,
5000);
if (evaluatedType instanceof RubyClassType) {
RubyMixinClass mixinClass = mixinModel
.createRubyClass((RubyClassType) evaluatedType);
if (mixinClass != null)
return mixinClass.getSourceTypes();
}
return new IType[0];
}
private void selectionOnTypeDeclaration(ModuleDeclaration parsedUnit,
TypeDeclaration typeDeclaration) {
// if (typeDeclaration instanceof RubyClassDeclaration) {
// RubyClassDeclaration rcd = (RubyClassDeclaration) typeDeclaration;
// IType[] types = getSourceTypesForClass(parsedUnit, rcd
// .getClassName());
// selectionElements.addAll(Arrays.asList(types));
// }
IModelElement elementAt = null;
try {
elementAt = sourceModule
.getElementAt(typeDeclaration.sourceStart() + 1);
} catch (ModelException e) {
RubyPlugin.log(e);
}
if (elementAt != null)
selectionElements.add(elementAt);
}
private void selectionOnMethodDeclaration(ModuleDeclaration parsedUnit,
MethodDeclaration methodDeclaration) {
IModelElement elementAt = null;
try {
elementAt = sourceModule.getElementAt(methodDeclaration
.sourceStart() + 1);
} catch (ModelException e) {
RubyPlugin.log(e);
}
if (elementAt != null)
selectionElements.add(elementAt);
}
private void selectOnMethod(ModuleDeclaration parsedUnit,
CallExpression parentCall) {
String methodName = parentCall.getName();
ASTNode receiver = parentCall.getReceiver();
final List<IModelElement> availableMethods = new ArrayList<IModelElement>();
if (receiver == null) {
IEvaluatedType type = RubyTypeInferencingUtils.determineSelfClass(
mixinModel, sourceModule, parsedUnit, parentCall
.sourceStart());
if ((type != null) && "Object".equals(type.getTypeName())) { //$NON-NLS-1$
ExpressionTypeGoal goal = new ExpressionTypeGoal(
new BasicContext(sourceModule, parsedUnit), parsedUnit);
IEvaluatedType type2 = inferencer.evaluateType(goal, 2000);
if (type2 != null) {
type = type2;
}
}
IMethod[] m = RubyModelUtils.searchClassMethodsExact(mixinModel,
sourceModule, parsedUnit, type, methodName);
addArrayToCollection(m, availableMethods);
} else {
ExpressionTypeGoal goal = new ExpressionTypeGoal(new BasicContext(
sourceModule, parsedUnit), receiver);
IEvaluatedType type = inferencer.evaluateType(goal, 5000);
IMethod[] m = RubyModelUtils.searchClassMethodsExact(mixinModel,
sourceModule, parsedUnit, type, methodName);
addArrayToCollection(m, availableMethods);
if (receiver instanceof VariableReference) {
IMethod[] availableMethods2 = RubyModelUtils
.getSingletonMethods(mixinModel,
(VariableReference) receiver, parsedUnit,
sourceModule, methodName);
addArrayToCollection(availableMethods2, availableMethods);
}
}
if (availableMethods.isEmpty()) {
searchMethodDeclarations(sourceModule.getScriptProject(),
methodName, availableMethods);
}
if (!availableMethods.isEmpty()) {
for (int i = 0, size = availableMethods.size(); i < size; ++i) {
final IMethod m = (IMethod) availableMethods.get(i);
if (methodName.equals(methodName)) {
selectionElements.add(m);
}
}
}
}
private static void searchMethodDeclarations(IScriptProject project,
String methodName, final List<IModelElement> availableMethods) {
final SearchRequestor requestor = new SearchRequestor() {
@Override
public void acceptSearchMatch(SearchMatch match)
throws CoreException {
IModelElement modelElement = (IModelElement) match.getElement();
ISourceModule sm = (ISourceModule) modelElement
.getAncestor(IModelElement.SOURCE_MODULE);
IModelElement elementAt = sm.getElementAt(match.getOffset());
if (elementAt.getElementType() == IModelElement.METHOD) {
availableMethods.add(elementAt);
}
}
};
final IDLTKSearchScope scope = SearchEngine.createSearchScope(project);
try {
final SearchEngine engine = new SearchEngine();
final SearchPattern pattern = SearchPattern.createPattern(
methodName, IDLTKSearchConstants.METHOD,
IDLTKSearchConstants.DECLARATIONS,
SearchPattern.R_EXACT_MATCH
| SearchPattern.R_CASE_SENSITIVE,
DLTKLanguageManager.getLanguageToolkit(project));
final SearchParticipant[] participants = new SearchParticipant[] { SearchEngine
.getDefaultSearchParticipant() };
engine.search(pattern, participants, scope, requestor, null);
} catch (CoreException e) {
RubyPlugin.log(e);
}
}
/**
* @param src
* @param dest
*/
private static void addArrayToCollection(IMember[] src, Collection<IModelElement> dest) {
if (src != null) {
for (int i = 0, size = src.length; i < size; ++i) {
dest.add(src[i]);
}
}
}
}