Skip to content

Commit 9ee3dd4

Browse files
author
Vincent Potucek
committed
formatSourceAndFixImportsAndDeclarations
1 parent e5e4414 commit 9ee3dd4

File tree

4 files changed

+552
-14
lines changed

4 files changed

+552
-14
lines changed

palantir-java-format/src/main/java/com/palantir/javaformat/java/Formatter.java

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414

1515
package com.palantir.javaformat.java;
1616

17+
import static com.palantir.javaformat.java.ImportOrderer.reorderImports;
18+
import static com.palantir.javaformat.java.RemoveUnusedDeclarations.removeUnusedDeclarations;
19+
import static com.palantir.javaformat.java.RemoveUnusedImports.removeUnusedImports;
1720
import static java.nio.charset.StandardCharsets.UTF_8;
1821

1922
import com.google.common.annotations.VisibleForTesting;
@@ -215,18 +218,8 @@ private static JavaInputAstVisitor createVisitor(
215218
}
216219

217220
static boolean errorDiagnostic(Diagnostic<?> input) {
218-
if (input.getKind() != Diagnostic.Kind.ERROR) {
219-
return false;
220-
}
221-
switch (input.getCode()) {
222-
case "compiler.err.invalid.meth.decl.ret.type.req":
223-
// accept constructor-like method declarations that don't match the name of their
224-
// enclosing class
225-
return false;
226-
default:
227-
break;
228-
}
229-
return true;
221+
return input.getKind() == Diagnostic.Kind.ERROR
222+
&& !input.getCode().equals("compiler.err.invalid.meth.decl.ret.type.req");
230223
}
231224

232225
/**
@@ -272,6 +265,21 @@ public String formatSourceAndFixImports(String input) throws FormatterException
272265
return formatted;
273266
}
274267

268+
/**
269+
* Formats an input string (a Java compilation unit) and fixes imports and redundant declarations.
270+
*
271+
* <p>Fixing imports includes ordering, spacing, and removal of unused import statements.
272+
*
273+
* @param input the input string
274+
* @return the output string
275+
* @throws FormatterException if the input string cannot be parsed
276+
* @see <a href="https://google.github.io/styleguide/javaguide.html#s3.3.3-import-ordering-and-spacing">Google Java
277+
* Style Guide - 3.3.3 Import ordering and spacing</a>
278+
*/
279+
public String formatSourceAndFixImportsAndDeclarations(String input) throws FormatterException {
280+
return formatSourceAndFixImports(removeUnusedDeclarations(input));
281+
}
282+
275283
/**
276284
* Fixes imports (e.g. ordering, spacing, and removal of unused import statements).
277285
*
@@ -282,7 +290,7 @@ public String formatSourceAndFixImports(String input) throws FormatterException
282290
* Style Guide - 3.3.3 Import ordering and spacing</a>
283291
*/
284292
public String fixImports(String input) throws FormatterException {
285-
return ImportOrderer.reorderImports(RemoveUnusedImports.removeUnusedImports(input), options.style());
293+
return reorderImports(removeUnusedImports(input), options.style());
286294
}
287295

