Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@

package com.palantir.javaformat.java;

import static com.palantir.javaformat.java.ImportOrderer.reorderImports;
import static com.palantir.javaformat.java.RemoveUnusedDeclarations.removeUnusedDeclarations;
import static com.palantir.javaformat.java.RemoveUnusedImports.removeUnusedImports;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.annotations.VisibleForTesting;
Expand Down Expand Up @@ -215,18 +218,8 @@ private static JavaInputAstVisitor createVisitor(
}

static boolean errorDiagnostic(Diagnostic<?> input) {
if (input.getKind() != Diagnostic.Kind.ERROR) {
return false;
}
switch (input.getCode()) {
case "compiler.err.invalid.meth.decl.ret.type.req":
// accept constructor-like method declarations that don't match the name of their
// enclosing class
return false;
default:
break;
}
return true;
return input.getKind() == Diagnostic.Kind.ERROR
Copy link
Author

@Pankraz76 Pankraz76 Jul 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extract this.

&& !input.getCode().equals("compiler.err.invalid.meth.decl.ret.type.req");
}

/**
Expand Down Expand Up @@ -272,6 +265,21 @@ public String formatSourceAndFixImports(String input) throws FormatterException
return formatted;
}

/**
* Formats an input string (a Java compilation unit) and fixes imports and redundant declarations.
*
* <p>Fixing imports includes ordering, spacing, and removal of unused import statements.
*
* @param input the input string
* @return the output string
* @throws FormatterException if the input string cannot be parsed
* @see <a 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</a>
*/
public String formatSourceAndFixImportsAndDeclarations(String input) throws FormatterException {
return formatSourceAndFixImports(removeUnusedDeclarations(input));
}

/**
* Fixes imports (e.g. ordering, spacing, and removal of unused import statements).
*
Expand All @@ -282,7 +290,7 @@ public String formatSourceAndFixImports(String input) throws FormatterException
* Style Guide - 3.3.3 Import ordering and spacing</a>
*/
public String fixImports(String input) throws FormatterException {
return ImportOrderer.reorderImports(RemoveUnusedImports.removeUnusedImports(input), options.style());
return reorderImports(removeUnusedImports(input), options.style());
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
/*
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved.
*
* 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.palantir.javaformat.java;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Range;
import com.google.common.collect.RangeMap;
import com.google.common.collect.TreeRangeMap;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.SourcePositions;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreePathScanner;
import com.sun.source.util.Trees;
import com.sun.tools.javac.api.JavacTool;
import com.sun.tools.javac.file.JavacFileManager;
import com.sun.tools.javac.util.Context;
import java.io.IOException;
import java.net.URI;
import java.util.Comparator;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.lang.model.element.Modifier;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;

/**
* Removes unused declarations from Java source code, including:
* - Redundant modifiers in interfaces (public, static, final, abstract)
* - Redundant modifiers in classes, enums, and annotations
* - Redundant final modifiers on method parameters (preserved now)
*/
public class RemoveUnusedDeclarations {
public static String removeUnusedDeclarations(String source) throws FormatterException {
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
JavacTask task = JavacTool.create()
.getTask(
null,
new JavacFileManager(new Context(), true, null),
diagnostics,
ImmutableList.of("-Xlint:-processing"),
null,
ImmutableList.of((JavaFileObject)
new SimpleJavaFileObject(URI.create("source"), JavaFileObject.Kind.SOURCE) {
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return source;
}
}));

try {
Iterable<? extends CompilationUnitTree> units = task.parse();
if (!units.iterator().hasNext()) {
throw new FormatterException("No compilation units found");
}

for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
if (diagnostic.getKind() == Diagnostic.Kind.ERROR) {
throw new FormatterException("Syntax error in source: " + diagnostic.getMessage(null));
}
}

UnusedDeclarationScanner scanner = new UnusedDeclarationScanner(task);
scanner.scan(units.iterator().next(), null);

return applyReplacements(source, scanner.getReplacements());
} catch (IOException e) {
throw new FormatterException("Error processing source file: " + e.getMessage());
}
}

