@@ -173,9 +173,9 @@ public Void visitIdentifier(com.sun.source.doctree.IdentifierTree node, Void aVo
173173 public Void visitReference (ReferenceTree referenceTree , Void unused ) {
174174 DCReference reference = (DCReference ) referenceTree ;
175175 long basePos =
176- reference
177- .pos ((DCTree .DCDocComment ) getCurrentPath ().getDocComment ())
178- .getStartPosition ();
176+ reference
177+ .pos ((DCTree .DCDocComment ) getCurrentPath ().getDocComment ())
178+ .getStartPosition ();
179179 // the position of trees inside the reference node aren't stored, but the qualifier's
180180 // start position is the beginning of the reference node
181181 if (reference .qualifierExpression != null ) {
@@ -203,62 +203,71 @@ public ReferenceScanner(long basePos) {
203203 @ Override
204204 public Void visitIdentifier (IdentifierTree node , Void aVoid ) {
205205 usedInJavadoc .put (
206- node .getName ().toString (),
207- basePos != -1
208- ? Range .closedOpen ((int ) basePos , (int ) basePos + node .getName ().length ())
209- : null );
206+ node .getName ().toString (),
207+ basePos != -1
208+ ? Range .closedOpen ((int ) basePos , (int ) basePos + node .getName ().length ())
209+ : null );
210210 return super .visitIdentifier (node , aVoid );
211211 }
212212 }
213213 }
214214 }
215-
216215 public static String removeUnusedImports (final String contents ) throws FormatterException {
217216 Context context = new Context ();
218217 JCCompilationUnit unit = parse (context , contents );
219- if (unit == null ) {
220- // error handling is done during formatting
221- return contents ;
222- }
223218 UnusedImportScanner scanner = new UnusedImportScanner (JavacTrees .instance (context ));
224219 scanner .scan (unit , null );
225- return applyReplacements (
226- contents , buildReplacements (contents , unit , scanner .usedNames , scanner .usedInJavadoc ));
220+ String s = applyReplacements (
221+ contents , buildReplacements (contents , unit , scanner .usedNames , scanner .usedInJavadoc ));
222+
223+ // Normalize newlines while preserving important blank lines
224+ String sep = Newlines .guessLineSeparator (contents );
225+
226+ // Ensure exactly one blank line after package declaration
227+ s = s .replaceAll ("(?m)^package .+" + sep + "\\ s+" + sep , "package $1" + sep + sep );
228+
229+ // Ensure exactly one blank line between last import and class declaration
230+ s = s .replaceAll ("(?m)import .+" + sep + "\\ s+" + sep + "(?=class|interface|enum|record)" ,
231+ "import $1" + sep + sep );
232+
233+ // Remove multiple blank lines elsewhere in imports section
234+ s = s .replaceAll ("(?m)^import .+" + sep + "\\ s+" + sep + "(?=import)" , "import $1" + sep );
235+
236+ return s ;
227237 }
228238
229239 private static JCCompilationUnit parse (Context context , String javaInput )
230- throws FormatterException {
240+ throws FormatterException {
231241 DiagnosticCollector <JavaFileObject > diagnostics = new DiagnosticCollector <>();
232242 context .put (DiagnosticListener .class , diagnostics );
233243 Options .instance (context ).put ("--enable-preview" , "true" );
234244 Options .instance (context ).put ("allowStringFolding" , "false" );
235245 JCCompilationUnit unit ;
236- JavacFileManager fileManager = new JavacFileManager (context , true , UTF_8 );
237- try {
246+ try (JavacFileManager fileManager = new JavacFileManager (context , true , UTF_8 )){
238247 fileManager .setLocation (StandardLocation .PLATFORM_CLASS_PATH , ImmutableList .of ());
239248 } catch (IOException e ) {
240249 // impossible
241250 throw new IOError (e );
242251 }
243252 SimpleJavaFileObject source =
244- new SimpleJavaFileObject (URI .create ("source" ), JavaFileObject .Kind .SOURCE ) {
245- @ Override
246- public CharSequence getCharContent (boolean ignoreEncodingErrors ) throws IOException {
247- return javaInput ;
248- }
249- };
253+ new SimpleJavaFileObject (URI .create ("source" ), JavaFileObject .Kind .SOURCE ) {
254+ @ Override
255+ public CharSequence getCharContent (boolean ignoreEncodingErrors ) throws IOException {
256+ return javaInput ;
257+ }
258+ };
250259 Log .instance (context ).useSource (source );
251260 ParserFactory parserFactory = ParserFactory .instance (context );
252261 JavacParser parser =
253- parserFactory .newParser (
254- javaInput ,
255- /* keepDocComments= */ true ,
256- /* keepEndPos= */ true ,
257- /* keepLineMap= */ true );
262+ parserFactory .newParser (
263+ javaInput ,
264+ /* keepDocComments= */ true ,
265+ /* keepEndPos= */ true ,
266+ /* keepLineMap= */ true );
258267 unit = parser .parseCompilationUnit ();
259268 unit .sourcefile = source ;
260269 Iterable <Diagnostic <? extends JavaFileObject >> errorDiagnostics =
261- Iterables .filter (diagnostics .getDiagnostics (), Formatter ::errorDiagnostic );
270+ Iterables .filter (diagnostics .getDiagnostics (), Formatter ::errorDiagnostic );
262271 if (!Iterables .isEmpty (errorDiagnostics )) {
263272 // error handling is done during formatting
264273 throw FormatterException .fromJavacDiagnostics (errorDiagnostics );
@@ -268,11 +277,13 @@ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOExcept
268277
269278 /** Construct replacements to fix unused imports. */
270279 private static RangeMap <Integer , String > buildReplacements (
271- String contents ,
272- JCCompilationUnit unit ,
273- Set <String > usedNames ,
274- Multimap <String , Range <Integer >> usedInJavadoc ) {
280+ String contents ,
281+ JCCompilationUnit unit ,
282+ Set <String > usedNames ,
283+ Multimap <String , Range <Integer >> usedInJavadoc ) {
275284 RangeMap <Integer , String > replacements = TreeRangeMap .create ();
285+ int size = unit .getImports ().size ();
286+ JCTree lastImport = size > 0 ? unit .getImports ().get (size - 1 ) : null ;
276287 for (JCTree importTree : unit .getImports ()) {
277288 String simpleName = getSimpleName (importTree );
278289 if (!isUnused (unit , usedNames , usedInJavadoc , importTree , simpleName )) {
@@ -282,34 +293,52 @@ private static RangeMap<Integer, String> buildReplacements(
282293 int endPosition = importTree .getEndPosition (unit .endPositions );
283294 endPosition = max (CharMatcher .isNot (' ' ).indexIn (contents , endPosition ), endPosition );
284295 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+
285304 if (endPosition + sep .length () < contents .length ()
286- && contents .subSequence (endPosition , endPosition + sep .length ()).toString ().equals (sep )) {
305+ && contents .subSequence (endPosition , endPosition + sep .length ()).toString ().equals (sep )) {
287306 endPosition += sep .length ();
288307 }
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+ }
289316 replacements .put (Range .closedOpen (importTree .getStartPosition (), endPosition ), "" );
290317 }
291318 return replacements ;
292319 }
293-
294320 private static String getSimpleName (JCTree importTree ) {
295321 return getQualifiedIdentifier (importTree ).getIdentifier ().toString ();
296322 }
297323
298324 private static boolean isUnused (
299- JCCompilationUnit unit ,
300- Set <String > usedNames ,
301- Multimap <String , Range <Integer >> usedInJavadoc ,
302- JCTree importTree ,
303- String simpleName ) {
325+ JCCompilationUnit unit ,
326+ Set <String > usedNames ,
327+ Multimap <String , Range <Integer >> usedInJavadoc ,
328+ JCTree importTree ,
329+ String simpleName ) {
304330 JCFieldAccess qualifiedIdentifier = getQualifiedIdentifier (importTree );
305331 String qualifier = qualifiedIdentifier .getExpression ().toString ();
306332 if (qualifier .equals ("java.lang" )) {
307333 return true ;
308334 }
335+ if (usedNames .contains (simpleName )){
336+ return false ;
337+ }
309338 if (unit .getPackageName () != null && unit .getPackageName ().toString ().equals (qualifier )) {
310339 return true ;
311340 }
312- if (qualifiedIdentifier .getIdentifier ().contentEquals ("*" )) {
341+ if (qualifiedIdentifier .getIdentifier ().contentEquals ("*" ) && !(( JCImport ) importTree ). isStatic () ) {
313342 return false ;
314343 }
315344
@@ -355,4 +384,4 @@ private static String applyReplacements(String source, RangeMap<Integer, String>
355384 }
356385 return sb .toString ();
357386 }
358- }
387+ }
0 commit comments