/*******************************************************************************
* Copyright (c) 2009, 2016, 2017 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
* Yannick de Lange <yannickl88@gmail.com>
*******************************************************************************/
package org.eclipse.php.internal.core.corext.util;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.dltk.ast.ASTNode;
import org.eclipse.dltk.ast.declarations.ModuleDeclaration;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.Position;
import org.eclipse.php.core.compiler.ast.nodes.*;
import org.eclipse.php.core.compiler.ast.visitor.PHPASTVisitor;
import org.eclipse.php.internal.core.compiler.ast.parser.ASTUtils;
/**
* @author Yannick de Lange <yannickl88@gmail.com>
*/
public class DocumentUtils {
private static class NamespaceFinder extends PHPASTVisitor {
Vector<NamespaceDeclaration> declarations = new Vector<NamespaceDeclaration>();
public boolean visit(NamespaceDeclaration n) throws Exception {
declarations.add(n);
return super.visit(n);
}
public NamespaceDeclaration getNamespaceDeclarationFor(UseStatement statement) {
for (NamespaceDeclaration n : declarations) {
if (n.sourceStart() <= statement.sourceStart() && n.sourceEnd() >= statement.sourceEnd()) {
return n;
}
}
return null;
}
}
private static class ReplaceAction {
public List<UseStatement> statements;
public int start, end;
public String indent;
public ReplaceAction(List<UseStatement> statements, int start, int end, String indent) {
this.statements = statements;
this.start = start;
this.end = end;
this.indent = indent;
}
}
public static List<UseStatement> flatten(List<UseStatement> statements) {
Vector<UseStatement> total = new Vector<UseStatement>();
for (UseStatement statement : statements) {
for (UsePart part : statement.getParts()) {
Vector<UsePart> parts = new Vector<UsePart>();
parts.add(part);
total.add(new UseStatement(statement.start(), statement.end(), parts, statement.getStatementType()));
}
}
return total;
}
/**
* Check if a classname is used in a string. The "excludePositions" list
* contains positions where we should NOT look if the classname is used
* (typically in comment sections). It is important that the
* "excludePositions" list is sorted by increasing position, or this method
* won't work correctly.
*/
public static boolean containsUseStatement(UsePart part, String contents, List<Position> excludePositions) {
String className = null != part.getAlias() ? part.getAlias().toString()
: (part.getNamespace() != null ? part.getNamespace().toString() : "");
Pattern p = Pattern.compile("(?i)\\b(" + Pattern.quote(className) + ")\\b");
Matcher m = p.matcher(contents);
int restartPos = 0;
while (m.find()) {
// No exclusion list, so we're good
if (excludePositions.isEmpty()) {
return true;
}
for (int i = restartPos; i < excludePositions.size(); i++) {
Position position = excludePositions.get(i);
// If current exclusion is before current match,
// we have to continue our checks
if (position.getOffset() + position.getLength() <= m.start()) {
// No further exclusions, we are good, current match is
// pertinent
if (i == excludePositions.size() - 1) {
return true;
}
restartPos++;
continue;
}
// Current exclusion is after current match, so we're also good
if (position.getOffset() >= m.end()) {
return true;
}
// Here we know that current exclusion overlaps current match,
// so current match is non-pertinent, we have to continue with
// next match.
// Assert that current exclusion FULLY overlaps current match.
assert position.getOffset() <= m.start() && m.end() <= position.getOffset() + position.getLength();
break;
}
}
// No pertinent match was found
return false;
}
/**
* Returns the positions of a list of ASTNodes. It is important that the
* returned list is sorted by increasing position, or the other
* DocumentUtils methods (using this list) won't work correctly.
*
* @param nodes
* Node positions to exclude
* @return sorted positions to exclude
*/
public static List<Position> getExcludeSortedAndFilteredPositions(ASTNode[] nodes) {
List<Position> excludePositions = new ArrayList<Position>();
for (ASTNode n : nodes) {
if (n instanceof PHPDocBlock) {
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=490434
// Do not handle PHPDoc comments for now
continue;
}
excludePositions.add(new Position(n.sourceStart(), n.sourceEnd() - n.sourceStart()));
}
return excludePositions;
}
/**
* Remove all the given use statements from a document. Be warned that the
* "excludePositions" list will be updated to match the content of the
* returned string. It is important that this list is sorted by increasing
* position and that the statement positions don't intersect with any of the
* exclude positions, or this method won't work correctly.
*/
public static String stripUseStatements(UseStatement[] statements, IDocument old_doc,
List<Position> excludePositions) {
return stripUseStatements(statements, old_doc, 0, old_doc.getLength(), excludePositions);
}
/**
* Remove all the given use statements from a document. Be warned that the
* "excludePositions" list will be updated to match the content of the
* returned string. It is important that this list is sorted by increasing
* position and that the statement positions don't intersect with any of the
* exclude positions, or this method won't work correctly.
*/
public static String stripUseStatements(UseStatement[] statements, IDocument old_doc, int start, int end,
List<Position> excludePositions) {
int removedLength = 0;
IDocument doc = new Document(old_doc.get());
for (UseStatement statement : statements) {
if (statement.sourceStart() < start || statement.sourceEnd() > end) {
continue;
}
int length = statement.sourceEnd() - statement.sourceStart();
try {
doc.replace(statement.sourceStart() - removedLength, length, "");
} catch (BadLocationException e) {
}
for (int i = excludePositions.size() - 1; i >= 0; i--) {
Position position = excludePositions.get(i);
int offset = position.getOffset() + removedLength;
if (offset >= statement.sourceEnd()) {
position.setOffset(position.getOffset() - length);
} else if (offset + position.getLength() > statement.sourceStart()) {
// statement positions should never intersect with exclude
// positions, so (for safety) remove them from the exclusion
// list
excludePositions.remove(i);
} else {
break;
}
}
removedLength += length;
}
try {
return doc.get(start, end - start - removedLength);
} catch (BadLocationException e) {
return doc.get();
}
}
/**
* Create a string from the given UseStatements
*/
public static String createStringFromUseStatement(List<UseStatement> statements, String indent) {
StringBuilder total = new StringBuilder();
for (UseStatement statement : statements) {
total.append(createStringFromUseStatement(statement, indent));
}
return total.toString().trim();
}
public static String createStringFromUseStatement(UseStatement statement) {
return createStringFromUseStatement(statement, "");
}
public static String createStringFromUseStatement(UseStatement statement, boolean addIndentedUsePrefixAndNewline) {
return createStringFromUseStatement(statement, "", addIndentedUsePrefixAndNewline);
}
public static String createStringFromUseStatement(UseStatement statement, String indent) {
return createStringFromUseStatement(statement, indent, true);
}
/**
* Create a string from the given UseStatement
*/
public static String createStringFromUseStatement(UseStatement statement, String indent,
boolean addIndentedUsePrefixAndNewline) {
String use = "";
if (addIndentedUsePrefixAndNewline) {
use = indent + "use ";
switch (statement.getStatementType()) {
case UseStatement.T_NONE:
break;
case UseStatement.T_FUNCTION:
use += "function ";
break;
case UseStatement.T_CONST:
use += "const ";
break;
}
}
boolean first = true;
for (UsePart part : statement.getParts()) {
if (!first) {
use += ", ";
}
use += part.getNamespace().getFullyQualifiedName();
if (part.getAlias() != null) {
use += " as " + part.getAlias().getName();
}
first = false;
}
if (addIndentedUsePrefixAndNewline) {
return use + ";\n";
}
return use;
}
/**
* Filter and sort a list of Use statements from a document
*/
public static List<UseStatement> filterAndSort(UseStatement[] statements, IDocument doc,
ModuleDeclaration moduleDeclaration) {
Vector<UseStatement> total = new Vector<UseStatement>();
NamespaceFinder visitor = new NamespaceFinder();
try {
moduleDeclaration.traverse(visitor);
} catch (Exception e1) {
}
for (UseStatement statement : statements) {
List<Position> excludePositions;
if (moduleDeclaration instanceof PHPModuleDeclaration) {
excludePositions = getExcludeSortedAndFilteredPositions(
((PHPModuleDeclaration) moduleDeclaration).getCommentList().toArray(new ASTNode[0]));
} else {
excludePositions = new ArrayList<Position>();
}
String contents;
NamespaceDeclaration currentNamespace = visitor.getNamespaceDeclarationFor(statement);
if (currentNamespace != null && currentNamespace.isBracketed()) {
contents = stripUseStatements(statements, doc, currentNamespace.sourceStart(),
currentNamespace.sourceEnd(), excludePositions);
} else {
contents = stripUseStatements(statements, doc, excludePositions);
}
Vector<UsePart> parts = new Vector<UsePart>();
for (UsePart part : statement.getParts()) {
if (containsUseStatement(part, contents, excludePositions)) {
parts.add(part);
}
}
if (parts.size() > 0) {
total.add(new UseStatement(statement.start(), statement.end(), parts, statement.getStatementType()));
}
}
// sort and remove duplicate UseStatements
Set<UseStatement> set = new TreeSet<>(new Comparator<UseStatement>() {
@Override
public int compare(UseStatement a, UseStatement b) {
if (a == b) {
return 0;
}
if (a.getStatementType() != b.getStatementType()) {
return a.getStatementType() - b.getStatementType();
}
String partA = createStringFromUseStatement(a, false).toLowerCase();
String partB = createStringFromUseStatement(b, false).toLowerCase();
String[] partsA = partA.split("\\\\");
String[] partsB = partB.split("\\\\");
int checkLength = Math.min(partsA.length, partsB.length);
for (int i = 0; i < checkLength; i++) {
int comp = partsA[i].compareTo(partsB[i]);
if (comp != 0) {
return comp;
}
}
return partsA.length - partsB.length;
}
});
set.addAll(total);
return new ArrayList<UseStatement>(set);
}
/**
* Sort the blocks of use statements from a document
*/
public static void sortUseStatements(ModuleDeclaration moduleDeclaration, IDocument doc) {
UseStatement[] statements = ASTUtils.getUseStatements(moduleDeclaration, doc.getLength());
int start = 0;
Vector<ReplaceAction> queue = new Vector<ReplaceAction>();
while (start < statements.length) {
int last_item = statements.length - 1;
for (int i = start; i < statements.length - 1; i++) {
try {
if (doc.getLineOfOffset(
statements[i + 1].sourceStart()) > doc.getLineOfOffset(statements[i].sourceStart()) + 1) {
last_item = i;
break;
}
} catch (BadLocationException e) {
last_item = i;
break;
}
}
String indent = "";
try {
int lineOffset = doc.getLineOffset(doc.getLineOfOffset(statements[start].sourceStart()));
indent = doc.get(lineOffset, statements[start].sourceStart() - lineOffset);
} catch (BadLocationException e1) {
e1.printStackTrace();
}
queue.add(new ReplaceAction(
filterAndSort(Arrays.copyOfRange(statements, start, last_item + 1), doc, moduleDeclaration),
statements[start].sourceStart(), statements[last_item].sourceEnd(), indent));
start = last_item + 1; // start at the next one
}
int offset = 0;
for (ReplaceAction item : queue) {
List<UseStatement> sorted = item.statements;
int length = item.end - item.start;
String newNamespaces = createStringFromUseStatement(sorted, item.indent);
try {
doc.replace(item.start - offset, length, newNamespaces);
} catch (BadLocationException e) {
}
offset += length - newNamespaces.length();
}
}
}