Skip to content

Commit 622732f

Browse files
author
Vincent Potucek
committed
fix RemoveUnusedImportsStep leftovers
1 parent 3ed4724 commit 622732f

File tree

2 files changed

+1273
-213
lines changed

2 files changed

+1273
-213
lines changed

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

Lines changed: 119 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@
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;
2125
import com.google.common.collect.Multimap;
2226
import com.google.common.collect.Range;
2327
import com.google.common.collect.RangeMap;
24-
import com.google.common.collect.RangeSet;
2528
import com.google.common.collect.TreeRangeMap;
26-
import com.google.common.collect.TreeRangeSet;
2729
import com.palantir.javaformat.Newlines;
2830
import com.sun.source.doctree.DocCommentTree;
2931
import com.sun.source.doctree.ReferenceTree;
@@ -36,20 +38,30 @@
3638
import com.sun.source.util.TreePathScanner;
3739
import com.sun.source.util.TreeScanner;
3840
import com.sun.tools.javac.api.JavacTrees;
41+
import com.sun.tools.javac.file.JavacFileManager;
42+
import com.sun.tools.javac.parser.ParserFactory;
3943
import com.sun.tools.javac.tree.DCTree;
4044
import com.sun.tools.javac.tree.DCTree.DCReference;
4145
import com.sun.tools.javac.tree.JCTree;
4246
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
4347
import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
44-
import com.sun.tools.javac.tree.JCTree.JCIdent;
4548
import com.sun.tools.javac.tree.JCTree.JCImport;
4649
import com.sun.tools.javac.util.Context;
50+
import com.sun.tools.javac.util.Log;
4751
import com.sun.tools.javac.util.Options;
52+
import java.io.IOException;
4853
import java.lang.reflect.Method;
54+
import java.net.URI;
4955
import java.util.LinkedHashSet;
5056
import java.util.List;
5157
import java.util.Map;
5258
import java.util.Set;
59+
import javax.annotation.Nullable;
60+
import javax.tools.DiagnosticCollector;
61+
import javax.tools.DiagnosticListener;
62+
import javax.tools.JavaFileObject;
63+
import javax.tools.SimpleJavaFileObject;
64+
import javax.tools.StandardLocation;
5365

5466
/**
5567
* Removes unused imports from a source file. Imports that are only used in javadoc are also removed, and the references
@@ -76,15 +88,12 @@ public class RemoveUnusedImports {
7688
private static final class UnusedImportScanner extends TreePathScanner<Void, Void> {
7789

7890
private final Set<String> usedNames = new LinkedHashSet<>();
79-
8091
private final Multimap<String, Range<Integer>> usedInJavadoc = HashMultimap.create();
81-
82-
final JavacTrees trees;
83-
final DocTreeScanner docTreeSymbolScanner;
92+
private final DocTreeScanner docTreeSymbolScanner = new DocTreeScanner();
93+
private final JavacTrees trees;
8494

8595
private UnusedImportScanner(JavacTrees trees) {
8696
this.trees = trees;
87-
docTreeSymbolScanner = new DocTreeScanner();
8897
}
8998

9099
/** Skip the imports themselves when checking for usage. */
@@ -202,21 +211,50 @@ public Void visitIdentifier(IdentifierTree node, Void aVoid) {
202211
}
203212
}
204213

