• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2016 Google Inc. All Rights Reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.googlejavaformat.java;
18 
19 import static java.lang.Math.max;
20 import static java.nio.charset.StandardCharsets.UTF_8;
21 
22 import com.google.common.base.CharMatcher;
23 import com.google.common.collect.HashMultimap;
24 import com.google.common.collect.ImmutableList;
25 import com.google.common.collect.Iterables;
26 import com.google.common.collect.Multimap;
27 import com.google.common.collect.Range;
28 import com.google.common.collect.RangeMap;
29 import com.google.common.collect.RangeSet;
30 import com.google.common.collect.TreeRangeMap;
31 import com.google.common.collect.TreeRangeSet;
32 import com.google.googlejavaformat.Newlines;
33 import com.sun.source.doctree.DocCommentTree;
34 import com.sun.source.doctree.ReferenceTree;
35 import com.sun.source.tree.CaseTree;
36 import com.sun.source.tree.IdentifierTree;
37 import com.sun.source.tree.ImportTree;
38 import com.sun.source.tree.Tree;
39 import com.sun.source.util.DocTreePath;
40 import com.sun.source.util.DocTreePathScanner;
41 import com.sun.source.util.TreePathScanner;
42 import com.sun.source.util.TreeScanner;
43 import com.sun.tools.javac.api.JavacTrees;
44 import com.sun.tools.javac.file.JavacFileManager;
45 import com.sun.tools.javac.parser.JavacParser;
46 import com.sun.tools.javac.parser.ParserFactory;
47 import com.sun.tools.javac.tree.DCTree;
48 import com.sun.tools.javac.tree.DCTree.DCReference;
49 import com.sun.tools.javac.tree.JCTree;
50 import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
51 import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
52 import com.sun.tools.javac.tree.JCTree.JCImport;
53 import com.sun.tools.javac.util.Context;
54 import com.sun.tools.javac.util.Log;
55 import com.sun.tools.javac.util.Options;
56 import java.io.IOError;
57 import java.io.IOException;
58 import java.lang.reflect.Method;
59 import java.net.URI;
60 import java.util.LinkedHashSet;
61 import java.util.List;
62 import java.util.Map;
63 import java.util.Set;
64 import javax.tools.Diagnostic;
65 import javax.tools.DiagnosticCollector;
66 import javax.tools.DiagnosticListener;
67 import javax.tools.JavaFileObject;
68 import javax.tools.SimpleJavaFileObject;
69 import javax.tools.StandardLocation;
70 
71 /**
72  * Removes unused imports from a source file. Imports that are only used in javadoc are also
73  * removed, and the references in javadoc are replaced with fully qualified names.
74  */
75 public class RemoveUnusedImports {
76 
77   // Visits an AST, recording all simple names that could refer to imported
78   // types and also any javadoc references that could refer to imported
79   // types (`@link`, `@see`, `@throws`, etc.)
80   //
81   // No attempt is made to determine whether simple names occur in contexts
82   // where they are type names, so there will be false positives. For example,
83   // `List` is not identified as unused import below:
84   //
85   // ```
86   // import java.util.List;
87   // class List {}
88   // ```
89   //
90   // This is still reasonably effective in practice because type names differ
91   // from other kinds of names in casing convention, and simple name
92   // clashes between imported and declared types are rare.
93   private static class UnusedImportScanner extends TreePathScanner<Void, Void> {
94 
95     private final Set<String> usedNames = new LinkedHashSet<>();
96     private final Multimap<String, Range<Integer>> usedInJavadoc = HashMultimap.create();
97     final JavacTrees trees;
98     final DocTreeScanner docTreeSymbolScanner;
99 
UnusedImportScanner(JavacTrees trees)100     private UnusedImportScanner(JavacTrees trees) {
101       this.trees = trees;
102       docTreeSymbolScanner = new DocTreeScanner();
103     }
104 
105     /** Skip the imports themselves when checking for usage. */
106     @Override
visitImport(ImportTree importTree, Void usedSymbols)107     public Void visitImport(ImportTree importTree, Void usedSymbols) {
108       return null;
109     }
110 
111     @Override
visitIdentifier(IdentifierTree tree, Void unused)112     public Void visitIdentifier(IdentifierTree tree, Void unused) {
113       if (tree == null) {
114         return null;
115       }
116       usedNames.add(tree.getName().toString());
117       return null;
118     }
119 
120     // TODO(cushon): remove this override when pattern matching in switch is no longer a preview
121     // feature, and TreePathScanner visits CaseTree#getLabels instead of CaseTree#getExpressions
122     @SuppressWarnings("unchecked") // reflection
123     @Override
visitCase(CaseTree tree, Void unused)124     public Void visitCase(CaseTree tree, Void unused) {
125       if (CASE_TREE_GET_LABELS != null) {
126         try {
127           scan((List<? extends Tree>) CASE_TREE_GET_LABELS.invoke(tree), null);
128         } catch (ReflectiveOperationException e) {
129           throw new LinkageError(e.getMessage(), e);
130         }
131       }
132       return super.visitCase(tree, null);
133     }
134 
135     private static final Method CASE_TREE_GET_LABELS = caseTreeGetLabels();
136 
caseTreeGetLabels()137     private static Method caseTreeGetLabels() {
138       try {
139         return CaseTree.class.getMethod("getLabels");
140       } catch (NoSuchMethodException e) {
141         return null;
142       }
143     }
144 
145     @Override
scan(Tree tree, Void unused)146     public Void scan(Tree tree, Void unused) {
147       if (tree == null) {
148         return null;
149       }
150       scanJavadoc();
151       return super.scan(tree, unused);
152     }
153 
scanJavadoc()154     private void scanJavadoc() {
155       if (getCurrentPath() == null) {
156         return;
157       }
158       DocCommentTree commentTree = trees.getDocCommentTree(getCurrentPath());
159       if (commentTree == null) {
160         return;
161       }
162       docTreeSymbolScanner.scan(new DocTreePath(getCurrentPath(), commentTree), null);
163     }
164 
165     // scan javadoc comments, checking for references to imported types
166     class DocTreeScanner extends DocTreePathScanner<Void, Void> {
167       @Override
visitIdentifier(com.sun.source.doctree.IdentifierTree node, Void aVoid)168       public Void visitIdentifier(com.sun.source.doctree.IdentifierTree node, Void aVoid) {
169         return null;
170       }
171 
172       @Override
visitReference(ReferenceTree referenceTree, Void unused)173       public Void visitReference(ReferenceTree referenceTree, Void unused) {
174         DCReference reference = (DCReference) referenceTree;
175         long basePos =
176             reference
177                 .pos((DCTree.DCDocComment) getCurrentPath().getDocComment())
178                 .getStartPosition();
179         // the position of trees inside the reference node aren't stored, but the qualifier's
180         // start position is the beginning of the reference node
181         if (reference.qualifierExpression != null) {
182           new ReferenceScanner(basePos).scan(reference.qualifierExpression, null);
183         }
184         // Record uses inside method parameters. The javadoc tool doesn't use these, but
185         // IntelliJ does.
186         if (reference.paramTypes != null) {
187           for (JCTree param : reference.paramTypes) {
188             // TODO(cushon): get start positions for the parameters
189             new ReferenceScanner(-1).scan(param, null);
190           }
191         }
192         return null;
193       }
194 
195       // scans the qualifier and parameters of a javadoc reference for possible type names
196       private class ReferenceScanner extends TreeScanner<Void, Void> {
197         private final long basePos;
198 
ReferenceScanner(long basePos)199         public ReferenceScanner(long basePos) {
200           this.basePos = basePos;
201         }
202 
203         @Override
visitIdentifier(IdentifierTree node, Void aVoid)204         public Void visitIdentifier(IdentifierTree node, Void aVoid) {
205           usedInJavadoc.put(
206               node.getName().toString(),
207               basePos != -1
208                   ? Range.closedOpen((int) basePos, (int) basePos + node.getName().length())
209                   : null);
210           return super.visitIdentifier(node, aVoid);
211         }
212       }
213     }
214   }
215 
removeUnusedImports(final String contents)216   public static String removeUnusedImports(final String contents) throws FormatterException {
217     Context context = new Context();
218     JCCompilationUnit unit = parse(context, contents);
219     if (unit == null) {
220       // error handling is done during formatting
221       return contents;
222     }
223     UnusedImportScanner scanner = new UnusedImportScanner(JavacTrees.instance(context));
224     scanner.scan(unit, null);
225     return applyReplacements(
226         contents, buildReplacements(contents, unit, scanner.usedNames, scanner.usedInJavadoc));
227   }
228 
parse(Context context, String javaInput)229   private static JCCompilationUnit parse(Context context, String javaInput)
230       throws FormatterException {
231     DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
232     context.put(DiagnosticListener.class, diagnostics);
233     Options.instance(context).put("--enable-preview", "true");
234     Options.instance(context).put("allowStringFolding", "false");
235     JCCompilationUnit unit;
236     JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8);
237     try {
238       fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of());
239     } catch (IOException e) {
240       // impossible
241       throw new IOError(e);
242     }
243     SimpleJavaFileObject source =
244         new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) {
245           @Override
246           public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
247             return javaInput;
248           }
249         };
250     Log.instance(context).useSource(source);
251     ParserFactory parserFactory = ParserFactory.instance(context);
252     JavacParser parser =
253         parserFactory.newParser(
254             javaInput,
255             /* keepDocComments= */ true,
256             /* keepEndPos= */ true,
257             /* keepLineMap= */ true);
258     unit = parser.parseCompilationUnit();
259     unit.sourcefile = source;
260     Iterable<Diagnostic<? extends JavaFileObject>> errorDiagnostics =
261         Iterables.filter(diagnostics.getDiagnostics(), Formatter::errorDiagnostic);
262     if (!Iterables.isEmpty(errorDiagnostics)) {
263       // error handling is done during formatting
264       throw FormatterException.fromJavacDiagnostics(errorDiagnostics);
265     }
266     return unit;
267   }
268 
269   /** Construct replacements to fix unused imports. */
buildReplacements( String contents, JCCompilationUnit unit, Set<String> usedNames, Multimap<String, Range<Integer>> usedInJavadoc)270   private static RangeMap<Integer, String> buildReplacements(
271       String contents,
272       JCCompilationUnit unit,
273       Set<String> usedNames,
274       Multimap<String, Range<Integer>> usedInJavadoc) {
275     RangeMap<Integer, String> replacements = TreeRangeMap.create();
276     for (JCImport importTree : unit.getImports()) {
277       String simpleName = getSimpleName(importTree);
278       if (!isUnused(unit, usedNames, usedInJavadoc, importTree, simpleName)) {
279         continue;
280       }
281       // delete the import
282       int endPosition = importTree.getEndPosition(unit.endPositions);
283       endPosition = max(CharMatcher.isNot(' ').indexIn(contents, endPosition), endPosition);
284       String sep = Newlines.guessLineSeparator(contents);
285       if (endPosition + sep.length() < contents.length()
286           && contents.subSequence(endPosition, endPosition + sep.length()).toString().equals(sep)) {
287         endPosition += sep.length();
288       }
289       replacements.put(Range.closedOpen(importTree.getStartPosition(), endPosition), "");
290     }
291     return replacements;
292   }
293 
getSimpleName(JCImport importTree)294   private static String getSimpleName(JCImport importTree) {
295     return getQualifiedIdentifier(importTree).getIdentifier().toString();
296   }
297 
isUnused( JCCompilationUnit unit, Set<String> usedNames, Multimap<String, Range<Integer>> usedInJavadoc, JCImport importTree, String simpleName)298   private static boolean isUnused(
299       JCCompilationUnit unit,
300       Set<String> usedNames,
301       Multimap<String, Range<Integer>> usedInJavadoc,
302       JCImport importTree,
303       String simpleName) {
304     JCFieldAccess qualifiedIdentifier = getQualifiedIdentifier(importTree);
305     String qualifier = qualifiedIdentifier.getExpression().toString();
306     if (qualifier.equals("java.lang")) {
307       return true;
308     }
309     if (unit.getPackageName() != null && unit.getPackageName().toString().equals(qualifier)) {
310       return true;
311     }
312     if (qualifiedIdentifier.getIdentifier().contentEquals("*")) {
313       return false;
314     }
315 
316     if (usedNames.contains(simpleName)) {
317       return false;
318     }
319     if (usedInJavadoc.containsKey(simpleName)) {
320       return false;
321     }
322     return true;
323   }
324 
getQualifiedIdentifier(JCImport importTree)325   private static JCFieldAccess getQualifiedIdentifier(JCImport importTree) {
326     // Use reflection because the return type is JCTree in some versions and JCFieldAccess in others
327     try {
328       return (JCFieldAccess) JCImport.class.getMethod("getQualifiedIdentifier").invoke(importTree);
329     } catch (ReflectiveOperationException e) {
330       throw new LinkageError(e.getMessage(), e);
331     }
332   }
333 
334   /** Applies the replacements to the given source, and re-format any edited javadoc. */
applyReplacements(String source, RangeMap<Integer, String> replacements)335   private static String applyReplacements(String source, RangeMap<Integer, String> replacements) {
336     // save non-empty fixed ranges for reformatting after fixes are applied
337     RangeSet<Integer> fixedRanges = TreeRangeSet.create();
338 
339     // Apply the fixes in increasing order, adjusting ranges to account for
340     // earlier fixes that change the length of the source. The output ranges are
341     // needed so we can reformat fixed regions, otherwise the fixes could just
342     // be applied in descending order without adjusting offsets.
343     StringBuilder sb = new StringBuilder(source);
344     int offset = 0;
345     for (Map.Entry<Range<Integer>, String> replacement : replacements.asMapOfRanges().entrySet()) {
346       Range<Integer> range = replacement.getKey();
347       String replaceWith = replacement.getValue();
348       int start = offset + range.lowerEndpoint();
349       int end = offset + range.upperEndpoint();
350       sb.replace(start, end, replaceWith);
351       if (!replaceWith.isEmpty()) {
352         fixedRanges.add(Range.closedOpen(start, end));
353       }
354       offset += replaceWith.length() - (range.upperEndpoint() - range.lowerEndpoint());
355     }
356     return sb.toString();
357   }
358 }
359