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