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