/*******************************************************************************
* Copyright (c) 2017 Alex Xu 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:
* Alex Xu - initial API and implementation
*******************************************************************************/
package org.eclipse.php.internal.core.ast.rewrite;
import java.util.*;
import java.util.Map.Entry;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.dltk.compiler.CharOperation;
import org.eclipse.dltk.core.ISourceModule;
import org.eclipse.php.core.ast.nodes.*;
import org.eclipse.php.core.compiler.ast.nodes.NamespaceReference;
import org.eclipse.php.internal.core.PHPCorePlugin;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.TextEdit;
/**
* The {@link ImportRewrite} helps updating imports following a import order and
* on-demand imports threshold as configured by a project.
* <p>
* The import rewrite is created on a compilation unit and collects references
* to types that are added or removed. When adding imports, e.g. using
* {@link #addImport(String)}, the import rewrite evaluates if the type can be
* imported and returns the a reference to the type that can be used in code.
* This reference is either unqualified if the import could be added, or fully
* qualified if the import failed due to a conflict with another element of the
* same name.
* </p>
* <p>
* On {@link #rewriteImports(IProgressMonitor)} the rewrite translates these
* descriptions into text edits that can then be applied to the original source.
* The rewrite infrastructure tries to generate minimal text changes and only
* works on the import statements. It is possible to combine the result of an
* import rewrite with the result of a
* {@link org.eclipse.jdt.core.dom.rewrite.ASTRewrite} as long as no import
* statements are modified by the AST rewrite.
* </p>
* <p>
* The options controlling the import order and on-demand thresholds are:
* <ul>
* <li>{@link #setImportOrder(String[])} specifies the import groups and their
* preferred order</li>
* <li>{@link #setOnDemandImportThreshold(int)} specifies the number of imports
* in a group needed for a on-demand import statement (star import)</li>
* <li>{@link #setStaticOnDemandImportThreshold(int)} specifies the number of
* static imports in a group needed for a on-demand import statement (star
* import)</li>
* </ul>
* This class is not intended to be subclassed.
* </p>
*
* @since 5.0
*/
public final class ImportRewrite {
/**
* A {@link ImportRewrite.ImportRewriteContext} can optionally be used in
* e.g.
* {@link ImportRewrite#addImport(String, ImportRewrite.ImportRewriteContext)}
* to give more information about the types visible in the scope. These
* types can be for example inherited inner types where it is unnecessary to
* add import statements for.
*
* </p>
* <p>
* This class can be implemented by clients.
* </p>
*/
public static abstract class ImportRewriteContext {
/**
* Result constant signaling that the given element is know in the
* context.
*/
public final static int RES_NAME_FOUND = 1;
/**
* Result constant signaling that the given element is not know in the
* context.
*/
public final static int RES_NAME_UNKNOWN = 2;
/**
* Result constant signaling that the given element is conflicting with
* an other element in the context.
*/
public final static int RES_NAME_CONFLICT = 3;
/**
* Kind constant specifying that the element is a type import.
*/
public final static int KIND_TYPE = 1;
/**
* Kind constant specifying that the element is a static constant
* import.
*/
public final static int KIND_STATIC_CONSTANT = 2;
/**
* Kind constant specifying that the element is a static method import.
*/
public final static int KIND_STATIC_METHOD = 3;
/**
* Searches for the given element in the context and reports if the
* element is known ({@link #RES_NAME_FOUND}), unknown (
* {@link #RES_NAME_UNKNOWN}) or if its name conflicts (
* {@link #RES_NAME_CONFLICT}) with an other element.
*
* @param qualifier
* The qualifier of the element, can be package or the
* qualified name of a type
* @param name
* The simple name of the element; either a type, method or
* field name or * for on-demand imports.
* @param kind
* The kind of the element. Can be either {@link #KIND_TYPE},
* {@link #KIND_STATIC_CONSTANT} or
* {@link #KIND_STATIC_METHOD}. Implementors should be
* prepared for new, currently unspecified kinds and return
* {@link #RES_NAME_UNKNOWN} by default.
* @return Returns the result of the lookup. Can be either
* {@link #RES_NAME_FOUND}, {@link #RES_NAME_UNKNOWN} or
* {@link #RES_NAME_CONFLICT}.
*/
public abstract int findInContext(NamespaceDeclaration namespace, String qualifier, String name, int kind);
}
public static final String ENCLOSING_TYPE_SEPARATOR = NamespaceReference.NAMESPACE_DELIMITER;
private static final char STATIC_PREFIX = 's';
private static final char NORMAL_PREFIX = 'n';
private static final char ALIAS_PREFIX = 'a';
private final ImportRewriteContext defaultContext;
private final ISourceModule compilationUnit;
private final Program astRoot;
private final Map<NamespaceDeclaration, Boolean> restoreExistingImports = new HashMap<>();
private final Map<NamespaceDeclaration, List<String>> existingImports;
private String[] importOrder;
private Map<NamespaceDeclaration, List<String>> addedImports;
private Map<NamespaceDeclaration, List<String>> removedImports;
private String[] createdImports;
private boolean filterImplicitImports;
/**
* Creates a {@link ImportRewrite} from a an AST ({@link Program}). The AST
* has to be created from a {@link ISourceModule}, that means
* {@link ASTParser#setSource(ISourceModule)} has been used when creating
* the AST. If <code>restoreExistingImports</code> is <code>true</code>, all
* existing imports are kept, and new imports will be inserted at best
* matching locations. If <code>restoreExistingImports</code> is
* <code>false</code>, the existing imports will be removed and only the
* newly added imports will be created.
* <p>
* Note that this method is more efficient than using
* {@link #create(ISourceModule, boolean)} if an AST is already available.
* </p>
*
* @param astRoot
* the AST root node to create the imports for
* @param restoreExistingImports
* specifies if the existing imports should be kept or removed.
* @return the created import rewriter.
* @throws IllegalArgumentException
* thrown when the passed AST is null or was not created from a
* compilation unit.
*/
public static ImportRewrite create(Program astRoot, boolean restoreExistingImports) {
if (astRoot == null) {
throw new IllegalArgumentException("AST must not be null"); //$NON-NLS-1$
}
ISourceModule typeRoot = astRoot.getSourceModule();
Map<NamespaceDeclaration, List<String>> existingImport = null;
if (restoreExistingImports) {
existingImport = new HashMap<NamespaceDeclaration, List<String>>();
Iterator<Entry<NamespaceDeclaration, List<UseStatement>>> ite = astRoot.getUseStatements().entrySet()
.iterator();
while (ite.hasNext()) {
Entry<NamespaceDeclaration, List<UseStatement>> entry = ite.next();
List<String> imports = new ArrayList<>();
for (UseStatement useStatement : entry.getValue()) {
for (UseStatementPart part : useStatement.parts()) {
StringBuilder buf = new StringBuilder();
if (part.getAlias() != null) {
buf.append(ALIAS_PREFIX);
buf.append(part.getAlias().getName());
} else {
buf.append(NORMAL_PREFIX);
buf.append(part.getName().getName());
}
imports.add(buf.toString());
}
}
existingImport.put(entry.getKey(), imports);
}
}
return new ImportRewrite((ISourceModule) typeRoot, astRoot, existingImport);
}
private ImportRewrite(ISourceModule cu, Program astRoot, Map<NamespaceDeclaration, List<String>> existingImports) {
this.compilationUnit = cu;
this.astRoot = astRoot; // might be null
List<NamespaceDeclaration> namespaces = astRoot.getNamespaceDeclarations();
if (existingImports != null) {
this.existingImports = existingImports;
if (namespaces.size() > 0) {
for (NamespaceDeclaration namespace : namespaces) {
this.restoreExistingImports.put(namespace, !existingImports.get(namespace).isEmpty());
}
} else {
this.restoreExistingImports.put(null, !existingImports.get(null).isEmpty());
}
} else {
this.existingImports = new HashMap<NamespaceDeclaration, List<String>>();
if (namespaces.size() > 0) {
for (NamespaceDeclaration namespace : namespaces) {
this.restoreExistingImports.put(namespace, false);
this.existingImports.put(namespace, new ArrayList<>());
}
} else {
this.restoreExistingImports.put(null, false);
this.existingImports.put(null, new ArrayList<>());
}
}
this.filterImplicitImports = true;
this.defaultContext = new ImportRewriteContext() {
public int findInContext(NamespaceDeclaration namespace, String qualifier, String name, int kind) {
return findInImports(namespace, qualifier, name, kind);
}
};
this.addedImports = null; // Initialized on use
this.removedImports = null; // Initialized on use
this.createdImports = null;
this.importOrder = CharOperation.NO_STRINGS;
}
/**
* Defines the import groups and order to be used by the
* {@link ImportRewrite}. Imports are added to the group matching their
* qualified name most. The empty group name groups all imports not matching
* any other group. Static imports are managed in separate groups. Static
* import group names are prefixed with a '#' character.
*
* @param order
* A list of strings defining the import groups. A group name
* must be a valid package name or empty. If can be prefixed by
* the '#' character for static import groups
*/
public void setImportOrder(String[] order) {
if (order == null)
throw new IllegalArgumentException("Order must not be null"); //$NON-NLS-1$
this.importOrder = order;
}
/**
* The compilation unit for which this import rewrite was created for.
*
* @return the compilation unit for which this import rewrite was created
* for.
*/
public ISourceModule getSourceModule() {
return this.compilationUnit;
}
public Program getProgram() {
return this.astRoot;
}
/**
* Returns the default rewrite context that only knows about the imported
* types. Clients can write their own context and use the default context
* for the default behavior.
*
* @return the default import rewrite context.
*/
public ImportRewriteContext getDefaultImportRewriteContext() {
return this.defaultContext;
}
/**
* Specifies that implicit imports (types in default package, package
* <code>java.lang</code> or in the same package as the rewrite compilation
* unit should not be created except if necessary to resolve an on-demand
* import conflict. The filter is enabled by default.
*
* @param filterImplicitImports
* if set, implicit imports will be filtered.
*/
public void setFilterImplicitImports(boolean filterImplicitImports) {
this.filterImplicitImports = filterImplicitImports;
}
private static int compareImport(char prefix, String qualifier, String name, String curr) {
if (curr.charAt(0) == ALIAS_PREFIX && curr.endsWith(name)) {
return ImportRewriteContext.RES_NAME_CONFLICT;
}
if (curr.charAt(0) != prefix || !curr.endsWith(name)) {
return ImportRewriteContext.RES_NAME_UNKNOWN;
}
curr = curr.substring(1); // remove the prefix
if (curr.length() == name.length()) {
if (qualifier.length() == 0) {
return ImportRewriteContext.RES_NAME_FOUND;
}
return ImportRewriteContext.RES_NAME_CONFLICT;
}
// at this place: curr.length > name.length
int dotPos = curr.length() - name.length() - 1;
if (curr.charAt(dotPos) != NamespaceReference.NAMESPACE_SEPARATOR) {
return ImportRewriteContext.RES_NAME_UNKNOWN;
}
if (qualifier.length() != dotPos || !curr.startsWith(qualifier)) {
return ImportRewriteContext.RES_NAME_CONFLICT;
}
return ImportRewriteContext.RES_NAME_FOUND;
}
/**
* Not API, package visibility as accessed from an anonymous type
*/
/* package */final int findInImports(NamespaceDeclaration namespace, String qualifier, String name, int kind) {
boolean allowAmbiguity = (kind == ImportRewriteContext.KIND_STATIC_METHOD)
|| (name.length() == 1 && name.charAt(0) == '*');
if (this.existingImports.get(namespace) == null) {
this.existingImports.put(namespace, new ArrayList<String>());
}
List<String> imports = this.existingImports.get(namespace);
char prefix = (kind == ImportRewriteContext.KIND_TYPE) ? NORMAL_PREFIX : STATIC_PREFIX;
for (int i = imports.size() - 1; i >= 0; i--) {
String curr = imports.get(i);
int res = compareImport(prefix, qualifier, name, curr);
if (res != ImportRewriteContext.RES_NAME_UNKNOWN) {
if (!allowAmbiguity || res == ImportRewriteContext.RES_NAME_FOUND) {
return res;
}
}
}
return ImportRewriteContext.RES_NAME_UNKNOWN;
}
/**
* Adds a new import to the rewriter's record and returns a type reference
* that can be used in the code. The type binding can only be an array or
* non-generic type.
* <p>
* No imports are added for types that are already known. If a import for a
* type is recorded to be removed, this record is discarded instead.
* </p>
* <p>
* The content of the compilation unit itself is actually not modified in
* any way by this method; rather, the rewriter just records that a new
* import has been added.
* </p>
*
* @param qualifiedTypeName
* the qualified type name of the type to be added
* @param context
* an optional context that knows about types visible in the
* current scope or <code>null</code> to use the default context
* only using the available imports.
* @return returns a type to which the type binding can be assigned to. The
* returned type contains is unqualified when an import could be
* added or was already known. It is fully qualified, if an import
* conflict prevented the import.
*/
public String addImport(NamespaceDeclaration namespace, String qualifiedTypeName, ImportRewriteContext context) {
return internalAddImport(namespace, qualifiedTypeName, null, context);
}
public String addImport(NamespaceDeclaration namespace, String qualifiedTypeName, String alias,
ImportRewriteContext context) {
return internalAddImport(namespace, qualifiedTypeName, alias, context);
}
/**
* Adds a new import to the rewriter's record and returns a type reference
* that can be used in the code. The type binding can only be an array or
* non-generic type.
* <p>
* No imports are added for types that are already known. If a import for a
* type is recorded to be removed, this record is discarded instead.
* </p>
* <p>
* The content of the compilation unit itself is actually not modified in
* any way by this method; rather, the rewriter just records that a new
* import has been added.
* </p>
*
* @param qualifiedTypeName
* the qualified type name of the type to be added
* @return returns a type to which the type binding can be assigned to. The
* returned type contains is unqualified when an import could be
* added or was already known. It is fully qualified, if an import
* conflict prevented the import.
*/
public String addImport(NamespaceDeclaration namespace, String qualifiedTypeName) {
return addImport(namespace, qualifiedTypeName, this.defaultContext);
}
public String addImport(NamespaceDeclaration namespace, String qualifiedTypeName, String alias) {
return addImport(namespace, qualifiedTypeName, alias, this.defaultContext);
}
private String internalAddImport(NamespaceDeclaration namespace, String fullTypeName, String alias,
ImportRewriteContext context) {
int idx = fullTypeName.lastIndexOf(NamespaceReference.NAMESPACE_SEPARATOR);
String typeContainerName, typeName;
if (idx != -1) {
typeContainerName = fullTypeName.substring(0, idx);
typeName = fullTypeName.substring(idx + 1);
} else {
typeContainerName = ""; //$NON-NLS-1$
typeName = fullTypeName;
}
if (context == null)
context = this.defaultContext;
if (alias != null) {
typeName = alias;
fullTypeName += " as " + alias; //$NON-NLS-1$
}
int res = context.findInContext(namespace, typeContainerName, typeName, ImportRewriteContext.KIND_TYPE);
if (res == ImportRewriteContext.RES_NAME_CONFLICT) {
if (alias != null) {
return alias;
}
if (fullTypeName.charAt(0) != NamespaceReference.NAMESPACE_SEPARATOR) {
fullTypeName = NamespaceReference.NAMESPACE_SEPARATOR + fullTypeName;
}
return fullTypeName;
}
if (res == ImportRewriteContext.RES_NAME_UNKNOWN) {
addEntry(namespace, NORMAL_PREFIX + fullTypeName);
}
return typeName;
}
private void addEntry(NamespaceDeclaration namespace, String entry) {
this.existingImports.get(namespace).add(entry);
if (this.removedImports != null && this.removedImports.get(namespace) != null) {
if (this.removedImports.get(namespace).remove(entry)) {
return;
}
}
if (this.addedImports == null) {
this.addedImports = new HashMap<NamespaceDeclaration, List<String>>();
}
if (this.addedImports.get(namespace) == null) {
this.addedImports.put(namespace, new ArrayList<>());
}
this.addedImports.get(namespace).add(entry);
}
private boolean removeEntry(NamespaceDeclaration namespace, String entry) {
if (this.existingImports.get(namespace).remove(entry)) {
if (this.addedImports != null && this.addedImports.get(namespace) != null) {
if (this.addedImports.get(namespace).remove(entry)) {
return true;
}
}
if (this.removedImports == null) {
this.removedImports = new HashMap<NamespaceDeclaration, List<String>>();
}
if (this.removedImports.get(namespace) == null) {
this.removedImports.put(namespace, new ArrayList<>());
}
this.removedImports.get(namespace).add(entry);
return true;
}
return false;
}
/**
* Records to remove a import. No remove is recorded if no such import
* exists or if such an import is recorded to be added. In that case the
* record of the addition is discarded.
* <p>
* The content of the compilation unit itself is actually not modified in
* any way by this method; rather, the rewriter just records that an import
* has been removed.
* </p>
*
* @param qualifiedName
* The import name to remove.
* @return <code>true</code> is returned of an import of the given name
* could be found.
*/
public boolean removeImport(NamespaceDeclaration namespace, String qualifiedName) {
return removeEntry(namespace, NORMAL_PREFIX + qualifiedName);
}
/**
* Converts all modifications recorded by this rewriter into an object
* representing the corresponding text edits to the source code of the
* rewrite's compilation unit. The compilation unit itself is not modified.
* <p>
* Calling this methods does not discard the modifications on record.
* Subsequence modifications are added to the ones already on record. If
* this method is called again later, the resulting text edit object will
* accurately reflect the net cumulative affect of all those changes.
* </p>
*
* @param monitor
* the progress monitor or <code>null</code>
* @return text edit object describing the changes to the document
* corresponding to the changes recorded by this rewriter
* @throws CoreException
* the exception is thrown if the rewrite fails.
*/
public final TextEdit rewriteImports(IProgressMonitor monitor) throws CoreException {
if (monitor == null) {
monitor = new NullProgressMonitor();
}
try {
monitor.beginTask("Updating uses", 2); //$NON-NLS-1$
if (!hasRecordedChanges()) {
this.createdImports = CharOperation.NO_STRINGS;
return new MultiTextEdit();
}
Program usedAstRoot = this.astRoot;
if (usedAstRoot == null) {
ASTParser parser = ASTParser.newParser(this.compilationUnit);
usedAstRoot = parser.createAST(SubMonitor.convert(monitor, 1));
}
ImportRewriteAnalyzer computer = new ImportRewriteAnalyzer(this.compilationUnit, usedAstRoot,
this.importOrder, this.restoreExistingImports);
computer.setFilterImplicitImports(this.filterImplicitImports);
List<NamespaceDeclaration> namespaces = usedAstRoot.getNamespaceDeclarations();
if (namespaces.size() > 0) {
for (NamespaceDeclaration namespace : namespaces) {
computeImports(computer, namespace);
}
} else {
computeImports(computer, null);
}
TextEdit result = computer.getResultingEdits(SubMonitor.convert(monitor, 1));
this.createdImports = computer.getCreatedImports();
return result;
} catch (Exception e) {
PHPCorePlugin.log(e);
} finally {
monitor.done();
}
return null;
}
private void computeImports(ImportRewriteAnalyzer computer, NamespaceDeclaration namespace) {
if (this.addedImports != null && this.addedImports.get(namespace) != null) {
for (int i = 0; i < this.addedImports.get(namespace).size(); i++) {
String curr = this.addedImports.get(namespace).get(i);
computer.addImport(namespace, curr.substring(1), STATIC_PREFIX == curr.charAt(0));
}
}
if (this.removedImports != null && this.removedImports.get(namespace) != null) {
for (int i = 0; i < this.removedImports.get(namespace).size(); i++) {
String curr = this.removedImports.get(namespace).get(i);
computer.removeImport(namespace, curr.substring(1));
}
}
}
/**
* Returns all new non-static imports created by the last invocation of
* {@link #rewriteImports(IProgressMonitor)} or <code>null</code> if these
* methods have not been called yet.
* <p>
* Note that this list doesn't need to be the same as the added imports (see
* {@link #getAddedImports()}) as implicit imports are not created and some
* imports are represented by on-demand imports instead.
* </p>
*
* @return the created imports
*/
public String[] getCreatedImports() {
return this.createdImports;
}
/**
* Returns all non-static imports that are recorded to be added.
*
* @return the imports recorded to be added.
*/
public String[] getAddedImports() {
return filterFromList(this.addedImports, NORMAL_PREFIX);
}
/**
* Returns all static imports that are recorded to be added.
*
* @return the static imports recorded to be added.
*/
public String[] getAddedStaticImports() {
return filterFromList(this.addedImports, STATIC_PREFIX);
}
/**
* Returns all non-static imports that are recorded to be removed.
*
* @return the imports recorded to be removed.
*/
public String[] getRemovedImports() {
return filterFromList(this.removedImports, NORMAL_PREFIX);
}
/**
* Returns all static imports that are recorded to be removed.
*
* @return the static imports recorded to be removed.
*/
public String[] getRemovedStaticImports() {
return filterFromList(this.removedImports, STATIC_PREFIX);
}
/**
* Returns <code>true</code> if imports have been recorded to be added or
* removed.
*
* @return boolean returns if any changes to imports have been recorded.
*/
public boolean hasRecordedChanges() {
boolean hasRecordedChanges = false;
List<NamespaceDeclaration> namespaces = astRoot.getNamespaceDeclarations();
if (namespaces.size() > 0) {
for (NamespaceDeclaration namespace : namespaces) {
hasRecordedChanges = hasRecordedChanges(namespace);
if (hasRecordedChanges)
return hasRecordedChanges;
}
return hasRecordedChanges;
}
return hasRecordedChanges(null);
}
private boolean hasRecordedChanges(NamespaceDeclaration namespace) {
return !this.restoreExistingImports.get(namespace)
|| (this.addedImports != null && this.addedImports.get(namespace) != null
&& !this.addedImports.get(namespace).isEmpty())
|| (this.removedImports != null && this.removedImports.get(namespace) != null
&& !this.removedImports.get(namespace).isEmpty());
}
private static String[] filterFromList(Map<NamespaceDeclaration, List<String>> imports, char prefix) {
if (imports == null) {
return CharOperation.NO_STRINGS;
}
ArrayList<String> res = new ArrayList<String>();
for (List<String> strings : imports.values()) {
for (int i = 0; i < strings.size(); i++) {
String curr = strings.get(i);
if (prefix == curr.charAt(0)) {
res.add(curr.substring(1));
}
}
}
return res.toArray(new String[res.size()]);
}
}