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 ;
36
40
import com .sun .source .util .TreePathScanner ;
37
41
import com .sun .source .util .TreeScanner ;
38
42
import com .sun .tools .javac .api .JavacTrees ;
43
+ import com .sun .tools .javac .file .JavacFileManager ;
44
+ import com .sun .tools .javac .parser .ParserFactory ;
39
45
import com .sun .tools .javac .tree .DCTree ;
40
46
import com .sun .tools .javac .tree .DCTree .DCReference ;
41
47
import com .sun .tools .javac .tree .JCTree ;
42
48
import com .sun .tools .javac .tree .JCTree .JCCompilationUnit ;
43
49
import com .sun .tools .javac .tree .JCTree .JCFieldAccess ;
44
- import com .sun .tools .javac .tree .JCTree .JCIdent ;
45
50
import com .sun .tools .javac .tree .JCTree .JCImport ;
46
51
import com .sun .tools .javac .util .Context ;
52
+ import com .sun .tools .javac .util .Log ;
47
53
import com .sun .tools .javac .util .Options ;
54
+ import java .io .IOError ;
55
+ import java .io .IOException ;
48
56
import java .lang .reflect .Method ;
57
+ import java .net .URI ;
49
58
import java .util .LinkedHashSet ;
50
59
import java .util .List ;
51
60
import java .util .Map ;
52
61
import java .util .Set ;
62
+ import javax .tools .DiagnosticCollector ;
63
+ import javax .tools .DiagnosticListener ;
64
+ import javax .tools .JavaFileObject ;
65
+ import javax .tools .SimpleJavaFileObject ;
66
+ import javax .tools .StandardLocation ;
53
67
54
68
/**
55
69
* Removes unused imports from a source file. Imports that are only used in javadoc are also removed, and the references
@@ -76,15 +90,12 @@ public class RemoveUnusedImports {
76
90
private static final class UnusedImportScanner extends TreePathScanner <Void , Void > {
77
91
78
92
private final Set <String > usedNames = new LinkedHashSet <>();
79
-
80
93
private final Multimap <String , Range <Integer >> usedInJavadoc = HashMultimap .create ();
81
-
82
- final JavacTrees trees ;
83
- final DocTreeScanner docTreeSymbolScanner ;
94
+ private final DocTreeScanner docTreeSymbolScanner = new DocTreeScanner ();
95
+ private final JavacTrees trees ;
84
96
85
97
private UnusedImportScanner (JavacTrees trees ) {
86
98
this .trees = trees ;
87
- docTreeSymbolScanner = new DocTreeScanner ();
88
99
}
89
100
90
101
/** Skip the imports themselves when checking for usage. */
@@ -202,21 +213,50 @@ public Void visitIdentifier(IdentifierTree node, Void aVoid) {
202
213
}
203
214
}
204
215
205
- public static String removeUnusedImports (final String contents ) throws FormatterException {
216
+ public static String removeUnusedImports (final String contents ) {
206
217
Context context = new Context ();
207
218
JCCompilationUnit unit = parse (context , contents );
208
- if (unit == null ) {
209
- // error handling is done during formatting
210
- return contents ;
211
- }
212
219
UnusedImportScanner scanner = new UnusedImportScanner (JavacTrees .instance (context ));
213
220
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 , "$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)" , "$1" + sep + sep );
232
+
233
+ // Remove multiple blank lines elsewhere in imports section
234
+ s = s .replaceAll ("(?m)^(import .+)" + sep + "\\ s+" + sep + "(?=import)" , "$1" + sep );
235
+
236
+ return s ;
215
237
}
216
238
217
- private static JCCompilationUnit parse (Context context , String javaInput ) throws FormatterException {
239
+ private static JCCompilationUnit parse (Context context , String javaInput ) {
240
+ context .put (DiagnosticListener .class , new DiagnosticCollector <JavaFileObject >());
241
+ Options .instance (context ).put ("--enable-preview" , "true" );
218
242
Options .instance (context ).put ("allowStringFolding" , "false" );
219
- return Formatter .parseJcCompilationUnit (context , javaInput );
243
+ try (JavacFileManager fileManager = new JavacFileManager (context , true , UTF_8 )) {
244
+ fileManager .setLocation (StandardLocation .PLATFORM_CLASS_PATH , ImmutableList .of ());
245
+ } catch (IOException e ) {
246
+ throw new IOError (e );
247
+ }
248
+ SimpleJavaFileObject source = new SimpleJavaFileObject (URI .create ("source" ), JavaFileObject .Kind .SOURCE ) {
249
+ @ Override
250
+ public CharSequence getCharContent (boolean ignoreEncodingErrors ) {
251
+ return javaInput ;
252
+ }
253
+ };
254
+ Log .instance (context ).useSource (source );
255
+ JCCompilationUnit unit = ParserFactory .instance (context )
256
+ .newParser (javaInput , true , true , true )
257
+ .parseCompilationUnit ();
258
+ unit .sourcefile = source ;
259
+ return unit ;
220
260
}
221
261
222
262
/** Construct replacements to fix unused imports. */
@@ -226,63 +266,80 @@ private static RangeMap<Integer, String> buildReplacements(
226
266
Set <String > usedNames ,
227
267
Multimap <String , Range <Integer >> usedInJavadoc ) {
228
268
RangeMap <Integer , String > replacements = TreeRangeMap .create ();
229
- for (JCImport importTree : unit .getImports ()) {
230
- String simpleName = getSimpleName (importTree );
269
+ int size = unit .getImports ().size ();
270
+ JCTree lastImport = size > 0 ? unit .getImports ().get (size - 1 ) : null ;
271
+ for (JCTree importTree : unit .getImports ()) {
272
+ String simpleName =
273
+ getQualifiedIdentifier (importTree ).getIdentifier ().toString ();
231
274
if (!isUnused (unit , usedNames , usedInJavadoc , importTree , simpleName )) {
232
275
continue ;
233
276
}
234
277
// delete the import
235
278
int endPosition = importTree .getEndPosition (unit .endPositions );
236
- endPosition = Math . max (CharMatcher .isNot (' ' ).indexIn (contents , endPosition ), endPosition );
279
+ endPosition = max (CharMatcher .isNot (' ' ).indexIn (contents , endPosition ), endPosition );
237
280
String sep = Newlines .guessLineSeparator (contents );
281
+
282
+ // Check if there's an empty line after this import
283
+ boolean hasEmptyLineAfter = false ;
284
+ if (endPosition + sep .length () * 2 <= contents .length ()) {
285
+ String nextTwoLines = contents .substring (endPosition , endPosition + sep .length () * 2 );
286
+ hasEmptyLineAfter = nextTwoLines .equals (sep + sep );
287
+ }
288
+
238
289
if (endPosition + sep .length () < contents .length ()
239
290
&& contents .subSequence (endPosition , endPosition + sep .length ())
240
291
.toString ()
241
292
.equals (sep )) {
242
293
endPosition += sep .length ();
243
294
}
295
+
296
+ // If this isn't the last import and there's an empty line after, preserve it
297
+ if ((size == 1 || importTree != lastImport ) && !hasEmptyLineAfter ) {
298
+ while (endPosition + sep .length () <= contents .length ()
299
+ && contents .regionMatches (endPosition , sep , 0 , sep .length ())) {
300
+ endPosition += sep .length ();
301
+ }
302
+ }
244
303
replacements .put (Range .closedOpen (importTree .getStartPosition (), endPosition ), "" );
245
304
}
246
305
return replacements ;
247
306
}
248
307
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 ();
255
- }
256
-
257
308
private static boolean isUnused (
258
309
JCCompilationUnit unit ,
259
310
Set <String > usedNames ,
260
311
Multimap <String , Range <Integer >> usedInJavadoc ,
261
- ImportTree importTree ,
312
+ JCTree importTree ,
262
313
String simpleName ) {
263
- String qualifier = ((JCFieldAccess ) importTree .getQualifiedIdentifier ())
264
- .getExpression ()
265
- .toString ();
314
+ JCFieldAccess qualifiedIdentifier = getQualifiedIdentifier (importTree );
315
+ String qualifier = qualifiedIdentifier .getExpression ().toString ();
266
316
if (qualifier .equals ("java.lang" )) {
267
317
return true ;
268
318
}
319
+ if (usedNames .contains (simpleName )) {
320
+ return false ;
321
+ }
269
322
if (unit .getPackageName () != null && unit .getPackageName ().toString ().equals (qualifier )) {
270
323
return true ;
271
324
}
272
- if (importTree .getQualifiedIdentifier () instanceof JCFieldAccess
273
- && ((JCFieldAccess ) importTree .getQualifiedIdentifier ())
274
- .getIdentifier ()
275
- .contentEquals ("*" )) {
325
+ if (qualifiedIdentifier .getIdentifier ().contentEquals ("*" ) && !((JCImport ) importTree ).isStatic ()) {
276
326
return false ;
277
327
}
278
328
279
329
if (usedNames .contains (simpleName )) {
280
330
return false ;
281
331
}
282
- if (usedInJavadoc .containsKey (simpleName )) {
283
- return false ;
332
+ return !usedInJavadoc .containsKey (simpleName );
333
+ }
334
+
335
+ private static JCFieldAccess getQualifiedIdentifier (JCTree importTree ) {
336
+ // Use reflection because the return type is JCTree in some versions and JCFieldAccess in others
337
+ try {
338
+ return (JCFieldAccess )
339
+ JCImport .class .getMethod ("getQualifiedIdentifier" ).invoke (importTree );
340
+ } catch (ReflectiveOperationException e ) {
341
+ throw new LinkageError (e .getMessage (), e );
284
342
}
285
- return true ;
286
343
}
287
344
288
345
/** Applies the replacements to the given source, and re-format any edited javadoc. */
0 commit comments