/*******************************************************************************
* Copyright (c) 2009, 2015 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.compiler.ast.parser;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.dltk.annotations.Nullable;
import org.eclipse.dltk.ast.ASTNode;
import org.eclipse.dltk.ast.ASTVisitor;
import org.eclipse.dltk.ast.Modifiers;
import org.eclipse.dltk.ast.declarations.*;
import org.eclipse.dltk.ast.expressions.CallArgumentsList;
import org.eclipse.dltk.ast.expressions.CallExpression;
import org.eclipse.dltk.ast.references.ConstantReference;
import org.eclipse.dltk.ast.references.TypeReference;
import org.eclipse.dltk.ast.references.VariableReference;
import org.eclipse.dltk.ast.statements.Block;
import org.eclipse.dltk.ast.statements.Statement;
import org.eclipse.dltk.core.DLTKCore;
import org.eclipse.dltk.core.ISourceModule;
import org.eclipse.dltk.ti.IContext;
import org.eclipse.php.core.compiler.ast.nodes.*;
import org.eclipse.php.internal.core.Constants;
import org.eclipse.php.internal.core.Logger;
import org.eclipse.php.internal.core.typeinference.context.ContextFinder;
import org.eclipse.php.internal.core.typeinference.context.FileContext;
public class ASTUtils {
private static final Pattern VAR_COMMENT_PATTERN1 = Pattern.compile(
"(.*?@var\\p{javaWhitespace}+)([$][^$\\p{javaWhitespace}]+)(\\p{javaWhitespace}+)([^$\\p{javaWhitespace}]+).*", //$NON-NLS-1$
Pattern.CASE_INSENSITIVE);
private static final Pattern VAR_COMMENT_PATTERN2 = Pattern.compile(
"(.*?@var\\p{javaWhitespace}+)([^$\\p{javaWhitespace}]+)(\\p{javaWhitespace}+)([$][^$\\p{javaWhitespace}]+).*", //$NON-NLS-1$
Pattern.CASE_INSENSITIVE);
/**
* Parses @@var comment using regular expressions
*
* @param content
* Content of the @@var comment token
* @param start
* Token start position
* @param end
* Token end position
* @return {@link VarComment}
*/
public static VarComment parseVarComment(String content, int start, int end) {
Matcher m = null;
String types = null, varName = null;
int typeStart = -1, varStart = -1, varEnd = -1;
boolean foundMatch = false;
if ((m = VAR_COMMENT_PATTERN1.matcher(content)).matches()) {
types = m.group(4);
varName = m.group(2);
varStart = start + m.group(1).length();
varEnd = varStart + varName.length();
typeStart = varEnd + m.group(3).length();
foundMatch = true;
} else if ((m = VAR_COMMENT_PATTERN2.matcher(content)).matches()) {
types = m.group(2);
varName = m.group(4);
typeStart = start + m.group(1).length();
varStart = typeStart + types.length() + m.group(3).length();
varEnd = varStart + varName.length();
foundMatch = true;
}
if (foundMatch) {
List<TypeReference> typeReferences = new LinkedList<TypeReference>();
if (types != null) {
int pipeIdx = types.indexOf(Constants.TYPE_SEPARATOR_CHAR);
while (pipeIdx >= 0) {
String typeName = types.substring(0, pipeIdx);
int typeEnd = typeStart + typeName.length();
if (typeName.length() > 0) {
typeReferences.add(new TypeReference(typeStart, typeEnd, typeName));
}
types = types.substring(pipeIdx + 1);
typeStart += pipeIdx + 1;
pipeIdx = types.indexOf(Constants.TYPE_SEPARATOR_CHAR);
}
String typeName = types;
int typeEnd = typeStart + typeName.length();
if (typeName.length() > 0) {
typeReferences.add(new TypeReference(typeStart, typeEnd, typeName));
}
}
VariableReference varReference = new VariableReference(varStart, varEnd, varName);
VarComment varComment = new VarComment(start, end, varReference,
typeReferences.toArray(new TypeReference[typeReferences.size()]));
return varComment;
}
return null;
}
/**
* Strips single or double quotes from the start and from the end of the
* given string
*
* @param name
* String
* @return
*/
public static String stripQuotes(String name) {
int len = name.length();
if (len > 1 && (name.charAt(0) == '\'' && name.charAt(len - 1) == '\''
|| name.charAt(0) == '"' && name.charAt(len - 1) == '"')) {
name = name.substring(1, len - 1);
}
return name;
}
/**
* Finds minimal ast node, that covers given position
*
* @param unit
* @param position
* @return
*/
@Nullable
public static ASTNode findMinimalNode(@Nullable ModuleDeclaration unit, int start, int end) {
if (unit == null) {
return null;
}
class Visitor extends ASTVisitor {
ASTNode result = null;
int start, end;
public Visitor(int start, int end) {
this.start = start;
this.end = end;
}
public ASTNode getResult() {
return result;
}
public boolean visitGeneral(ASTNode s) throws Exception {
int realStart = s.sourceStart();
int realEnd = s.sourceEnd();
if (s instanceof Block) {
realStart = realEnd = -42; // never select on blocks
// ssanders: BEGIN - Modify narrowing logic
}
if (realStart <= start && realEnd >= end) {
if (result != null) {
if ((s.sourceStart() >= result.sourceStart()) && (s.sourceEnd() <= result.sourceEnd())) {
// now we could not handle ConstantReference in
// StaticConstantAccess
if (s instanceof ConstantReference && (result instanceof StaticConstantAccess)) {
return false;
}
result = s;
}
} else {
result = s;
}
// ssanders: END
if (DLTKCore.DEBUG_SELECTION)
System.out.println("Found " + s.getClass().getName()); //$NON-NLS-1$
}
return true;
}
}
Visitor visitor = new Visitor(start, end);
try {
unit.traverse(visitor);
} catch (Exception e) {
Logger.logException(e);
}
return visitor.getResult();
}
/**
* Finds minimal ast node, that covers given position
*
* @param unit
* @param position
* @return
*/
public static ASTNode findMaximalNodeEndingAt(ModuleDeclaration unit, final int boundaryOffset) {
class Visitor extends ASTVisitor {
ASTNode result = null;
public ASTNode getResult() {
return result;
}
public boolean visitGeneral(ASTNode s) throws Exception {
if (s.sourceStart() < 0 || s.sourceEnd() < 0) {
return true;
}
int sourceEnd = s.sourceEnd();
if (Math.abs(sourceEnd - boundaryOffset) <= 0) {
result = s;
}
return true;
}
}
Visitor visitor = new Visitor();
try {
unit.traverse(visitor);
} catch (Exception e) {
e.printStackTrace();
}
return visitor.getResult();
}
/**
* This method builds list of AST nodes which enclose the given AST node.
*
* @param module
* @param node
* @return
*/
public static ASTNode[] restoreWayToNode(ModuleDeclaration module, final ASTNode node) {
final Stack<ASTNode> stack = new Stack<ASTNode>();
ASTVisitor visitor = new ASTVisitor() {
boolean found = false;
public boolean visitGeneral(ASTNode n) throws Exception {
if (found) {
return super.visitGeneral(n);
}
stack.push(n);
if (n.equals(node)) {
found = true;
}
return super.visitGeneral(n);
}
public void endvisitGeneral(ASTNode n) throws Exception {
super.endvisitGeneral(n);
if (found) {
return;
}
stack.pop();
}
};
try {
module.traverse(visitor);
} catch (Exception e) {
Logger.logException(e);
}
return (ASTNode[]) stack.toArray(new ASTNode[stack.size()]);
}
/**
* Finds type inference context for the given AST node.
*
* @param sourceModule
* Source module element
* @param unit
* Module declaration AST node
* @param target
* AST node to find context for
*/
public static IContext findContext(final ISourceModule sourceModule, final ModuleDeclaration unit,
final ASTNode target) {
ContextFinder visitor = new ContextFinder(sourceModule) {
private IContext context;
public IContext getContext() {
return context;
}
public boolean visitGeneral(ASTNode node) throws Exception {
if (node == target) {
context = contextStack.peek();
return false;
}
return context == null;
}
};
try {
unit.traverse(visitor);
} catch (Exception e) {
Logger.logException(e);
}
return visitor.getContext();
}
/**
* Finds type inference context for the given offset.
*
* @param sourceModule
* Source module element
* @param unit
* Module decalaration AST node
* @param offset
* Offset in the filetarget
*/
public static IContext findContext(final ISourceModule sourceModule, final ModuleDeclaration unit,
final int offset) {
ContextFinder visitor = new ContextFinder(sourceModule) {
private IContext context;
public IContext getContext() {
return context;
}
public boolean visitGeneral(ASTNode node) throws Exception {
if (!(node instanceof ASTError) && node.sourceStart() <= offset && node.sourceEnd() >= offset) {
if (!contextStack.isEmpty()) {
context = contextStack.peek();
}
}
if (node.sourceEnd() < offset || node.sourceStart() > offset) {
// node is before or after our search
return false;
}
// search inside - we are looking for minimal node
return true;
}
};
try {
unit.traverse(visitor);
} catch (Exception e) {
Logger.logException(e);
}
if (visitor.getContext() == null) {
/*
* offset can be bigger than unit.end when sourceunit have syntax
* error on end
*/
Logger.log(Logger.WARNING_DEBUG, "Context is null"); //$NON-NLS-1$
return new FileContext(sourceModule, unit, offset);
}
return visitor.getContext();
}
/**
* Finds next declaration after the PHP-doc block
*
* @param moduleDeclaration
* AST root node
* @param offset
* Offset somewhere in the PHP-doc block
* @return declaration after the PHP-doc block or <code>null</code> if first
* coming statement is not declaration.
*/
public static Declaration findDeclarationAfterPHPdoc(ModuleDeclaration moduleDeclaration, final int offset) {
final Declaration[] decl = new Declaration[1];
ASTVisitor visitor = new ASTVisitor() {
boolean found = false;
public boolean visit(MethodDeclaration m) {
if (!found && m.sourceStart() > offset) {
decl[0] = m;
found = true;
return false;
}
return !found;
}
public boolean visit(TypeDeclaration t) {
if (!found && t.sourceStart() > offset) {
decl[0] = t;
found = true;
return false;
}
return !found;
}
@Override
public boolean visit(Statement s) throws Exception {
if (s.getKind() == ASTNodeKinds.FIELD_DECLARATION) {
if (!found && s.sourceStart() > offset) {
decl[0] = (Declaration) s;
found = true;
return false;
}
}
return super.visit(s);
}
public boolean visitGeneral(ASTNode n) {
if (!found && n.sourceStart() > offset) {
found = true;
return false;
}
return !found;
}
};
try {
moduleDeclaration.traverse(visitor);
} catch (Exception e) {
Logger.logException(e);
}
return decl[0];
}
/**
* Creates declaration of constant for the given call expression in case if
* it represents define() call expression.
*
* @param callExpression
* Call expression
* @return constant declaration if the given call expression represents
* define() expression, otherwise <code>null</code>
*/
public static FieldDeclaration getConstantDeclaration(CallExpression callExpression) {
String name = callExpression.getName();
if ("define".equalsIgnoreCase(name)) { //$NON-NLS-1$
CallArgumentsList args = callExpression.getArgs();
if (args != null && args.getChilds() != null && !args.getChilds().isEmpty()) {
ASTNode argument = (ASTNode) args.getChilds().get(0);
if (argument instanceof Scalar) {
String constant = ASTUtils.stripQuotes(((Scalar) argument).getValue());
FieldDeclaration fieldDeclaration = new FieldDeclaration(constant, argument.sourceStart(),
argument.sourceEnd(), callExpression.sourceStart(), callExpression.sourceEnd());
fieldDeclaration.setModifier(Modifiers.AccGlobal | Modifiers.AccConstant | Modifiers.AccFinal);
return fieldDeclaration;
}
}
}
return null;
}
/**
* Finds USE statement by the alias name
*
* @param moduleDeclaration
* The AST root node
* @param aliasName
* The alias name.
* @param offset
* Current position in the file (this is needed since we don't
* want to take USE statements placed below current position into
* account)
* @return USE statement part node, or <code>null</code> in case relevant
* statement couldn't be found
*/
public static UsePart findUseStatementByAlias(ModuleDeclaration moduleDeclaration, String aliasName, int offset) {
FindUseStatementByAliasASTVisitor visitor = new FindUseStatementByAliasASTVisitor(aliasName, offset);
try {
moduleDeclaration.traverse(visitor);
} catch (Exception e) {
Logger.logException(e);
}
return visitor.getResult();
}
/**
* Finds USE statement according to the given namespace name
*
* @param moduleDeclaration
* The AST root node
* @param namespace
* Namespace name
* @param offset
* Current position in the file (this is needed since we don't
* want to take USE statements placed below current position into
* account)
* @return USE statement part node, or <code>null</code> in case relevant
* statement couldn't be found
*/
public static UsePart findUseStatementByNamespace(ModuleDeclaration moduleDeclaration, final String namespace,
final int offset) {
FindUseStatementByNamespaceASTVisitor visitor = new FindUseStatementByNamespaceASTVisitor(namespace, offset);
try {
moduleDeclaration.traverse(visitor);
} catch (Exception e) {
Logger.logException(e);
}
return visitor.getResult();
}
/**
* Returns all USE statements declared before the specified offset
*
* @param moduleDeclaration
* The AST root node
* @param offset
* Current position in the file (this is needed since we don't
* want to take USE statements placed below current position into
* account)
* @return USE statements list
*/
public static UseStatement[] getUseStatements(ModuleDeclaration moduleDeclaration, final int offset) {
GetUseStatementsASTVisitor visitor = new GetUseStatementsASTVisitor(offset);
try {
moduleDeclaration.traverse(visitor);
} catch (Exception e) {
Logger.logException(e);
}
return visitor.getResult();
}
}