Skip to content

Commit 3632838

Browse files
arimu1abashev
authored andcommitted
Stop an unused import from leaving two blank lines behind
The command line formats first and fixes imports afterwards. When RemoveUnusedImports deleted an import that sat between two blank lines, such as the only import between the package line and the class, it left both blank lines, and nothing ran after it to collapse them. A second run did, so --replace wrote files that --set-exit-if-changed then reported (#37, from google/google-java-format#598 and google/google-java-format#1436). The Gradle and Spotless entry point fixes imports before it formats and was not affected, so the two disagreed about these files. This ports google/google-java-format#1437 by arimu1: adjacent unused imports are deleted as one range, and a range that sits between blank lines, or at the start of the file, takes one of them along. The code is adapted to this codebase. The four RemoveUnusedImportsTest cases come from the upstream PR, and a MainTest checks that the command line and formatSourceAndFixImports now give the same file. Of the 15,747 files of the JDK 21 sources, 304 format differently, by 305 blank lines fewer in all and no other change. All 304 are files the old code changed again on a second run, and the new output is stable on every one of them.
1 parent 53ac7d8 commit 3632838

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)