Skip to content

Commit 3b6fc15

Browse files
author
Vincent Potucek
committed
RemoveUnusedImportsTest
1 parent 0411f2a commit 3b6fc15

File tree

2 files changed

+339
-263
lines changed

2 files changed

+339
-263
lines changed

palantir-java-format/src/main/java/com/palantir/javaformat/java/RemoveUnusedImports.java

Lines changed: 122 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,21 @@
1616

1717
package com.palantir.javaformat.java;
1818

19+
import static java.lang.Math.max;
20+
import static java.nio.charset.StandardCharsets.UTF_8;
21+
1922
import com.google.common.base.CharMatcher;
2023
import com.google.common.collect.HashMultimap;
24+
import com.google.common.collect.ImmutableList;
25+
import com.google.common.collect.Iterables;
2126
import com.google.common.collect.Multimap;
2227
import com.google.common.collect.Range;
2328
import com.google.common.collect.RangeMap;
2429
import com.google.common.collect.RangeSet;
2530
import com.google.common.collect.TreeRangeMap;
2631
import com.google.common.collect.TreeRangeSet;
2732
import com.palantir.javaformat.Newlines;
33+
import com.palantir.javaformat.java.FormatterException;
2834
import com.sun.source.doctree.DocCommentTree;
2935
import com.sun.source.doctree.ReferenceTree;
3036
import com.sun.source.tree.CaseTree;
@@ -36,24 +42,36 @@
3642
import com.sun.source.util.TreePathScanner;
3743
import com.sun.source.util.TreeScanner;
3844
import com.sun.tools.javac.api.JavacTrees;
45+
import com.sun.tools.javac.file.JavacFileManager;
46+
import com.sun.tools.javac.parser.JavacParser;
47+
import com.sun.tools.javac.parser.ParserFactory;
3948
import com.sun.tools.javac.tree.DCTree;
4049
import com.sun.tools.javac.tree.DCTree.DCReference;
4150
import com.sun.tools.javac.tree.JCTree;
4251
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
4352
import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
44-
import com.sun.tools.javac.tree.JCTree.JCIdent;
4553
import com.sun.tools.javac.tree.JCTree.JCImport;
4654
import com.sun.tools.javac.util.Context;
55+
import com.sun.tools.javac.util.Log;
4756
import com.sun.tools.javac.util.Options;
57+
import java.io.IOError;
58+
import java.io.IOException;
4859
import java.lang.reflect.Method;
60+
import java.net.URI;
4961
import java.util.LinkedHashSet;
5062
import java.util.List;
5163
import java.util.Map;
5264
import java.util.Set;
65+
import javax.tools.Diagnostic;
66+
import javax.tools.DiagnosticCollector;
67+
import javax.tools.DiagnosticListener;
68+
import javax.tools.JavaFileObject;
69+
import javax.tools.SimpleJavaFileObject;
70+
import javax.tools.StandardLocation;
5371