private static final class UnusedDeclarationScanner extends TreePathScanner<Void, Void> {
private final RangeMap<Integer, String> replacements = TreeRangeMap.create();
private final SourcePositions sourcePositions;
private final Trees trees;

private static final ImmutableList<Modifier> CANONICAL_MODIFIER_ORDER = ImmutableList.of(
Modifier.PUBLIC,
Modifier.PROTECTED,
Modifier.PRIVATE,
Modifier.ABSTRACT,
Modifier.STATIC,
Modifier.FINAL,
Modifier.SEALED,
Modifier.NON_SEALED,
Modifier.TRANSIENT,
Modifier.VOLATILE,
Modifier.SYNCHRONIZED,
Modifier.NATIVE,
Modifier.STRICTFP);

private UnusedDeclarationScanner(JavacTask task) {
this.sourcePositions = Trees.instance(task).getSourcePositions();
this.trees = Trees.instance(task);
}

public RangeMap<Integer, String> getReplacements() {
return replacements;
}

@Override
public Void visitClass(ClassTree node, Void _unused) {
TreePath parentPath = getCurrentPath().getParentPath();
Kind parentKind = parentPath != null ? parentPath.getLeaf().getKind() : null;

if (node.getKind() == Tree.Kind.INTERFACE) {
checkForRedundantModifiers(node, Set.of(Modifier.PUBLIC, Modifier.ABSTRACT, Modifier.STATIC));
} else if ((parentPath != null ? parentPath.getLeaf().getKind() : null) == Tree.Kind.INTERFACE) {
checkForRedundantModifiers(node, Set.of(Modifier.PUBLIC, Modifier.STATIC));
} else if (node.getKind() == Tree.Kind.ANNOTATION_TYPE) {
checkForRedundantModifiers(node, Set.of(Modifier.ABSTRACT));
} else if (node.getModifiers().getFlags().contains(Modifier.SEALED)) {
checkForRedundantModifiers(node, Set.of(Modifier.PUBLIC));
} else {
checkForRedundantModifiers(node, Set.of()); // Always sort
}

return super.visitClass(node, null);
}

@Override
public Void visitMethod(MethodTree node, Void _unused) {
TreePath parentPath = getCurrentPath().getParentPath();
Kind parentKind = parentPath != null ? parentPath.getLeaf().getKind() : null;

if (parentKind == Tree.Kind.INTERFACE) {
if (!node.getModifiers().getFlags().contains(Modifier.DEFAULT)
&& !node.getModifiers().getFlags().contains(Modifier.STATIC)) {
checkForRedundantModifiers(node, Set.of(Modifier.PUBLIC, Modifier.ABSTRACT));
} else {
checkForRedundantModifiers(node, Set.of());
}
} else if (parentKind == Tree.Kind.ANNOTATION_TYPE) {
checkForRedundantModifiers(node, Set.of(Modifier.ABSTRACT));
} else {
checkForRedundantModifiers(node, Set.of()); // Always sort
}

return super.visitMethod(node, null);
}

@Override
public Void visitVariable(VariableTree node, Void _unused) {
TreePath parentPath = getCurrentPath().getParentPath();
Kind parentKind = parentPath != null ? parentPath.getLeaf().getKind() : null;

if (node.getKind() == Tree.Kind.ENUM) {
// Enum constants should have no modifiers
checkForRedundantModifiers(node, Set.of(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL));
} else if (parentKind == Tree.Kind.INTERFACE || parentKind == Tree.Kind.ANNOTATION_TYPE) {
checkForRedundantModifiers(node, Set.of(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL));
} else if (node.getKind() == Kind.RECORD) {
// Record components should have no modifiers
checkForRedundantModifiers(node, Set.of(Modifier.PUBLIC, Modifier.FINAL));
} else {
checkForRedundantModifiers(node, Set.of()); // Always sort
}

return super.visitVariable(node, null);
}

private void checkForRedundantModifiers(Tree node, Set<Modifier> redundantModifiers) {
ModifiersTree modifiers = getModifiers(node);
if (modifiers == null) return;
try {
addReplacementForModifiers(
node,
modifiers.getFlags().stream()
.filter(redundantModifiers::contains)
.collect(Collectors.toSet()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@Nullable
private ModifiersTree getModifiers(Tree node) {
if (node instanceof ClassTree) return ((ClassTree) node).getModifiers();
if (node instanceof MethodTree) return ((MethodTree) node).getModifiers();
if (node instanceof VariableTree) return ((VariableTree) node).getModifiers();
return null;
}

private void addReplacementForModifiers(Tree node, Set<Modifier> toRemove) throws IOException {
TreePath path = trees.getPath(getCurrentPath().getCompilationUnit(), node);
if (path == null) return;

CompilationUnitTree unit = path.getCompilationUnit();
String source = unit.getSourceFile().getCharContent(true).toString();

ModifiersTree modifiers = getModifiers(node);
if (modifiers == null) return;

long modifiersStart = sourcePositions.getStartPosition(unit, modifiers);
long modifiersEnd = sourcePositions.getEndPosition(unit, modifiers);
if (modifiersStart == -1 || modifiersEnd == -1) return;

String newModifiersText = modifiers.getFlags().stream()
.filter(m -> !toRemove.contains(m))
.sorted(Comparator.comparingInt(mod -> {
int idx = CANONICAL_MODIFIER_ORDER.indexOf(mod);
return idx == -1 ? Integer.MAX_VALUE : idx;
}))
.map(Modifier::toString)
.collect(Collectors.joining(" "));

long annotationsEnd = modifiersStart;
for (AnnotationTree annotation : modifiers.getAnnotations()) {
long end = sourcePositions.getEndPosition(unit, annotation);
if (end > annotationsEnd) annotationsEnd = end;
}

int effectiveStart = (int) annotationsEnd;
while (effectiveStart < modifiersEnd && Character.isWhitespace(source.charAt(effectiveStart))) {
effectiveStart++;
}

String current = source.substring(effectiveStart, (int) modifiersEnd);
if (!newModifiersText.trim().equals(current.trim())) {
int globalEnd = (int) modifiersEnd;
if (newModifiersText.isEmpty()) {
while (globalEnd < source.length() && Character.isWhitespace(source.charAt(globalEnd))) {
globalEnd++;
}
}
replacements.put(Range.closedOpen(effectiveStart, globalEnd), newModifiersText);
}
}
}

private static String applyReplacements(String source, RangeMap<Integer, String> replacements) {
StringBuilder sb = new StringBuilder(source);
for (Map.Entry<Range<Integer>, String> entry :
replacements.asDescendingMapOfRanges().entrySet()) {
Range<Integer> range = entry.getKey();
sb.replace(range.lowerEndpoint(), range.upperEndpoint(), entry.getValue());
}
return sb.toString();
}
}
Loading