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
+ String s = applyReplacements (
220
+ contents , buildReplacements (contents , unit , scanner .usedNames , scanner .usedInJavadoc ));
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
+ return s ;
215
235
}
216
236
217
- private static JCCompilationUnit parse (Context context , String javaInput ) throws FormatterException {
237
+ private static JCCompilationUnit parse (Context context , String javaInput ) {
238
+ context .put (DiagnosticListener .class , new DiagnosticCollector <JavaFileObject >());
239
+ Options .instance (context ).put ("--enable-preview" , "true" );
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,94 @@ 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
+ }
305
+ if ((size == 1 || importTree != lastImport ) && !checkForEmptyLineAfter (contents , endPosition , sep )) {
306
+ while (endPosition + sep .length () <= contents .length ()
307
+ && contents .regionMatches (endPosition , sep , 0 , sep .length ())) {
242
308
endPosition += sep .length ();
243
309
}
244
- replacements .put (Range .closedOpen (importTree .getStartPosition (), endPosition ), "" );
245
310
}
246
- return replacements ;
311
+ return endPosition ;
247
312
}
248
313
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 ();
314
+ private static boolean checkForEmptyLineAfter (String contents , int endPosition , String sep ) {
315
+ return endPosition + sep .length () * 2 <= contents .length ()
316
+ && contents .substring (endPosition , endPosition + sep .length () * 2 )
317
+ .equals (sep + sep );
255
318
}
256
319
257
320
private static boolean isUnused (
258
321
JCCompilationUnit unit ,
259
322
Set <String > usedNames ,
260
323
Multimap <String , Range <Integer >> usedInJavadoc ,
261
- ImportTree importTree ,
324
+ JCTree importTree ,
262
325
String simpleName ) {
263
- String qualifier = ((JCFieldAccess ) importTree .getQualifiedIdentifier ())
264
- .getExpression ()
265
- .toString ();
326
+ JCFieldAccess qualifiedIdentifier = getQualifiedIdentifier (importTree );
327
+ String qualifier = qualifiedIdentifier .getExpression ().toString ();
266
328
if (qualifier .equals ("java.lang" )) {
267
329
return true ;
268
330
}
331
+ if (usedNames .contains (simpleName )) {
332
+ return false ;
333
+ }
269
334
if (unit .getPackageName () != null && unit .getPackageName ().toString ().equals (qualifier )) {
270
335
return true ;
271
336
}
272
- if (importTree .getQualifiedIdentifier () instanceof JCFieldAccess
273
- && ((JCFieldAccess ) importTree .getQualifiedIdentifier ())
274
- .getIdentifier ()
275
- .contentEquals ("*" )) {
337
+ if (qualifiedIdentifier .getIdentifier ().contentEquals ("*" ) && !((JCImport ) importTree ).isStatic ()) {
276
338
return false ;
277
339
}
340
+ return !usedInJavadoc .containsKey (simpleName );
341
+ }
278
342
279
- if (usedNames .contains (simpleName )) {
280
- return false ;
343
+ private static JCFieldAccess getQualifiedIdentifier (JCTree importTree ) {
344
+ // Use reflection because the return type is JCTree in some versions and JCFieldAccess in others
345
+ try {
346
+ return (JCFieldAccess )
347
+ JCImport .class .getMethod ("getQualifiedIdentifier" ).invoke (importTree );
348
+ } catch (ReflectiveOperationException e ) {
349
+ throw new RuntimeException (e );
281
350
}
282
- if (usedInJavadoc .containsKey (simpleName )) {
283
- return false ;
284
- }
285
- return true ;
286
351
}
287
352
288
353
/** Applies the replacements to the given source, and re-format any edited javadoc. */
289
354
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
355
// Apply the fixes in increasing order, adjusting ranges to account for
294
356
// earlier fixes that change the length of the source. The output ranges are
295
357
// needed so we can reformat fixed regions, otherwise the fixes could just
@@ -300,12 +362,7 @@ private static String applyReplacements(String source, RangeMap<Integer, String>
300
362
replacements .asMapOfRanges ().entrySet ()) {
301
363
Range <Integer > range = replacement .getKey ();
302
364
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 ));
308
- }
365
+ sb .replace (offset + range .lowerEndpoint (), offset + range .upperEndpoint (), replaceWith );
309
366
offset += replaceWith .length () - (range .upperEndpoint () - range .lowerEndpoint ());
310
367
}
311
368
return sb .toString ();
0 commit comments