@@ -130,7 +130,10 @@ private String reorderImports() throws FormatterException {
130130 private static final Comparator <Import > GOOGLE_IMPORT_COMPARATOR = Comparator .comparing (
131131 Import ::isStatic , trueFirst ())
132132 .thenComparing (Import ::isModule , trueFirst ())
133- .thenComparing (Import ::imported );
133+ .thenComparing (Import ::imported )
134+ // Imports that compare equal collapse into one, so two that are written differently -- one of them
135+ // carrying a comment, say -- must not compare equal, or that comment disappears with it.
136+ .thenComparing (Import ::declaration );
134137
135138 /**
136139 * A {@link Comparator} that orders {@link Import}s by AOSP Style, defined at
@@ -144,7 +147,8 @@ private String reorderImports() throws FormatterException {
144147 .thenComparing (Import ::isAndroid , trueFirst ())
145148 .thenComparing (Import ::isThirdParty , trueFirst ())
146149 .thenComparing (Import ::isJava , trueFirst ())
147- .thenComparing (Import ::imported );
150+ .thenComparing (Import ::imported )
151+ .thenComparing (Import ::declaration );
148152
149153 /**
150154 * Determines whether to insert a blank line between the {@code prev} and {@code curr} {@link Import}s based on
@@ -196,14 +200,22 @@ class Import {
196200 private final boolean isStatic ;
197201 private final boolean isModule ;
198202 private final String trailing ;
199- private final ImmutableList < String > comments ;
203+ private final String declaration ;
200204
201- Import (String imported , String trailing , boolean isStatic , boolean isModule , ImmutableList < String > comments ) {
205+ Import (String imported , String trailing , boolean isStatic , boolean isModule , String declaration ) {
202206 this .imported = imported ;
203207 this .trailing = trailing ;
204208 this .isStatic = isStatic ;
205209 this .isModule = isModule ;
206- this .comments = comments ;
210+ this .declaration = declaration ;
211+ }
212+
213+ /**
214+ * The declaration as it will be written: the {@code import} keyword through the semicolon, with whitespace
215+ * normalized and any comments left in the slot the author wrote them in.
216+ */
217+ String declaration () {
218+ return declaration ;
207219 }
208220
209221 /** The name being imported, for example {@code java.util.List}. */
@@ -262,45 +274,52 @@ public boolean isThirdParty() {
262274 @ Override
263275 public String toString () {
264276 StringBuilder sb = new StringBuilder ();
265- sb .append ("import " );
266- if (isModule ()) {
267- sb .append ("module " );
268- } else if (isStatic ()) {
269- sb .append ("static " );
270- }
271- sb .append (imported ()).append (';' );
272- StringBuilder tail = new StringBuilder ();
273- for (String comment : comments ) {
274- if (!endsInNewline (tail )) {
275- tail .append (' ' );
276- }
277- tail .append (comment );
278- if (comment .startsWith ("//" )) {
279- // A // comment swallows the rest of its line, so nothing may follow it there.
280- tail .append (lineSeparator );
281- }
282- }
283- String trailingText = trailing ();
284- if (endsInNewline (tail )) {
285- // Don't double the line terminator the trailing text already starts with.
286- int newline = Newlines .hasNewlineAt (trailingText , 0 );
287- if (newline > 0 ) {
288- trailingText = trailingText .substring (newline );
289- }
290- }
291- tail .append (trailingText );
292- if (tail .toString ().trim ().isEmpty ()) {
277+ sb .append (declaration ());
278+ if (trailing ().trim ().isEmpty ()) {
293279 sb .append (lineSeparator );
294280 } else {
295- sb .append (tail );
296- if (!endsInNewline (tail )) {
297- sb .append (lineSeparator );
298- }
281+ sb .append (trailing ());
299282 }
300283 return sb .toString ();
301284 }
302285 }
303286
287+ /**
288+ * Renders one import declaration from the toks it is made of. Whitespace between the toks is normalized, since the
289+ * style guide puts one import on a line of its own, but a comment stays in the slot the author wrote it in rather
290+ * than being moved to wherever the rendered declaration can accommodate it.
291+ */
292+ private final class Declaration {
293+ private final StringBuilder text = new StringBuilder ();
294+ private boolean atLineStart = false ;
295+
296+ /** Appends one tok: a keyword, an identifier, {@code .}, {@code *}, {@code ;}, or a comment. */
297+ void append (String piece ) {
298+ if (text .length () > 0 && !atLineStart && needsSpaceBefore (piece )) {
299+ text .append (' ' );
300+ }
301+ text .append (piece );
302+ atLineStart = false ;
303+ if (piece .startsWith ("//" )) {
304+ // A // comment swallows the rest of its line, so the declaration continues on the next one.
305+ text .append (lineSeparator );
306+ atLineStart = true ;
307+ }
308+ }
309+
310+ private boolean needsSpaceBefore (String piece ) {
311+ if (piece .equals ("." ) || piece .equals (";" ) || piece .equals ("*" )) {
312+ return false ;
313+ }
314+ return text .charAt (text .length () - 1 ) != '.' ;
315+ }
316+
317+ @ Override
318+ public String toString () {
319+ return text .toString ();
320+ }
321+ }
322+
304323 private String tokString (int start , int end ) {
305324 StringBuilder sb = new StringBuilder ();
306325 for (int i = start ; i < end ; i ++) {
@@ -341,31 +360,33 @@ private ImportsAndIndex scanImports(int i) throws FormatterException {
341360 // of our tests here and protects us from running off the end of the toks list. Since it is
342361 // zero-width it doesn't matter if we include it in our string concatenation at the end.
343362 while (i < toks .size () && tokenAt (i ).equals ("import" )) {
363+ Declaration declaration = new Declaration ();
364+ declaration .append (tokenAt (i ));
344365 i ++;
345- // Comments between the tokens of the import are collected and re-emitted after the
346- // semicolon, so nothing is dropped.
347- List <String > comments = new ArrayList <>();
348- i = skipIgnored (i , comments );
366+ i = skipIgnored (i , declaration );
349367 boolean isModule = isModuleKeyword (i );
350368 if (isModule ) {
369+ declaration .append (tokenAt (i ));
351370 i ++;
352- i = skipIgnored (i , comments );
371+ i = skipIgnored (i , declaration );
353372 }
354373 boolean isStatic = !isModule && tokenAt (i ).equals ("static" );
355374 if (isStatic ) {
375+ declaration .append (tokenAt (i ));
356376 i ++;
357- i = skipIgnored (i , comments );
377+ i = skipIgnored (i , declaration );
358378 }
359379 if (!isIdentifierToken (i )) {
360380 throw new FormatterException ("Unexpected token after import: " + tokenAt (i ));
361381 }
362- StringAndIndex imported = scanImported (i );
382+ StringAndIndex imported = scanImported (i , declaration );
363383 String importedName = imported .string ;
364384 i = imported .index ;
365- i = skipIgnored (i , comments );
385+ i = skipIgnored (i , declaration );
366386 if (!tokenAt (i ).equals (";" )) {
367387 throw new FormatterException ("Expected ; after import" );
368388 }
389+ declaration .append (";" );
369390 while (tokenAt (i ).equals (";" )) {
370391 // Extra semicolons are not allowed by the JLS but are accepted by javac.
371392 i ++;
@@ -393,8 +414,7 @@ private ImportsAndIndex scanImports(int i) throws FormatterException {
393414 i ++;
394415 }
395416 }
396- imports .add (
397- new Import (importedName , trailing .toString (), isStatic , isModule , ImmutableList .copyOf (comments )));
417+ imports .add (new Import (importedName , trailing .toString (), isStatic , isModule , declaration .toString ()));
398418 // Remember the position just after the import we just saw, before skipping blank lines.
399419 // If the next thing after the blank lines is not another import then we don't want to
400420 // include those blank lines in the text to be replaced.
@@ -437,32 +457,39 @@ private static class StringAndIndex {
437457 }
438458
439459 /**
440- * Scans the imported thing, the dot-separated name that comes after import [static] and before the semicolon. We
441- * don't allow spaces inside the dot-separated name. Wildcard imports are supported: if the input is {@code import
442- * java.util.*;} then the returned string will be {@code java.util.*}.
460+ * Scans the imported thing, the dot-separated name that comes after import [static] and before the semicolon.
461+ * Whitespace, line terminators and comments may appear between its parts, as they may anywhere else in the
462+ * declaration; the returned name contains none of them. Wildcard imports are supported: if the input is
463+ * {@code import java.util.*;} then the returned string will be {@code java.util.*}.
443464 *
444465 * @param start the index of the start of the identifier. If the import is {@code import java.util.List;} then this
445466 * index points to the token {@code java}.
467+ * @param declaration collects the toks scanned, so a comment between the parts of the name keeps its place
446468 * @return the parsed import ({@code java.util.List} in the example) and the index of the first token after the
447469 * imported thing ({@code ;} in the example).
448470 * @throws FormatterException if the imported name could not be parsed.
449471 */
450- private StringAndIndex scanImported (int start ) throws FormatterException {
472+ private StringAndIndex scanImported (int start , Declaration declaration ) throws FormatterException {
451473 int i = start ;
452474 StringBuilder imported = new StringBuilder ();
453475 // At the start of each iteration of this loop, i points to an identifier.
454476 // On exit from the loop, i points to a token after an identifier or after *.
455477 while (true ) {
456478 Preconditions .checkState (isIdentifierToken (i ));
457479 imported .append (tokenAt (i ));
480+ declaration .append (tokenAt (i ));
458481 i ++;
482+ i = skipIgnored (i , declaration );
459483 if (!tokenAt (i ).equals ("." )) {
460484 return new StringAndIndex (imported .toString (), i );
461485 }
462486 imported .append ('.' );
487+ declaration .append ("." );
463488 i ++;
489+ i = skipIgnored (i , declaration );
464490 if (tokenAt (i ).equals ("*" )) {
465491 imported .append ('*' );
492+ declaration .append ("*" );
466493 return new StringAndIndex (imported .toString (), i + 1 );
467494 } else if (!isIdentifierToken (i )) {
468495 throw new FormatterException ("Could not parse imported name, at: " + tokenAt (i ));
@@ -514,21 +541,21 @@ private boolean isModuleKeyword(int i) {
514541 if (!tokenAt (i ).equals ("module" )) {
515542 return false ;
516543 }
517- return isIdentifierToken (skipIgnored (i + 1 , new ArrayList <> ()));
544+ return isIdentifierToken (skipIgnored (i + 1 , new Declaration ()));
518545 }
519546
520547 /**
521- * Skips whitespace, line terminators and comments starting at {@code i}, appending the text of each comment to
522- * {@code comments }, and returns the index of the first token that is none of those. Javadoc comments are not
523- * skipped: the formatter moves them onto a line of their own, so an import carrying one is rejected, as it was
524- * before module imports were supported.
548+ * Skips whitespace, line terminators and comments starting at {@code i}, appending each comment to
549+ * {@code declaration }, and returns the index of the first token that is none of those. Javadoc comments are not
550+ * skipped: the formatter moves them onto a line of their own, which would separate the imports, so an import
551+ * carrying one is rejected, as it was before module imports were supported.
525552 */
526- private int skipIgnored (int i , List < String > comments ) {
553+ private int skipIgnored (int i , Declaration declaration ) {
527554 while (i < toks .size ()) {
528555 if (isSpaceToken (i ) || isNewlineToken (i )) {
529556 i ++;
530557 } else if (isSlashSlashCommentToken (i ) || isBlockCommentToken (i )) {
531- comments . add (tokenAt (i ).trim ());
558+ declaration . append (tokenAt (i ).trim ());
532559 i ++;
533560 } else {
534561 break ;
0 commit comments