Skip to content

Commit 304839c

Browse files
author
Vincent Potucek
committed
feat: Add ReplaceObsoletesStep
1 parent 61eabd6 commit 304839c

File tree

3 files changed

+522
-5
lines changed

3 files changed

+522
-5
lines changed

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+
}

0 commit comments

Comments
 (0)