blob: 5904917046bc67adc641eaa67292f11251685b5d [file] [log] [blame]
/*
* Copyright 2016 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.googlejavaformat.java;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.CharMatcher;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.common.collect.Range;
import com.google.common.collect.RangeMap;
import com.google.common.collect.RangeSet;
import com.google.common.collect.TreeRangeMap;
import com.google.common.collect.TreeRangeSet;
import com.google.googlejavaformat.FormattingError;
import com.google.googlejavaformat.Newlines;
import java.io.IOError;
import java.io.IOException;
import java.net.URI;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.DiagnosticListener;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardLocation;
import org.openjdk.source.doctree.DocCommentTree;
import org.openjdk.source.doctree.ReferenceTree;
import org.openjdk.source.tree.IdentifierTree;
import org.openjdk.source.tree.ImportTree;
import org.openjdk.source.tree.Tree;
import org.openjdk.source.util.DocTreePath;
import org.openjdk.source.util.DocTreePathScanner;
import org.openjdk.source.util.TreePathScanner;
import org.openjdk.source.util.TreeScanner;
import org.openjdk.tools.javac.api.JavacTrees;
import org.openjdk.tools.javac.file.JavacFileManager;
import org.openjdk.tools.javac.parser.JavacParser;
import org.openjdk.tools.javac.parser.ParserFactory;
import org.openjdk.tools.javac.tree.DCTree;
import org.openjdk.tools.javac.tree.DCTree.DCReference;
import org.openjdk.tools.javac.tree.JCTree;
import org.openjdk.tools.javac.tree.JCTree.JCCompilationUnit;
import org.openjdk.tools.javac.tree.JCTree.JCFieldAccess;
import org.openjdk.tools.javac.tree.JCTree.JCIdent;
import org.openjdk.tools.javac.tree.JCTree.JCImport;
import org.openjdk.tools.javac.util.Context;
import org.openjdk.tools.javac.util.Log;
import org.openjdk.tools.javac.util.Options;
/**
* Removes unused imports from a source file. Imports that are only used in javadoc are also
* removed, and the references in javadoc are replaced with fully qualified names.
*/
public class RemoveUnusedImports {
/** Configuration for javadoc-only imports. */
public enum JavadocOnlyImports {
/** Remove imports that are only used in javadoc, and fully qualify any {@code @link} tags. */
REMOVE,
/** Keep imports that are only used in javadoc. */
KEEP
}
// Visits an AST, recording all simple names that could refer to imported
// types and also any javadoc references that could refer to imported
// types (`@link`, `@see`, `@throws`, etc.)
//
// No attempt is made to determine whether simple names occur in contexts
// where they are type names, so there will be false positives. For example,
// `List` is not identified as unused import below:
//
// ```
// import java.util.List;
// class List {}
// ```
//
// This is still reasonably effective in practice because type names differ
// from other kinds of names in casing convention, and simple name
// clashes between imported and declared types are rare.
private static class UnusedImportScanner extends TreePathScanner<Void, Void> {
private final Set<String> usedNames = new LinkedHashSet<>();
private final Multimap<String, Range<Integer>> usedInJavadoc = HashMultimap.create();
final JavacTrees trees;
final DocTreeScanner docTreeSymbolScanner;
private UnusedImportScanner(JavacTrees trees) {
this.trees = trees;
docTreeSymbolScanner = new DocTreeScanner();
}
/** Skip the imports themselves when checking for usage. */
@Override
public Void visitImport(ImportTree importTree, Void usedSymbols) {
return null;
}
@Override
public Void visitIdentifier(IdentifierTree tree, Void unused) {
if (tree == null) {
return null;
}
usedNames.add(tree.getName().toString());
return null;
}
@Override
public Void scan(Tree tree, Void unused) {
if (tree == null) {
return null;
}
scanJavadoc();
return super.scan(tree, unused);
}
private void scanJavadoc() {
if (getCurrentPath() == null) {
return;
}
DocCommentTree commentTree = trees.getDocCommentTree(getCurrentPath());
if (commentTree == null) {
return;
}
docTreeSymbolScanner.scan(new DocTreePath(getCurrentPath(), commentTree), null);
}
// scan javadoc comments, checking for references to imported types
class DocTreeScanner extends DocTreePathScanner<Void, Void> {
@Override
public Void visitIdentifier(org.openjdk.source.doctree.IdentifierTree node, Void aVoid) {
return null;
}
@Override
public Void visitReference(ReferenceTree referenceTree, Void unused) {
DCReference reference = (DCReference) referenceTree;
long basePos =
reference.getSourcePosition((DCTree.DCDocComment) getCurrentPath().getDocComment());
// the position of trees inside the reference node aren't stored, but the qualifier's
// start position is the beginning of the reference node
if (reference.qualifierExpression != null) {
new ReferenceScanner(basePos).scan(reference.qualifierExpression, null);
}
// Record uses inside method parameters. The javadoc tool doesn't use these, but
// IntelliJ does.
if (reference.paramTypes != null) {
for (JCTree param : reference.paramTypes) {
// TODO(cushon): get start positions for the parameters
new ReferenceScanner(-1).scan(param, null);
}
}
return null;
}
// scans the qualifier and parameters of a javadoc reference for possible type names
private class ReferenceScanner extends TreeScanner<Void, Void> {
private final long basePos;
public ReferenceScanner(long basePos) {
this.basePos = basePos;
}
@Override
public Void visitIdentifier(IdentifierTree node, Void aVoid) {
usedInJavadoc.put(
node.getName().toString(),
basePos != -1
? Range.closedOpen((int) basePos, (int) basePos + node.getName().length())
: null);
return super.visitIdentifier(node, aVoid);
}
}
}
}
public static String removeUnusedImports(
final String contents, JavadocOnlyImports javadocOnlyImports) {
Context context = new Context();
JCCompilationUnit unit = parse(context, contents);
if (unit == null) {
// error handling is done during formatting
return contents;
}
UnusedImportScanner scanner = new UnusedImportScanner(JavacTrees.instance(context));
scanner.scan(unit, null);
return applyReplacements(
contents,
buildReplacements(
contents, unit, scanner.usedNames, scanner.usedInJavadoc, javadocOnlyImports));
}
private static JCCompilationUnit parse(Context context, String javaInput) {
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
context.put(DiagnosticListener.class, diagnostics);
Options.instance(context).put("allowStringFolding", "false");
JCCompilationUnit unit;
JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8);
try {
fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of());
} catch (IOException e) {
// impossible
throw new IOError(e);
}
SimpleJavaFileObject source =
new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) {
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return javaInput;
}
};
Log.instance(context).useSource(source);
ParserFactory parserFactory = ParserFactory.instance(context);
JavacParser parser =
parserFactory.newParser(
javaInput, /*keepDocComments=*/ true, /*keepEndPos=*/ true, /*keepLineMap=*/ true);
unit = parser.parseCompilationUnit();
unit.sourcefile = source;
Iterable<Diagnostic<? extends JavaFileObject>> errorDiagnostics =
Iterables.filter(diagnostics.getDiagnostics(), Formatter.ERROR_DIAGNOSTIC);
if (!Iterables.isEmpty(errorDiagnostics)) {
// error handling is done during formatting
throw FormattingError.fromJavacDiagnostics(errorDiagnostics);
}
return unit;
}
/** Construct replacements to fix unused imports. */
private static RangeMap<Integer, String> buildReplacements(
String contents,
JCCompilationUnit unit,
Set<String> usedNames,
Multimap<String, Range<Integer>> usedInJavadoc,
JavadocOnlyImports javadocOnlyImports) {
RangeMap<Integer, String> replacements = TreeRangeMap.create();
for (JCImport importTree : unit.getImports()) {
String simpleName = getSimpleName(importTree);
if (!isUnused(unit, usedNames, usedInJavadoc, javadocOnlyImports, importTree, simpleName)) {
continue;
}
// delete the import
int endPosition = importTree.getEndPosition(unit.endPositions);
endPosition = Math.max(CharMatcher.isNot(' ').indexIn(contents, endPosition), endPosition);
String sep = Newlines.guessLineSeparator(contents);
if (endPosition + sep.length() < contents.length()
&& contents.subSequence(endPosition, endPosition + sep.length()).equals(sep)) {
endPosition += sep.length();
}
replacements.put(Range.closedOpen(importTree.getStartPosition(), endPosition), "");
// fully qualify any javadoc references with the same simple name as a deleted
// non-static import
if (!importTree.isStatic()) {
for (Range<Integer> docRange : usedInJavadoc.get(simpleName)) {
if (docRange == null) {
continue;
}
String replaceWith = importTree.getQualifiedIdentifier().toString();
replacements.put(docRange, replaceWith);
}
}
}
return replacements;
}
private static String getSimpleName(JCImport importTree) {
return importTree.getQualifiedIdentifier() instanceof JCIdent
? ((JCIdent) importTree.getQualifiedIdentifier()).getName().toString()
: ((JCFieldAccess) importTree.getQualifiedIdentifier()).getIdentifier().toString();
}
private static boolean isUnused(
JCCompilationUnit unit,
Set<String> usedNames,
Multimap<String, Range<Integer>> usedInJavadoc,
JavadocOnlyImports javadocOnlyImports,
JCImport importTree,
String simpleName) {
String qualifier =
importTree.getQualifiedIdentifier() instanceof JCFieldAccess
? ((JCFieldAccess) importTree.getQualifiedIdentifier()).getExpression().toString()
: null;
if (qualifier.equals("java.lang")) {
return true;
}
if (unit.getPackageName() != null && unit.getPackageName().toString().equals(qualifier)) {
return true;
}
if (importTree.getQualifiedIdentifier() instanceof JCFieldAccess
&& ((JCFieldAccess) importTree.getQualifiedIdentifier())
.getIdentifier()
.contentEquals("*")) {
return false;
}
if (usedNames.contains(simpleName)) {
return false;
}
if (usedInJavadoc.containsKey(simpleName) && javadocOnlyImports == JavadocOnlyImports.KEEP) {
return false;
}
return true;
}
/** Applies the replacements to the given source, and re-format any edited javadoc. */
private static String applyReplacements(String source, RangeMap<Integer, String> replacements) {
// save non-empty fixed ranges for reformatting after fixes are applied
RangeSet<Integer> fixedRanges = TreeRangeSet.create();
// Apply the fixes in increasing order, adjusting ranges to account for
// earlier fixes that change the length of the source. The output ranges are
// needed so we can reformat fixed regions, otherwise the fixes could just
// be applied in descending order without adjusting offsets.
StringBuilder sb = new StringBuilder(source);
int offset = 0;
for (Map.Entry<Range<Integer>, String> replacement : replacements.asMapOfRanges().entrySet()) {
Range<Integer> range = replacement.getKey();
String replaceWith = replacement.getValue();
int start = offset + range.lowerEndpoint();
int end = offset + range.upperEndpoint();
sb.replace(start, end, replaceWith);
if (!replaceWith.isEmpty()) {
fixedRanges.add(Range.closedOpen(start, end));
}
offset += replaceWith.length() - (range.upperEndpoint() - range.lowerEndpoint());
}
String result = sb.toString();
// If there were any non-empty replaced ranges (e.g. javadoc), reformat the fixed regions.
// We could avoid formatting twice in --fix-imports=also mode, but that is not the default
// and removing imports won't usually affect javadoc.
if (!fixedRanges.isEmpty()) {
try {
result = new Formatter().formatSource(result, fixedRanges.asRanges());
} catch (FormatterException e) {
// javadoc reformatting is best-effort
}
}
return result;
}
}