Skip to content

Commit 6dfcb65

Browse files
author
Vincent Potucek
committed
feat: Add ReplaceObsoletesStep
1 parent 13ce22d commit 6dfcb65

File tree

6 files changed

+656
-5
lines changed

6 files changed

+656
-5
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.idea/
2+
!.idea/icon.png
23
*.ims
34
*.iml
45

.idea/icon.png

1.83 KB
Loading

core/src/main/java/com/google/googlejavaformat/java/Formatter.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414

1515
package com.google.googlejavaformat.java;
1616

17+
import static com.google.googlejavaformat.java.ImportOrderer.reorderImports;
18+
import static com.google.googlejavaformat.java.RemoveUnusedDeclarations.removeUnusedDeclarations;
19+
import static com.google.googlejavaformat.java.RemoveUnusedImports.removeUnusedImports;
20+
import static com.google.googlejavaformat.java.StringWrapper.wrap;
1721
import static java.nio.charset.StandardCharsets.UTF_8;
1822

1923
import com.google.common.collect.ImmutableList;
@@ -232,11 +236,12 @@ public String formatSource(String input) throws FormatterException {
232236
* Google Java Style Guide - 3.3.3 Import ordering and spacing</a>
233237
*/
234238
public String formatSourceAndFixImports(String input) throws FormatterException {
235-
input = ImportOrderer.reorderImports(input, options.style());
236-
input = RemoveUnusedImports.removeUnusedImports(input);
237-
String formatted = formatSource(input);
238-
formatted = StringWrapper.wrap(formatted, this);
239-
return formatted;
239+
return wrap(
240+
formatSource(
241+
removeUnusedDeclarations(
242+
removeUnusedImports(
243+
reorderImports(input, options.style())))),
244+
this);
240245
}
241246

242247
/**
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package com.google.googlejavaformat.java;
2+
3+
import com.google.common.collect.ImmutableList;
4+
import com.google.common.collect.Range;
5+
import com.google.common.collect.RangeMap;
6+
import com.google.common.collect.TreeRangeMap;
7+
import com.sun.source.tree.*;
8+
import com.sun.source.util.JavacTask;
9+
import com.sun.source.util.SourcePositions;
10+
import com.sun.source.util.TreePath;
11+
import com.sun.source.util.TreePathScanner;
12+
import com.sun.source.util.Trees;
13+
import com.sun.tools.javac.api.JavacTool;
14+
import com.sun.tools.javac.file.JavacFileManager;
15+
import com.sun.tools.javac.util.Context;
16+
17+
import javax.lang.model.element.Modifier;
18+
import javax.tools.Diagnostic;
19+
import javax.tools.DiagnosticCollector;
20+
import javax.tools.JavaFileObject;
21+
import javax.tools.SimpleJavaFileObject;
22+
import java.io.IOException;
23+
import java.net.URI;
24+
import java.util.*;
25+
import java.util.stream.Collectors;
26+
27+
/**
28+
* Removes unused declarations from Java source code, including:
29+
* - Redundant modifiers in interfaces (public, static, final, abstract)
30+
* - Redundant modifiers in classes, enums, and annotations
31+
* - Redundant final modifiers on method parameters (preserved now)
32+
*/
33+
public class RemoveUnusedDeclarations {
34+
public static String removeUnusedDeclarations(String source) throws FormatterException {
35+
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
36+
var task = JavacTool.create().getTask(
37+
null,
38+
new JavacFileManager(new Context(), true, null),
39+
diagnostics,
40+
ImmutableList.of("-Xlint:-processing"),
41+
null,
42+
ImmutableList.of((JavaFileObject) new SimpleJavaFileObject(URI.create("source"),
43+
JavaFileObject.Kind.SOURCE) {
44+
@Override
45+
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
46+
return source;
47+
}
48+
}));
49+
50+
try {
51+
Iterable<? extends CompilationUnitTree> units = task.parse();
52+
if (!units.iterator().hasNext()) {
53+
throw new FormatterException("No compilation units found");
54+
}
55+
56+
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
57+
if (diagnostic.getKind() == Diagnostic.Kind.ERROR) {
58+
throw new FormatterException("Syntax error in source: " + diagnostic.getMessage(null));
59+
}
60+
}
61+
62+
var scanner = new UnusedDeclarationScanner(task);
63+
scanner.scan(units.iterator().next(), null);
64+
65+
return applyReplacements(source, scanner.getReplacements());
66+
} catch (IOException e) {
67+
throw new FormatterException("Error processing source file: " + e.getMessage());
68+
}
69+
}
70+
71+
private static class UnusedDeclarationScanner extends TreePathScanner<Void, Void> {
72+
private final RangeMap<Integer, String> replacements = TreeRangeMap.create();
73+
private final SourcePositions sourcePositions;
74+
private final Trees trees;
75+
76+
private static final ImmutableList<Modifier> CANONICAL_MODIFIER_ORDER = ImmutableList.of(
77+
Modifier.PUBLIC, Modifier.PROTECTED, Modifier.PRIVATE,
78+
Modifier.ABSTRACT, Modifier.STATIC, Modifier.FINAL,
79+
Modifier.TRANSIENT, Modifier.VOLATILE, Modifier.SYNCHRONIZED,
80+
Modifier.NATIVE, Modifier.STRICTFP
81+
);
82+
83+
private UnusedDeclarationScanner(JavacTask task) {
84+
this.sourcePositions = Trees.instance(task).getSourcePositions();
85+
this.trees = Trees.instance(task);
86+
}
87+
88+
public RangeMap<Integer, String> getReplacements() {
89+
return replacements;
90+
}
91+
92+
@Override
93+
public Void visitClass(ClassTree node, Void unused) {
94+
var parentPath = getCurrentPath().getParentPath();
95+
if (node.getKind() == Tree.Kind.INTERFACE) {
96+
checkForRedundantModifiers(node, Set.of(Modifier.PUBLIC, Modifier.ABSTRACT));
97+
} else if ((parentPath != null ? parentPath.getLeaf().getKind() : null) == Tree.Kind.INTERFACE) {
98+
checkForRedundantModifiers(node, Set.of(Modifier.PUBLIC, Modifier.STATIC));
99+
} else if (node.getKind() == Tree.Kind.ANNOTATION_TYPE) {
100+
checkForRedundantModifiers(node, Set.of(Modifier.ABSTRACT));
101+
} else {
102+
checkForRedundantModifiers(node, Set.of()); // Always sort
103+
}
104+
105+
return super.visitClass(node, unused);
106+
}
107+
108+
@Override
109+
public Void visitMethod(MethodTree node, Void unused) {
110+
var parentPath = getCurrentPath().getParentPath();
111+
var parentKind = parentPath != null ? parentPath.getLeaf().getKind() : null;
112+
113+
if (parentKind == Tree.Kind.INTERFACE) {
114+
if (!node.getModifiers().getFlags().contains(Modifier.DEFAULT) &&
115+
!node.getModifiers().getFlags().contains(Modifier.STATIC)) {
116+
checkForRedundantModifiers(node, Set.of(Modifier.PUBLIC, Modifier.ABSTRACT));
117+
} else {
118+
checkForRedundantModifiers(node, Set.of());
119+
}
120+
} else if (parentKind == Tree.Kind.ANNOTATION_TYPE) {
121+
checkForRedundantModifiers(node, Set.of(Modifier.ABSTRACT));
122+
} else {
123+
checkForRedundantModifiers(node, Set.of()); // Always sort
124+
}
125+
126+
return super.visitMethod(node, unused);
127+
}
128+
129+
@Override
130+
public Void visitVariable(VariableTree node, Void unused) {
131+
var parentPath = getCurrentPath().getParentPath();
132+
var parentKind = parentPath != null ? parentPath.getLeaf().getKind() : null;
133+
134+
if (node.getKind() == Tree.Kind.ENUM) {
135+
return super.visitVariable(node, unused);
136+
}
137+
138+
if (parentKind == Tree.Kind.INTERFACE || parentKind == Tree.Kind.ANNOTATION_TYPE) {
139+
checkForRedundantModifiers(node, Set.of(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL));
140+
} else {
141+
checkForRedundantModifiers(node, Set.of()); // Always sort
142+
}
143+
144+
return super.visitVariable(node, unused);
145+
}
146+
147+
private void checkForRedundantModifiers(Tree node, Set<Modifier> redundantModifiers) {
148+
var modifiers = getModifiers(node);
149+
if (modifiers == null) return;
150+
try {
151+
addReplacementForModifiers(node, new LinkedHashSet<>(modifiers.getFlags()).stream()
152+
.filter(redundantModifiers::contains)
153+
.collect(Collectors.toSet()));
154+
} catch (IOException e) {
155+
throw new RuntimeException(e);
156+
}
157+
}
158+
159+
private ModifiersTree getModifiers(Tree node) {
160+
if (node instanceof ClassTree) return ((ClassTree) node).getModifiers();
161+
if (node instanceof MethodTree) return ((MethodTree) node).getModifiers();
162+
if (node instanceof VariableTree) return ((VariableTree) node).getModifiers();
163+
return null;
164+
}
165+
166+
private void addReplacementForModifiers(Tree node, Set<Modifier> toRemove) throws IOException {
167+
TreePath path = trees.getPath(getCurrentPath().getCompilationUnit(), node);
168+
if (path == null) return;
169+
170+
CompilationUnitTree unit = path.getCompilationUnit();
171+
String source = unit.getSourceFile().getCharContent(true).toString();
172+
173+
ModifiersTree modifiers = getModifiers(node);
174+
if (modifiers == null) return;
175+
176+
long modifiersStart = sourcePositions.getStartPosition(unit, modifiers);
177+
long modifiersEnd = sourcePositions.getEndPosition(unit, modifiers);
178+
if (modifiersStart == -1 || modifiersEnd == -1) return;
179+
180+
String newModifiersText = modifiers.getFlags().stream()
181+
.filter(m -> !toRemove.contains(m))
182+
.collect(Collectors.toCollection(LinkedHashSet::new)).stream()
183+
.sorted(Comparator.comparingInt(mod -> {
184+
int idx = CANONICAL_MODIFIER_ORDER.indexOf(mod);
185+
return idx == -1 ? Integer.MAX_VALUE : idx;
186+
}))
187+
.map(Modifier::toString)
188+
.collect(Collectors.joining(" "));
189+
190+
long annotationsEnd = modifiersStart;
191+
for (AnnotationTree annotation : modifiers.getAnnotations()) {
192+
long end = sourcePositions.getEndPosition(unit, annotation);
193+
if (end > annotationsEnd) annotationsEnd = end;
194+
}
195+
196+
int effectiveStart = (int) annotationsEnd;
197+
while (effectiveStart < modifiersEnd && Character.isWhitespace(source.charAt(effectiveStart))) {
198+
effectiveStart++;
199+
}
200+
201+
String current = source.substring(effectiveStart, (int) modifiersEnd);
202+
if (!newModifiersText.trim().equals(current.trim())) {
203+
int globalEnd = (int) modifiersEnd;
204+
if (newModifiersText.isEmpty()) {
205+
while (globalEnd < source.length() && Character.isWhitespace(source.charAt(globalEnd))) {
206+
globalEnd++;
207+
}
208+
}
209+
replacements.put(Range.closedOpen(effectiveStart, globalEnd), newModifiersText);
210+
}
211+
}
212+
}
213+
214+
private static String applyReplacements(String source, RangeMap<Integer, String> replacements) {
215+
StringBuilder sb = new StringBuilder(source);
216+
for (Map.Entry<Range<Integer>, String> entry : replacements.asDescendingMapOfRanges().entrySet()) {
217+
Range<Integer> range = entry.getKey();
218+
sb.replace(range.lowerEndpoint(), range.upperEndpoint(), entry.getValue());
219+
}
220+
return sb.toString();
221+
}
222+
}

