diff --git a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/core/dom/JavacCompilationUnitResolver.java b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/core/dom/JavacCompilationUnitResolver.java index 51952a813e9..5e81fa7d8cd 100644 --- a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/core/dom/JavacCompilationUnitResolver.java +++ b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/core/dom/JavacCompilationUnitResolver.java @@ -28,6 +28,7 @@ import java.util.function.Function; import java.util.stream.Collectors; +import javax.lang.model.element.TypeElement; import javax.tools.Diagnostic; import javax.tools.DiagnosticListener; import javax.tools.JavaFileManager; @@ -42,6 +43,7 @@ import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.Signature; import org.eclipse.jdt.core.WorkingCopyOwner; +import org.eclipse.jdt.core.compiler.CategorizedProblem; import org.eclipse.jdt.core.compiler.CharOperation; import org.eclipse.jdt.core.compiler.IProblem; import org.eclipse.jdt.core.compiler.InvalidInputException; @@ -57,6 +59,7 @@ import org.eclipse.jdt.internal.compiler.lookup.BinaryTypeBinding; import org.eclipse.jdt.internal.compiler.lookup.LookupEnvironment; import org.eclipse.jdt.internal.compiler.lookup.PackageBinding; +import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory; import org.eclipse.jdt.internal.compiler.util.Util; import org.eclipse.jdt.internal.core.CancelableNameEnvironment; import org.eclipse.jdt.internal.core.JavaModelManager; @@ -65,18 +68,25 @@ import org.eclipse.jdt.internal.core.util.BindingKeyParser; import org.eclipse.jdt.internal.javac.JavacProblemConverter; import org.eclipse.jdt.internal.javac.JavacUtils; +import org.eclipse.jdt.internal.javac.UnusedProblemFactory; +import org.eclipse.jdt.internal.javac.UnusedTreeScanner; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.Tree; import com.sun.source.util.JavacTask; import com.sun.source.util.TaskEvent; import com.sun.source.util.TaskListener; import com.sun.tools.javac.api.JavacTool; import com.sun.tools.javac.api.MultiTaskListener; +import com.sun.tools.javac.code.Symbol.PackageSymbol; import com.sun.tools.javac.file.JavacFileManager; import com.sun.tools.javac.parser.JavadocTokenizer; import com.sun.tools.javac.parser.Scanner; import com.sun.tools.javac.parser.ScannerFactory; import com.sun.tools.javac.parser.Tokens.Comment.CommentStyle; import com.sun.tools.javac.parser.Tokens.TokenKind; +import com.sun.tools.javac.tree.JCTree.JCClassDecl; import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; import com.sun.tools.javac.util.Context; import com.sun.tools.javac.util.DiagnosticSource; @@ -470,6 +480,7 @@ private Map result = new HashMap<>(sourceUnits.length, 1.f); Map filesToUnits = new HashMap<>(); + final UnusedProblemFactory unusedProblemFactory = new UnusedProblemFactory(new DefaultProblemFactory(), compilerOptions); var problemConverter = new JavacProblemConverter(compilerOptions, context); DiagnosticListener diagnosticListener = diagnostic -> { findTargetDOM(filesToUnits, diagnostic).ifPresent(dom -> { @@ -488,6 +499,63 @@ public void finished(TaskEvent e) { if (e.getCompilationUnit() instanceof JCCompilationUnit u) { problemConverter.registerUnit(e.getSourceFile(), u); } + + if (e.getKind() == TaskEvent.Kind.ANALYZE) { + final JavaFileObject file = e.getSourceFile(); + final CompilationUnit dom = filesToUnits.get(file); + if (dom == null) { + return; + } + + final TypeElement currentTopLevelType = e.getTypeElement(); + UnusedTreeScanner scanner = new UnusedTreeScanner<>() { + @Override + public Void visitClass(ClassTree node, Void p) { + if (node instanceof JCClassDecl classDecl) { + /** + * If a Java file contains multiple top-level types, it will + * trigger multiple ANALYZE taskEvents for the same compilation + * unit. Each ANALYZE taskEvent corresponds to the completion + * of analysis for a single top-level type. Therefore, in the + * ANALYZE task event listener, we only visit the class and nested + * classes that belong to the currently analyzed top-level type. + */ + if (Objects.equals(currentTopLevelType, classDecl.sym) + || !(classDecl.sym.owner instanceof PackageSymbol)) { + return super.visitClass(node, p); + } else { + return null; // Skip if it does not belong to the currently analyzed top-level type. + } + } + + return super.visitClass(node, p); + } + }; + final CompilationUnitTree unit = e.getCompilationUnit(); + try { + scanner.scan(unit, null); + } catch (Exception ex) { + ILog.get().error("Internal error when visiting the AST Tree. " + ex.getMessage(), ex); + } + + List unusedProblems = scanner.getUnusedPrivateMembers(unusedProblemFactory); + if (!unusedProblems.isEmpty()) { + addProblemsToDOM(dom, unusedProblems); + } + + List unusedImports = scanner.getUnusedImports(unusedProblemFactory); + List topTypes = unit.getTypeDecls(); + int typeCount = topTypes.size(); + // Once all top level types of this Java file have been resolved, + // we can report the unused import to the DOM. + if (typeCount <= 1) { + addProblemsToDOM(dom, unusedImports); + } else if (typeCount > 1 && topTypes.get(typeCount - 1) instanceof JCClassDecl lastType) { + if (Objects.equals(currentTopLevelType, lastType.sym)) { + addProblemsToDOM(dom, unusedImports); + } + } + } } }); // must be 1st thing added to context @@ -602,6 +670,17 @@ public boolean visit(Javadoc javadoc) { return result; } + private void addProblemsToDOM(CompilationUnit dom, Collection problems) { + IProblem[] previous = dom.getProblems(); + IProblem[] newProblems = Arrays.copyOf(previous, previous.length + problems.size()); + int start = previous.length; + for (CategorizedProblem problem : problems) { + newProblems[start] = problem; + start++; + } + dom.setProblems(newProblems); + } + private Optional findTargetDOM(Map filesToUnits, Object obj) { if (obj == null) { return Optional.empty(); diff --git a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacCompilationResult.java b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacCompilationResult.java index afdb12b8828..d3632a3d801 100644 --- a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacCompilationResult.java +++ b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacCompilationResult.java @@ -13,11 +13,14 @@ package org.eclipse.jdt.internal.javac; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.stream.Stream; +import org.eclipse.jdt.core.compiler.CategorizedProblem; import org.eclipse.jdt.internal.compiler.CompilationResult; import org.eclipse.jdt.internal.compiler.env.ICompilationUnit; @@ -26,6 +29,8 @@ public class JavacCompilationResult extends CompilationResult { private Set javacSimpleNameReferences = new TreeSet<>(); private Set javacRootReferences = new TreeSet<>(); private boolean isMigrated = false; + private List unusedMembers = null; + private List unusedImports = null; public JavacCompilationResult(ICompilationUnit compilationUnit) { this(compilationUnit, 0, 0, Integer.MAX_VALUE); @@ -65,4 +70,31 @@ public void migrateReferenceInfo() { this.javacQualifiedReferences.clear(); this.isMigrated = true; } + + public void setUnusedImports(List newUnusedImports) { + this.unusedImports = newUnusedImports; + } + + public void addUnusedMembers(List problems) { + if (this.unusedMembers == null) { + this.unusedMembers = new ArrayList<>(); + } + + this.unusedMembers.addAll(problems); + } + + public List getAdditionalProblems() { + if (this.unusedMembers == null && this.unusedImports == null) { + return null; + } + + List problems = new ArrayList<>(); + if (this.unusedImports != null) { + problems.addAll(this.unusedImports); + } + if (this.unusedMembers != null) { + problems.addAll(this.unusedMembers); + } + return problems; + } } diff --git a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacCompiler.java b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacCompiler.java index 5b01970c98e..dcc2070e5e1 100644 --- a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacCompiler.java +++ b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacCompiler.java @@ -31,6 +31,7 @@ import org.eclipse.core.resources.IResource; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.compiler.CategorizedProblem; import org.eclipse.jdt.core.compiler.IProblem; import org.eclipse.jdt.internal.compiler.CompilationResult; import org.eclipse.jdt.internal.compiler.Compiler; @@ -53,11 +54,13 @@ public class JavacCompiler extends Compiler { JavacConfig compilerConfig; + IProblemFactory problemFactory; public JavacCompiler(INameEnvironment environment, IErrorHandlingPolicy policy, CompilerConfiguration compilerConfig, ICompilerRequestor requestor, IProblemFactory problemFactory) { super(environment, policy, compilerConfig.compilerOptions(), requestor, problemFactory); this.compilerConfig = JavacConfig.createFrom(compilerConfig); + this.problemFactory = problemFactory; } @Override @@ -105,7 +108,7 @@ public void compile(ICompilationUnit[] sourceUnits) { .collect(Collectors.groupingBy(this::computeOutputDirectory)); // Register listener to intercept intermediate results from Javac task. - JavacTaskListener javacListener = new JavacTaskListener(this.compilerConfig, outputSourceMapping); + JavacTaskListener javacListener = new JavacTaskListener(this.compilerConfig, outputSourceMapping, this.problemFactory); MultiTaskListener mtl = MultiTaskListener.instance(javacContext); mtl.add(javacListener); @@ -167,20 +170,24 @@ public int errorCount() { for (int i = 0; i < sourceUnits.length; i++) { ICompilationUnit in = sourceUnits[i]; CompilationResult result = new CompilationResult(in, i, sourceUnits.length, Integer.MAX_VALUE); + List problems = new ArrayList<>(); if (javacListener.getResults().containsKey(in)) { result = javacListener.getResults().get(in); ((JavacCompilationResult) result).migrateReferenceInfo(); result.unitIndex = i; result.totalUnitsKnown = sourceUnits.length; + List additionalProblems = ((JavacCompilationResult) result).getAdditionalProblems(); + if (additionalProblems != null && !additionalProblems.isEmpty()) { + problems.addAll(additionalProblems); + } } if (javacProblems.containsKey(in)) { - JavacProblem[] problems = javacProblems.get(in).toArray(new JavacProblem[0]); - result.problems = problems; // JavaBuilder is responsible - // for converting the problems - // to IMarkers - result.problemCount = problems.length; + problems.addAll(javacProblems.get(in)); } + // JavaBuilder is responsible for converting the problems to IMarkers + result.problems = problems.toArray(new CategorizedProblem[0]); + result.problemCount = problems.size(); this.requestor.acceptResult(result); if (result.compiledTypes != null) { for (Object type : result.compiledTypes.values()) { diff --git a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacTaskListener.java b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacTaskListener.java index 3f29df12f22..73609209d36 100644 --- a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacTaskListener.java +++ b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/JavacTaskListener.java @@ -26,7 +26,9 @@ import javax.tools.JavaFileObject; import org.eclipse.core.resources.IContainer; +import org.eclipse.core.runtime.ILog; import org.eclipse.jdt.internal.compiler.ClassFile; +import org.eclipse.jdt.internal.compiler.IProblemFactory; import org.eclipse.jdt.internal.compiler.env.ICompilationUnit; import com.sun.source.tree.ClassTree; @@ -35,7 +37,6 @@ import com.sun.source.tree.MemberSelectTree; import com.sun.source.util.TaskEvent; import com.sun.source.util.TaskListener; -import com.sun.source.util.TreeScanner; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.ClassSymbol; import com.sun.tools.javac.code.Symbol.PackageSymbol; @@ -52,6 +53,7 @@ public class JavacTaskListener implements TaskListener { private Map sourceOutputMapping = new HashMap<>(); private Map results = new HashMap<>(); + private UnusedProblemFactory problemFactory; private static final Set PRIMITIVE_TYPES = new HashSet(Arrays.asList( "byte", "short", @@ -63,7 +65,9 @@ public class JavacTaskListener implements TaskListener { "boolean" )); - public JavacTaskListener(JavacConfig config, Map> outputSourceMapping) { + public JavacTaskListener(JavacConfig config, Map> outputSourceMapping, + IProblemFactory problemFactory) { + this.problemFactory = new UnusedProblemFactory(problemFactory, config.compilerOptions()); for (Entry> entry : outputSourceMapping.entrySet()) { IContainer currentOutput = entry.getKey(); entry.getValue().forEach(cu -> sourceOutputMapping.put(cu, currentOutput)); @@ -84,9 +88,9 @@ public void finished(TaskEvent e) { final Map visitedClasses = new HashMap(); final Set hierarchyRecorded = new HashSet<>(); final TypeElement currentTopLevelType = e.getTypeElement(); - TreeScanner scanner = new TreeScanner() { + UnusedTreeScanner scanner = new UnusedTreeScanner<>() { @Override - public Object visitClass(ClassTree node, Object p) { + public Void visitClass(ClassTree node, Void p) { if (node instanceof JCClassDecl classDecl) { /** * If a Java file contains multiple top-level types, it will @@ -116,7 +120,7 @@ public Object visitClass(ClassTree node, Object p) { } @Override - public Object visitIdentifier(IdentifierTree node, Object p) { + public Void visitIdentifier(IdentifierTree node, Void p) { if (node instanceof JCIdent id && id.sym instanceof TypeSymbol typeSymbol) { String qualifiedName = typeSymbol.getQualifiedName().toString(); @@ -126,7 +130,7 @@ public Object visitIdentifier(IdentifierTree node, Object p) { } @Override - public Object visitMemberSelect(MemberSelectTree node, Object p) { + public Void visitMemberSelect(MemberSelectTree node, Void p) { if (node instanceof JCFieldAccess field) { if (field.sym != null && !(field.type instanceof MethodType || field.type instanceof UnknownType)) { @@ -221,7 +225,14 @@ private void recordTypeHierarchy(ClassSymbol classSymbol) { }; final CompilationUnitTree unit = e.getCompilationUnit(); - scanner.scan(unit, null); + try { + scanner.scan(unit, null); + } catch (Exception ex) { + ILog.get().error("Internal error when visiting the AST Tree. " + ex.getMessage(), ex); + } + + result.addUnusedMembers(scanner.getUnusedPrivateMembers(this.problemFactory)); + result.setUnusedImports(scanner.getUnusedImports(this.problemFactory)); } } diff --git a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/UnusedProblemFactory.java b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/UnusedProblemFactory.java new file mode 100644 index 00000000000..e074a6996ff --- /dev/null +++ b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/UnusedProblemFactory.java @@ -0,0 +1,225 @@ +/******************************************************************************* +* Copyright (c) 2024 Microsoft Corporation and others. +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Eclipse Public License 2.0 +* which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Microsoft Corporation - initial API and implementation +*******************************************************************************/ + +package org.eclipse.jdt.internal.javac; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.tools.JavaFileObject; + +import org.eclipse.jdt.core.compiler.CategorizedProblem; +import org.eclipse.jdt.core.compiler.IProblem; +import org.eclipse.jdt.internal.compiler.IProblemFactory; +import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; +import org.eclipse.jdt.internal.compiler.problem.ProblemReporter; +import org.eclipse.jdt.internal.compiler.problem.ProblemSeverities; + +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.Tree; +import com.sun.tools.javac.code.Symbol.ClassSymbol; +import com.sun.tools.javac.code.Symbol.MethodSymbol; +import com.sun.tools.javac.code.Symbol.VarSymbol; +import com.sun.tools.javac.tree.JCTree.JCClassDecl; +import com.sun.tools.javac.tree.JCTree.JCImport; +import com.sun.tools.javac.tree.JCTree.JCMethodDecl; +import com.sun.tools.javac.tree.JCTree.JCVariableDecl; + +public class UnusedProblemFactory { + private Map> filesToUnusedImports = new HashMap<>(); + private IProblemFactory problemFactory; + private CompilerOptions compilerOptions; + + public UnusedProblemFactory(IProblemFactory problemFactory, CompilerOptions compilerOptions) { + this.problemFactory = problemFactory; + this.compilerOptions = compilerOptions; + } + + public UnusedProblemFactory(IProblemFactory problemFactory, Map compilerOptions) { + this.problemFactory = problemFactory; + this.compilerOptions = new CompilerOptions(compilerOptions); + } + + public List addUnusedImports(CompilationUnitTree unit, Map unusedImports) { + int severity = this.toSeverity(IProblem.UnusedImport); + if (severity == ProblemSeverities.Ignore || severity == ProblemSeverities.Optional) { + return null; + } + + Map unusedWarning = new LinkedHashMap<>(); + final char[] fileName = unit.getSourceFile().getName().toCharArray(); + for (Entry unusedImport : unusedImports.entrySet()) { + String importName = unusedImport.getKey(); + JCImport importNode = unusedImport.getValue(); + int pos = importNode.qualid.getStartPosition(); + int endPos = pos + importName.length() - 1; + int line = (int) unit.getLineMap().getLineNumber(pos); + int column = (int) unit.getLineMap().getColumnNumber(pos); + String[] arguments = new String[] { importName }; + CategorizedProblem problem = problemFactory.createProblem(fileName, + IProblem.UnusedImport, + arguments, + arguments, + severity, pos, endPos, line, column); + unusedWarning.put(importName, problem); + } + + JavaFileObject file = unit.getSourceFile(); + Map newUnusedImports = mergeUnusedImports(filesToUnusedImports.get(file), unusedWarning); + filesToUnusedImports.put(file, newUnusedImports); + return new ArrayList<>(newUnusedImports.values()); + } + + public List addUnusedPrivateMembers(CompilationUnitTree unit, List unusedPrivateDecls) { + if (unit == null) { + return Collections.emptyList(); + } + + final char[] fileName = unit.getSourceFile().getName().toCharArray(); + List problems = new ArrayList<>(); + for (Tree decl : unusedPrivateDecls) { + CategorizedProblem problem = null; + if (decl instanceof JCClassDecl classDecl) { + int severity = this.toSeverity(IProblem.UnusedPrivateType); + if (severity == ProblemSeverities.Ignore || severity == ProblemSeverities.Optional) { + continue; + } + + int pos = classDecl.getPreferredPosition(); + int startPos = pos; + int endPos = pos; + String shortName = classDecl.name.toString(); + JavaFileObject fileObject = unit.getSourceFile(); + try { + CharSequence charContent = fileObject.getCharContent(true); + String content = charContent.toString(); + if (content != null && content.length() > pos) { + String temp = content.substring(pos); + int index = temp.indexOf(shortName); + if (index >= 0) { + startPos = pos + index; + endPos = startPos + shortName.length() - 1; + } + } + } catch (IOException e) { + // ignore + } + + int line = (int) unit.getLineMap().getLineNumber(startPos); + int column = (int) unit.getLineMap().getColumnNumber(startPos); + problem = problemFactory.createProblem(fileName, + IProblem.UnusedPrivateType, new String[] { + shortName + }, new String[] { + shortName + }, + severity, startPos, endPos, line, column); + } else if (decl instanceof JCMethodDecl methodDecl) { + int problemId = methodDecl.sym.isConstructor() ? IProblem.UnusedPrivateConstructor + : IProblem.UnusedPrivateMethod; + int severity = this.toSeverity(problemId); + if (severity == ProblemSeverities.Ignore || severity == ProblemSeverities.Optional) { + continue; + } + + String selector = methodDecl.name.toString(); + String typeName = methodDecl.sym.owner.name.toString(); + String[] params = methodDecl.params.stream().map(variableDecl -> { + return variableDecl.vartype.toString(); + }).toArray(String[]::new); + String[] arguments = new String[] { + typeName, selector, String.join(", ", params) + }; + + int pos = methodDecl.getPreferredPosition(); + int endPos = pos + methodDecl.name.toString().length() - 1; + int line = (int) unit.getLineMap().getLineNumber(pos); + int column = (int) unit.getLineMap().getColumnNumber(pos); + problem = problemFactory.createProblem(fileName, + problemId, arguments, arguments, + severity, pos, endPos, line, column); + } else if (decl instanceof JCVariableDecl variableDecl) { + int pos = variableDecl.getPreferredPosition(); + int endPos = pos + variableDecl.name.toString().length() - 1; + int line = (int) unit.getLineMap().getLineNumber(pos); + int column = (int) unit.getLineMap().getColumnNumber(pos); + int problemId = IProblem.LocalVariableIsNeverUsed; + String[] arguments = null; + String name = variableDecl.name.toString(); + VarSymbol varSymbol = variableDecl.sym; + if (varSymbol.owner instanceof ClassSymbol) { + problemId = IProblem.UnusedPrivateField; + String typeName = varSymbol.owner.name.toString(); + arguments = new String[] { + typeName, name + }; + } else if (varSymbol.owner instanceof MethodSymbol methodSymbol) { + if (methodSymbol.params().indexOf(varSymbol) >= 0) { + problemId = IProblem.ArgumentIsNeverUsed; + } else { + problemId = IProblem.LocalVariableIsNeverUsed; + } + arguments = new String[] { name }; + } + + int severity = this.toSeverity(problemId); + if (severity == ProblemSeverities.Ignore || severity == ProblemSeverities.Optional) { + continue; + } + + problem = problemFactory.createProblem(fileName, + problemId, arguments, arguments, + severity, pos, endPos, line, column); + } + + problems.add(problem); + } + + return problems; + } + + // Merge the entries that exist in both maps + private Map mergeUnusedImports(Map map1, Map map2) { + if (map1 == null) { + return map2; + } else if (map2 == null) { + return map2; + } + + Map mergedMap = new LinkedHashMap<>(); + for (Entry entry : map1.entrySet()) { + if (map2.containsKey(entry.getKey())) { + mergedMap.put(entry.getKey(), entry.getValue()); + } + } + + return mergedMap; + } + + private int toSeverity(int jdtProblemId) { + int irritant = ProblemReporter.getIrritant(jdtProblemId); + if (irritant != 0) { + int res = this.compilerOptions.getSeverity(irritant); + res &= ~ProblemSeverities.Optional; // reject optional flag at this stage + return res; + } + + return ProblemSeverities.Warning; + } +} diff --git a/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/UnusedTreeScanner.java b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/UnusedTreeScanner.java new file mode 100644 index 00000000000..cafa76b0f82 --- /dev/null +++ b/org.eclipse.jdt.core.javac/src/org/eclipse/jdt/internal/javac/UnusedTreeScanner.java @@ -0,0 +1,221 @@ +/******************************************************************************* +* Copyright (c) 2024 Microsoft Corporation and others. +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Eclipse Public License 2.0 +* which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Microsoft Corporation - initial API and implementation +*******************************************************************************/ + +package org.eclipse.jdt.internal.javac; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.core.compiler.CategorizedProblem; + +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.ImportTree; +import com.sun.source.tree.MemberReferenceTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreeScanner; +import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.code.Symbol.ClassSymbol; +import com.sun.tools.javac.code.Symbol.MethodSymbol; +import com.sun.tools.javac.code.Symbol.VarSymbol; +import com.sun.tools.javac.tree.JCTree.JCClassDecl; +import com.sun.tools.javac.tree.JCTree.JCFieldAccess; +import com.sun.tools.javac.tree.JCTree.JCIdent; +import com.sun.tools.javac.tree.JCTree.JCImport; +import com.sun.tools.javac.tree.JCTree.JCMemberReference; +import com.sun.tools.javac.tree.JCTree.JCMethodDecl; +import com.sun.tools.javac.tree.JCTree.JCNewClass; +import com.sun.tools.javac.tree.JCTree.JCVariableDecl; + +public class UnusedTreeScanner extends TreeScanner { + final Set privateDecls = new LinkedHashSet<>(); + final Set usedElements = new HashSet<>(); + final Map unusedImports = new LinkedHashMap<>(); + private CompilationUnitTree unit = null; + + @Override + public R visitCompilationUnit(CompilationUnitTree node, P p) { + this.unit = node; + return super.visitCompilationUnit(node, p); + } + + @Override + public R visitImport(ImportTree node, P p) { + if (node instanceof JCImport jcImport) { + String importClass = jcImport.qualid.toString(); + this.unusedImports.put(importClass, jcImport); + } + + return super.visitImport(node, p); + } + + @Override + public R visitClass(ClassTree node, P p) { + if (node instanceof JCClassDecl classDecl && this.isPrivateDeclaration(classDecl)) { + this.privateDecls.add(classDecl); + } + + return super.visitClass(node, p); + } + + @Override + public R visitIdentifier(IdentifierTree node, P p) { + if (node instanceof JCIdent id && isPrivateSymbol(id.sym)) { + this.usedElements.add(id.sym); + } + + if (node instanceof JCIdent id && isMemberSymbol(id.sym)) { + String name = id.toString(); + String ownerName = id.sym.owner.toString(); + if (!ownerName.isBlank()) { + String starImport = ownerName + ".*"; + String usualImport = ownerName + "." + name; + if (this.unusedImports.containsKey(starImport)) { + this.unusedImports.remove(starImport); + } else if (this.unusedImports.containsKey(usualImport)) { + this.unusedImports.remove(usualImport); + } + } + } + + return super.visitIdentifier(node, p); + } + + @Override + public R visitMemberSelect(MemberSelectTree node, P p) { + if (node instanceof JCFieldAccess field) { + if (isPrivateSymbol(field.sym)) { + this.usedElements.add(field.sym); + } + } + + return super.visitMemberSelect(node, p); + } + + @Override + public R visitMethod(MethodTree node, P p) { + boolean isPrivateMethod = this.isPrivateDeclaration(node); + if (isPrivateMethod) { + this.privateDecls.add(node); + } + + return super.visitMethod(node, p); + } + + @Override + public R visitVariable(VariableTree node, P p) { + boolean isPrivateVariable = this.isPrivateDeclaration(node); + if (isPrivateVariable) { + this.privateDecls.add(node); + } + + return super.visitVariable(node, p); + } + + @Override + public R visitMemberReference(MemberReferenceTree node, P p) { + if (node instanceof JCMemberReference member && isPrivateSymbol(member.sym)) { + this.usedElements.add(member.sym); + } + + return super.visitMemberReference(node, p); + } + + @Override + public R visitNewClass(NewClassTree node, P p) { + if (node instanceof JCNewClass newClass) { + Symbol targetClass = newClass.def != null ? newClass.def.sym : newClass.type.tsym; + if (isPrivateSymbol(targetClass)) { + this.usedElements.add(targetClass); + } + } + + return super.visitNewClass(node, p); + } + + private boolean isPrivateDeclaration(Tree tree) { + if (tree instanceof JCClassDecl classTree) { + return (classTree.getModifiers().flags & Flags.PRIVATE) != 0; + } else if (tree instanceof JCMethodDecl methodTree) { + boolean isDefaultConstructor = methodTree.getParameters().isEmpty() && methodTree.getReturnType() == null; + return (methodTree.getModifiers().flags & Flags.PRIVATE) != 0 && !isDefaultConstructor; + } else if (tree instanceof JCVariableDecl variable) { + Symbol owner = variable.sym == null ? null : variable.sym.owner; + if (owner instanceof ClassSymbol) { + return (variable.getModifiers().flags & Flags.PRIVATE) != 0; + } else if (owner instanceof MethodSymbol) { + return true; + } + } + + return false; + } + + private boolean isPrivateSymbol(Symbol symbol) { + if (symbol instanceof ClassSymbol + || symbol instanceof MethodSymbol) { + return (symbol.flags() & Flags.PRIVATE) != 0; + } else if (symbol instanceof VarSymbol) { + if (symbol.owner instanceof ClassSymbol) { + return (symbol.flags() & Flags.PRIVATE) != 0; + } else if (symbol.owner instanceof MethodSymbol) { + return true; + } + } + + return false; + } + + private boolean isMemberSymbol(Symbol symbol) { + if (symbol instanceof ClassSymbol + || symbol instanceof MethodSymbol) { + return true; + } + + if (symbol instanceof VarSymbol) { + return symbol.owner instanceof ClassSymbol; + } + + return false; + } + + public List getUnusedImports(UnusedProblemFactory problemFactory) { + return problemFactory.addUnusedImports(this.unit, this.unusedImports); + } + + public List getUnusedPrivateMembers(UnusedProblemFactory problemFactory) { + List unusedPrivateMembers = new ArrayList<>(); + for (Tree decl : this.privateDecls) { + if (decl instanceof JCClassDecl classDecl && !this.usedElements.contains(classDecl.sym)) { + unusedPrivateMembers.add(decl); + } else if (decl instanceof JCMethodDecl methodDecl && !this.usedElements.contains(methodDecl.sym)) { + unusedPrivateMembers.add(decl); + } else if (decl instanceof JCVariableDecl variableDecl && !this.usedElements.contains(variableDecl.sym)) { + unusedPrivateMembers.add(decl); + } + } + + return problemFactory.addUnusedPrivateMembers(unit, unusedPrivateMembers); + } +}