/*
* Copyright 2012 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.errorprone.apply;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.errorprone.ErrorProneEndPosMap;
import com.google.errorprone.JDKCompatible;
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCImport;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Represents a list of import statements. Supports adding and removing
* import statements and pretty printing the result as source code. Correctly
* sorts the imports according to Google Java Style Guide rules.
*
* @author eaftan@google.com (Eddie Aftandilian)
*/
public class ImportStatements {
private int startPos = Integer.MAX_VALUE;
private int endPos = -1;
private final Set<String> importStrings;
private boolean hasExistingImports;
/**
* An Ordering that sorts import statements based on the Google Java Style
* Guide.
*/
private static final Ordering<String> IMPORT_ORDERING = new Ordering<String>() {
@Override
public int compare(String s1, String s2) {
return ComparisonChain.start()
.compare(Kind.getKind(s1), Kind.getKind(s2))
.compare(s1, s2)
.result();
}
};
/**
* A regex to use for finding the top-level package in an import
* statement.
*/
private static final Pattern TOPLEVEL_PATTERN =
Pattern.compile("import\\s+(static\\s+|)([^.]+).");
public static ImportStatements create(JCCompilationUnit compilationUnit) {
return new ImportStatements(compilationUnit.getPackageName(),
compilationUnit.getImports(), JDKCompatible.getEndPosMap(compilationUnit));
}
public ImportStatements(JCExpression packageTree, List<JCImport> importTrees,
ErrorProneEndPosMap endPosMap) {
// find start, end positions for current list of imports (for replacement)
if (importTrees.isEmpty()) {
// start/end positions are just after the package expression
hasExistingImports = false;
startPos = endPosMap.getEndPosition(packageTree) + 2; // +2 for semicolon and newline
endPos = startPos;
} else {
// process list of imports and find start/end positions
hasExistingImports = true;
for (JCImport importTree : importTrees) {
int currStartPos = importTree.getStartPosition();
int currEndPos = endPosMap.getEndPosition(importTree);
startPos = Math.min(startPos, currStartPos);
endPos = Math.max(endPos, currEndPos);
}
}
// sanity check for start/end positions
Preconditions.checkState(startPos <= endPos);
// convert list of JCImports to list of strings
importStrings = new TreeSet<>(IMPORT_ORDERING);
importStrings.addAll(Lists.transform(importTrees, new Function<JCImport, String>() {
@Override
public String apply(JCImport input) {
String importExpr = input.toString();
return importExpr.substring(0, importExpr.length() - 2); // snip trailing ";\n"
}
}));
}
/**
* Return the start position of the import statements.
*/
public int getStartPos() {
return startPos;
}
/**
* Return the end position of the import statements.
*/
public int getEndPos() {
return endPos;
}
/**
* Add an import to the list of imports. If the import is already in the
* list, does nothing. The import should be of the form "import foo.bar".
*
* @param importToAdd a string representation of the import to add
* @return true if the import was added
*/
public boolean add(String importToAdd) {
return importStrings.add(importToAdd);
}
/**
* Add all imports in a collection to this list of imports. Does not add
* any imports that are already in the list.
*
* @param importsToAdd a collection of imports to add
* @return true if any imports were added to the list
*/
public boolean addAll(Collection<String> importsToAdd) {
return importStrings.addAll(importsToAdd);
}
/**
* Remove an import from the list of imports. If the import is not in the
* list, does nothing. The import should be of the form "import foo.bar".
*
* @param importToRemove a string representation of the import to remove
* @return true if the import was removed
*/
public boolean remove(String importToRemove) {
return importStrings.remove(importToRemove);
}
/**
* Removes all imports in a collection to this list of imports. Does not
* remove any imports that are not in the list.
*
* @param importsToRemove a collection of imports to remove
* @return true if any imports were removed from the list
*/
public boolean removeAll(Collection<String> importsToRemove) {
return importStrings.removeAll(importsToRemove);
}
/**
* Returns a string representation of the imports, as proper Java code.
* Includes newlines in the correct places as defined by the Google
* Java Style Guide.
*/
@Override
public String toString() {
if (importStrings.size() == 0) {
return "";
}
StringBuilder result = new StringBuilder();
if (!hasExistingImports) {
// insert a newline after the package expression, then add imports
result.append('\n');
}
// output sorted imports, with line breaks between sections
Kind prevKind = null;
String prevTopLevel = null;
for (String importString : importStrings) {
Kind currKind = Kind.getKind(importString);
String currTopLevel = getTopLevel(importString);
if (prevKind != null && prevKind != currKind) {
result.append('\n');
} else if (currKind == Kind.THIRD_PARTY) {
if (prevTopLevel != null && !prevTopLevel.equals(currTopLevel)) {
result.append('\n');
}
}
result.append(importString).append(";\n");
prevKind = currKind;
prevTopLevel = currTopLevel;
}
String replacementString = result.toString();
if (!hasExistingImports) {
return replacementString;
} else {
return replacementString.substring(0, replacementString.length() - 1); // trim last newline
}
}
/**
* Given an import string, returns the top-level package for that
* import.
*/
@VisibleForTesting
static String getTopLevel(String importString) {
Matcher m = TOPLEVEL_PATTERN.matcher(importString);
if (m.find()) {
return m.group(2);
} else {
throw new IllegalArgumentException(importString + " is not a valid import statement");
}
}
/**
* An enumeration of the different kinds of import statements we might
* encounter. Each kind must be sorted into its own bucket in the import
* statements.
*/
private enum Kind {
STATIC, // import static
GOOGLE, // import com.google...
THIRD_PARTY, // import org.foo.bar...
JAVA, // import java...
JAVAX; // import javax...
/**
* Determines the Kind of an import statement.
*
* @param importString the import statement as a string
* @return the kind of the import statement
*/
public static Kind getKind(String importString) {
if (importString.startsWith("import static")) {
return STATIC;
} else if (importString.startsWith("import com.google.")) {
return GOOGLE;
} else if (importString.startsWith("import java.")) {
return JAVA;
} else if (importString.startsWith("import javax.")) {
return JAVAX;
} else {
return THIRD_PARTY;
}
}
}
}