16
16
17
17
package com .palantir .javaformat .java ;
18
18
19
+ import static java .lang .Math .max ;
20
+ import static java .nio .charset .StandardCharsets .UTF_8 ;
21
+
19
22
import com .google .common .base .CharMatcher ;
20
23
import com .google .common .collect .HashMultimap ;
24
+ import com .google .common .collect .ImmutableList ;
21
25
import com .google .common .collect .Multimap ;
22
26
import com .google .common .collect .Range ;
23
27
import com .google .common .collect .RangeMap ;
24
- import com .google .common .collect .RangeSet ;
25
28
import com .google .common .collect .TreeRangeMap ;
26
- import com .google .common .collect .TreeRangeSet ;
27
29
import com .palantir .javaformat .Newlines ;
28
30
import com .sun .source .doctree .DocCommentTree ;
29
31
import com .sun .source .doctree .ReferenceTree ;
36
38
import com .sun .source .util .TreePathScanner ;
37
39
import com .sun .source .util .TreeScanner ;
38
40
import com .sun .tools .javac .api .JavacTrees ;
41
+ import com .sun .tools .javac .file .JavacFileManager ;
42
+ import com .sun .tools .javac .parser .ParserFactory ;
39
43
import com .sun .tools .javac .tree .DCTree ;
40
44
import com .sun .tools .javac .tree .DCTree .DCReference ;
41
45
import com .sun .tools .javac .tree .JCTree ;
42
46
import com .sun .tools .javac .tree .JCTree .JCCompilationUnit ;
43
47
import com .sun .tools .javac .tree .JCTree .JCFieldAccess ;
44
- import com .sun .tools .javac .tree .JCTree .JCIdent ;
45
48
import com .sun .tools .javac .tree .JCTree .JCImport ;
46
49
import com .sun .tools .javac .util .Context ;
50
+ import com .sun .tools .javac .util .Log ;
47
51
import com .sun .tools .javac .util .Options ;
52
+ import java .io .IOException ;
48
53
import java .lang .reflect .Method ;
54
+ import java .net .URI ;
49
55
import java .util .LinkedHashSet ;
50
56
import java .util .List ;
51
57
import java .util .Map ;
52
58
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 ;
53
65
54
66
/**
55
67
* 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 {
76
88
private static final class UnusedImportScanner extends TreePathScanner <Void , Void > {
77
89
78
90
private final Set <String > usedNames = new LinkedHashSet <>();
79
-
80
91
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 ;
84
94
85
95
private UnusedImportScanner (JavacTrees trees ) {
86
96
this .trees = trees ;
87
- docTreeSymbolScanner = new DocTreeScanner ();
88
97
}
89
98
90
99
/** Skip the imports themselves when checking for usage. */
@@ -202,21 +211,50 @@ public Void visitIdentifier(IdentifierTree node, Void aVoid) {
202
211
}
203
212
}
204
213
205
- public static String removeUnusedImports (final String contents ) throws FormatterException {
214
+ public static String removeUnusedImports (final String contents ) {
206
215
Context context = new Context ();
207
216
JCCompilationUnit unit = parse (context , contents );
208
- if (unit == null ) {
209
- // error handling is done during formatting
210
- return contents ;
211
- }
212
217
UnusedImportScanner scanner = new UnusedImportScanner (JavacTrees .instance (context ));
213
218
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 ));
215
236
}
216
237
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 >());
218
240
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 ;
220
258
}
221
259
222
260
/** Construct replacements to fix unused imports. */
@@ -226,70 +264,93 @@ private static RangeMap<Integer, String> buildReplacements(
226
264
Set <String > usedNames ,
227
265
Multimap <String , Range <Integer >> usedInJavadoc ) {
228
266
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 ())) {
242
307
endPosition += sep .length ();
243
308
}
244
- replacements .put (Range .closedOpen (importTree .getStartPosition (), endPosition ), "" );
245
309
}
246
- return replacements ;
310
+ return endPosition ;
247
311
}
248
312
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 );
255
317
}
256
318
257
319
private static boolean isUnused (
258
320
JCCompilationUnit unit ,
259
321
Set <String > usedNames ,
260
322
Multimap <String , Range <Integer >> usedInJavadoc ,
261
- ImportTree importTree ,
323
+ JCTree importTree ,
262
324
String simpleName ) {
263
- String qualifier = ((JCFieldAccess ) importTree .getQualifiedIdentifier ())
264
- .getExpression ()
265
- .toString ();
325
+ JCFieldAccess qualifiedIdentifier = getQualifiedIdentifier (importTree );
326
+ String qualifier = qualifiedIdentifier .getExpression ().toString ();
266
327
if (qualifier .equals ("java.lang" )) {
267
328
return true ;
268
329
}
330
+ if (usedNames .contains (simpleName )) {
331
+ return false ;
332
+ }
269
333
if (unit .getPackageName () != null && unit .getPackageName ().toString ().equals (qualifier )) {
270
334
return true ;
271
335
}
272
- if (importTree .getQualifiedIdentifier () instanceof JCFieldAccess
273
- && ((JCFieldAccess ) importTree .getQualifiedIdentifier ())
274
- .getIdentifier ()
275
- .contentEquals ("*" )) {
336
+ if (qualifiedIdentifier .getIdentifier ().contentEquals ("*" ) && !((JCImport ) importTree ).isStatic ()) {
276
337
return false ;
277
338
}
339
+ return !usedInJavadoc .containsKey (simpleName );
340
+ }
278
341
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 );
281
349
}
282
- if (usedInJavadoc .containsKey (simpleName )) {
283
- return false ;
284
- }
285
- return true ;
286
350
}
287
351
288
352
/** Applies the replacements to the given source, and re-format any edited javadoc. */
289
353
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
-
293
354
// Apply the fixes in increasing order, adjusting ranges to account for
294
355
// earlier fixes that change the length of the source. The output ranges are
295
356
// 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>
299
360
for (Map .Entry <Range <Integer >, String > replacement :
300
361
replacements .asMapOfRanges ().entrySet ()) {
301
362
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 ());
308
367
}
309
- offset += replaceWith .length () - (range .upperEndpoint () - range .lowerEndpoint ());
310
368
}
311
369
return sb .toString ();
312
370
}
0 commit comments