blob: 8d8a6d9e1ef85301c3acdef10e1fc64a676982df [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010 xored software, Inc.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* xored software, Inc. - initial API and Implementation (Alex Panchenko)
*******************************************************************************/
package org.eclipse.dltk.javascript.internal.core.codeassist;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import org.eclipse.core.runtime.Path;
import org.eclipse.dltk.annotations.Nullable;
import org.eclipse.dltk.ast.ASTNode;
import org.eclipse.dltk.ast.utils.ASTUtil;
import org.eclipse.dltk.codeassist.ScriptSelectionEngine;
import org.eclipse.dltk.compiler.env.IModuleSource;
import org.eclipse.dltk.core.DLTKCore;
import org.eclipse.dltk.core.IMember;
import org.eclipse.dltk.core.IModelElement;
import org.eclipse.dltk.core.IModelElementVisitor;
import org.eclipse.dltk.core.IParent;
import org.eclipse.dltk.core.IProjectFragment;
import org.eclipse.dltk.core.IScriptProject;
import org.eclipse.dltk.core.ISourceModule;
import org.eclipse.dltk.core.ISourceRange;
import org.eclipse.dltk.core.ModelException;
import org.eclipse.dltk.core.ScriptModelUtil;
import org.eclipse.dltk.core.SourceRange;
import org.eclipse.dltk.core.model.LocalVariable;
import org.eclipse.dltk.core.model.UnresolvedElement;
import org.eclipse.dltk.internal.javascript.ti.IReferenceAttributes;
import org.eclipse.dltk.internal.javascript.ti.JSDocSupport;
import org.eclipse.dltk.internal.javascript.ti.JSDocSupport.ParameterNode;
import org.eclipse.dltk.internal.javascript.ti.JSDocSupport.TypeNode;
import org.eclipse.dltk.internal.javascript.ti.JSDocSupport.TypedElementNode;
import org.eclipse.dltk.internal.javascript.ti.PositionReachedException;
import org.eclipse.dltk.internal.javascript.ti.TypeInferencer2;
import org.eclipse.dltk.internal.javascript.validation.JavaScriptValidations;
import org.eclipse.dltk.javascript.ast.Argument;
import org.eclipse.dltk.javascript.ast.FunctionStatement;
import org.eclipse.dltk.javascript.ast.Identifier;
import org.eclipse.dltk.javascript.ast.MultiLineComment;
import org.eclipse.dltk.javascript.ast.PropertyInitializer;
import org.eclipse.dltk.javascript.ast.Script;
import org.eclipse.dltk.javascript.ast.StringLiteral;
import org.eclipse.dltk.javascript.core.NodeFinder;
import org.eclipse.dltk.javascript.parser.JavaScriptParserUtil;
import org.eclipse.dltk.javascript.parser.jsdoc.JSDocTag;
import org.eclipse.dltk.javascript.typeinference.IValueReference;
import org.eclipse.dltk.javascript.typeinference.ReferenceKind;
import org.eclipse.dltk.javascript.typeinference.ReferenceLocation;
import org.eclipse.dltk.javascript.typeinference.ValueReferenceUtil;
import org.eclipse.dltk.javascript.typeinfo.IRMethod;
import org.eclipse.dltk.javascript.typeinfo.IRType;
import org.eclipse.dltk.javascript.typeinfo.ITypeSystem;
import org.eclipse.dltk.javascript.typeinfo.JSDocTypeRegion;
import org.eclipse.dltk.javascript.typeinfo.JSDocTypeUtil;
import org.eclipse.dltk.javascript.typeinfo.JSTypeSet;
import org.eclipse.dltk.javascript.typeinfo.TypeInfoManager;
import org.eclipse.dltk.javascript.typeinfo.model.Element;
import org.eclipse.dltk.javascript.typeinfo.model.Member;
import org.eclipse.dltk.javascript.typeinfo.model.Method;
import org.eclipse.dltk.javascript.typeinfo.model.Property;
import org.eclipse.dltk.javascript.typeinfo.model.Type;
import org.eclipse.dltk.javascript.typeinfo.model.TypeKind;
public class JavaScriptSelectionEngine2 extends ScriptSelectionEngine {
private static final boolean DEBUG = false;
public static boolean isJSDocTypeSelectionEnabled() {
return true;
}
@SuppressWarnings("serial")
private static class ModelElementFound extends RuntimeException {
final IModelElement element;
public ModelElementFound(IModelElement element) {
this.element = element;
}
}
private static class Visitor implements IModelElementVisitor {
private final int nameStart;
private final int nameEnd;
public Visitor(int nameStart, int nameEnd) {
this.nameStart = nameStart;
this.nameEnd = nameEnd;
}
public boolean visit(IModelElement element) {
if (element instanceof IMember) {
IMember member = (IMember) element;
try {
ISourceRange range = member.getNameRange();
if (range.getOffset() == nameStart
&& range.getLength() == nameEnd - nameStart) {
throw new ModelElementFound(element);
}
} catch (ModelException e) {
//
}
}
return true;
}
}
private static boolean isStringLiteralInObjectLiteral(ASTNode node) {
if (node instanceof StringLiteral) {
final StringLiteral literal = (StringLiteral) node;
if (literal.getParent() instanceof PropertyInitializer) {
final PropertyInitializer initializer = (PropertyInitializer) literal
.getParent();
return initializer.getName() == literal;
}
}
return false;
}
public IModelElement[] select(final IModuleSource module, int position,
int i) {
if (!(module.getModelElement() instanceof ISourceModule) || i == -1) {
return null;
}
String content = module.getSourceContents();
if (position < 0 || position > content.length()) {
return null;
}
if (DEBUG) {
System.out
.println("select in " + module.getFileName() + " at " + position); //$NON-NLS-1$ //$NON-NLS-2$
}
final Script script = JavaScriptParserUtil.parse(module, null);
NodeFinder finder = new NodeFinder(position, i + 1);
finder.locate(script);
ASTNode node = finder.getNode();
if (node != null) {
if (DEBUG) {
System.out.println(node.getClass().getName() + "=" + node); //$NON-NLS-1$
}
if (node instanceof Identifier
|| isStringLiteralInObjectLiteral(node)) {
final TypeInferencer2 inferencer2 = new TypeInferencer2();
final SelectionVisitor visitor = new SelectionVisitor(
inferencer2, node);
inferencer2.setVisitor(visitor);
inferencer2.setModelElement(module.getModelElement());
try {
inferencer2.doInferencing(script);
} catch (PositionReachedException e) {
//
}
final IValueReference value = visitor.getValue();
if (value == null) {
if (DEBUG) {
System.out.println("value is null or not found"); //$NON-NLS-1$
}
return null;
}
ITypeSystem.CURRENT.runWith(inferencer2, new Runnable() {
public void run() {
toModelElements(inferencer2, visitor, module, value);
}
});
return null;
} else if (node instanceof MultiLineComment
&& isJSDocTypeSelectionEnabled()) {
final MultiLineComment comment = (MultiLineComment) node;
if (comment.isDocumentation()) {
final JSDocTag tag = JSDocSupport.parse(comment).getTagAt(
position);
if (tag == null || position < tag.valueStart()) {
return null;
}
final int valueOffset = tag.toValueOffset(position);
if (JSDocTag.PARAM.equals(tag.name())) {
final ParameterNode paramNode = JSDocSupport
.parseParameter(tag);
if (paramNode != null) {
if (paramNode.isInParameter(valueOffset)) {
final FunctionStatement function = findFunctionByDocumentation(
script, comment);
if (function != null) {
final Argument argument = function
.getArgument(paramNode.name);
if (argument != null) {
return new IModelElement[] { new LocalVariable(
module.getModelElement(),
paramNode.name,
argument.start(),
argument.end(),
argument.start(),
argument.end() - 1, null) };
}
}
} else if (paramNode.isInType(valueOffset)) {
findTypeInTypeExpression(module, tag,
paramNode, valueOffset);
}
}
} else if (JSDocTag.RETURN.equals(tag.name())
|| JSDocTag.RETURNS.equals(tag.name())) {
final TypedElementNode typedNode = JSDocSupport
.parseOptionalType(tag);
if (typedNode != null
&& typedNode.isInType(valueOffset)) {
findTypeInTypeExpression(module, tag, typedNode,
valueOffset);
}
} else if (JSDocTag.TYPE.equals(tag.name())) {
final TypeNode typeNode = JSDocSupport.parseType(tag);
if (typeNode != null && typeNode.isInType(valueOffset)) {
findTypeInTypeExpression(module, tag, typeNode,
valueOffset);
}
}
}
}
}
// TODO Auto-generated method stub
return null;
}
private void findTypeInTypeExpression(IModuleSource module, JSDocTag tag,
TypedElementNode node, int valueOffset) {
final ISourceModule m = (ISourceModule) module.getModelElement();
final TypeInferencer2 inferencer2 = new TypeInferencer2();
inferencer2.setModelElement(module.getModelElement());
final JSDocTypeRegion typeRegion = JSDocTypeUtil.findTypeAt(
inferencer2, node.getTypeExpression(),
valueOffset - node.getTypeExpressionStart());
if (typeRegion != null) {
final Type type = inferencer2.getKnownType(typeRegion.name());
if (type != null) {
final int typeOffsetInFile = tag.fromValueOffset(typeRegion
.start() + node.getTypeExpressionStart());
convertAndReportElement(m, type, new SourceRange(
typeOffsetInFile, typeRegion.length()));
}
}
}
/**
* Finds the function the specified documentation belongs to.
*/
@Nullable
private static FunctionStatement findFunctionByDocumentation(Script script,
MultiLineComment comment) {
for (FunctionStatement function : ASTUtil.select(script,
FunctionStatement.class, true)) {
if (function.getDocumentation() == comment
|| JSDocSupport.getComment(function) == comment) {
return function;
}
}
return null;
}
private void toModelElements(TypeInferencer2 inferencer2,
SelectionVisitor visitor, IModuleSource module,
IValueReference value) {
final ReferenceKind kind = value.getKind();
if (DEBUG) {
System.out.println(value + "," + kind); //$NON-NLS-1$
}
final ReferenceLocation location = value.getLocation();
if (DEBUG) {
System.out.println(location);
}
ISourceModule m = (ISourceModule) module.getModelElement();
if (kind == ReferenceKind.ARGUMENT || kind == ReferenceKind.LOCAL) {
if (location == ReferenceLocation.UNKNOWN) {
return;
}
final IModelElement result = locateModelElement(location);
if (result != null
&& (result.getElementType() == IModelElement.FIELD || result
.getElementType() == IModelElement.METHOD)) {
reportElement(result);
return;
}
final IRType type = JavaScriptValidations.typeOf(value);
reportElement(new LocalVariable(m, value.getName(),
location.getDeclarationStart(),
location.getDeclarationEnd(), location.getNameStart(),
location.getNameEnd() - 1, type == null ? null
: type.getName()));
return;
} else if (kind == ReferenceKind.FUNCTION
|| kind == ReferenceKind.GLOBAL || kind == ReferenceKind.FIELD) {
if (location == ReferenceLocation.UNKNOWN) {
return;
}
final IModelElement result = locateModelElement(location);
if (result != null) {
reportElement(result);
return;
}
} else if (kind == ReferenceKind.PROPERTY) {
final Collection<Property> properties = ValueReferenceUtil
.extractElements(value, Property.class);
if (properties != null) {
convertAndReportElements(m, properties);
return;
}
final IModelElement result = locateModelElement(location);
if (result != null) {
reportElement(result);
return;
}
} else if (kind == ReferenceKind.METHOD) {
final List<IRMethod> methods = ValueReferenceUtil.extractElements(
value, IRMethod.class);
if (methods != null) {
IValueReference[] arguments = visitor.getArguments();
if (arguments == null) {
arguments = new IValueReference[0];
}
final IRMethod method = JavaScriptValidations.selectMethod(
methods, arguments, true);
if (method.getSource() instanceof Method) {
convertAndReportElement(m, (Method) method.getSource(),
null);
return;
}
}
} else if (kind == ReferenceKind.TYPE) {
final LinkedHashSet<Type> t = new LinkedHashSet<Type>();
JSTypeSet types = value.getDeclaredTypes();
if (types != null) {
Collections.addAll(t, types.toArray());
}
types = value.getTypes();
if (types != null) {
Collections.addAll(t, types.toArray());
}
if (!t.isEmpty()) {
convertAndReportElements(m, t);
return;
}
}
if (location != ReferenceLocation.UNKNOWN
&& location.getSourceModule() != null) {
reportElement(new UnresolvedElement(location.getSourceModule(),
value.getName(), location.getNameStart(),
location.getNameEnd() - 1));
return;
}
return;
}
/**
* @param module
* @param elements
* @return
*/
private void convertAndReportElements(ISourceModule module,
Collection<? extends Element> elements) {
for (Element element : elements) {
convertAndReportElement(module, element, null);
}
}
private void convertAndReportElement(ISourceModule module, Element element,
ISourceRange range) {
try {
final IModelElement me = convert(module, element);
reportElement(me != null ? me : element, range);
} catch (ModelException e) {
//
}
}
/**
* @param module
* @param element
* @return
* @throws ModelException
*/
private IModelElement convert(ISourceModule module, Element element)
throws ModelException {
Type type;
if (element instanceof Type) {
type = (Type) element;
} else {
type = ((Member) element).getDeclaringType();
}
if (type != null && type.getKind() == TypeKind.PREDEFINED) {
final List<String> path = new ArrayList<String>();
path.add(type.getName());
if (element != type) {
path.add(element.getName());
}
return resolveBuiltin(module.getScriptProject(), path);
}
if (type != null && type.getKind() == TypeKind.JAVASCRIPT) {
ReferenceLocation location = (ReferenceLocation) element
.getAttribute(IReferenceAttributes.LOCATION);
if (location != null && location != ReferenceLocation.UNKNOWN) {
if (element instanceof Property) {
return new LocalVariable(module, element.getName(),
location.getDeclarationStart(),
location.getDeclarationEnd(),
location.getNameStart(), location.getNameEnd() - 1,
null);
}
final IModelElement result = locateModelElement(location);
if (result != null) {
return result;
}
} else {
// TODO this only goes 1 deep, need support for nested types..
IModelElement[] children = module.getChildren();
for (IModelElement modelElement : children) {
if (modelElement.getElementType() == IModelElement.METHOD
&& modelElement.getElementName().equals(
type.getName())) {
IModelElement[] children2 = ((IParent) modelElement)
.getChildren();
for (IModelElement child : children2) {
if (child.getElementName()
.equals(element.getName()))
return child;
}
return modelElement;
}
}
}
}
return TypeInfoManager.convertElement(module, element);
}
private IModelElement locateModelElement(final ReferenceLocation location) {
ISourceModule module = location.getSourceModule();
if (module != null) {
try {
ScriptModelUtil.reconcile(module);
module.accept(new Visitor(location.getNameStart(), location
.getNameEnd()));
} catch (ModelException e) {
if (DLTKCore.DEBUG) {
e.printStackTrace();
}
} catch (ModelElementFound e) {
return e.element;
}
}
return null;
}
/**
* @param project
* @param segments
* @return
* @throws ModelException
*/
private IModelElement resolveBuiltin(IScriptProject project,
List<String> segments) throws ModelException {
for (IProjectFragment fragment : project.getProjectFragments()) {
if (fragment.isBuiltin()) {
ISourceModule m = fragment.getScriptFolder(Path.EMPTY)
.getSourceModule("builtins.js"); //$NON-NLS-1$
if (!m.exists()) {
return null;
}
IModelElement me = m;
SEGMENT_LOOP: for (String segment : segments) {
if (me instanceof IParent) {
final IModelElement[] children = ((IParent) me)
.getChildren();
for (IModelElement child : children) {
if (segment.equals(child.getElementName())) {
me = child;
continue SEGMENT_LOOP;
}
}
}
return null;
}
return me;
}
}
return null;
}
}