/*******************************************************************************
* Copyright (c) 2009 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:
* IBM Corporation - initial API and implementation
* Zend Technologies
*******************************************************************************/
package org.eclipse.php.internal.core.typeinference;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.dltk.ast.ASTNode;
import org.eclipse.dltk.ast.ASTVisitor;
import org.eclipse.dltk.ast.declarations.MethodDeclaration;
import org.eclipse.dltk.ast.declarations.ModuleDeclaration;
import org.eclipse.dltk.ast.expressions.Expression;
import org.eclipse.dltk.ast.references.VariableReference;
import org.eclipse.dltk.core.*;
import org.eclipse.dltk.evaluation.types.UnknownType;
import org.eclipse.dltk.internal.core.SourceRefElement;
import org.eclipse.dltk.ti.IContext;
import org.eclipse.dltk.ti.ISourceModuleContext;
import org.eclipse.dltk.ti.goals.ExpressionTypeGoal;
import org.eclipse.dltk.ti.types.IEvaluatedType;
import org.eclipse.php.core.compiler.ast.nodes.LambdaFunctionDeclaration;
import org.eclipse.php.core.compiler.ast.nodes.ReturnStatement;
import org.eclipse.php.core.compiler.ast.nodes.YieldExpression;
import org.eclipse.php.internal.core.typeinference.context.IModelCacheContext;
import org.eclipse.php.internal.core.typeinference.context.MethodContext;
import org.eclipse.php.internal.core.typeinference.evaluators.VariableReferenceEvaluator;
/**
* This utility allows to determine types of expressions represented in AST
* tree. Results are cached until instance of this class is alive.
*/
public class BindingUtility {
/** Default time limit for type inference evaluation */
private static final int TIME_LIMIT = 1000;
private ISourceModule sourceModule;
private ASTNode rootNode;
private Map<SourceRange, IEvaluatedType> evaluatedTypesCache = new HashMap<SourceRange, IEvaluatedType>();
private int timeLimit = TIME_LIMIT;
private IModelAccessCache modelAccessCache;
private IPHPTypeInferencer cachedInferencer = null;
/**
* Creates new instance of binding utility.
*
* @param sourceModule
* Source module of the file.
*/
public BindingUtility(ISourceModule sourceModule) {
this.sourceModule = sourceModule;
this.rootNode = SourceParserUtil.getModuleDeclaration(sourceModule);
}
/**
* Creates new instance of binding utility
*
* @param sourceModule
* Source module of the file.
* @param rootNode
* AST tree of the the file represented by the given source
* module.
*/
public BindingUtility(ISourceModule sourceModule, ASTNode rootNode) {
this.sourceModule = sourceModule;
this.rootNode = rootNode;
}
public BindingUtility(ISourceModule sourceModule, IModelAccessCache modelAccessCache) {
this(sourceModule);
this.modelAccessCache = modelAccessCache;
}
/**
* Sets new time limit in milliseconds for the type inference evaluation.
* Default value is 500ms.
*
* @param timeLimit
*/
public void setTimeLimit(int timeLimit) {
this.timeLimit = timeLimit;
}
/**
* This method returns evaluated type for the given AST expression node.
* Returns cached type from previous evaluations (if exists).
*
* @param node
* AST node that needs to be evaluated.
* @return evaluated type.
* @throws ModelException
*
* @throws IllegalArgumentException
* in case if context cannot be found for the given node.
* @throws NullPointerException
* if the given node is <code>null</code>.
*/
public IEvaluatedType getType(ASTNode node) throws ModelException {
if (node == null) {
throw new NullPointerException();
}
return getType(new SourceRange(node));
}
/**
* This method returns evaluated type for the given model element. Returns
* cached type from previous evaluations (if exists).
*
* @param element
* Model element.
* @return evaluated type.
*
* @throws IllegalArgumentException
* in case if context cannot be found for the given node.
* @throws NullPointerException
* if the given element is <code>null</code>.
* @throws ModelException
*/
public IEvaluatedType getType(SourceRefElement element) throws ModelException {
if (element == null) {
throw new NullPointerException();
}
ISourceModule elementModule = element.getSourceModule();
if (!elementModule.equals(sourceModule)) {
throw new IllegalArgumentException("Unknown model element"); //$NON-NLS-1$
}
return getType(new SourceRange(element.getSourceRange()));
}
/**
* This method returns evaluated type for the expression under the given
* offset and length. Returns cached type from previous evaluations (if
* exists).
*
* @param startOffset
* Starting offset of the expression.
* @param length
* Length of the expression.
* @return evaluated type.
*
* @throws IllegalArgumentException
* in case if context cannot be found for the given node.
*/
public IEvaluatedType getType(int startOffset, int length) throws ModelException {
return getType(new SourceRange(startOffset, length));
}
protected IPHPTypeInferencer getTypeInferencer() {
return cachedInferencer == null ? new PHPTypeInferencer() : cachedInferencer;
}
protected IEvaluatedType getType(SourceRange sourceRange, IContext context, ASTNode node) {
return getTypeInferencer().evaluateType(new ExpressionTypeGoal(context, node), timeLimit);
}
protected ContextFinder getContext(SourceRange sourceRange) throws ModelException {
ContextFinder contextFinder = new ContextFinder(sourceRange);
try {
rootNode.traverse(contextFinder);
} catch (Exception e) {
throw new ModelException(e, IStatus.ERROR);
}
if (contextFinder.getNode() == null) {
throw new ModelException(
new IllegalArgumentException("AST node can not be found for the given source range: " //$NON-NLS-1$
+ sourceRange),
IStatus.ERROR);
}
return contextFinder;
}
protected IEvaluatedType getType(SourceRange sourceRange) throws ModelException {
if (!evaluatedTypesCache.containsKey(sourceRange)) {
ContextFinder contextFinder = getContext(sourceRange);
evaluatedTypesCache.put(sourceRange,
getType(sourceRange, contextFinder.getContext(), contextFinder.getNode()));
}
return evaluatedTypesCache.get(sourceRange);
}
/**
* This method returns model elements for the given AST expression node.
* This method uses cached evaluated type from previous evaluations (if
* exists).
*
* @param node
* AST node that needs to be evaluated.
* @return model element or <code>null</code> in case it couldn't be found
* @throws ModelException
*
* @throws IllegalArgumentException
* in case if context cannot be found for the given node.
* @throws NullPointerException
* if the given node is <code>null</code>.
*/
public IModelElement[] getModelElement(ASTNode node) throws ModelException {
if (node == null) {
throw new NullPointerException();
}
return getModelElement(new SourceRange(node), true);
}
/**
* This method returns model elements for the given model element. This
* method uses cached evaluated type from previous evaluations (if exists).
*
* @param element
* Source Reference Model element.
* @return model element or <code>null</code> in case it couldn't be found
*
* @throws IllegalArgumentException
* in case if context cannot be found for the given node.
* @throws NullPointerException
* if the given element is <code>null</code>.
* @throws ModelException
*/
public IModelElement[] getModelElement(SourceRefElement element) throws ModelException {
if (element == null) {
throw new NullPointerException();
}
ISourceModule elementModule = element.getSourceModule();
if (!elementModule.equals(sourceModule)) {
throw new IllegalArgumentException("Unknown model element"); //$NON-NLS-1$
}
return getModelElement(new SourceRange(element.getSourceRange()), true);
}
/**
* This method returns model elements for the expression under the given
* offset and length, and will filter the results using the file-network.
* This method uses cached evaluated type from previous evaluations (if
* exists).
*
* @param startOffset
* Starting offset of the expression.
* @param length
* Length of the expression.
* @return model element or <code>null</code> in case it couldn't be found
* @throws ModelException
*
* @throws IllegalArgumentException
* in case if context cannot be found for the given node.
* @see #getModelElement(int, int, boolean)
*/
public IModelElement[] getModelElement(int startOffset, int length) throws ModelException {
return getModelElement(startOffset, length, true);
}
/**
* This method returns model elements for the expression under the given
* offset and length, and will filter the results using the file-network.
* This method uses cached evaluated type from previous evaluations (if
* exists).
*
* @param startOffset
* Starting offset of the expression.
* @param length
* Length of the expression.
* @param cache
* Model access cache if available
* @return model element or <code>null</code> in case it couldn't be found
* @throws ModelException
*
* @throws IllegalArgumentException
* in case if context cannot be found for the given node.
* @see #getModelElement(int, int, boolean)
*/
public IModelElement[] getModelElement(int startOffset, int length, IModelAccessCache cache) throws ModelException {
return getModelElement(startOffset, length, true, cache);
}
/**
* This method returns model elements for the expression under the given
* offset and length. This method uses cached evaluated type from previous
* evaluations (if exists).
*
* @param startOffset
* Starting offset of the expression.
* @param length
* Length of the expression.
* @param filter
* Filter the results using the 'File-Network'.
* @return model element or <code>null</code> in case it couldn't be found
* @throws ModelException
*
* @throws IllegalArgumentException
* in case if context cannot be found for the given node.
*/
public IModelElement[] getModelElement(int startOffset, int length, boolean filter) throws ModelException {
return getModelElement(new SourceRange(startOffset, length), filter);
}
/**
* This method returns model elements for the expression under the given
* offset and length. This method uses cached evaluated type from previous
* evaluations (if exists).
*
* @param startOffset
* Starting offset of the expression.
* @param length
* Length of the expression.
* @param filter
* Filter the results using the 'File-Network'.
* @param cache
* Model access cache if available
* @return model element or <code>null</code> in case it couldn't be found
* @throws ModelException
*
* @throws IllegalArgumentException
* in case if context cannot be found for the given node.
*/
public IModelElement[] getModelElement(int startOffset, int length, boolean filter, IModelAccessCache cache)
throws ModelException {
return getModelElement(new SourceRange(startOffset, length), filter, cache);
}
protected IModelElement[] getModelElement(SourceRange sourceRange, boolean filter) throws ModelException {
return getModelElement(sourceRange, filter, null);
}
protected IModelElement[] getModelElement(SourceRange sourceRange, boolean filter, IModelAccessCache cache)
throws ModelException {
ContextFinder contextFinder = getContext(sourceRange);
if (!evaluatedTypesCache.containsKey(sourceRange)) {
evaluatedTypesCache.put(sourceRange,
getType(sourceRange, contextFinder.getContext(), contextFinder.getNode()));
}
IEvaluatedType evaluatedType = evaluatedTypesCache.get(sourceRange);
return PHPTypeInferenceUtils.getModelElements(evaluatedType, (ISourceModuleContext) contextFinder.getContext(),
sourceRange.getOffset(), cache);
}
/**
* get the IModelElement at the given position.
*
* @param start
* @param length
* @return the IModelElement instance which represents a IField node, or
* null.
* @throws Exception
*/
public IModelElement getFieldByPosition(int start, int length) throws Exception {
SourceRange sourceRange = new SourceRange(start, length);
ContextFinder contextFinder = getContext(sourceRange);
ASTNode node = contextFinder.getNode();
if (node instanceof VariableReference) {
ASTNode localScopeNode = rootNode;
IContext context = contextFinder.getContext();
if (context instanceof MethodContext) {
localScopeNode = ((MethodContext) context).getMethodNode();
}
VariableReferenceEvaluator.LocalReferenceDeclSearcher varDecSearcher = new VariableReferenceEvaluator.LocalReferenceDeclSearcher(
sourceModule, (VariableReference) node, localScopeNode);
rootNode.traverse(varDecSearcher);
Declaration[] decls = varDecSearcher.getDeclarations();
if (decls != null && decls.length > 0) {
return this.sourceModule.getElementAt(decls[0].getNode().sourceStart());
}
}
return null;
}
/**
* Finds binding context for the given AST node for internal usages only.
*/
private class ContextFinder extends org.eclipse.php.internal.core.typeinference.context.ContextFinder {
private SourceRange sourceRange;
private IContext context;
private ASTNode node;
public ContextFinder(SourceRange sourceRange) {
super(sourceModule);
this.sourceRange = sourceRange;
}
public IContext getContext() {
if (context instanceof IModelCacheContext) {
((IModelCacheContext) context).setCache(modelAccessCache);
}
return context;
}
/**
* Returns found AST node
*
* @return AST node
*/
public ASTNode getNode() {
return node;
}
public boolean visitGeneral(ASTNode node) throws Exception {
if (node.sourceStart() > (sourceRange.getOffset() + sourceRange.getLength())) {
return false;
}
if (node.sourceEnd() < sourceRange.getOffset()) {
return false;
}
if (node.sourceStart() <= sourceRange.getOffset()
&& node.sourceEnd() >= (sourceRange.getOffset() + sourceRange.getLength())) {
if (!contextStack.isEmpty()) {
this.context = contextStack.peek();
this.node = node;
}
}
// search inside - we are looking for minimal node
return true;
}
}
public IEvaluatedType[] getFunctionReturnType(IMethod functionElement) {
ISourceModule sourceModule = functionElement.getSourceModule();
ModuleDeclaration sourceModuleDeclaration = SourceParserUtil.getModuleDeclaration(sourceModule);
MethodDeclaration functionDeclaration = null;
try {
functionDeclaration = PHPModelUtils.getNodeByMethod(sourceModuleDeclaration, functionElement);
} catch (ModelException e) {
if (DLTKCore.DEBUG) {
e.printStackTrace();
}
}
// FileContext fileContext = new FileContext(sourceModule,
// sourceModuleDeclaration);
final List<IEvaluatedType> evaluated = new LinkedList<IEvaluatedType>();
final List<Expression> returnExpressions = new LinkedList<Expression>();
final List<Expression> yieldExpressions = new LinkedList<Expression>();
if (functionDeclaration != null) {
final MethodDeclaration topDeclaration = functionDeclaration;
ASTVisitor visitor = new ASTVisitor() {
public boolean visitGeneral(ASTNode node) throws Exception {
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=464921
// do not evaluate content of inner lambda functions
if (node instanceof LambdaFunctionDeclaration
// but never exclude top node (even if this case
// cannot
// happen here)
&& node != topDeclaration) {
return false;
}
if (node instanceof ReturnStatement) {
ReturnStatement statement = (ReturnStatement) node;
Expression expr = statement.getExpr();
if (expr == null) {
evaluated.add(PHPSimpleTypes.VOID);
} else {
returnExpressions.add(expr);
}
} else if (node instanceof YieldExpression) {
YieldExpression statement = (YieldExpression) node;
Expression expr = statement.getExpr();
yieldExpressions.add(expr);
}
return super.visitGeneral(node);
}
};
try {
functionDeclaration.traverse(visitor);
} catch (Exception e) {
if (DLTKCore.DEBUG) {
e.printStackTrace();
}
}
}
for (Expression expr : returnExpressions) {
SourceRange sourceRange = new SourceRange(expr);
try {
ContextFinder contextFinder = getContext(sourceRange);
IContext context = contextFinder.getContext();
IEvaluatedType resolvedExpression = PHPTypeInferenceUtils.resolveExpression(getTypeInferencer(),
sourceModule, sourceModuleDeclaration, context, expr);
if (resolvedExpression != null) {
evaluated.add(resolvedExpression);
} else {
evaluated.add(UnknownType.INSTANCE);
}
} catch (ModelException e) {
if (DLTKCore.DEBUG) {
e.printStackTrace();
}
continue;
}
}
if (yieldExpressions.size() > 0) {
GeneratorClassType generator = new GeneratorClassType();
for (Expression expr : yieldExpressions) {
if (expr == null) {
generator.getTypes().add(PHPSimpleTypes.NULL);
} else {
SourceRange sourceRange = new SourceRange(expr);
try {
ContextFinder contextFinder = getContext(sourceRange);
IContext context = contextFinder.getContext();
IEvaluatedType resolvedExpression = PHPTypeInferenceUtils.resolveExpression(getTypeInferencer(),
sourceModule, sourceModuleDeclaration, context, expr);
if (resolvedExpression != null) {
generator.getTypes().add(resolvedExpression);
}
} catch (ModelException e) {
if (DLTKCore.DEBUG) {
e.printStackTrace();
}
continue;
}
}
evaluated.add(generator);
}
}
return evaluated.toArray(new IEvaluatedType[evaluated.size()]);
}
/**
* Set type inferencer for this utility
*
* @param cachedInferencer
*/
public void setCachedInferencer(IPHPTypeInferencer cachedInferencer) {
this.cachedInferencer = cachedInferencer;
}
}