205-
public static String removeUnusedImports(final String contents) throws FormatterException {
214+
public static String removeUnusedImports(final String contents) {
206215
Context context = new Context();
207216
JCCompilationUnit unit = parse(context, contents);
208-
if (unit == null) {
209-
// error handling is done during formatting
210-
return contents;
211-
}
212217
UnusedImportScanner scanner = new UnusedImportScanner(JavacTrees.instance(context));
213218
scanner.scan(unit, null);
214-
return applyReplacements(contents, buildReplacements(contents, unit, scanner.usedNames, scanner.usedInJavadoc));
219+
220+
String s = contents;
221+
222+
// Normalize newlines while preserving important blank lines
223+
String sep = Newlines.guessLineSeparator(contents);
224+
225+
// Ensure exactly one blank line after package declaration
226+
s = s.replaceAll("(?m)^(package .+)" + sep + "\\s+" + sep, "$1" + sep + sep);
227+
228+
// Ensure exactly one blank line between last import and class declaration
229+
s = s.replaceAll("(?m)^(import .+)" + sep + "\\s+" + sep + "(?=class|interface|enum|record)", "$1" + sep + sep);
230+
231+
// Remove multiple blank lines elsewhere in imports section
232+
s = s.replaceAll("(?m)^(import .+)" + sep + "\\s+" + sep + "(?=import)", "$1" + sep);
233+
234+
// Apply replacements last, after formatting
235+
return applyReplacements(s, buildReplacements(s, unit, scanner.usedNames, scanner.usedInJavadoc));
215236
}
216237

217-
private static JCCompilationUnit parse(Context context, String javaInput) throws FormatterException {
238+
private static JCCompilationUnit parse(Context context, String javaInput) {
239+
context.put(DiagnosticListener.class, new DiagnosticCollector<JavaFileObject>());
218240
Options.instance(context).put("allowStringFolding", "false");
219-
return Formatter.parseJcCompilationUnit(context, javaInput);
241+
try (JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8)) {
242+
fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of());
243+
} catch (IOException e) {
244+
throw new RuntimeException(e);
245+
}
246+
SimpleJavaFileObject source = new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) {
247+
@Override
248+
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
249+
return javaInput;
250+
}
251+
};
252+
Log.instance(context).useSource(source);
253+
JCCompilationUnit unit = ParserFactory.instance(context)
254+
.newParser(javaInput, true, true, true)
255+
.parseCompilationUnit();
256+
unit.sourcefile = source;
257+
return unit;
220258
}
221259

