From 633ce6be13f82ef1d57cd9fe0033e0edd0d58183 Mon Sep 17 00:00:00 2001 From: Liam Miller-Cushon Date: Tue, 20 Oct 2026 02:30:57 -0700 Subject: [PATCH] Run unused-import removal, modifier reordering, and import sorting as a single pre-formatting pipeline. Consolidate unused-import removal, modifier reordering, and import sorting into a unified Cleanups pass that runs before core layout. Unused imports are removed and imports are sorted before formatting. When no non-whitespace pass modifies the input, the parsed compilation unit is reused directly during formatting. RELNOTES=`Formatter.getFormatReplacements` is now strictly whitespace-only and no longer reorders modifiers; use `Formatter.formatSource(String, Collection>)` to format character ranges with modifier reordering (or `JavaFormatterOptions.builder().reorderModifiers(false)` to disable modifier reordering). PiperOrigin-RevId: 1002946984 --- .../googlejavaformat/java/CleanupPass.java | 48 +++ .../googlejavaformat/java/Cleanups.java | 233 +++++++++++++++ .../java/FormatFileCallable.java | 29 +- .../googlejavaformat/java/Formatter.java | 109 +++++-- .../googlejavaformat/java/JavaInput.java | 4 + .../googlejavaformat/java/CleanupsTest.java | 273 ++++++++++++++++++ .../GoogleJavaFormatImportOptimizer.java | 7 +- 7 files changed, 658 insertions(+), 45 deletions(-) create mode 100644 core/src/main/java/com/google/googlejavaformat/java/CleanupPass.java create mode 100644 core/src/main/java/com/google/googlejavaformat/java/Cleanups.java create mode 100644 core/src/test/java/com/google/googlejavaformat/java/CleanupsTest.java diff --git a/core/src/main/java/com/google/googlejavaformat/java/CleanupPass.java b/core/src/main/java/com/google/googlejavaformat/java/CleanupPass.java new file mode 100644 index 000000000..69d022a56 --- /dev/null +++ b/core/src/main/java/com/google/googlejavaformat/java/CleanupPass.java @@ -0,0 +1,48 @@ +/* + * Copyright 2026 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.googlejavaformat.java; + +import com.google.common.collect.ImmutableList; + +/// Standard cleanup passes applied before formatting. +enum CleanupPass { + /// Removes unused imports. + REMOVE_UNUSED_IMPORTS { + @Override + ImmutableList findReplacements(Cleanups cleanups) throws FormatterException { + return RemoveUnusedImports.findUnusedImports( + cleanups.context(), cleanups.unit(), cleanups.contents()); + } + }, + /// Reorders modifiers into JLS order. + REORDER_MODIFIERS { + @Override + ImmutableList findReplacements(Cleanups cleanups) throws FormatterException { + JavaInput javaInput = new JavaInput(cleanups.contents()); + return ModifierOrderer.findModifierReplacements(javaInput, cleanups.ranges()); + } + }, + /// Reorders imports into canonical style order. + SORT_IMPORTS { + @Override + ImmutableList findReplacements(Cleanups cleanups) throws FormatterException { + return ImportOrderer.findReplacements(cleanups.contents(), cleanups.style()); + } + }; + + /// Finds replacements to apply to the current pipeline state. The returned replacements must be + /// relative to `cleanups.contents()`, sorted by start position, and non-overlapping. + abstract ImmutableList findReplacements(Cleanups cleanups) throws FormatterException; +} diff --git a/core/src/main/java/com/google/googlejavaformat/java/Cleanups.java b/core/src/main/java/com/google/googlejavaformat/java/Cleanups.java new file mode 100644 index 000000000..e6a420a6e --- /dev/null +++ b/core/src/main/java/com/google/googlejavaformat/java/Cleanups.java @@ -0,0 +1,233 @@ +/* + * Copyright 2026 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.googlejavaformat.java; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.Math.max; +import static java.lang.Math.min; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableRangeSet; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Range; +import com.google.common.collect.RangeSet; +import com.google.common.collect.Sets; +import com.google.common.collect.TreeRangeSet; +import com.google.googlejavaformat.java.JavaFormatterOptions.Style; +import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; +import com.sun.tools.javac.util.Context; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import javax.tools.Diagnostic; +import javax.tools.JavaFileObject; +import org.jspecify.annotations.Nullable; + +/// Applies non-whitespace cleanups before core formatting. +/// +/// Each pass produces replacements relative to the current contents; the requested character ranges +/// are remapped through every pass so formatting still targets the code the caller selected. +final class Cleanups { + + /// The cleaned contents, the requested character ranges remapped onto them (plus any modified + /// regions), and the javac unit for `contents` if it can be reused for formatting. + record Result( + String contents, + Collection> characterRanges, + @Nullable JCCompilationUnit unit) {} + + /// One replacement: `[inStart, inEnd)` in the old contents became `[outStart, outEnd)`. + private record Span(int inStart, int inEnd, int outStart, int outEnd) {} + + /// The result of applying sorted, non-overlapping replacements to some text. + private record Edit(String contents, ImmutableList spans) { + + /// Applies sorted, non-overlapping replacements to `contents`. + static Edit apply(String contents, List replacements) { + StringBuilder sb = new StringBuilder(contents.length()); + ImmutableList.Builder spans = + ImmutableList.builderWithExpectedSize(replacements.size()); + int cursor = 0; + for (Replacement replacement : replacements) { + Range range = replacement.replaceRange(); + int inStart = range.lowerEndpoint(); + int inEnd = range.upperEndpoint(); + checkArgument( + inStart >= cursor, + "Replacements must be sorted and non-overlapping, but %s overlaps previous end %s", + range, + cursor); + sb.append(contents, cursor, inStart); + int outStart = sb.length(); + sb.append(replacement.replacementString()); + int outEnd = sb.length(); + spans.add(new Span(inStart, inEnd, outStart, outEnd)); + cursor = inEnd; + } + sb.append(contents, cursor, contents.length()); + return new Edit(sb.toString(), spans.build()); + } + + /// Maps an offset in the old contents to the corresponding offset in the new contents. + /// + /// Offsets before a span are unchanged relative to the previous span; offsets inside a span are + /// clamped to its replacement text; offsets after all spans are shifted by the final span. + int map(int offset) { + Span prev = null; + for (Span span : spans) { + if (offset < span.inStart()) { + return prev == null ? offset : prev.outEnd() + (offset - prev.inEnd()); + } + if (offset <= span.inEnd()) { + return span.outStart() + min(offset - span.inStart(), span.outEnd() - span.outStart()); + } + prev = span; + } + return prev == null ? offset : prev.outEnd() + (offset - prev.inEnd()); + } + } + + private record Parsed(Context context, JCCompilationUnit unit) {} + + /// Returns the enabled passes for formatting or fixing imports. + static ImmutableSet enabledPasses( + boolean removeUnusedImports, boolean reorderModifiers, boolean sortImports) { + EnumSet passes = EnumSet.noneOf(CleanupPass.class); + if (removeUnusedImports) { + passes.add(CleanupPass.REMOVE_UNUSED_IMPORTS); + } + if (reorderModifiers) { + passes.add(CleanupPass.REORDER_MODIFIERS); + } + if (sortImports) { + passes.add(CleanupPass.SORT_IMPORTS); + } + return Sets.immutableEnumSet(passes); + } + + static Result apply( + String contents, + Collection> characterRanges, + Style style, + ImmutableSet passes) + throws FormatterException { + JavaInput.validateRanges(characterRanges, contents.length()); + if (passes.isEmpty()) { + return new Result(contents, characterRanges, null); + } + RangeSet normalized = TreeRangeSet.create(); + for (Range range : characterRanges) { + normalized.add( + range.isEmpty() && range.lowerEndpoint() < contents.length() + ? Range.closedOpen(range.lowerEndpoint(), range.lowerEndpoint() + 1) + : range); + } + Cleanups cleanups = new Cleanups(contents, ImmutableRangeSet.copyOf(normalized), style); + for (CleanupPass pass : passes) { + cleanups.applyReplacements(pass.findReplacements(cleanups)); + } + return cleanups.result(); + } + + private String contents; + private ImmutableRangeSet ranges; + private @Nullable Parsed parsed; + private final Style style; + + private Cleanups(String contents, ImmutableRangeSet ranges, Style style) { + this.contents = contents; + this.ranges = ranges; + this.style = style; + } + + String contents() { + return contents; + } + + ImmutableSet> ranges() { + return ranges.asRanges(); + } + + Style style() { + return style; + } + + private Parsed parsed() throws FormatterException { + if (parsed != null) { + return parsed; + } + Context ctx = new Context(); + List> errorDiagnostics = new ArrayList<>(); + JCCompilationUnit cu = + Trees.parse(ctx, errorDiagnostics, /* allowStringFolding= */ false, contents); + if (!errorDiagnostics.isEmpty()) { + throw FormatterException.fromJavacDiagnostics(errorDiagnostics); + } + Parsed p = new Parsed(ctx, cu); + this.parsed = p; + return p; + } + + JCCompilationUnit unit() throws FormatterException { + return parsed().unit(); + } + + Context context() throws FormatterException { + return parsed().context(); + } + + Result result() { + return new Result(contents, ranges.asRanges(), parsed == null ? null : parsed.unit()); + } + + /// Applies a pass's replacements to the current contents, remapping requested ranges and adding + /// modified regions. + private void applyReplacements(List replacements) { + if (replacements.isEmpty()) { + return; + } + Edit edit = Edit.apply(contents, replacements); + RangeSet newRanges = TreeRangeSet.create(); + for (Range range : ranges.asRanges()) { + int mappedStart = edit.map(range.lowerEndpoint()); + int mappedEnd = edit.map(range.upperEndpoint()); + if (mappedStart < mappedEnd) { + newRanges.add(Range.closedOpen(mappedStart, mappedEnd)); + } + } + for (Span span : edit.spans()) { + Range r = reformatRange(span, edit.contents()); + if (!r.isEmpty()) { + newRanges.add(r); + } + } + + this.contents = edit.contents(); + this.ranges = ImmutableRangeSet.copyOf(newRanges); + this.parsed = null; + } + + /// Returns the range to reformat for the given span. + private static Range reformatRange(Span span, String contents) { + int start = span.outStart(); + int end = span.outEnd(); + if (start == end && !contents.isEmpty()) { + start = max(0, start - 1); + end = min(contents.length(), end + 1); + } + return start < end ? Range.closedOpen(start, end) : Range.closedOpen(0, 0); + } +} diff --git a/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java b/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java index d7f3ec118..7d424570d 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java +++ b/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java @@ -66,31 +66,28 @@ static Result create( public Result call() { try { if (parameters.fixImportsOnly()) { - return Result.create(path, input, fixImports(input), /* exception= */ null); + return Result.create( + path, + input, + Formatter.fixImports( + input, options.style(), parameters.removeUnusedImports(), parameters.sortImports()), + /* exception= */ null); } Formatter formatter = new Formatter(options); - String formatted = formatter.formatSource(input, characterRanges(input).asRanges()); - formatted = fixImports(formatted); - if (parameters.reflowLongStrings()) { - formatted = StringWrapper.wrap(options.maxLineLength(), formatted, formatter); - } + String formatted = + formatter.formatSource( + input, + characterRanges(input).asRanges(), + parameters.removeUnusedImports(), + parameters.sortImports(), + parameters.reflowLongStrings()); return Result.create(path, input, formatted, /* exception= */ null); } catch (FormatterException e) { return Result.create(path, input, /* output= */ null, e); } } - private String fixImports(String input) throws FormatterException { - if (parameters.removeUnusedImports()) { - input = RemoveUnusedImports.removeUnusedImports(input); - } - if (parameters.sortImports()) { - input = ImportOrderer.reorderImports(input, options.style()); - } - return input; - } - private RangeSet characterRanges(String input) { final RangeSet characterRanges = TreeRangeSet.create(); diff --git a/core/src/main/java/com/google/googlejavaformat/java/Formatter.java b/core/src/main/java/com/google/googlejavaformat/java/Formatter.java index bfb084294..4f6f6588f 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/Formatter.java +++ b/core/src/main/java/com/google/googlejavaformat/java/Formatter.java @@ -38,6 +38,7 @@ import java.util.List; import javax.tools.Diagnostic; import javax.tools.JavaFileObject; +import org.jspecify.annotations.Nullable; /// This is google-java-format, a new Java formatter that follows the Google Java Style Guide quite /// precisely---to the letter and to the spirit. @@ -99,15 +100,17 @@ JavaFormatterOptions options() { /// @param options the [JavaFormatterOptions] static void format(final JavaInput javaInput, JavaOutput javaOutput, JavaFormatterOptions options) throws FormatterException { - Context context = new Context(); - List> errorDiagnostics = new ArrayList<>(); - JCCompilationUnit unit = - Trees.parse( - context, errorDiagnostics, /* allowStringFolding= */ false, javaInput.getText()); - - javaInput.setCompilationUnit(unit); - if (!errorDiagnostics.isEmpty()) { - throw FormatterException.fromJavacDiagnostics(errorDiagnostics); + JCCompilationUnit unit = javaInput.getCompilationUnit(); + if (unit == null) { + Context context = new Context(); + List> errorDiagnostics = new ArrayList<>(); + unit = + Trees.parse( + context, errorDiagnostics, /* allowStringFolding= */ false, javaInput.getText()); + if (!errorDiagnostics.isEmpty()) { + throw FormatterException.fromJavacDiagnostics(errorDiagnostics); + } + javaInput.setCompilationUnit(unit); } LayoutBuilder builder = new LayoutBuilder(javaInput, javaOutput); ImmutableSet.Builder markdownJavadocPositions = ImmutableSet.builder(); @@ -157,6 +160,43 @@ public String formatSource(String input) throws FormatterException { return formatSource(input, ImmutableList.of(Range.closedOpen(0, input.length()))); } + /// Format an input string (a Java compilation unit), for only the specified character ranges. + /// These ranges are extended as necessary (e.g., to encompass whole lines). + /// + /// @param input the input string + /// @param characterRanges the character ranges to be reformatted + /// @return the output string + /// @throws FormatterException if the input string cannot be parsed + public String formatSource(String input, Collection> characterRanges) + throws FormatterException { + return formatSource( + input, + characterRanges, + /* removeUnusedImports= */ false, + /* sortImports= */ false, + /* reflowLongStrings= */ false); + } + + String formatSource( + String input, + Collection> characterRanges, + boolean removeUnusedImports, + boolean sortImports, + boolean reflowLongStrings) + throws FormatterException { + ImmutableSet passes = + Cleanups.enabledPasses(removeUnusedImports, options.reorderModifiers(), sortImports); + Cleanups.Result cleaned = Cleanups.apply(input, characterRanges, options.style(), passes); + String formatted = + JavaOutput.applyReplacements( + cleaned.contents(), + getFormatReplacements(cleaned.contents(), cleaned.characterRanges(), cleaned.unit())); + if (reflowLongStrings) { + formatted = StringWrapper.wrap(options.maxLineLength(), formatted, this); + } + return formatted; + } + /// Formats an input string (a Java compilation unit) and fixes imports. /// /// Fixing imports includes ordering, spacing, and removal of unused import statements. @@ -168,40 +208,57 @@ public String formatSource(String input) throws FormatterException { /// href="https://google.github.io/styleguide/javaguide.html#s3.3.3-import-ordering-and-spacing"> /// Google Java Style Guide - 3.3.3 Import ordering and spacing public String formatSourceAndFixImports(String input) throws FormatterException { - input = ImportOrderer.reorderImports(input, options.style()); - input = RemoveUnusedImports.removeUnusedImports(input); - String formatted = formatSource(input); - formatted = StringWrapper.wrap(formatted, this); - return formatted; + return formatSource( + input, + ImmutableList.of(Range.closedOpen(0, input.length())), + /* removeUnusedImports= */ true, + /* sortImports= */ true, + /* reflowLongStrings= */ true); } - /// Format an input string (a Java compilation unit), for only the specified character ranges. - /// These ranges are extended as necessary (e.g., to encompass whole lines). + /// Fixes imports in the given Java source string without performing general code formatting. /// /// @param input the input string - /// @param characterRanges the character ranges to be reformatted - /// @return the output string + /// @param style the code style controlling import ordering + /// @param removeUnusedImports whether to remove unused imports + /// @param sortImports whether to sort imports + /// @return the output string with imports fixed /// @throws FormatterException if the input string cannot be parsed - public String formatSource(String input, Collection> characterRanges) + public static String fixImports( + String input, + JavaFormatterOptions.Style style, + boolean removeUnusedImports, + boolean sortImports) throws FormatterException { - return JavaOutput.applyReplacements(input, getFormatReplacements(input, characterRanges)); + ImmutableSet passes = + Cleanups.enabledPasses(removeUnusedImports, /* reorderModifiers= */ false, sortImports); + Cleanups.Result result = + Cleanups.apply(input, ImmutableList.of(Range.closedOpen(0, input.length())), style, passes); + return result.contents(); } /// Emit a list of [Replacement]s to convert from input to output. /// + /// This is a whitespace-only API that performs core layout and whitespace formatting on the + /// requested ranges. It does not perform AST-based cleanups (such as brace insertion or modifier + /// cleanup), nor does it modify unformatted code outside the requested ranges (such as file-wide + /// blank line or trailing whitespace cleanup). + /// /// @param input the input compilation unit /// @param characterRanges the character ranges to reformat /// @return a list of [Replacement]s, sorted from low index to high index, without overlaps /// @throws FormatterException if the input string cannot be parsed public ImmutableList getFormatReplacements( String input, Collection> characterRanges) throws FormatterException { - JavaInput javaInput = new JavaInput(input); + return getFormatReplacements(input, characterRanges, /* unit= */ null); + } - // TODO(cushon): this is only safe because the modifier ordering doesn't affect whitespace, - // and doesn't change the replacements that are output. This is not true in general for - // 'de-linting' changes (e.g. import ordering). - if (options.reorderModifiers()) { - javaInput = ModifierOrderer.reorderModifiers(javaInput, characterRanges); + ImmutableList getFormatReplacements( + String input, Collection> characterRanges, @Nullable JCCompilationUnit unit) + throws FormatterException { + JavaInput javaInput = new JavaInput(input); + if (unit != null) { + javaInput.setCompilationUnit(unit); } String lineSeparator = Newlines.guessLineSeparator(input); diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java b/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java index 5dfdbb34a..6b2773427 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java +++ b/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java @@ -687,6 +687,10 @@ public int getColumnNumber(int inputPosition) { return unit.getLineMap().getColumnNumber(inputPosition); } + @Nullable JCCompilationUnit getCompilationUnit() { + return unit; + } + // TODO(cushon): refactor JavaInput so the CompilationUnit can be passed into // the constructor. void setCompilationUnit(JCCompilationUnit unit) { diff --git a/core/src/test/java/com/google/googlejavaformat/java/CleanupsTest.java b/core/src/test/java/com/google/googlejavaformat/java/CleanupsTest.java new file mode 100644 index 000000000..8a6f4fddf --- /dev/null +++ b/core/src/test/java/com/google/googlejavaformat/java/CleanupsTest.java @@ -0,0 +1,273 @@ +/* + * Copyright 2026 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.googlejavaformat.java; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Range; +import com.google.googlejavaformat.java.JavaFormatterOptions.Style; +import org.junit.Test; + +/// Tests for [Cleanups]. +public class CleanupsTest { + + @Test + public void formatSource_rangePastEndOfInput_throwsFormatterException() { + String input = + """ + class Test { + void f() {} + } + """; + assertThrows( + FormatterException.class, + () -> + new Formatter() + .formatSource( + input, + ImmutableList.of(Range.closedOpen(input.length() + 1, input.length() + 1)), + /* removeUnusedImports= */ true, + /* sortImports= */ false, + /* reflowLongStrings= */ false)); + } + + @Test + public void removeUnusedImports_wholeFile_noExtraBlankLine() throws Exception { + // b/30464184: removing unused imports before formatting leaves no extra blank line + String input = + """ + package test; + + import java.util.ArrayList; + import java.util.List; + + class Test { + String xs; + } + """; + String expected = + """ + package test; + + class Test { + String xs; + } + """; + String formatted = new Formatter().formatSourceAndFixImports(input); + assertThat(formatted).isEqualTo(expected); + } + + @Test + public void formatSource_noPassesEnabled() throws Exception { + Formatter formatter = + new Formatter(JavaFormatterOptions.builder().reorderModifiers(false).build()); + String input = + """ + class Test { + final static public int X = 1; + } + """; + String formatted = formatter.formatSource(input); + assertThat(formatted) + .isEqualTo( + """ + class Test { + final static public int X = 1; + } + """); + } + + @Test + public void removeUnusedImports_partialFormat_noExtraBlankLine() throws Exception { + String input = + """ + package test; + import java.util.ArrayList; + import java.util.List; + class Test { + void f() { + int x = 1; + } + } + """; + int start = input.indexOf("int x = 1;"); + int end = input.indexOf(";", start) + 1; + String formatted = + new Formatter() + .formatSource( + input, + ImmutableList.of(Range.closedOpen(start, end)), + /* removeUnusedImports= */ true, + /* sortImports= */ true, + /* reflowLongStrings= */ false); + assertThat(formatted) + .isEqualTo( + """ + package test; + + class Test { + void f() { + int x = 1; + } + } + """); + } + + @Test + public void fixImports_removesUnusedAndSorts() throws Exception { + String input = + """ + package test; + + import java.util.List; + import java.util.ArrayList; + import java.util.Collections; + + class Test { + List xs = Collections.emptyList(); + } + """; + String expected = + """ + package test; + + import java.util.Collections; + import java.util.List; + + class Test { + List xs = Collections.emptyList(); + } + """; + String result = + Formatter.fixImports( + input, Style.GOOGLE, /* removeUnusedImports= */ true, /* sortImports= */ true); + assertThat(result).isEqualTo(expected); + } + + @Test + public void reorderModifiers_withinRequestedRange() throws Exception { + String input = + """ + class Test { + final static public int X = 1; + final static public int Y = 2; + } + """; + // Target only the X declaration line + int start = input.indexOf("final static public int X"); + int end = input.indexOf(";", start) + 1; + String formatted = + new Formatter().formatSource(input, ImmutableList.of(Range.closedOpen(start, end))); + assertThat(formatted) + .isEqualTo( + """ + class Test { + public static final int X = 1; + final static public int Y = 2; + } + """); + } + + @Test + public void getFormatReplacements_isWhitespaceOnly_doesNotReorderModifiers() throws Exception { + String input = + """ + class Test { + final static public int X = 1; + } + """; + ImmutableList replacements = + new Formatter() + .getFormatReplacements(input, ImmutableList.of(Range.closedOpen(0, input.length()))); + String applied = JavaOutput.applyReplacements(input, replacements); + assertThat(applied).contains("final static public int X = 1;"); + } + + @Test + public void parseReuseWhenNoEdits() throws Exception { + String input = + """ + class Test { + int x; + } + """; + Cleanups.Result result = + Cleanups.apply( + input, + ImmutableList.of(Range.closedOpen(0, input.length())), + Style.GOOGLE, + Cleanups.enabledPasses( + /* removeUnusedImports= */ true, + /* reorderModifiers= */ false, + /* sortImports= */ false)); + assertThat(result.unit()).isNotNull(); + assertThat(result.contents()).isEqualTo(input); + } + + @Test + public void parseNotReusedWhenEditsOccur() throws Exception { + String input = + """ + class Test { + static public int x; + } + """; + Cleanups.Result result = + Cleanups.apply( + input, + ImmutableList.of(Range.closedOpen(0, input.length())), + Style.GOOGLE, + Cleanups.enabledPasses( + /* removeUnusedImports= */ true, + /* reorderModifiers= */ true, + /* sortImports= */ false)); + assertThat(result.unit()).isNull(); + assertThat(result.contents()).contains("public static int x;"); + } + + @Test + public void rangeRemappingThroughSeveralPasses() throws Exception { + String input = + """ + package test; + + import java.util.ArrayList; + import java.util.List; + + class Test { + final static public int X = 1; + } + """; + int start = input.indexOf("final static public int X"); + int end = input.indexOf(";", start) + 1; + Cleanups.Result result = + Cleanups.apply( + input, + ImmutableList.of(Range.closedOpen(start, end)), + Style.GOOGLE, + Cleanups.enabledPasses( + /* removeUnusedImports= */ true, + /* reorderModifiers= */ true, + /* sortImports= */ true)); + + assertThat(result.contents()).doesNotContain("ArrayList"); + assertThat(result.contents()).contains("public static final int X = 1;"); + int expectedStart = result.contents().indexOf("public static final int X = 1;"); + int expectedEnd = result.contents().indexOf(";", expectedStart) + 1; + assertThat(result.characterRanges()).contains(Range.closedOpen(expectedStart, expectedEnd)); + } +} diff --git a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatImportOptimizer.java b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatImportOptimizer.java index 8dea26391..39c9eac6e 100644 --- a/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatImportOptimizer.java +++ b/idea_plugin/src/main/java/com/google/googlejavaformat/intellij/GoogleJavaFormatImportOptimizer.java @@ -16,10 +16,9 @@ package com.google.googlejavaformat.intellij; +import com.google.googlejavaformat.java.Formatter; import com.google.googlejavaformat.java.FormatterException; -import com.google.googlejavaformat.java.ImportOrderer; import com.google.googlejavaformat.java.JavaFormatterOptions; -import com.google.googlejavaformat.java.RemoveUnusedImports; import com.intellij.ide.highlighter.JavaFileType; import com.intellij.lang.ImportOptimizer; import com.intellij.openapi.editor.Document; @@ -57,7 +56,9 @@ public boolean supports(@NotNull PsiFile file) { final String origText = document.getText(); String text; try { - text = ImportOrderer.reorderImports(RemoveUnusedImports.removeUnusedImports(origText), style); + text = + Formatter.fixImports( + origText, style, /* removeUnusedImports= */ true, /* sortImports= */ true); } catch (FormatterException e) { Notifications.displayParsingErrorNotification(project, file.getName()); return () -> {};