Skip to content

Commit 6e0d142

Browse files
author
Vincent Potucek
committed
feat: Add ReplaceObsoletesStep
1 parent 61eabd6 commit 6e0d142

File tree

3 files changed

+545
-40
lines changed

3 files changed

+545
-40
lines changed

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

Lines changed: 33 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414

1515
package com.google.googlejavaformat.java;
1616

17+
import static com.google.googlejavaformat.Newlines.guessLineSeparator;
18+
import static com.google.googlejavaformat.java.ImportOrderer.reorderImports;
19+
import static com.google.googlejavaformat.java.ModifierOrderer.reorderModifiers;
20+
import static com.google.googlejavaformat.java.RemoveUnusedDeclarations.removeUnusedDeclarations;
21+
import static com.google.googlejavaformat.java.RemoveUnusedImports.removeUnusedImports;
22+
import static com.google.googlejavaformat.java.StringWrapper.wrap;
1723
import static java.nio.charset.StandardCharsets.UTF_8;
1824

1925
import com.google.common.collect.ImmutableList;
@@ -117,17 +123,16 @@ static void format(final JavaInput javaInput, JavaOutput javaOutput, JavaFormatt
117123
Options.instance(context).put("allowStringFolding", "false");
118124
Options.instance(context).put("--enable-preview", "true");
119125
JCCompilationUnit unit;
120-
JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8);
121-
try {
122-
fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of());
126+
try (JavacFileManager fileManager = new JavacFileManager(context, true, UTF_8)) {
127+
fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, ImmutableList.of());
123128
} catch (IOException e) {
124-
// impossible
125-
throw new IOError(e);
129+
throw new RuntimeException(e);
126130
}
131+
127132
SimpleJavaFileObject source =
128133
new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) {
129134
@Override
130-
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
135+
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
131136
return javaInput.getText();
132137
}
133138
};
@@ -154,7 +159,7 @@ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOExcept
154159
if (Runtime.version().feature() >= 21) {
155160
visitor =
156161
createVisitor(
157-
"com.google.googlejavaformat.java.java21.Java21InputAstVisitor", builder, options);
162+
builder, options);
158163
} else {
159164
visitor = new JavaInputAstVisitor(builder, options.indentationMultiplier());
160165
}
@@ -168,9 +173,9 @@ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOExcept
168173
}
169174

170175
private static JavaInputAstVisitor createVisitor(
171-
final String className, final OpsBuilder builder, final JavaFormatterOptions options) {
176+
final OpsBuilder builder, final JavaFormatterOptions options) {
172177
try {
173-
return Class.forName(className)
178+
return Class.forName("com.google.googlejavaformat.java.java21.Java21InputAstVisitor")
174179
.asSubclass(JavaInputAstVisitor.class)
175180
.getConstructor(OpsBuilder.class, int.class)
176181
.newInstance(builder, options.indentationMultiplier());
@@ -183,15 +188,9 @@ static boolean errorDiagnostic(Diagnostic<?> input) {
183188
if (input.getKind() != Diagnostic.Kind.ERROR) {
184189
return false;
185190
}
186-
switch (input.getCode()) {
187-
case "compiler.err.invalid.meth.decl.ret.type.req":
188-
// accept constructor-like method declarations that don't match the name of their
189-
// enclosing class
190-
return false;
191-
default:
192-
break;
193-
}
194-
return true;
191+
// accept constructor-like method declarations that don't match the name of their
192+
// enclosing class
193+
return !input.getCode().equals("compiler.err.invalid.meth.decl.ret.type.req");
195194
}
196195

197196
/**
@@ -232,11 +231,11 @@ public String formatSource(String input) throws FormatterException {
232231
* Google Java Style Guide - 3.3.3 Import ordering and spacing</a>
233232
*/
234233
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;
234+
return wrap(
235+
formatSource(
236+
removeUnusedDeclarations(
237+
removeUnusedImports(
238+
reorderImports(input, options.style())))), this);
240239
}
241240

242241
/**
@@ -256,32 +255,30 @@ public String formatSource(String input, Collection<Range<Integer>> characterRan
256255
/**
257256
* Emit a list of {@link Replacement}s to convert from input to output.
258257
*
259-
* @param input the input compilation unit
258+
* @param input the input compilation unit
260259
* @param characterRanges the character ranges to reformat
261260
* @return a list of {@link Replacement}s, sorted from low index to high index, without overlaps
262261
* @throws FormatterException if the input string cannot be parsed
263262
*/
264263
public ImmutableList<Replacement> getFormatReplacements(
265264
String input, Collection<Range<Integer>> characterRanges) throws FormatterException {
266-
JavaInput javaInput = new JavaInput(input);
267-
268265
// TODO(cushon): this is only safe because the modifier ordering doesn't affect whitespace,
269266
// and doesn't change the replacements that are output. This is not true in general for
270267
// 'de-linting' changes (e.g. import ordering).
271-
if (options.reorderModifiers()) {
272-
javaInput = ModifierOrderer.reorderModifiers(javaInput, characterRanges);
273-
}
268+
var javaInput = options.reorderModifiers()
269+
? reorderModifiers(new JavaInput(input), characterRanges)
270+
: new JavaInput(input);
271+
var lineSeparator = guessLineSeparator(input);
272+
return formatReplacements(javaInput, new JavaOutput(lineSeparator, javaInput, new JavaCommentsHelper(lineSeparator, options)), characterRanges);
273+
}
274274

275-
String lineSeparator = Newlines.guessLineSeparator(input);
276-
JavaOutput javaOutput =
277-
new JavaOutput(lineSeparator, javaInput, new JavaCommentsHelper(lineSeparator, options));
275+
private ImmutableList<Replacement> formatReplacements(JavaInput javaInput, JavaOutput javaOutput, Collection<Range<Integer>> characterRanges) throws FormatterException {
278276
try {
279277
format(javaInput, javaOutput, options);
280278
} catch (FormattingError e) {
281279
throw new FormatterException(e.diagnostics());
282280
}
283-
RangeSet<Integer> tokenRangeSet = javaInput.characterRangesToTokenRanges(characterRanges);
284-
return javaOutput.getFormatReplacements(tokenRangeSet);
281+
return javaOutput.getFormatReplacements(javaInput.characterRangesToTokenRanges(characterRanges));
285282
}
286283

287284
/**
@@ -294,14 +291,10 @@ public static RangeSet<Integer> lineRangesToCharRanges(
294291
lines.add(input.length() + 1);
295292

296293
final RangeSet<Integer> characterRanges = TreeRangeSet.create();
297-
for (Range<Integer> lineRange :
298-
lineRanges.subRangeSet(Range.closedOpen(0, lines.size() - 1)).asRanges()) {
299-
int lineStart = lines.get(lineRange.lowerEndpoint());
294+
for (Range<Integer> lineRange : lineRanges.subRangeSet(Range.closedOpen(0, lines.size() - 1)).asRanges()) {
300295
// Exclude the trailing newline. This isn't strictly necessary, but handling blank lines
301296
// as empty ranges is convenient.
302-
int lineEnd = lines.get(lineRange.upperEndpoint()) - 1;
303-
Range<Integer> range = Range.closedOpen(lineStart, lineEnd);
304-
characterRanges.add(range);
297+
characterRanges.add(Range.closedOpen(lines.get(lineRange.lowerEndpoint()), lines.get(lineRange.upperEndpoint()) - 1));
305298
}
306299
return characterRanges;
307300
}
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)