core/src/test/java/com/google/googlejavaformat/java/FormatterTest.java

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,4 +510,137 @@ public void removeTrailingTabsInComments() throws Exception {
510510
+ " }\n"
511511
+ "}\n");
512512
}
513+
514+
// @Test
515+
// @Disabled
516+
// public void removesRedundantPublicInterfaceModifiers() throws FormatterException {
517+
// String input = """
518+
// interface Test {
519+
// public static final int CONST = 1;
520+
// public abstract void method();
521+
// }
522+
// """;
523+
// String expected = """
524+
// interface Test {
525+
// int CONST = 1;
526+
// void method();
527+
// }
528+
// """;
529+
// assertThat(new Formatter().formatSource(input)).isEqualTo(expected);
530+
// }
531+
532+
@Test
533+
public void preservesFinalParameters() throws FormatterException {
534+
String input = """
535+
class Test {
536+
void method(final String param1, @Nullable final String param2) {}
537+
}
538+
""";
539+
String expected = """
540+
class Test {
541+
void method(final String param1, @Nullable final String param2) {}
542+
}
543+
""";
544+
assertThat(new Formatter().formatSource(input)).isEqualTo(expected);
545+
}
546+
547+
// @Test
548+
// @Disabled
549+
// public void reordersModifiers() throws FormatterException {
550+
// String input = """
551+
// class Test {
552+
// public final static String VALUE = "test";
553+
// protected final abstract void doSomething();
554+
// }
555+
// """;
556+
// String expected = """
557+
// class Test {
558+
// public static final String VALUE = "test";
559+
//
560+
// protected abstract void doSomething();
561+
// }
562+
// """;
563+
// assertThat(new Formatter().formatSource(input)).isEqualTo(expected);
564+
// }
565+
566+
// @Test
567+
// @Disabled
568+
// public void handlesNestedClasses() throws FormatterException {
569+
// String input = """
570+
// class Outer {
571+
// public static interface Inner {
572+
// public static final int VAL = 1;
573+
// }
574+
// }
575+
// """;
576+
// String expected = """
577+
// class Outer {
578+
// interface Inner {
579+
// int VAL = 1;
580+
// }
581+
// }
582+
// """;
583+
// assertThat(new Formatter().formatSource(input)).isEqualTo(expected);
584+
// }
585+
586+
@Test
587+
public void preservesMeaningfulModifiers() throws FormatterException {
588+
String input = """
589+
class Test {
590+
private int field;
591+
protected abstract void method();
592+
public static final class Inner {}
593+
}
594+
""";
595+
String expected = """
596+
class Test {
597+
private int field;
598+
599+
protected abstract void method();
600+
601+
public static final class Inner {}
602+
}
603+
""";
604+
assertThat(new Formatter().formatSource(input)).isEqualTo(expected);
605+
}
606+
607+
// @Test
608+
// @Disabled
609+
// public void handlesRecords() throws FormatterException {
610+
// String input = """
611+
// public record TestRecord(
612+
// public final String name,
613+
// public static final int MAX = 100
614+
// ) {
615+
// public static void doSomething() {}
616+
// }
617+
// """;
618+
// String expected = """
619+
// public record TestRecord(
620+
// String name,
621+
// int MAX = 100
622+
// ) {
623+
// static void doSomething() {}
624+
// }
625+
// """;
626+
// assertThat(new Formatter().formatSource(input)).isEqualTo(expected);
627+
// }
628+
629+
// @Test
630+
// @Disabled
631+
// public void handlesSealedClasses() throws FormatterException {
632+
// String input = """
633+
// public sealed abstract class Shape
634+
// permits public final class Circle, public non-sealed class Rectangle {
635+
// public abstract double area();
636+
// }
637+
// """;
638+
// String expected = """
639+
// public sealed abstract class Shape
640+
// permits Circle, Rectangle {
641+
// public abstract double area();
642+
// }
643+
// """;
644+
// assertThat(new Formatter().formatSource(input)).isEqualTo(expected);
645+
// }
513646
}

0 commit comments

Comments
 (0)