Skip to content

Support outline view for decompiled source #2742

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

Merged
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -29,12 +29,15 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IClassFile;
Expand All @@ -46,16 +49,37 @@
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.ToolFactory;
import org.eclipse.jdt.core.compiler.IScanner;
import org.eclipse.jdt.core.compiler.ITerminalSymbols;
import org.eclipse.jdt.core.compiler.InvalidInputException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration;
import org.eclipse.jdt.core.dom.AnnotationTypeMemberDeclaration;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.PackageDeclaration;
import org.eclipse.jdt.core.dom.RecordDeclaration;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.internal.core.SourceMethod;
import org.eclipse.jdt.ls.core.internal.DecompilerResult;
import org.eclipse.jdt.ls.core.internal.JDTUtils;
import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin;
import org.eclipse.jdt.ls.core.internal.ResourceUtils;
import org.eclipse.jdt.ls.core.internal.hover.JavaElementLabels;
import org.eclipse.jdt.ls.core.internal.managers.ContentProviderManager;
import org.eclipse.jdt.ls.core.internal.preferences.PreferenceManager;
import org.eclipse.lsp4j.DocumentSymbol;
import org.eclipse.lsp4j.DocumentSymbolParams;
Expand Down Expand Up @@ -156,6 +180,10 @@ private void collectChildren(ITypeRoot unit, IJavaElement[] elements, ArrayList<

private List<DocumentSymbol> getHierarchicalOutline(ITypeRoot unit, IProgressMonitor monitor) {
try {
if (unit instanceof IClassFile && unit.getSourceRange() == null) { // no source attached
return getHierarchicalOutlineFromDecompiledSource(unit, monitor);
}

IJavaElement[] children = unit.getChildren();
Stream<IJavaElement> childrenStream = Stream.of(filter(children));
if (unit instanceof IClassFile) {
Expand Down Expand Up @@ -358,24 +386,278 @@ else if (type.isEnum()) {
return SymbolKind.String;
}

private static IScanner getScanner() {
if (fScanner == null) {
fScanner = ToolFactory.createScanner(true, false, false, true);
private static IScanner getScanner() {
if (fScanner == null) {
fScanner = ToolFactory.createScanner(true, false, false, true);
}
return fScanner;
}

private int getNextToken(IScanner scanner) {
int token = 0;
while (token == 0) {
try {
token = scanner.getNextToken();
} catch (InvalidInputException e) {
// ignore
// JavaLanguageServerPlugin.logException("Problem with folding range", e);
}
return fScanner;
}
return token;
}

private int getNextToken(IScanner scanner) {
int token = 0;
while (token == 0) {
try {
token = scanner.getNextToken();
} catch (InvalidInputException e) {
// ignore
// JavaLanguageServerPlugin.logException("Problem with folding range", e);
private List<DocumentSymbol> getHierarchicalOutlineFromDecompiledSource(ITypeRoot unit, IProgressMonitor monitor) {
ContentProviderManager contentProvider = JavaLanguageServerPlugin.getContentProviderManager();
DecompilerResult decompileResult;
try {
decompileResult = contentProvider.getSourceResult(((IClassFile) unit), new NullProgressMonitor());
} catch (Exception e) {
JavaLanguageServerPlugin.logException(e.getMessage(), e);
return Collections.emptyList();
}

if (monitor != null && monitor.isCanceled()) {
return Collections.emptyList();
}

String contents = decompileResult.getContent();
if (contents == null || contents.isBlank()) {
return Collections.emptyList();
}

final ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setResolveBindings(false);
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setStatementsRecovery(true);
parser.setBindingsRecovery(false);
parser.setIgnoreMethodBodies(true);
parser.setEnvironment(new String[0], new String[0], null, true);
/**
* See the java doc for { @link ASTParser#setSource(char[]) },
* the user need specify the compiler options explicitly.
*/
Map<String, String> javaOptions = JavaCore.getOptions();
javaOptions.put(JavaCore.COMPILER_SOURCE, JavaCore.latestSupportedJavaVersion());
javaOptions.put(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, JavaCore.latestSupportedJavaVersion());
javaOptions.put(JavaCore.COMPILER_COMPLIANCE, JavaCore.latestSupportedJavaVersion());
javaOptions.put(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, JavaCore.ENABLED);
parser.setCompilerOptions(javaOptions);
parser.setUnitName(unit.getElementName());
parser.setSource(contents.toCharArray());
CompilationUnit astUnit = (CompilationUnit) parser.createAST(monitor);
DocumentSymbolVisitor visitor = new DocumentSymbolVisitor(astUnit);
astUnit.accept(visitor);
return visitor.getSymbols();
}

private static class DocumentSymbolVisitor extends ASTVisitor{
private CompilationUnit astUnit = null;
private List<DocumentSymbol> symbols = new ArrayList<>();
private Map<ASTNode, DocumentSymbol> typeMappings = new HashMap<>();

public DocumentSymbolVisitor(CompilationUnit astUnit) {
this.astUnit = astUnit;
}

public List<DocumentSymbol> getSymbols() {
return symbols;
}

@Override
public boolean visit(PackageDeclaration node) {
DocumentSymbol symbol = getDocumentSymbol(node.getName().getFullyQualifiedName(), node, node.getName());
symbols.add(symbol);
return super.visit(node);
}

@Override
public boolean visit(TypeDeclaration node) {
addAsTypeDocumentSymbol(node);
return super.visit(node);
}

@Override
public boolean visit(EnumDeclaration node) {
addAsTypeDocumentSymbol(node);
return super.visit(node);
}

@Override
public boolean visit(RecordDeclaration node) {
DocumentSymbol typeSymbol = addAsTypeDocumentSymbol(node);
typeSymbol.setChildren(new ArrayList<>());
List<?> recordComponents = node.recordComponents();
for (Object recordComponent : recordComponents) {
if (recordComponent instanceof SingleVariableDeclaration svd) {
DocumentSymbol component = getDocumentSymbol(svd.getName().toString(), svd, svd.getName());
component.setKind(SymbolKind.Field);
typeSymbol.getChildren().add(component);
}
}
return super.visit(node);
}

@Override
public boolean visit(AnnotationTypeDeclaration node) {
addAsTypeDocumentSymbol(node);
return super.visit(node);
}

@Override
public boolean visit(AnnotationTypeMemberDeclaration node) {
String memberName = node.getName().getIdentifier() + "()";
String typeName = node.getType().toString();
DocumentSymbol symbol = getDocumentSymbol(memberName, node, node.getName());
symbol.setDetail(" : " + typeName);
addAsChildDocumentSymbol(node, symbol);
return super.visit(node);
}

@Override
public boolean visit(EnumConstantDeclaration node) {
DocumentSymbol symbol = getDocumentSymbol(node.getName().toString(), node, node.getName());
addAsChildDocumentSymbol(node, symbol);
return super.visit(node);
}

@Override
public boolean visit(FieldDeclaration node) {
List<?> fragments = node.fragments();
for (Object fragment : fragments) {
if (fragment instanceof VariableDeclarationFragment df) {
DocumentSymbol symbol = getDocumentSymbol(
df.getName().toString(), node, df.getName());
addAsChildDocumentSymbol(node, symbol);
}
}

return super.visit(node);
}

@Override
public boolean visit(MethodDeclaration node) {
StringBuilder name = new StringBuilder(node.getName().getIdentifier());
name.append("(");
List<?> parameters = node.parameters();
if (parameters != null) {
List<String> params = new ArrayList<>();
for (Object parameter : parameters) {
if (parameter instanceof SingleVariableDeclaration vd) {
String typeName = vd.getType().toString() + (vd.isVarargs() ? "..." : "");
params.add(typeName);
} else {
params.add("Object");
}
}
name.append(String.join(", ", params));
}
name.append(")");
String returnType = null;
if (node.getReturnType2() != null) {
returnType = node.getReturnType2().toString();
} else {
returnType = node.isConstructor() ? null : "void";
}
return token;
DocumentSymbol symbol = getDocumentSymbol(name.toString(), node, node.getName());
symbol.setDetail(returnType == null ? "" : " : " + returnType);
addAsChildDocumentSymbol(node, symbol);
return super.visit(node);
}

private DocumentSymbol addAsTypeDocumentSymbol(AbstractTypeDeclaration node) {
DocumentSymbol symbol = getDocumentSymbol(node.getName().getIdentifier(), node, node.getName());
symbols.add(symbol);
typeMappings.put(node, symbol);
return symbol;
}

private void addAsChildDocumentSymbol(ASTNode currentNode, DocumentSymbol symbol) {
ASTNode parent = currentNode.getParent();
if (parent == null) {
return;
}

DocumentSymbol parentSymbol = typeMappings.get(parent);
if (parentSymbol == null) {
return;
}

if (parentSymbol.getChildren() == null) {
parentSymbol.setChildren(new ArrayList<>());
}

parentSymbol.getChildren().add(symbol);
}

private DocumentSymbol getDocumentSymbol(String name, ASTNode node, ASTNode nameNode) {
DocumentSymbol symbol = new DocumentSymbol();
symbol.setName(name);
symbol.setKind(getKind(node));
symbol.setRange(getRange(node));
symbol.setSelectionRange(getRange(nameNode));
symbol.setDetail("");
if (node instanceof BodyDeclaration bd
&& containsModifier(bd.modifiers(), "@Deprecated")) {
symbol.setTags(List.of(SymbolTag.Deprecated));
}
return symbol;
}

private Range getRange(ASTNode node) {
int start = node.getStartPosition();
int end = start + node.getLength();
Position startPosition = new Position(
astUnit.getLineNumber(start) - 1, // convert 1-based to 0-based
astUnit.getColumnNumber(start) // zero-based
);
Position endPosition = new Position(
astUnit.getLineNumber(end) - 1, // convert 1-based to 0-based
astUnit.getColumnNumber(end) // zero-based
);
return new Range(startPosition, endPosition);
}

private SymbolKind getKind(ASTNode node) {
switch (node.getNodeType()) {
case ASTNode.PACKAGE_DECLARATION:
return SymbolKind.Package;
case ASTNode.TYPE_DECLARATION:
return ((TypeDeclaration) node).isInterface() ? SymbolKind.Interface : SymbolKind.Class;
case ASTNode.ENUM_DECLARATION:
return SymbolKind.Enum;
case ASTNode.RECORD_DECLARATION:
return SymbolKind.Class;
case ASTNode.ANNOTATION_TYPE_DECLARATION:
return SymbolKind.Property;
case ASTNode.FIELD_DECLARATION:
List<?> modifiers = ((FieldDeclaration) node).modifiers();
if (containsModifier(modifiers, "static") && containsModifier(modifiers, "final")) {
return SymbolKind.Constant;
}
return SymbolKind.Field;
case ASTNode.ENUM_CONSTANT_DECLARATION:
return SymbolKind.EnumMember;
case ASTNode.METHOD_DECLARATION:
return ((MethodDeclaration) node).isConstructor() ? SymbolKind.Constructor : SymbolKind.Method;
case ASTNode.ANNOTATION_TYPE_MEMBER_DECLARATION:
return SymbolKind.Method;
default:
return SymbolKind.String;
}
}

private boolean containsModifier(List<?> modifiers, String target) {
if (modifiers == null || modifiers.isEmpty()) {
return false;
}

for (Object modifier : modifiers) {
if (Objects.equals(modifier.toString(), target)) {
return true;
}
}

return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,21 @@ public void testLombok() throws Exception {
assertFalse(method.isPresent());
}

@Test
public void testDecompiledSource() throws Exception {
importProjects("eclipse/reference");
IProject project = WorkspaceHelper.getProject("reference");
List<? extends DocumentSymbol> symbols = internalGetHierarchicalSymbols(project, monitor, "org.sample.Foo");
symbols.size();
assertEquals(2, symbols.size());
assertHasHierarchicalSymbol("org.sample", null, SymbolKind.Package, symbols);
assertHasHierarchicalSymbol("Foo", null, SymbolKind.Enum, symbols);
assertHasHierarchicalSymbol("FOO1", "Foo", SymbolKind.EnumMember, symbols);
assertHasHierarchicalSymbol("value", "Foo", SymbolKind.Field, symbols);
assertHasHierarchicalSymbol("getValue() : int", "Foo", SymbolKind.Method, symbols);
assertHasHierarchicalSymbol("Foo(int)", "Foo", SymbolKind.Constructor, symbols);
}

private List<? extends DocumentSymbol> internalGetHierarchicalSymbols(IProject project, IProgressMonitor monitor, String className)
throws JavaModelException, UnsupportedEncodingException, InterruptedException, ExecutionException {
String uri = ClassFileUtil.getURI(project, className);
Expand Down