Skip to content

Commit c6f717f

Browse files
authored
Merge pull request #44 from openjavaformat/comments-between-imports
Keep comments between imports with the import after them
2 parents eda37c1 + 56c7a01 commit c6f717f

3 files changed

Lines changed: 163 additions & 14 deletions

File tree

‎open-java-format/src/main/java/com/palantir/javaformat/java/ImportOrderer.java‎

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,12 @@ private ImportOrderer(String text, ImmutableList<Tok> toks, Style style) {
186186
class Import {
187187
private final String imported;
188188
private final boolean isStatic;
189+
private final String leading;
189190
private final String trailing;
190191

191-
Import(String imported, String trailing, boolean isStatic) {
192+
Import(String imported, String leading, String trailing, boolean isStatic) {
192193
this.imported = imported;
194+
this.leading = leading;
193195
this.trailing = trailing;
194196
this.isStatic = isStatic;
195197
}
@@ -227,9 +229,18 @@ boolean isJava() {
227229
}
228230

229231
/**
230-
* The {@code //} comment lines after the final {@code ;}, up to and including the line terminator of the last
231-
* one. Note: In case two imports were separated by a space (which is disallowed by the style guide), the
232-
* trailing whitespace of the first import does not include a line terminator.
232+
* The comments that stood between the previous import and this one, including the line terminator after the
233+
* last of them, or empty. They move with this import.
234+
*/
235+
String leading() {
236+
return leading;
237+
}
238+
239+
/**
240+
* A block comment on the import's own line and the {@code //} comment lines after the final {@code ;}, up to
241+
* and including the line terminator of the last one. Note: In case two imports were separated by a space
242+
* (which is disallowed by the style guide), the trailing whitespace of the first import does not include a
243+
* line terminator.
233244
*/
234245
String trailing() {
235246
return trailing;
@@ -240,11 +251,12 @@ public boolean isThirdParty() {
240251
return !(isAndroid() || isJava());
241252
}
242253

243-
// One or multiple lines, the import itself and following comments, including the line
244-
// terminator.
254+
// One or multiple lines, the comments before the import, the import itself and following comments, including
255+
// the line terminator.
245256
@Override
246257
public String toString() {
247258
StringBuilder sb = new StringBuilder();
259+
sb.append(leading());
248260
sb.append("import ");
249261
if (isStatic()) {
250262
sb.append("static ");
@@ -282,18 +294,22 @@ private static class ImportsAndIndex {
282294
*
283295
* <pre>{@code
284296
* <imports> -> (<end-of-line> | <import>)*
285-
* <import> -> "import" <whitespace> ("static" <whitespace>)?
297+
* <import> -> <comments-and-line-breaks>? "import" <whitespace> ("static" <whitespace>)?
286298
* <identifier> ("." <identifier>)* ("." "*")? <whitespace>? ";"
287-
* <whitespace>? <end-of-line>? (<line-comment> <end-of-line>)*
299+
* <whitespace>? (<block-comment> <whitespace>?)? <end-of-line>? (<line-comment> <end-of-line>)*
288300
* }</pre>
289301
*
302+
* The comments before an import are the ones between it and the previous import, so the first import has none: the
303+
* text before it is left where it is.
304+
*
290305
* @param i the index to start parsing at.
291306
* @return the result of parsing the imports.
292307
* @throws FormatterException if imports could not parsed according to the grammar.
293308
*/
294309
private ImportsAndIndex scanImports(int i) throws FormatterException {
295310
int afterLastImport = i;
296311
ImmutableSortedSet.Builder<Import> imports = ImmutableSortedSet.orderedBy(importComparator);
312+
String leading = "";
297313
// JavaInput.buildToks appends a zero-width EOF token after all tokens. It won't match any
298314
// of our tests here and protects us from running off the end of the toks list. Since it is
299315
// zero-width it doesn't matter if we include it in our string concatenation at the end.
@@ -330,6 +346,15 @@ private ImportsAndIndex scanImports(int i) throws FormatterException {
330346
trailing.append(tokenAt(i));
331347
i++;
332348
}
349+
// A block comment on the import's own line stays with the import, as a line comment there does.
350+
if (isBlockCommentToken(i)) {
351+
trailing.append(tokenAt(i));
352+
i++;
353+
if (isSpaceToken(i)) {
354+
trailing.append(tokenAt(i));
355+
i++;
356+
}
357+
}
333358
if (isNewlineToken(i)) {
334359
trailing.append(tokenAt(i));
335360
i++;
@@ -344,14 +369,25 @@ private ImportsAndIndex scanImports(int i) throws FormatterException {
344369
i++;
345370
}
346371
}
347-
imports.add(new Import(importedName, trailing.toString(), isStatic));
372+
imports.add(new Import(importedName, leading, trailing.toString(), isStatic));
348373
// Remember the position just after the import we just saw, before skipping blank lines.
349374
// If the next thing after the blank lines is not another import then we don't want to
350375
// include those blank lines in the text to be replaced.
351376
afterLastImport = i;
352377
while (isNewlineToken(i) || isSpaceToken(i)) {
353378
i++;
354379
}
380+
// Comments between this import and the next one go with the next one, so they move with it when the
381+
// imports are sorted. Comments after the last import belong to whatever follows it.
382+
leading = "";
383+
int next = i;
384+
while (isCommentToken(next) || isNewlineToken(next) || isSpaceToken(next)) {
385+
next++;
386+
}
387+
if (next > i && tokenAt(next).equals("import")) {
388+
leading = CharMatcher.whitespace().trimTrailingFrom(tokString(i, next)) + lineSeparator;
389+
i = next;
390+
}
355391
}
356392
return new ImportsAndIndex(imports.build(), afterLastImport);
357393
}
@@ -470,6 +506,14 @@ private boolean isSlashSlashCommentToken(int i) {
470506
return toks.get(i).isSlashSlashComment();
471507
}
472508

509+
private boolean isBlockCommentToken(int i) {
510+
return toks.get(i).isComment() && !toks.get(i).isSlashSlashComment();
511+
}
512+
513+
private boolean isCommentToken(int i) {
514+
return toks.get(i).isComment();
515+
}
516+
473517
private boolean isNewlineToken(int i) {
474518
return toks.get(i).isNewline();
475519
}

‎open-java-format/src/test/java/com/palantir/javaformat/java/GoogleImportStyleTest.java‎

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -391,25 +391,109 @@ public static List<Object[]> parameters() {
391391
"!!Could not parse imported name, at: ",
392392
}
393393
},
394+
// A comment between two imports goes with the import after it (#39, from
395+
// google/google-java-format#424); a comment on an import's own line stays with that import.
394396
{
395397
{
396398
"import com.foo.Second;",
397399
"import com.foo.First;",
398-
"/* we don't support block comments",
399-
" between imports either */",
400+
"/* A block comment between imports",
401+
" goes with the import after it. */",
400402
"import com.foo.Third;",
401403
},
402404
{
403-
"!!Imports not contiguous (perhaps a comment separates them?)",
405+
"import com.foo.First;",
406+
"import com.foo.Second;",
407+
"/* A block comment between imports",
408+
" goes with the import after it. */",
409+
"import com.foo.Third;",
404410
}
405411
},
406412
{
407413
{
408-
"import com.foo.Second; /* no block comments after imports */", //
414+
"import com.foo.Second; /* A block comment after an import stays with it. */", //
409415
"import com.foo.First;",
410416
},
411417
{
412-
"!!Imports not contiguous (perhaps a comment separates them?)",
418+
"import com.foo.First;", //
419+
"import com.foo.Second; /* A block comment after an import stays with it. */",
420+
}
421+
},
422+
{
423+
{
424+
"import b.B;", //
425+
"",
426+
"// why we need A",
427+
"import a.A;",
428+
"",
429+
"class T {}",
430+
},
431+
{
432+
"// why we need A", //
433+
"import a.A;",
434+
"import b.B;",
435+
"",
436+
"class T {}",
437+
}
438+
},
439+
{
440+
{
441+
"package foo;",
442+
"",
443+
"import groovy.transform.CompileStatic;",
444+
"",
445+
"/**",
446+
" * Created.",
447+
" */",
448+
"import java.util.ArrayList;",
449+
"",
450+
"/**",
451+
" * Created.",
452+
" */",
453+
"@CompileStatic",
454+
"public class Broken {",
455+
" ArrayList<?> list;",
456+
"}",
457+
},
458+
{
459+
"package foo;",
460+
"",
461+
"import groovy.transform.CompileStatic;",
462+
"/**",
463+
" * Created.",
464+
" */",
465+
"import java.util.ArrayList;",
466+
"",
467+
"/**",
468+
" * Created.",
469+
" */",
470+
"@CompileStatic",
471+
"public class Broken {",
472+
" ArrayList<?> list;",
473+
"}",
474+
}
475+
},
476+
{
477+
{
478+
"import java.lang.reflect.Field;",
479+
"",
480+
"//import org.jline.nativ.JLineLibrary;",
481+
"//import org.jline.nativ.JLineNativeLoader;",
482+
"import org.jline.terminal.Attributes;",
483+
"",
484+
"import static org.jline.terminal.TerminalBuilder.PROP_NON_BLOCKING_READS;",
485+
"",
486+
"class T {}",
487+
},
488+
{
489+
"import static org.jline.terminal.TerminalBuilder.PROP_NON_BLOCKING_READS;",
490+
"",
491+
"import java.lang.reflect.Field;",
492+
"//import org.jline.nativ.JLineLibrary;",
493+
"//import org.jline.nativ.JLineNativeLoader;",
494+
"import org.jline.terminal.Attributes;",
495+
"",
496+
"class T {}",
413497
}
414498
},
415499
{

‎open-java-format/src/test/java/com/palantir/javaformat/java/MainTest.java‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,27 @@ public void noReflowLongStrings() throws Exception {
605605
assertThat(out.toString()).isEqualTo(joiner.join(expected));
606606
}
607607

608+
// A comment between two imports goes with the import after it, where it used to fail the whole file with "Imports
609+
// not contiguous" (#39, from google/google-java-format#424). A second run leaves the result alone.
610+
@Test
611+
public void commentBetweenImportsMovesWithTheImportAfterIt() throws Exception {
612+
String[] input = {
613+
"import b.B;", "", "// why we need A", "import a.A;", "", "class T {", " A a;", " B b;", "}", "",
614+
};
615+
String[] expected = {
616+
"// why we need A", "import a.A;", "import b.B;", "", "class T {", " A a;", " B b;", "}", "",
617+
};
618+
for (String[] source : ImmutableList.of(input, expected)) {
619+
StringWriter out = new StringWriter();
620+
Main main = new Main(
621+
new PrintWriter(out, true),
622+
new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
623+
new ByteArrayInputStream(joiner.join(source).getBytes(UTF_8)));
624+
assertThat(main.format("-")).isEqualTo(0);
625+
assertThat(out.toString()).isEqualTo(joiner.join(expected));
626+
}
627+
}
628+
608629
private static ProcessBuilder formatterMain(String... args) {
609630
return new ProcessBuilder(ImmutableList.<String>builder()
610631
.add(Paths.get(System.getProperty("java.home"))

0 commit comments

Comments
 (0)