Skip to content

Commit f38b759

Browse files
authored
Merge pull request #43 from openjavaformat/unused-import-blank-line
Stop an unused import from leaving two blank lines behind
2 parents c6f717f + 3632838 commit f38b759

3 files changed

Lines changed: 158 additions & 3 deletions

File tree

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

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ private static RangeMap<Integer, String> buildReplacements(
226226
Set<String> usedNames,
227227
Multimap<String, Range<Integer>> usedInJavadoc) {
228228
RangeMap<Integer, String> replacements = TreeRangeMap.create();
229+
String sep = Newlines.guessLineSeparator(contents);
229230
for (JCImport importTree : unit.getImports()) {
230231
String simpleName = getSimpleName(importTree);
231232
if (!isUnused(unit, usedNames, usedInJavadoc, importTree, simpleName)) {
@@ -234,16 +235,62 @@ private static RangeMap<Integer, String> buildReplacements(
234235
// delete the import
235236
int endPosition = importTree.getEndPosition(unit.endPositions);
236237
endPosition = Math.max(CharMatcher.isNot(' ').indexIn(contents, endPosition), endPosition);
237-
String sep = Newlines.guessLineSeparator(contents);
238238
if (endPosition + sep.length() < contents.length()
239239
&& contents.subSequence(endPosition, endPosition + sep.length())
240240
.toString()
241241
.equals(sep)) {
242242
endPosition += sep.length();
243243
}
244-
replacements.put(Range.closedOpen(importTree.getStartPosition(), endPosition), "");
244+
// putCoalescing merges adjacent unused imports into one range, so the blank-line cleanup below sees the
245+
// whole deleted import block (TreeRangeMap.put does not coalesce).
246+
replacements.putCoalescing(Range.closedOpen(importTree.getStartPosition(), endPosition), "");
245247
}
246-
return replacements;
248+
// Removing a whole import block can leave the blank line that preceded it stacked on the blank line that
249+
// followed it (package, blank, imports, blank, type). Collapse one of them, so a single formatting pass leaves
250+
// one blank line, as the second one did.
251+
return collapseBlankLinesAroundDeletedImports(contents, replacements, sep);
252+
}
253+
254+
/**
255+
* Extends contiguous deleted-import ranges so that a blank line that both preceded and followed the imports is not
256+
* left doubled after the deletion.
257+
*/
258+
private static RangeMap<Integer, String> collapseBlankLinesAroundDeletedImports(
259+
String contents, RangeMap<Integer, String> replacements, String sep) {
260+
if (replacements.asMapOfRanges().isEmpty()) {
261+
return replacements;
262+
}
263+
RangeMap<Integer, String> adjusted = TreeRangeMap.create();
264+
for (Range<Integer> range : replacements.asMapOfRanges().keySet()) {
265+
int start = range.lowerEndpoint();
266+
int end = range.upperEndpoint();
267+
// Eat one trailing blank line when the deletion sits between blank lines, or at the start of the file,
268+
// where a leading blank line would otherwise remain after the last import is removed.
269+
if (isBlankLineAfter(contents, end, sep) && (start == 0 || isBlankLineBefore(contents, start, sep))) {
270+
end += sep.length();
271+
}
272+
adjusted.putCoalescing(Range.closedOpen(start, end), "");
273+
}
274+
return adjusted;
275+
}
276+
277+
/** True if {@code pos} is immediately preceded by an empty line. */
278+
private static boolean isBlankLineBefore(String contents, int pos, String sep) {
279+
if (pos < sep.length() || !contents.regionMatches(pos - sep.length(), sep, 0, sep.length())) {
280+
return false;
281+
}
282+
int endOfPreviousLine = pos - sep.length();
283+
if (endOfPreviousLine == 0) {
284+
// The file begins with a blank line before the deleted import.
285+
return true;
286+
}
287+
return endOfPreviousLine >= sep.length()
288+
&& contents.regionMatches(endOfPreviousLine - sep.length(), sep, 0, sep.length());
289+
}
290+
291+
/** True if {@code pos} is immediately followed by an empty line (a line break). */
292+
private static boolean isBlankLineAfter(String contents, int pos, String sep) {
293+
return pos + sep.length() <= contents.length() && contents.regionMatches(pos, sep, 0, sep.length());
247294
}
248295

249296
private static String getSimpleName(ImportTree importTree) {

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,46 @@ public void importRemovalLines() throws Exception {
266266
assertThat(out.toString()).isEqualTo(joiner.join(expected));
267267
}
268268

269+
// An unused import between two blank lines must not leave both of them behind: one run of the command line gives
270+
// what a second run would, and what the entry point of the Gradle and Spotless step gives (#37, from
271+
// google/google-java-format#1436).
272+
@Test
273+
public void unusedImportRemovalLeavesOneBlankLine() throws Exception {
274+
String[] input = {
275+
"package com.example;",
276+
"",
277+
"import static io.grpc.MethodDescriptor.generateFullMethodName;",
278+
"",
279+
"/**",
280+
" * Javadoc for class.",
281+
" */",
282+
"public class TestBug {",
283+
"}",
284+
"",
285+
};
286+
String[] expected = {
287+
"package com.example;", //
288+
"",
289+
"/**",
290+
" * Javadoc for class.",
291+
" */",
292+
"public class TestBug {}",
293+
"",
294+
};
295+
StringWriter out = new StringWriter();
296+
Main main = new Main(
297+
new PrintWriter(out, true),
298+
new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
299+
new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8)));
300+
assertThat(main.format("-")).isEqualTo(0);
301+
assertThat(out.toString()).isEqualTo(joiner.join(expected));
302+
303+
Formatter formatter = Formatter.createFormatter(JavaFormatterOptions.builder()
304+
.style(JavaFormatterOptions.Style.OJF)
305+
.build());
306+
assertThat(formatter.formatSourceAndFixImports(joiner.join(input))).isEqualTo(joiner.join(expected));
307+
}
308+
269309
// test that errors are reported on the right line when imports are removed
270310
@Test
271311
public void importRemoveErrorParseError() throws Exception {

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,74 @@ public static List<Object[]> parameters() {
254254
"interface Test { private static void foo() {} }",
255255
},
256256
},
257+
// An unused import between blank lines takes one of them with it (#37, from
258+
// google/google-java-format#1436 and google/google-java-format#1437).
259+
{
260+
{
261+
"package com.example;",
262+
"",
263+
"import static io.grpc.MethodDescriptor.generateFullMethodName;",
264+
"",
265+
"/**",
266+
" * Javadoc for class.",
267+
" */",
268+
"public class TestBug {}",
269+
},
270+
{
271+
"package com.example;", //
272+
"",
273+
"/**",
274+
" * Javadoc for class.",
275+
" */",
276+
"public class TestBug {}",
277+
},
278+
},
279+
{
280+
{
281+
"package com.example;",
282+
"",
283+
"import com.foo.Unused1;",
284+
"import com.foo.Unused2;",
285+
"",
286+
"public class TestBug {}",
287+
},
288+
{
289+
"package com.example;", //
290+
"",
291+
"public class TestBug {}",
292+
},
293+
},
294+
{
295+
{
296+
"import com.foo.Unused;", //
297+
"",
298+
"public class TestBug {}",
299+
},
300+
{
301+
"public class TestBug {}",
302+
},
303+
},
304+
{
305+
{
306+
"package com.example;",
307+
"",
308+
"import java.util.List;",
309+
"import com.foo.Unused;",
310+
"",
311+
"public class TestBug {",
312+
" List<String> xs;",
313+
"}",
314+
},
315+
{
316+
"package com.example;",
317+
"",
318+
"import java.util.List;",
319+
"",
320+
"public class TestBug {",
321+
" List<String> xs;",
322+
"}",
323+
},
324+
},
257325
};
258326
ImmutableList.Builder<Object[]> builder = ImmutableList.builder();
259327
for (String[][] inputAndOutput : inputsOutputs) {

0 commit comments

Comments
 (0)