1616
1717package com .palantir .javaformat .java ;
1818
19+ import static java .lang .Math .max ;
20+ import static java .nio .charset .StandardCharsets .UTF_8 ;
21+
1922import com .google .common .base .CharMatcher ;
2023import com .google .common .collect .HashMultimap ;
24+ import com .google .common .collect .ImmutableList ;
25+ import com .google .common .collect .Iterables ;
2126import com .google .common .collect .Multimap ;
2227import com .google .common .collect .Range ;
2328import com .google .common .collect .RangeMap ;
2429import com .google .common .collect .RangeSet ;
2530import com .google .common .collect .TreeRangeMap ;
2631import com .google .common .collect .TreeRangeSet ;
2732import com .palantir .javaformat .Newlines ;
33+ import com .palantir .javaformat .java .FormatterException ;
2834import com .sun .source .doctree .DocCommentTree ;
2935import com .sun .source .doctree .ReferenceTree ;
3036import com .sun .source .tree .CaseTree ;
3642import com .sun .source .util .TreePathScanner ;
3743import com .sun .source .util .TreeScanner ;
3844import com .sun .tools .javac .api .JavacTrees ;
45+ import com .sun .tools .javac .file .JavacFileManager ;
46+ import com .sun .tools .javac .parser .JavacParser ;
47+ import com .sun .tools .javac .parser .ParserFactory ;
3948import com .sun .tools .javac .tree .DCTree ;
4049import com .sun .tools .javac .tree .DCTree .DCReference ;
4150import com .sun .tools .javac .tree .JCTree ;
4251import com .sun .tools .javac .tree .JCTree .JCCompilationUnit ;
4352import com .sun .tools .javac .tree .JCTree .JCFieldAccess ;
44- import com .sun .tools .javac .tree .JCTree .JCIdent ;
4553import com .sun .tools .javac .tree .JCTree .JCImport ;
4654import com .sun .tools .javac .util .Context ;
55+ import com .sun .tools .javac .util .Log ;
4756import com .sun .tools .javac .util .Options ;
57+ import java .io .IOError ;
58+ import java .io .IOException ;
4859import java .lang .reflect .Method ;
60+ import java .net .URI ;
4961import java .util .LinkedHashSet ;
5062import java .util .List ;
5163import java .util .Map ;
5264import java .util .Set ;
65+ import javax .tools .Diagnostic ;
66+ import javax .tools .DiagnosticCollector ;
67+ import javax .tools .DiagnosticListener ;
68+ import javax .tools .JavaFileObject ;
69+ import javax .tools .SimpleJavaFileObject ;
70+ import javax .tools .StandardLocation ;
5371
5472/**
55- * Removes unused imports from a source file. Imports that are only used in javadoc are also removed, and the references
56- * in javadoc are replaced with fully qualified names.
73+ * Removes unused imports from a source file. Imports that are only used in javadoc are also
74+ * removed, and the references in javadoc are replaced with fully qualified names.
5775 */
5876public class RemoveUnusedImports {
5977
@@ -73,12 +91,10 @@ public class RemoveUnusedImports {
7391 // This is still reasonably effective in practice because type names differ
7492 // from other kinds of names in casing convention, and simple name
7593 // clashes between imported and declared types are rare.
76- private static final class UnusedImportScanner extends TreePathScanner <Void , Void > {
94+ private static class UnusedImportScanner extends TreePathScanner <Void , Void > {
7795
7896 private final Set <String > usedNames = new LinkedHashSet <>();
79-
8097 private final Multimap <String , Range <Integer >> usedInJavadoc = HashMultimap .create ();
81-
8298 final JavacTrees trees ;
8399 final DocTreeScanner docTreeSymbolScanner ;
84100
@@ -102,7 +118,7 @@ public Void visitIdentifier(IdentifierTree tree, Void unused) {
102118 return null ;
103119 }
104120
105- // TODO(fwindheuser ): remove this override when pattern matching in switch is no longer a preview
121+ // TODO(cushon ): remove this override when pattern matching in switch is no longer a preview
106122 // feature, and TreePathScanner visits CaseTree#getLabels instead of CaseTree#getExpressions
107123 @ SuppressWarnings ("unchecked" ) // reflection
108124 @ Override
@@ -111,15 +127,14 @@ public Void visitCase(CaseTree tree, Void unused) {
111127 try {
112128 scan ((List <? extends Tree >) CASE_TREE_GET_LABELS .invoke (tree ), null );
113129 } catch (ReflectiveOperationException e ) {
114- throw new RuntimeException (e .getMessage (), e );
130+ throw new LinkageError (e .getMessage (), e );
115131 }
116132 }
117133 return super .visitCase (tree , null );
118134 }
119135
120136 private static final Method CASE_TREE_GET_LABELS = caseTreeGetLabels ();
121137
122- @ SuppressWarnings ("for-rollout:NullAway" )
123138 private static Method caseTreeGetLabels () {
124139 try {
125140 return CaseTree .class .getMethod ("getLabels" );
@@ -128,7 +143,6 @@ private static Method caseTreeGetLabels() {
128143 }
129144 }
130145
131- @ SuppressWarnings ("for-rollout:VoidUsed" )
132146 @ Override
133147 public Void scan (Tree tree , Void unused ) {
134148 if (tree == null ) {
@@ -159,9 +173,10 @@ public Void visitIdentifier(com.sun.source.doctree.IdentifierTree node, Void aVo
159173 @ Override
160174 public Void visitReference (ReferenceTree referenceTree , Void unused ) {
161175 DCReference reference = (DCReference ) referenceTree ;
162- long basePos = reference
163- .pos ((DCTree .DCDocComment ) getCurrentPath ().getDocComment ())
164- .getStartPosition ();
176+ long basePos =
177+ reference
178+ .pos ((DCTree .DCDocComment ) getCurrentPath ().getDocComment ())
179+ .getStartPosition ();
165180 // the position of trees inside the reference node aren't stored, but the qualifier's
166181 // start position is the beginning of the reference node
167182 if (reference .qualifierExpression != null ) {
@@ -186,37 +201,78 @@ public ReferenceScanner(long basePos) {
186201 this .basePos = basePos ;
187202 }
188203
189- @ SuppressWarnings ("for-rollout:VoidUsed" )
190204 @ Override
191205 public Void visitIdentifier (IdentifierTree node , Void aVoid ) {
192206 usedInJavadoc .put (
193207 node .getName ().toString (),
194208 basePos != -1
195- ? Range .closedOpen (
196- (int ) basePos ,
197- (int ) basePos + node .getName ().length ())
209+ ? Range .closedOpen ((int ) basePos , (int ) basePos + node .getName ().length ())
198210 : null );
199211 return super .visitIdentifier (node , aVoid );
200212 }
201213 }
202214 }
203215 }
204-
205216 public static String removeUnusedImports (final String contents ) throws FormatterException {
206217 Context context = new Context ();
207218 JCCompilationUnit unit = parse (context , contents );
208- if (unit == null ) {
209- // error handling is done during formatting
210- return contents ;
211- }
212219 UnusedImportScanner scanner = new UnusedImportScanner (JavacTrees .instance (context ));
213220 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 , "package $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)" ,
232+ "import $1" + sep + sep );
233+
234+ // Remove multiple blank lines elsewhere in imports section
235+ s = s .replaceAll ("(?m)^import .+" + sep + "\\ s+" + sep + "(?=import)" , "import $1" + sep );
236+
237+ return s ;
215238 }
216239
217- private static JCCompilationUnit parse (Context context , String javaInput ) throws FormatterException {
240+ private static JCCompilationUnit parse (Context context , String javaInput )
241+ throws FormatterException {
242+ DiagnosticCollector <JavaFileObject > diagnostics = new DiagnosticCollector <>();
243+ context .put (DiagnosticListener .class , diagnostics );
244+ Options .instance (context ).put ("--enable-preview" , "true" );
218245 Options .instance (context ).put ("allowStringFolding" , "false" );
219- return Formatter .parseJcCompilationUnit (context , javaInput );
246+ JCCompilationUnit unit ;
247+ try (JavacFileManager fileManager = new JavacFileManager (context , true , UTF_8 )){
248+ fileManager .setLocation (StandardLocation .PLATFORM_CLASS_PATH , ImmutableList .of ());
249+ } catch (IOException e ) {
250+ // impossible
251+ throw new IOError (e );
252+ }
253+ SimpleJavaFileObject source =
254+ new SimpleJavaFileObject (URI .create ("source" ), JavaFileObject .Kind .SOURCE ) {
255+ @ Override
256+ public CharSequence getCharContent (boolean ignoreEncodingErrors ) throws IOException {
257+ return javaInput ;
258+ }
259+ };
260+ Log .instance (context ).useSource (source );
261+ ParserFactory parserFactory = ParserFactory .instance (context );
262+ JavacParser parser =
263+ parserFactory .newParser (
264+ javaInput ,
265+ /* keepDocComments= */ true ,
266+ /* keepEndPos= */ true ,
267+ /* keepLineMap= */ true );
268+ unit = parser .parseCompilationUnit ();
269+ unit .sourcefile = source ;
270+ Iterable <Diagnostic <? extends JavaFileObject >> errorDiagnostics =
271+ Iterables .filter (diagnostics .getDiagnostics (), Formatter ::errorDiagnostic );
272+ if (!Iterables .isEmpty (errorDiagnostics )) {
273+ // error handling is done during formatting
274+ }
275+ return unit ;
220276 }
221277
222278 /** Construct replacements to fix unused imports. */
@@ -226,53 +282,63 @@ private static RangeMap<Integer, String> buildReplacements(
226282 Set <String > usedNames ,
227283 Multimap <String , Range <Integer >> usedInJavadoc ) {
228284 RangeMap <Integer , String > replacements = TreeRangeMap .create ();
229- for (JCImport importTree : unit .getImports ()) {
285+ int size = unit .getImports ().size ();
286+ JCTree lastImport = size > 0 ? unit .getImports ().get (size - 1 ) : null ;
287+ for (JCTree importTree : unit .getImports ()) {
230288 String simpleName = getSimpleName (importTree );
231289 if (!isUnused (unit , usedNames , usedInJavadoc , importTree , simpleName )) {
232290 continue ;
233291 }
234292 // delete the import
235293 int endPosition = importTree .getEndPosition (unit .endPositions );
236- endPosition = Math . max (CharMatcher .isNot (' ' ).indexIn (contents , endPosition ), endPosition );
294+ endPosition = max (CharMatcher .isNot (' ' ).indexIn (contents , endPosition ), endPosition );
237295 String sep = Newlines .guessLineSeparator (contents );
296+
297+ // Check if there's an empty line after this import
298+ boolean hasEmptyLineAfter = false ;
299+ if (endPosition + sep .length () * 2 <= contents .length ()) {
300+ String nextTwoLines = contents .substring (endPosition , endPosition + sep .length () * 2 );
301+ hasEmptyLineAfter = nextTwoLines .equals (sep + sep );
302+ }
303+
238304 if (endPosition + sep .length () < contents .length ()
239- && contents .subSequence (endPosition , endPosition + sep .length ())
240- .toString ()
241- .equals (sep )) {
305+ && contents .subSequence (endPosition , endPosition + sep .length ()).toString ().equals (sep )) {
242306 endPosition += sep .length ();
243307 }
308+
309+ // If this isn't the last import and there's an empty line after, preserve it
310+ if ((size == 1 || importTree != lastImport ) && !hasEmptyLineAfter ) {
311+ while (endPosition + sep .length () <= contents .length ()
312+ && contents .regionMatches (endPosition , sep , 0 , sep .length ())) {
313+ endPosition += sep .length ();
314+ }
315+ }
244316 replacements .put (Range .closedOpen (importTree .getStartPosition (), endPosition ), "" );
245317 }
246318 return replacements ;
247319 }
248-
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 ();
320+ private static String getSimpleName (JCTree importTree ) {
321+ return getQualifiedIdentifier (importTree ).getIdentifier ().toString ();
255322 }
256323
257324 private static boolean isUnused (
258325 JCCompilationUnit unit ,
259326 Set <String > usedNames ,
260327 Multimap <String , Range <Integer >> usedInJavadoc ,
261- ImportTree importTree ,
328+ JCTree importTree ,
262329 String simpleName ) {
263- String qualifier = ((JCFieldAccess ) importTree .getQualifiedIdentifier ())
264- .getExpression ()
265- .toString ();
330+ JCFieldAccess qualifiedIdentifier = getQualifiedIdentifier (importTree );
331+ String qualifier = qualifiedIdentifier .getExpression ().toString ();
266332 if (qualifier .equals ("java.lang" )) {
267333 return true ;
268334 }
335+ if (usedNames .contains (simpleName )){
336+ return false ;
337+ }
269338 if (unit .getPackageName () != null && unit .getPackageName ().toString ().equals (qualifier )) {
270339 return true ;
271340 }
272- if (importTree .getQualifiedIdentifier () instanceof JCFieldAccess
273- && ((JCFieldAccess ) importTree .getQualifiedIdentifier ())
274- .getIdentifier ()
275- .contentEquals ("*" )) {
341+ if (qualifiedIdentifier .getIdentifier ().contentEquals ("*" ) && !((JCImport ) importTree ).isStatic ()) {
276342 return false ;
277343 }
278344
@@ -285,6 +351,15 @@ private static boolean isUnused(
285351 return true ;
286352 }
287353
354+ private static JCFieldAccess getQualifiedIdentifier (JCTree importTree ) {
355+ // Use reflection because the return type is JCTree in some versions and JCFieldAccess in others
356+ try {
357+ return (JCFieldAccess ) JCImport .class .getMethod ("getQualifiedIdentifier" ).invoke (importTree );
358+ } catch (ReflectiveOperationException e ) {
359+ throw new LinkageError (e .getMessage (), e );
360+ }
361+ }
362+
288363 /** Applies the replacements to the given source, and re-format any edited javadoc. */
289364 private static String applyReplacements (String source , RangeMap <Integer , String > replacements ) {
290365 // save non-empty fixed ranges for reformatting after fixes are applied
@@ -296,8 +371,7 @@ private static String applyReplacements(String source, RangeMap<Integer, String>
296371 // be applied in descending order without adjusting offsets.
297372 StringBuilder sb = new StringBuilder (source );
298373 int offset = 0 ;
299- for (Map .Entry <Range <Integer >, String > replacement :
300- replacements .asMapOfRanges ().entrySet ()) {
374+ for (Map .Entry <Range <Integer >, String > replacement : replacements .asMapOfRanges ().entrySet ()) {
301375 Range <Integer > range = replacement .getKey ();
302376 String replaceWith = replacement .getValue ();
303377 int start = offset + range .lowerEndpoint ();
@@ -310,4 +384,4 @@ private static String applyReplacements(String source, RangeMap<Integer, String>
310384 }
311385 return sb .toString ();
312386 }
313- }
387+ }
0 commit comments