5472
/**
55-
* Removes unused imports from a source file. Imports that are only used in javadoc are also removed, and the references
56-
* in javadoc are replaced with fully qualified names.
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.
5775
*/
5876
public class RemoveUnusedImports {
5977

@@ -73,12 +91,10 @@ public class RemoveUnusedImports {
7391
// This is still reasonably effective in practice because type names differ
7492
// from other kinds of names in casing convention, and simple name
7593
// clashes between imported and declared types are rare.
76-
private static final class UnusedImportScanner extends TreePathScanner<Void, Void> {
94+
private static class UnusedImportScanner extends TreePathScanner<Void, Void> {
7795

7896
private final Set<String> usedNames = new LinkedHashSet<>();
79-
8097
private final Multimap<String, Range<Integer>> usedInJavadoc = HashMultimap.create();
81-
8298
final JavacTrees trees;
8399
final DocTreeScanner docTreeSymbolScanner;
84100

@@ -102,7 +118,7 @@ public Void visitIdentifier(IdentifierTree tree, Void unused) {
102118
return null;
103119
}
104120

105-
// TODO(fwindheuser): remove this override when pattern matching in switch is no longer a preview
121+
// TODO(cushon): remove this override when pattern matching in switch is no longer a preview
106122
// feature, and TreePathScanner visits CaseTree#getLabels instead of CaseTree#getExpressions
107123
@SuppressWarnings("unchecked") // reflection
108124
@Override
@@ -111,15 +127,14 @@ public Void visitCase(CaseTree tree, Void unused) {
111127
try {
112128
scan((List<? extends Tree>) CASE_TREE_GET_LABELS.invoke(tree), null);
113129
} catch (ReflectiveOperationException e) {
114-
throw new RuntimeException(e.getMessage(), e);
130+
throw new LinkageError(e.getMessage(), e);
115131
}
116132
}
117133
return super.visitCase(tree, null);
118134
}
119135

120136
private static final Method CASE_TREE_GET_LABELS = caseTreeGetLabels();
121137

122-
@SuppressWarnings("for-rollout:NullAway")
123138
private static Method caseTreeGetLabels() {
124139
try {
125140
return CaseTree.class.getMethod("getLabels");
@@ -128,7 +143,6 @@ private static Method caseTreeGetLabels() {
128143
}
129144
}
130145

131-
@SuppressWarnings("for-rollout:VoidUsed")
132146
@Override
133147
public Void scan(Tree tree, Void unused) {
134148
if (tree == null) {
@@ -159,9 +173,10 @@ public Void visitIdentifier(com.sun.source.doctree.IdentifierTree node, Void aVo
159173
@Override
160174
public Void visitReference(ReferenceTree referenceTree, Void unused) {
161175
DCReference reference = (DCReference) referenceTree;
162-
long basePos = reference
163-
.pos((DCTree.DCDocComment) getCurrentPath().getDocComment())
164-
.getStartPosition();
176+
long basePos =
177+
reference
178+
.pos((DCTree.DCDocComment) getCurrentPath().getDocComment())
179+
.getStartPosition();
165180
// the position of trees inside the reference node aren't stored, but the qualifier's
166181
// start position is the beginning of the reference node
167182
if (reference.qualifierExpression != null) {
@@ -186,37 +201,78 @@ public ReferenceScanner(long basePos) {
186201
this.basePos = basePos;
187202
}
188203

189-
@SuppressWarnings("for-rollout:VoidUsed")
190204
@Override
191205
public Void visitIdentifier(IdentifierTree node, Void aVoid) {
192206
usedInJavadoc.put(
193207
node.getName().toString(),
194208
basePos != -1
195-
? Range.closedOpen(
196-
(int) basePos,
197-
(int) basePos + node.getName().length())
209+
? Range.closedOpen((int) basePos, (int) basePos + node.getName().length())
198210
: null);
199211
return super.visitIdentifier(node, aVoid);
200212
}
201213
}
202214
}
203215
}
204-
205216
public static String removeUnusedImports(final String contents) throws FormatterException {
206217
Context context = new Context();
207218
JCCompilationUnit unit = parse(context, contents);
208-
if (unit == null) {
209-
// error handling is done during formatting
210-
return contents;
211-
}
212219
UnusedImportScanner scanner = new UnusedImportScanner(JavacTrees.instance(context));
213220
scanner.scan(unit, null);
214-
return applyReplacements(contents, buildReplacements(contents, unit, scanner.usedNames, scanner.usedInJavadoc));
221+
String s = applyReplacements(
222+
contents, buildReplacements(contents, unit, scanner.usedNames, scanner.usedInJavadoc));
223+
224+
// Normalize newlines while preserving important blank lines
225+
String sep = Newlines.guessLineSeparator(contents);
226+
227+
// Ensure exactly one blank line after package declaration
228+
s = s.replaceAll("(?m)^package .+" + sep + "\\s+" + sep, "package $1" + sep + sep);
229+
230+
// Ensure exactly one blank line between last import and class declaration
231+
s = s.replaceAll("(?m)import .+" + sep + "\\s+" + sep + "(?=class|interface|enum|record)",
232+
"import $1" + sep + sep);
233+
234+
// Remove multiple blank lines elsewhere in imports section
235+
s = s.replaceAll("(?m)^import .+" + sep + "\\s+" + sep + "(?=import)", "import $1" + sep);
236+
237+
return s;
215238
}
216239

217-
private static JCCompilationUnit parse(Context context, String javaInput) throws FormatterException {
240+
private static JCCompilationUnit parse(Context context, String javaInput)
241+
throws FormatterException {
242+
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
243+
context.put(DiagnosticListener.class, diagnostics);
244+
Options.instance(context).put("--enable-preview", "true");
218245
Options.instance(context).put("allowStringFolding", "false");
219-
return Formatter.parseJcCompilationUnit(context, javaInput);
246+
JCCompilationUnit unit;
247+
try (JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8)){
248+
fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of());
249+
} catch (IOException e) {
250+
// impossible
251+
throw new IOError(e);
252+
}
253+
SimpleJavaFileObject source =
254+
new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) {
255+
@Override
256+
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
257+
return javaInput;
258+
}
259+
};
260+
Log.instance(context).useSource(source);
261+
ParserFactory parserFactory = ParserFactory.instance(context);
262+
JavacParser parser =
263+
parserFactory.newParser(
264+
javaInput,
265+
/* keepDocComments= */ true,
266+
/* keepEndPos= */ true,
267+
/* keepLineMap= */ true);
268+
unit = parser.parseCompilationUnit();
269+
unit.sourcefile = source;
270+
Iterable<Diagnostic<? extends JavaFileObject>> errorDiagnostics =
271+
Iterables.filter(diagnostics.getDiagnostics(), Formatter::errorDiagnostic);
272+
if (!Iterables.isEmpty(errorDiagnostics)) {
273+
// error handling is done during formatting
274+
}
275+
return unit;
220276
}
221277

222278
/** Construct replacements to fix unused imports. */
@@ -226,53 +282,63 @@ private static RangeMap<Integer, String> buildReplacements(
226282
Set<String> usedNames,
227283
Multimap<String, Range<Integer>> usedInJavadoc) {
228284
RangeMap<Integer, String> replacements = TreeRangeMap.create();
229-
for (JCImport importTree : unit.getImports()) {
285+
int size = unit.getImports().size();
286+
JCTree lastImport = size > 0 ? unit.getImports().get(size - 1) : null;
287+
for (JCTree importTree : unit.getImports()) {
230288
String simpleName = getSimpleName(importTree);
231289
if (!isUnused(unit, usedNames, usedInJavadoc, importTree, simpleName)) {
232290
continue;
233291
}
234292
// delete the import
235293
int endPosition = importTree.getEndPosition(unit.endPositions);
236-
endPosition = Math.max(CharMatcher.isNot(' ').indexIn(contents, endPosition), endPosition);
294+
endPosition = max(CharMatcher.isNot(' ').indexIn(contents, endPosition), endPosition);
237295
String sep = Newlines.guessLineSeparator(contents);
296+
297+
// Check if there's an empty line after this import
298+
boolean hasEmptyLineAfter = false;
299+
if (endPosition + sep.length() * 2 <= contents.length()) {
300+
String nextTwoLines = contents.substring(endPosition, endPosition + sep.length() * 2);
301+
hasEmptyLineAfter = nextTwoLines.equals(sep + sep);
302+
}
303+
238304
if (endPosition + sep.length() < contents.length()
239-
&& contents.subSequence(endPosition, endPosition + sep.length())
240-
.toString()
241-
.equals(sep)) {
305+
&& contents.subSequence(endPosition, endPosition + sep.length()).toString().equals(sep)) {
242306
endPosition += sep.length();
243307
}
308+
309+
// If this isn't the last import and there's an empty line after, preserve it
310+
if ((size == 1 || importTree != lastImport) && !hasEmptyLineAfter) {
311+
while (endPosition + sep.length() <= contents.length()
312+
&& contents.regionMatches(endPosition, sep, 0, sep.length())) {
313+
endPosition += sep.length();
314+
}
315+
}
244316
replacements.put(Range.closedOpen(importTree.getStartPosition(), endPosition), "");
245317
}
246318
return replacements;
247319
}
248-
249-
private static String getSimpleName(ImportTree importTree) {
250-
return importTree.getQualifiedIdentifier() instanceof JCIdent
251-
? ((JCIdent) importTree.getQualifiedIdentifier()).getName().toString()
252-
: ((JCFieldAccess) importTree.getQualifiedIdentifier())
253-
.getIdentifier()
254-
.toString();
320+
private static String getSimpleName(JCTree importTree) {
321+
return getQualifiedIdentifier(importTree).getIdentifier().toString();
255322
}
256323

257324
private static boolean isUnused(
258325
JCCompilationUnit unit,
259326
Set<String> usedNames,
260327
Multimap<String, Range<Integer>> usedInJavadoc,
261-
ImportTree importTree,
328+
JCTree importTree,
262329
String simpleName) {
263-
String qualifier = ((JCFieldAccess) importTree.getQualifiedIdentifier())
264-
.getExpression()
265-
.toString();
330+
JCFieldAccess qualifiedIdentifier = getQualifiedIdentifier(importTree);
331+
String qualifier = qualifiedIdentifier.getExpression().toString();
266332
if (qualifier.equals("java.lang")) {
267333
return true;
268334
}
335+
if(usedNames.contains(simpleName)){
336+
return false;
337+
}
269338
if (unit.getPackageName() != null && unit.getPackageName().toString().equals(qualifier)) {
270339
return true;
271340
}
272-
if (importTree.getQualifiedIdentifier() instanceof JCFieldAccess
273-
&& ((JCFieldAccess) importTree.getQualifiedIdentifier())
274-
.getIdentifier()
275-
.contentEquals("*")) {
341+
if (qualifiedIdentifier.getIdentifier().contentEquals("*") && !((JCImport) importTree).isStatic()) {
276342
return false;
277343
}
278344

@@ -285,6 +351,15 @@ private static boolean isUnused(
285351
return true;
286352
}
287353

354+
private static JCFieldAccess getQualifiedIdentifier(JCTree importTree) {
355+
// Use reflection because the return type is JCTree in some versions and JCFieldAccess in others
356+
try {
357+
return (JCFieldAccess) JCImport.class.getMethod("getQualifiedIdentifier").invoke(importTree);
358+
} catch (ReflectiveOperationException e) {
359+
throw new LinkageError(e.getMessage(), e);
360+
}
361+
}
362+
288363
/** Applies the replacements to the given source, and re-format any edited javadoc. */
289364
private static String applyReplacements(String source, RangeMap<Integer, String> replacements) {
290365
// save non-empty fixed ranges for reformatting after fixes are applied
@@ -296,8 +371,7 @@ private static String applyReplacements(String source, RangeMap<Integer, String>
296371
// be applied in descending order without adjusting offsets.
297372
StringBuilder sb = new StringBuilder(source);
298373
int offset = 0;
299-
for (Map.Entry<Range<Integer>, String> replacement :
300-
replacements.asMapOfRanges().entrySet()) {
374+
for (Map.Entry<Range<Integer>, String> replacement : replacements.asMapOfRanges().entrySet()) {
301375
Range<Integer> range = replacement.getKey();
302376
String replaceWith = replacement.getValue();
303377
int start = offset + range.lowerEndpoint();
@@ -310,4 +384,4 @@ private static String applyReplacements(String source, RangeMap<Integer, String>
310384
}
311385
return sb.toString();
312386
}
313-
}
387+
}

0 commit comments

Comments
 (0)