288296
/**
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
/*
2+
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.palantir.javaformat.java;
17+
18+
import com.google.common.collect.ImmutableList;
19+
import com.google.common.collect.Range;
20+
import com.google.common.collect.RangeMap;
21+
import com.google.common.collect.TreeRangeMap;
22+
import com.sun.source.tree.AnnotationTree;
23+
import com.sun.source.tree.ClassTree;
24+
import com.sun.source.tree.CompilationUnitTree;
25+
import com.sun.source.tree.MethodTree;
26+
import com.sun.source.tree.ModifiersTree;
27+
import com.sun.source.tree.Tree;
28+
import com.sun.source.tree.Tree.Kind;
29+
import com.sun.source.tree.VariableTree;
30+
import com.sun.source.util.JavacTask;
31+
import com.sun.source.util.SourcePositions;
32+
import com.sun.source.util.TreePath;
33+
import com.sun.source.util.TreePathScanner;
34+
import com.sun.source.util.Trees;
35+
import com.sun.tools.javac.api.JavacTool;
36+
import com.sun.tools.javac.file.JavacFileManager;
37+
import com.sun.tools.javac.util.Context;
38+
import java.io.IOException;
39+
import java.net.URI;
40+
import java.util.Comparator;
41+
import java.util.Map;
42+
import java.util.Set;
43+
import java.util.stream.Collectors;
44+
import javax.annotation.Nullable;
45+
import javax.lang.model.element.Modifier;
46+
import javax.tools.Diagnostic;
47+
import javax.tools.DiagnosticCollector;
48+
import javax.tools.JavaFileObject;
49+
import javax.tools.SimpleJavaFileObject;
50+
51+
/**
52+
* Removes unused declarations from Java source code, including:
53+
* - Redundant modifiers in interfaces (public, static, final, abstract)
54+
* - Redundant modifiers in classes, enums, and annotations
55+
* - Redundant final modifiers on method parameters (preserved now)
56+
*/
57+
public class RemoveUnusedDeclarations {
58+
public static String removeUnusedDeclarations(String source) throws FormatterException {
59+
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
60+
JavacTask task = JavacTool.create()
61+
.getTask(
62+
null,
63+
new JavacFileManager(new Context(), true, null),
64+
diagnostics,
65+
ImmutableList.of("-Xlint:-processing"),
66+
null,
67+
ImmutableList.of((JavaFileObject)
68+
new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) {
69+
@Override
70+
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
71+
return source;
72+
}
73+
}));
74+
75+
try {
76+
Iterable<? extends CompilationUnitTree> units = task.parse();
77+
if (!units.iterator().hasNext()) {
78+
throw new FormatterException("No compilation units found");
79+
}
80+
81+
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
82+
if (diagnostic.getKind() == Diagnostic.Kind.ERROR) {
83+
throw new FormatterException("Syntax error in source: " + diagnostic.getMessage(null));
84+
}
85+
}
86+
87+
UnusedDeclarationScanner scanner = new UnusedDeclarationScanner(task);
88+
scanner.scan(units.iterator().next(), null);
89+
90+
return applyReplacements(source, scanner.getReplacements());
91+
} catch (IOException e) {
92+
throw new FormatterException("Error processing source file: " + e.getMessage());
93+
}
94+
}
95+
96+
private static final class UnusedDeclarationScanner extends TreePathScanner<Void, Void> {
97+
private final RangeMap<Integer, String> replacements = TreeRangeMap.create();
98+
private final SourcePositions sourcePositions;
99+
private final Trees trees;
100+
101+
private static final ImmutableList<Modifier> CANONICAL_MODIFIER_ORDER = ImmutableList.of(
102+
Modifier.PUBLIC,
103+
Modifier.PROTECTED,
104+
Modifier.PRIVATE,
105+
Modifier.ABSTRACT,
106+
Modifier.STATIC,
107+
Modifier.FINAL,
108+
Modifier.SEALED,
109+
Modifier.NON_SEALED,
110+
Modifier.TRANSIENT,
111+
Modifier.VOLATILE,
112+
Modifier.SYNCHRONIZED,
113+
Modifier.NATIVE,
114+
Modifier.STRICTFP);
115+
116+
private UnusedDeclarationScanner(JavacTask task) {
117+
this.sourcePositions = Trees.instance(task).getSourcePositions();
118+
this.trees = Trees.instance(task);
119+
}
120+
121+
public RangeMap<Integer, String> getReplacements() {
122+
return replacements;
123+
}
124+
125+
@Override
126+
public Void visitClass(ClassTree node, Void _unused) {
127+
TreePath parentPath = getCurrentPath().getParentPath();
128+
Kind parentKind = parentPath != null ? parentPath.getLeaf().getKind() : null;
129+
130+
if (node.getKind() == Tree.Kind.INTERFACE) {
131+
checkForRedundantModifiers(node, Set.of(Modifier.PUBLIC, Modifier.ABSTRACT, Modifier.STATIC));
132+
} else if ((parentPath != null ? parentPath.getLeaf().getKind() : null) == Tree.Kind.INTERFACE) {
133+
checkForRedundantModifiers(node, Set.of(Modifier.PUBLIC, Modifier.STATIC));
134+
} else if (node.getKind() == Tree.Kind.ANNOTATION_TYPE) {
135+
checkForRedundantModifiers(node, Set.of(Modifier.ABSTRACT));
136+
} else if (node.getModifiers().getFlags().contains(Modifier.SEALED)) {
137+
checkForRedundantModifiers(node, Set.of(Modifier.PUBLIC));
138+
} else {
139+
checkForRedundantModifiers(node, Set.of()); // Always sort
140+
}
141+
142+
return super.visitClass(node, null);
143+
}
144+
145+
@Override
146+
public Void visitMethod(MethodTree node, Void _unused) {
147+
TreePath parentPath = getCurrentPath().getParentPath();
148+
Kind parentKind = parentPath != null ? parentPath.getLeaf().getKind() : null;
149+
150+
if (parentKind == Tree.Kind.INTERFACE) {
151+
if (!node.getModifiers().getFlags().contains(Modifier.DEFAULT)
152+
&& !node.getModifiers().getFlags().contains(Modifier.STATIC)) {
153+
checkForRedundantModifiers(node, Set.of(Modifier.PUBLIC, Modifier.ABSTRACT));
154+
} else {
155+
checkForRedundantModifiers(node, Set.of());
156+
}
157+
} else if (parentKind == Tree.Kind.ANNOTATION_TYPE) {
158+
checkForRedundantModifiers(node, Set.of(Modifier.ABSTRACT));
159+
} else {
160+
checkForRedundantModifiers(node, Set.of()); // Always sort
161+
}
162+
163+
return super.visitMethod(node, null);
164+
}
165+
166+
@Override
167+
public Void visitVariable(VariableTree node, Void _unused) {
168+
TreePath parentPath = getCurrentPath().getParentPath();
169+
Kind parentKind = parentPath != null ? parentPath.getLeaf().getKind() : null;
170+
171+
if (node.getKind() == Tree.Kind.ENUM) {
172+
// Enum constants should have no modifiers
173+
checkForRedundantModifiers(node, Set.of(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL));
174+
} else if (parentKind == Tree.Kind.INTERFACE || parentKind == Tree.Kind.ANNOTATION_TYPE) {
175+
checkForRedundantModifiers(node, Set.of(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL));
176+
} else if (node.getKind() == Kind.RECORD) {
177+
// Record components should have no modifiers
178+
checkForRedundantModifiers(node, Set.of(Modifier.PUBLIC, Modifier.FINAL));
179+
} else {
180+
checkForRedundantModifiers(node, Set.of()); // Always sort
181+
}
182+
183+
return super.visitVariable(node, null);
184+
}
185+
186+
private void checkForRedundantModifiers(Tree node, Set<Modifier> redundantModifiers) {
187+
ModifiersTree modifiers = getModifiers(node);
188+
if (modifiers == null) return;
189+
try {
190+
addReplacementForModifiers(
191+
node,
192+
modifiers.getFlags().stream()
193+
.filter(redundantModifiers::contains)
194+
.collect(Collectors.toSet()));
195+
} catch (IOException e) {
196+
throw new RuntimeException(e);
197+
}
198+
}
199+
200+
@Nullable
201+
private ModifiersTree getModifiers(Tree node) {
202+
if (node instanceof ClassTree) return ((ClassTree) node).getModifiers();
203+
if (node instanceof MethodTree) return ((MethodTree) node).getModifiers();
204+
if (node instanceof VariableTree) return ((VariableTree) node).getModifiers();
205+
return null;
206+
}
207+
208+
private void addReplacementForModifiers(Tree node, Set<Modifier> toRemove) throws IOException {
209+
TreePath path = trees.getPath(getCurrentPath().getCompilationUnit(), node);
210+
if (path == null) return;
211+
212+
CompilationUnitTree unit = path.getCompilationUnit();
213+
String source = unit.getSourceFile().getCharContent(true).toString();
214+
215+
ModifiersTree modifiers = getModifiers(node);
216+
if (modifiers == null) return;
217+
218+
long modifiersStart = sourcePositions.getStartPosition(unit, modifiers);
219+
long modifiersEnd = sourcePositions.getEndPosition(unit, modifiers);
220+
if (modifiersStart == -1 || modifiersEnd == -1) return;
221+
222+
String newModifiersText = modifiers.getFlags().stream()
223+
.filter(m -> !toRemove.contains(m))
224+
.sorted(Comparator.comparingInt(mod -> {
225+
int idx = CANONICAL_MODIFIER_ORDER.indexOf(mod);
226+
return idx == -1 ? Integer.MAX_VALUE : idx;
227+
}))
228+
.map(Modifier::toString)
229+
.collect(Collectors.joining(" "));
230+
231+
long annotationsEnd = modifiersStart;
232+
for (AnnotationTree annotation : modifiers.getAnnotations()) {
233+
long end = sourcePositions.getEndPosition(unit, annotation);
234+
if (end > annotationsEnd) annotationsEnd = end;
235+
}
236+
237+
int effectiveStart = (int) annotationsEnd;
238+
while (effectiveStart < modifiersEnd && Character.isWhitespace(source.charAt(effectiveStart))) {
239+
effectiveStart++;
240+
}
241+
242+
String current = source.substring(effectiveStart, (int) modifiersEnd);
243+
if (!newModifiersText.trim().equals(current.trim())) {
244+
int globalEnd = (int) modifiersEnd;
245+
if (newModifiersText.isEmpty()) {
246+
while (globalEnd < source.length() && Character.isWhitespace(source.charAt(globalEnd))) {
247+
globalEnd++;
248+
}
249+
}
250+
replacements.put(Range.closedOpen(effectiveStart, globalEnd), newModifiersText);
251+
}
252+
}
253+
}
254+
255+
private static String applyReplacements(String source, RangeMap<Integer, String> replacements) {
256+
StringBuilder sb = new StringBuilder(source);
257+
for (Map.Entry<Range<Integer>, String> entry :
258+
replacements.asDescendingMapOfRanges().entrySet()) {
259+
Range<Integer> range = entry.getKey();
260+
sb.replace(range.lowerEndpoint(), range.upperEndpoint(), entry.getValue());
261+
}
262+
return sb.toString();
263+
}
264+
}

0 commit comments

Comments
 (0)