Skip to content

feat: Add RemoveUnusedDeclarations #1263

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
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
15 changes: 10 additions & 5 deletions core/src/main/java/com/google/googlejavaformat/java/Formatter.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@

package com.google.googlejavaformat.java;

import static com.google.googlejavaformat.java.ImportOrderer.reorderImports;
import static com.google.googlejavaformat.java.RemoveUnusedDeclarations.removeUnusedDeclarations;
import static com.google.googlejavaformat.java.RemoveUnusedImports.removeUnusedImports;
import static com.google.googlejavaformat.java.StringWrapper.wrap;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.collect.ImmutableList;
Expand Down Expand Up @@ -232,11 +236,12 @@ public String formatSource(String input) throws FormatterException {
* Google Java Style Guide - 3.3.3 Import ordering and spacing</a>
*/
public String formatSourceAndFixImports(String input) throws FormatterException {
input = ImportOrderer.reorderImports(input, options.style());
input = RemoveUnusedImports.removeUnusedImports(input);
String formatted = formatSource(input);
formatted = StringWrapper.wrap(formatted, this);
return formatted;
return wrap(
formatSource(
removeUnusedDeclarations(
removeUnusedImports(
reorderImports(input, options.style())))),
this);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package com.google.googlejavaformat.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.*;
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 javax.lang.model.element.Modifier;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import java.io.IOException;
import java.net.URI;
import java.util.*;
import java.util.stream.Collectors;

/**
* 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<>();
var 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));
}
}

var 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 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.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) {
var parentPath = getCurrentPath().getParentPath();
if (node.getKind() == Tree.Kind.INTERFACE) {
checkForRedundantModifiers(node, Set.of(Modifier.PUBLIC, Modifier.ABSTRACT));
} 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 {
checkForRedundantModifiers(node, Set.of()); // Always sort
}

return super.visitClass(node, unused);
}

@Override
public Void visitMethod(MethodTree node, Void unused) {
var parentPath = getCurrentPath().getParentPath();
var 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, unused);
}

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

if (node.getKind() == Tree.Kind.ENUM) {
return super.visitVariable(node, unused);
}

if (parentKind == Tree.Kind.INTERFACE || parentKind == Tree.Kind.ANNOTATION_TYPE) {
checkForRedundantModifiers(node, Set.of(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL));
} else {
checkForRedundantModifiers(node, Set.of()); // Always sort
}

return super.visitVariable(node, unused);
}

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

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))
.collect(Collectors.toCollection(LinkedHashSet::new)).stream()
.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();
}
}
133 changes: 133 additions & 0 deletions core/src/test/java/com/google/googlejavaformat/java/FormatterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -510,4 +510,137 @@ public void removeTrailingTabsInComments() throws Exception {
+ " }\n"
+ "}\n");
}

// @Test
// @Disabled
// public void removesRedundantPublicInterfaceModifiers() throws FormatterException {
// String input = """
// interface Test {
// public static final int CONST = 1;
// public abstract void method();
// }
// """;
// String expected = """
// interface Test {
// int CONST = 1;
// void method();
// }
// """;
// assertThat(new Formatter().formatSource(input)).isEqualTo(expected);
// }

@Test
public void preservesFinalParameters() throws FormatterException {
String input = """
class Test {
void method(final String param1, @Nullable final String param2) {}
}
""";
String expected = """
class Test {
void method(final String param1, @Nullable final String param2) {}
}
""";
assertThat(new Formatter().formatSource(input)).isEqualTo(expected);
}

// @Test
// @Disabled
// public void reordersModifiers() throws FormatterException {
// String input = """
// class Test {
// public final static String VALUE = "test";
// protected final abstract void doSomething();
// }
// """;
// String expected = """
// class Test {
// public static final String VALUE = "test";
//
// protected abstract void doSomething();
// }
// """;
// assertThat(new Formatter().formatSource(input)).isEqualTo(expected);
// }

// @Test
// @Disabled
// public void handlesNestedClasses() throws FormatterException {
// String input = """
// class Outer {
// public static interface Inner {
// public static final int VAL = 1;
// }
// }
// """;
// String expected = """
// class Outer {
// interface Inner {
// int VAL = 1;
// }
// }
// """;
// assertThat(new Formatter().formatSource(input)).isEqualTo(expected);
// }

@Test
public void preservesMeaningfulModifiers() throws FormatterException {
String input = """
class Test {
private int field;
protected abstract void method();
public static final class Inner {}
}
""";
String expected = """
class Test {
private int field;

protected abstract void method();

public static final class Inner {}
}
""";
assertThat(new Formatter().formatSource(input)).isEqualTo(expected);
}

// @Test
// @Disabled
// public void handlesRecords() throws FormatterException {
// String input = """
// public record TestRecord(
// public final String name,
// public static final int MAX = 100
// ) {
// public static void doSomething() {}
// }
// """;
// String expected = """
// public record TestRecord(
// String name,
// int MAX = 100
// ) {
// static void doSomething() {}
// }
// """;
// assertThat(new Formatter().formatSource(input)).isEqualTo(expected);
// }

// @Test
// @Disabled
// public void handlesSealedClasses() throws FormatterException {
// String input = """
// public sealed abstract class Shape
// permits public final class Circle, public non-sealed class Rectangle {
// public abstract double area();
// }
// """;
// String expected = """
// public sealed abstract class Shape
// permits Circle, Rectangle {
// public abstract double area();
// }
// """;
// assertThat(new Formatter().formatSource(input)).isEqualTo(expected);
// }
}
Loading