222260
/** Construct replacements to fix unused imports. */
@@ -226,70 +264,93 @@ private static RangeMap<Integer, String> buildReplacements(
226264
Set<String> usedNames,
227265
Multimap<String, Range<Integer>> usedInJavadoc) {
228266
RangeMap<Integer, String> replacements = TreeRangeMap.create();
229-
for (JCImport importTree : unit.getImports()) {
230-
String simpleName = getSimpleName(importTree);
231-
if (!isUnused(unit, usedNames, usedInJavadoc, importTree, simpleName)) {
232-
continue;
233-
}
234-
// delete the import
235-
int endPosition = importTree.getEndPosition(unit.endPositions);
236-
endPosition = Math.max(CharMatcher.isNot(' ').indexIn(contents, endPosition), endPosition);
237-
String sep = Newlines.guessLineSeparator(contents);
238-
if (endPosition + sep.length() < contents.length()
239-
&& contents.subSequence(endPosition, endPosition + sep.length())
240-
.toString()
241-
.equals(sep)) {
267+
int size = unit.getImports().size();
268+
unit.getImports().stream()
269+
.filter(importTree -> isUnused(
270+
unit,
271+
usedNames,
272+
usedInJavadoc,
273+
importTree,
274+
getQualifiedIdentifier(importTree).getIdentifier().toString()))
275+
.forEach(importTree -> replacements.put(
276+
Range.closedOpen(
277+
importTree.getStartPosition(),
278+
calculateEndPosition(
279+
contents,
280+
importTree,
281+
unit,
282+
Newlines.guessLineSeparator(contents),
283+
size,
284+
size > 0 ? unit.getImports().get(size - 1) : null)),
285+
""));
286+
287+
return replacements;
288+
}
289+
290+
private static int calculateEndPosition(
291+
String contents,
292+
JCTree importTree,
293+
JCCompilationUnit unit,
294+
String sep,
295+
int size,
296+
@Nullable JCTree lastImport) {
297+
int endPosition = importTree.getEndPosition(unit.endPositions);
298+
endPosition = max(CharMatcher.isNot(' ').indexIn(contents, endPosition), endPosition);
299+
if (endPosition + sep.length() < contents.length()
300+
&& contents.subSequence(endPosition, endPosition + sep.length())
301+
.toString()
302+
.equals(sep)) {
303+
endPosition += sep.length();
304+
} else if ((size == 1 || importTree != lastImport) && !checkForEmptyLineAfter(contents, endPosition, sep)) {
305+
while (endPosition + sep.length() <= contents.length()
306+
&& contents.regionMatches(endPosition, sep, 0, sep.length())) {
242307
endPosition += sep.length();
243308
}
244-
replacements.put(Range.closedOpen(importTree.getStartPosition(), endPosition), "");
245309
}
246-
return replacements;
310+
return endPosition;
247311
}
248312

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();
313+
private static boolean checkForEmptyLineAfter(String contents, int endPosition, String sep) {
314+
return endPosition + sep.length() * 2 <= contents.length()
315+
&& contents.substring(endPosition, endPosition + sep.length() * 2)
316+
.equals(sep + sep);
255317
}
256318

257319
private static boolean isUnused(
258320
JCCompilationUnit unit,
259321
Set<String> usedNames,
260322
Multimap<String, Range<Integer>> usedInJavadoc,
261-
ImportTree importTree,
323+
JCTree importTree,
262324
String simpleName) {
263-
String qualifier = ((JCFieldAccess) importTree.getQualifiedIdentifier())
264-
.getExpression()
265-
.toString();
325+
JCFieldAccess qualifiedIdentifier = getQualifiedIdentifier(importTree);
326+
String qualifier = qualifiedIdentifier.getExpression().toString();
266327
if (qualifier.equals("java.lang")) {
267328
return true;
268329
}
330+
if (usedNames.contains(simpleName)) {
331+
return false;
332+
}
269333
if (unit.getPackageName() != null && unit.getPackageName().toString().equals(qualifier)) {
270334
return true;
271335
}
272-
if (importTree.getQualifiedIdentifier() instanceof JCFieldAccess
273-
&& ((JCFieldAccess) importTree.getQualifiedIdentifier())
274-
.getIdentifier()
275-
.contentEquals("*")) {
336+
if (qualifiedIdentifier.getIdentifier().contentEquals("*") && !((JCImport) importTree).isStatic()) {
276337
return false;
277338
}
339+
return !usedInJavadoc.containsKey(simpleName);
340+
}
278341

279-
if (usedNames.contains(simpleName)) {
280-
return false;
342+
private static JCFieldAccess getQualifiedIdentifier(JCTree importTree) {
343+
// Use reflection because the return type is JCTree in some versions and JCFieldAccess in others
344+
try {
345+
return (JCFieldAccess)
346+
JCImport.class.getMethod("getQualifiedIdentifier").invoke(importTree);
347+
} catch (ReflectiveOperationException e) {
348+
throw new RuntimeException(e);
281349
}
282-
if (usedInJavadoc.containsKey(simpleName)) {
283-
return false;
284-
}
285-
return true;
286350
}
287351

288352
/** Applies the replacements to the given source, and re-format any edited javadoc. */
289353
private static String applyReplacements(String source, RangeMap<Integer, String> replacements) {
290-
// save non-empty fixed ranges for reformatting after fixes are applied
291-
RangeSet<Integer> fixedRanges = TreeRangeSet.create();
292-
293354
// Apply the fixes in increasing order, adjusting ranges to account for
294355
// earlier fixes that change the length of the source. The output ranges are
295356
// needed so we can reformat fixed regions, otherwise the fixes could just
@@ -299,14 +360,11 @@ private static String applyReplacements(String source, RangeMap<Integer, String>
299360
for (Map.Entry<Range<Integer>, String> replacement :
300361
replacements.asMapOfRanges().entrySet()) {
301362
Range<Integer> range = replacement.getKey();
302-
String replaceWith = replacement.getValue();
303-
int start = offset + range.lowerEndpoint();
304-
int end = offset + range.upperEndpoint();
305-
sb.replace(start, end, replaceWith);
306-
if (!replaceWith.isEmpty()) {
307-
fixedRanges.add(Range.closedOpen(start, end));
363+
if (replacement.getValue().isBlank()) {
364+
String replaceWith = replacement.getValue();
365+
sb.replace(offset + range.lowerEndpoint(), offset + range.upperEndpoint(), replaceWith);
366+
offset += replaceWith.length() - (range.upperEndpoint() - range.lowerEndpoint());
308367
}
309-
offset += replaceWith.length() - (range.upperEndpoint() - range.lowerEndpoint());
310368
}
311369
return sb.toString();
312370
}

0 commit comments

Comments
 (0)