Skip to content

Commit cf8220e

Browse files
author
Vincent Potucek
committed
fix RemoveUnusedImportsStep leftovers
1 parent 61eabd6 commit cf8220e

File tree

4 files changed

+894
-229
lines changed

4 files changed

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

0 commit comments

Comments
 (0)