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