From 2fe81cb9091ec3f4e8701edc7967cfaf3f945d66 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Fri, 25 Jan 2019 14:36:26 +0100 Subject: [PATCH] GH-508: Added support for call hierarchies. Closes #508 Signed-off-by: Akos Kitta --- org.eclipse.jdt.ls.core/META-INF/MANIFEST.MF | 1 + .../jdt/ls/core/internal/JDTUtils.java | 117 +++++++- .../internal/JavaLanguageServerPlugin.java | 12 + .../corext/callhierarchy/CallHierarchy.java | 2 +- .../handlers/CallHierarchyHandler.java | 279 ++++++++++++++++++ .../handlers/CallHierarchyResolveHandler.java | 47 +++ .../handlers/DocumentSymbolHandler.java | 13 +- .../core/internal/handlers/InitHandler.java | 3 + .../internal/handlers/JDTLanguageServer.java | 21 ++ .../internal/handlers/JsonRpcHelpers.java | 72 +++++ .../preferences/ClientPreferences.java | 10 +- .../org.eclipse.jdt.ls.tp.target | 8 +- .../hello/src/org/sample/CallHierarchy.java | 60 ++++ .../main/java/org/sample/CallHierarchy.java | 23 ++ .../java/org/sample/CallHierarchyOther.java | 16 + .../handlers/CallHierarchyHandlerTest.java | 218 ++++++++++++++ .../CallHierarchyResolveHandlerTest.java | 78 +++++ .../handlers/DocumentSymbolHandlerTest.java | 2 +- 18 files changed, 969 insertions(+), 13 deletions(-) create mode 100644 org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyHandler.java create mode 100644 org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyResolveHandler.java create mode 100644 org.eclipse.jdt.ls.tests/projects/eclipse/hello/src/org/sample/CallHierarchy.java create mode 100644 org.eclipse.jdt.ls.tests/projects/maven/salut/src/main/java/org/sample/CallHierarchy.java create mode 100644 org.eclipse.jdt.ls.tests/projects/maven/salut/src/main/java/org/sample/CallHierarchyOther.java create mode 100644 org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyHandlerTest.java create mode 100644 org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyResolveHandlerTest.java diff --git a/org.eclipse.jdt.ls.core/META-INF/MANIFEST.MF b/org.eclipse.jdt.ls.core/META-INF/MANIFEST.MF index fb6864da6a..25aefae65f 100644 --- a/org.eclipse.jdt.ls.core/META-INF/MANIFEST.MF +++ b/org.eclipse.jdt.ls.core/META-INF/MANIFEST.MF @@ -51,6 +51,7 @@ Export-Package: org.eclipse.jdt.ls.core.internal;x-friends:="org.eclipse.jdt.ls. org.eclipse.jdt.ls.core.internal.corrections.proposals;x-internal:=true, org.eclipse.jdt.ls.core.internal.handlers;x-friends:="org.eclipse.jdt.ls.tests", org.eclipse.jdt.ls.core.internal.highlighting;x-friends:="org.eclipse.jdt.ls.tests", + org.eclipse.jdt.ls.core.internal.hover;x-friends:="org.eclipse.jdt.ls.tests", org.eclipse.jdt.ls.core.internal.javadoc;x-friends:="org.eclipse.jdt.ls.tests", org.eclipse.jdt.ls.core.internal.lsp;x-friends:="org.eclipse.jdt.ls.tests", org.eclipse.jdt.ls.core.internal.managers;x-friends:="org.eclipse.jdt.ls.tests", diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTUtils.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTUtils.java index aac7485abd..00413307ad 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTUtils.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JDTUtils.java @@ -13,6 +13,9 @@ package org.eclipse.jdt.ls.core.internal; import static org.eclipse.core.resources.IResource.DEPTH_ONE; +import static org.eclipse.jdt.ls.core.internal.hover.JavaElementLabels.ALL_DEFAULT; +import static org.eclipse.jdt.ls.core.internal.hover.JavaElementLabels.M_APP_RETURNTYPE; +import static org.eclipse.jdt.ls.core.internal.hover.JavaElementLabels.ROOT_VARIABLE; import java.io.File; import java.io.IOException; @@ -32,6 +35,7 @@ import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; @@ -41,6 +45,7 @@ import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.URIUtil; import org.eclipse.jdt.core.CompletionProposal; +import org.eclipse.jdt.core.Flags; import org.eclipse.jdt.core.IAnnotatable; import org.eclipse.jdt.core.IAnnotation; import org.eclipse.jdt.core.IBuffer; @@ -104,6 +109,7 @@ import org.eclipse.jdt.ls.core.internal.hover.JavaElementLabelComposer; import org.eclipse.jdt.ls.core.internal.hover.JavaElementLabels; import org.eclipse.jdt.ls.core.internal.javadoc.JavaElementLinks; +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.managers.ProjectsManager; import org.eclipse.jdt.ls.core.internal.preferences.PreferenceManager; @@ -114,6 +120,7 @@ import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SymbolKind; import com.google.common.base.Charsets; import com.google.common.io.Files; @@ -306,14 +313,109 @@ public static String getPackageName(IJavaProject javaProject, String fileContent return (pkg == null || pkg.getName() == null)?"":pkg.getName().getFullyQualifiedName(); } + /** + * Returns with the human readable name of the element. For types with type + * arguments, it is {@code Comparable} instead of {@code Comparable}. First, + * this method tries to retrieve the + * {@link JavaElementLabels#getElementLabel(IJavaElement, long) label} of the + * element, then falls back to {@link IJavaElement#getElementName() element + * name}. Returns {@code null} if the argument does not have a name. + */ + public static String getName(IJavaElement element) { + Assert.isNotNull(element, "element"); + String name = JavaElementLabels.getElementLabel(element, ALL_DEFAULT); + return name == null ? element.getElementName() : name; + } /** - * Given the uri returns a {@link IClassFile}. - * May return null if it can not resolve the uri to a - * library. + * Returns with the details of the document symbol. This is usually the type + * information of a member. For methods, this is the type information of the + * return type. Can return with an empty string, but never {@code null}. + * + * @see JDTUtils#getName(IJavaElement) + */ + public static String getDetail(IJavaElement element) { + Assert.isNotNull(element, "element"); + String name = getName(element); + String nameWithDetails = JavaElementLabels.getElementLabel(element, ALL_DEFAULT | M_APP_RETURNTYPE | ROOT_VARIABLE); + if (nameWithDetails != null && nameWithDetails.startsWith(name)) { + return nameWithDetails.substring(name.length()); + } + return ""; + } + + /** + * Returns with the document symbol {@code SymbolKind kind} for the Java + * element. + */ + public static SymbolKind getSymbolKind(IJavaElement element) { + switch (element.getElementType()) { + case IJavaElement.ANNOTATION: + return SymbolKind.Property; // TODO: find a better mapping + case IJavaElement.CLASS_FILE: + case IJavaElement.COMPILATION_UNIT: + return SymbolKind.File; + case IJavaElement.FIELD: + return SymbolKind.Field; + case IJavaElement.IMPORT_CONTAINER: + case IJavaElement.IMPORT_DECLARATION: + return SymbolKind.Module; + case IJavaElement.INITIALIZER: + return SymbolKind.Constructor; + case IJavaElement.LOCAL_VARIABLE: + case IJavaElement.TYPE_PARAMETER: + return SymbolKind.Variable; + case IJavaElement.METHOD: + try { + // TODO handle `IInitializer`. What should be the `SymbolKind`? + if (element instanceof IMethod) { + if (((IMethod) element).isConstructor()) { + return SymbolKind.Constructor; + } + } + return SymbolKind.Method; + } catch (JavaModelException e) { + return SymbolKind.Method; + } + case IJavaElement.PACKAGE_DECLARATION: + return SymbolKind.Package; + case IJavaElement.TYPE: + try { + IType type = (IType) element; + if (type.isEnum()) { + return SymbolKind.Enum; + } else if (type.isInterface()) { + return SymbolKind.Interface; + } else { + return SymbolKind.Class; + } + } catch (JavaModelException e) { + return SymbolKind.Class; + } + } + return SymbolKind.String; + } + + /** + * {@code true} if the element is deprecated. Otherwise, {@code false}. + */ + public static boolean isDeprecated(IJavaElement element) throws JavaModelException { + Assert.isNotNull(element, "element"); + if (element instanceof ITypeRoot) { + return Flags.isDeprecated(((ITypeRoot) element).findPrimaryType().getFlags()); + } else if (element instanceof IMember) { + return Flags.isDeprecated(((IMember) element).getFlags()); + } + return false; + } + + /** + * Given the uri returns a {@link IClassFile}. May return null if it can not + * resolve the uri to a library. * * @see #toLocation(IClassFile, int, int) - * @param uri with 'jdt' scheme + * @param uri + * with 'jdt' scheme * @return class file */ public static IClassFile resolveClassFile(String uriString){ @@ -388,6 +490,13 @@ ISourceRange getRange(IJavaElement element) throws JavaModelException { }; /* default */ abstract ISourceRange getRange(IJavaElement element) throws JavaModelException; + + /** + * Sugar for {@link JDTUtils#toLocation(IJavaElement, LocationType)}. + */ + public Location toLocation(IJavaElement element) throws JavaModelException { + return JDTUtils.toLocation(element, this); + } } /** diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JavaLanguageServerPlugin.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JavaLanguageServerPlugin.java index a98c622346..86cb158808 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JavaLanguageServerPlugin.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/JavaLanguageServerPlugin.java @@ -76,6 +76,8 @@ import org.osgi.framework.BundleException; import org.osgi.util.tracker.ServiceTracker; +import com.google.common.base.Throwables; + public class JavaLanguageServerPlugin extends Plugin { private static final String JDT_UI_PLUGIN = "org.eclipse.jdt.ui"; @@ -377,6 +379,16 @@ public static void logInfo(String message) { } } + public static void logException(Throwable ex) { + if (context != null) { + String message = ex.getMessage(); + if (message == null) { + message = Throwables.getStackTraceAsString(ex); + } + logException(message, ex); + } + } + public static void logException(String message, Throwable ex) { if (context != null) { log(new Status(IStatus.ERROR, context.getBundle().getSymbolicName(), message, ex)); diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/corext/callhierarchy/CallHierarchy.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/corext/callhierarchy/CallHierarchy.java index 9acfeb6185..6889c53c99 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/corext/callhierarchy/CallHierarchy.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/corext/callhierarchy/CallHierarchy.java @@ -42,7 +42,7 @@ public class CallHierarchy { private static final String PREF_USE_FILTERS = "PREF_USE_FILTERS"; //$NON-NLS-1$ private static final String PREF_FILTERS_LIST = "PREF_FILTERS_LIST"; //$NON-NLS-1$ - private static final String DEFAULT_IGNORE_FILTERS = "java.*,javax.*"; //$NON-NLS-1$ + private static final String DEFAULT_IGNORE_FILTERS = ""; //$NON-NLS-1$ private static CallHierarchy fgInstance; private IJavaSearchScope fSearchScope; private StringMatcher[] fFilters; diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyHandler.java new file mode 100644 index 0000000000..c122ec9302 --- /dev/null +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyHandler.java @@ -0,0 +1,279 @@ +/******************************************************************************* + * Copyright (c) 2019 TypeFox and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * TypeFox - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.handlers; + +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toList; +import static org.eclipse.jdt.core.ICompilationUnit.NO_AST; +import static org.eclipse.jdt.core.IJavaElement.CLASS_FILE; +import static org.eclipse.jdt.core.IJavaElement.COMPILATION_UNIT; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.jdt.core.IClassFile; +import org.eclipse.jdt.core.ICodeAssist; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IField; +import org.eclipse.jdt.core.IInitializer; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IMember; +import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.IOpenable; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.ITypeRoot; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.ls.core.internal.JDTUtils; +import org.eclipse.jdt.ls.core.internal.JDTUtils.LocationType; +import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +import org.eclipse.jdt.ls.core.internal.corext.callhierarchy.CallHierarchy; +import org.eclipse.jdt.ls.core.internal.corext.callhierarchy.CallLocation; +import org.eclipse.jdt.ls.core.internal.corext.callhierarchy.MethodWrapper; +import org.eclipse.lsp4j.CallHierarchyDirection; +import org.eclipse.lsp4j.CallHierarchyItem; +import org.eclipse.lsp4j.CallHierarchyParams; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.services.TextDocumentService; + +/** + * Handler for the {@link TextDocumentService#callHierarchy(CallHierarchyParams) + * textDocument/callHierarchy} language server method. + */ +public class CallHierarchyHandler { + + protected static ThreadLocal THREAD_LOCAL = new ThreadLocal<>(); + + public CallHierarchyItem callHierarchy(CallHierarchyParams params, IProgressMonitor monitor) { + Assert.isNotNull(params, "params"); + Assert.isLegal(params.getResolve() >= 0, "'resolve' must a non-negative integer. Was: " + params.getResolve()); + + CallHierarchyDirection direction = params.getDirection() == null ? CallHierarchyDirection.Incoming : params.getDirection(); + String uri = params.getTextDocument().getUri(); + int line = params.getPosition().getLine(); + int character = params.getPosition().getCharacter(); + int resolve = params.getResolve(); + + try { + return getCallHierarchyItemAt(uri, line, character, resolve, direction, monitor); + } catch (OperationCanceledException e) { + return THREAD_LOCAL.get(); + } catch (JavaModelException e) { + JavaLanguageServerPlugin.log(e); + } finally { + if (THREAD_LOCAL.get() != null) { + THREAD_LOCAL.set(null); + } + } + return null; + } + + private CallHierarchyItem getCallHierarchyItemAt(String uri, int line, int character, int resolve, CallHierarchyDirection direction, IProgressMonitor monitor) throws JavaModelException { + Assert.isNotNull(uri, "uri"); + Assert.isLegal(resolve >= 0, "Expected a non negative integer for 'resolve'. Was: " + resolve); + Assert.isNotNull(direction, "direction"); + + ITypeRoot root = JDTUtils.resolveTypeRoot(uri); + if (root == null) { + return null; + } + + if (root instanceof ICompilationUnit) { + ICompilationUnit unit = (ICompilationUnit) root; + if (root.getResource() == null) { + return null; + } + reconcile(unit, monitor); + } + + checkMonitor(monitor); + + // Try to locate the member at the given position. + IMember candidate = null; + int offset = JsonRpcHelpers.toOffset(root, line, character); + List selectedElements = codeResolve(root, offset); + Stream possibleElements = selectedElements.stream().filter(CallHierarchy::isPossibleInputElement); + Optional firstElement = possibleElements.findFirst(); + if (firstElement.isPresent() && firstElement.get() instanceof IMember) { + candidate = (IMember) firstElement.get(); + } + + // If the member cannot be resolved, retrieve the enclosing method. + if (candidate == null) { + candidate = getEnclosingMember(root, offset); + if (candidate == null) { + return null; + } + } + + checkMonitor(monitor); + + MethodWrapper wrapper = toMethodWrapper(candidate, direction); + if (wrapper == null) { + return null; + } + + return toCallHierarchyItem(wrapper, resolve, monitor); + } + + private List codeResolve(IJavaElement input, int offset) throws JavaModelException { + if (input instanceof ICodeAssist) { + return Arrays.asList(((ICodeAssist) input).codeSelect(offset, 0)); + } + return emptyList(); + } + + private MethodWrapper toMethodWrapper(IMember member, CallHierarchyDirection direction) { + Assert.isNotNull(member, "member"); + + IMember[] members = { member }; + CallHierarchy callHierarchy = CallHierarchy.getDefault(); + final MethodWrapper[] result; + if (direction == CallHierarchyDirection.Incoming) { + result = callHierarchy.getCallerRoots(members); + } else { + result = callHierarchy.getCalleeRoots(members); + } + if (result == null || result.length < 1) { + return null; + } + return result[0]; + } + + private CallHierarchyItem toCallHierarchyItem(MethodWrapper wrapper, int resolve, IProgressMonitor monitor) { + checkMonitor(monitor); + + if (wrapper == null || wrapper.getMember() == null) { + return null; + } + + try { + IMember member = wrapper.getMember(); + Location fullLocation = getLocation(member, LocationType.FULL_RANGE); + Range range = fullLocation.getRange(); + String uri = fullLocation.getUri(); + CallHierarchyItem item = new CallHierarchyItem(); + item.setName(JDTUtils.getName(member)); + item.setKind(JDTUtils.getSymbolKind(member)); + item.setRange(range); + item.setSelectionRange(getLocation(member, LocationType.NAME_RANGE).getRange()); + item.setUri(uri); + item.setDetail(JDTUtils.getDetail(member)); + item.setDeprecated(JDTUtils.isDeprecated(member)); + + Collection callLocations = wrapper.getMethodCall().getCallLocations(); + // The `callLocations` should be `null` for the root item. + if (callLocations != null) { + item.setCallLocations(callLocations.stream().map(location -> { + IOpenable openable = location.getMember().getCompilationUnit(); + if (openable == null) { + openable = location.getMember().getTypeRoot(); + } + int[] start = JsonRpcHelpers.toLine(openable, location.getStart()); + int[] end = JsonRpcHelpers.toLine(openable, location.getEnd()); + Assert.isNotNull(start, "start"); + Assert.isNotNull(end, "end"); + Assert.isLegal(start[0] == end[0], "Expected equal start and end lines. Start was: " + Arrays.toString(start) + " End was:" + Arrays.toString(end)); + Range callRange = new Range(new Position(start[0], start[1]), new Position(end[0], end[1])); + return new Location(uri, callRange); + }).collect(toList())); + } + + // Set the copy of the unresolved item to the thread local, so that we can return with it in case of user abort. (`monitor#cancel()`) + if (THREAD_LOCAL.get() == null) { + THREAD_LOCAL.set(shallowCopy(item)); + } + + if (resolve > 0) { + item.setCalls(Arrays.stream(wrapper.getCalls(monitor)).map(w -> toCallHierarchyItem(w, resolve - 1, monitor)).collect(toList())); + } + return item; + } catch (JavaModelException e) { + JavaLanguageServerPlugin.logException("Error when converting to a call hierarchy item.", e); + } + return null; + } + + /** + * Throws an {@link OperationCanceledException} if the argument is canceled. + */ + private void checkMonitor(IProgressMonitor monitor) { + if (monitor != null && monitor.isCanceled()) { + throw new OperationCanceledException(); + } + } + + /** + * Returns with a copy of the argument without the + * {@link CallHierarchyItem#getCalls() calls} set. + */ + private CallHierarchyItem shallowCopy(CallHierarchyItem other) { + CallHierarchyItem copy = new CallHierarchyItem(); + copy.setName(other.getName()); + copy.setDetail(other.getDetail()); + copy.setKind(other.getKind()); + copy.setDeprecated(other.getDeprecated()); + copy.setUri(other.getUri()); + copy.setRange(other.getRange()); + copy.setSelectionRange(other.getSelectionRange()); + copy.setCallLocations(other.getCallLocations()); + return copy; + } + + /** + * Gets the location of the Java {@code element} based on the desired + * {@code locationType}. + */ + private Location getLocation(IJavaElement element, LocationType locationType) throws JavaModelException { + Assert.isNotNull(element, "element"); + Assert.isNotNull(locationType, "locationType"); + + Location location = locationType.toLocation(element); + if (location == null && element instanceof IType) { + IType type = (IType) element; + ICompilationUnit unit = (ICompilationUnit) type.getAncestor(COMPILATION_UNIT); + IClassFile classFile = (IClassFile) type.getAncestor(CLASS_FILE); + if (unit != null || (classFile != null && classFile.getSourceRange() != null)) { + location = locationType.toLocation(type); + } + } + if (location == null && element instanceof IMember && ((IMember) element).getClassFile() != null) { + location = JDTUtils.toLocation(((IMember) element).getClassFile()); + } + return location; + } + + private IMember getEnclosingMember(ITypeRoot root, int offset) { + Assert.isNotNull(root, "root"); + try { + IJavaElement enclosingElement = root.getElementAt(offset); + if (enclosingElement instanceof IMethod || enclosingElement instanceof IInitializer || enclosingElement instanceof IField) { + // opening on the enclosing type would be too confusing (since the type resolves to the constructors) + return (IMember) enclosingElement; + } + } catch (JavaModelException e) { + JavaLanguageServerPlugin.log(e); + } + return null; + } + + private void reconcile(ICompilationUnit unit, IProgressMonitor monitor) throws JavaModelException { + unit.reconcile(NO_AST, false, null, monitor); + } + +} diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyResolveHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyResolveHandler.java new file mode 100644 index 0000000000..7dffe88143 --- /dev/null +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyResolveHandler.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) 2019 TypeFox and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * TypeFox - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.handlers; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.lsp4j.CallHierarchyItem; +import org.eclipse.lsp4j.CallHierarchyParams; +import org.eclipse.lsp4j.ResolveCallHierarchyItemParams; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.services.TextDocumentService; + +/** + * Handler for the + * {@link TextDocumentService#resolveCallHierarchy(org.eclipse.lsp4j.ResolveCallHierarchyItemParams) + * callHierarchy/resolve} LS method. + */ +public class CallHierarchyResolveHandler extends CallHierarchyHandler { + + public CallHierarchyItem resolve(ResolveCallHierarchyItemParams params, IProgressMonitor monitor) { + Assert.isNotNull(params, "params"); + + return callHierarchy(toCallHierarchyParams(params), monitor); + } + + private CallHierarchyParams toCallHierarchyParams(ResolveCallHierarchyItemParams params) { + Assert.isNotNull(params.getItem(), "params.item"); + Assert.isNotNull(params.getDirection(), "params.direction"); + + CallHierarchyParams result = new CallHierarchyParams(); + result.setDirection(params.getDirection()); + result.setResolve(params.getResolve()); + CallHierarchyItem item = params.getItem(); + result.setTextDocument(new TextDocumentIdentifier(item.getUri())); + result.setPosition(item.getRange().getStart()); + return result; + } + +} diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/DocumentSymbolHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/DocumentSymbolHandler.java index 9f4d703c0b..95af5c19f2 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/DocumentSymbolHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/DocumentSymbolHandler.java @@ -39,6 +39,7 @@ import org.eclipse.jdt.core.IField; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IMember; +import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IParent; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.ITypeRoot; @@ -275,7 +276,17 @@ else if (type.isEnum()) { case IJavaElement.TYPE_PARAMETER: return SymbolKind.TypeParameter; case IJavaElement.METHOD: - return SymbolKind.Method; + try { + // TODO handle `IInitializer`. What should be the `SymbolKind`? + if (element instanceof IMethod) { + if (((IMethod) element).isConstructor()) { + return SymbolKind.Constructor; + } + } + return SymbolKind.Method; + } catch (JavaModelException e) { + return SymbolKind.Method; + } case IJavaElement.PACKAGE_DECLARATION: return SymbolKind.Package; } diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/InitHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/InitHandler.java index 60be9bcd70..7286933e94 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/InitHandler.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/InitHandler.java @@ -218,6 +218,9 @@ InitializeResult initialize(InitializeParams param) { if (!preferenceManager.getClientPreferences().isSelectionRangeDynamicRegistered()) { capabilities.setSelectionRangeProvider(Boolean.TRUE); } + if (!preferenceManager.getClientPreferences().isCallHierarchyDynamicRegistered()) { + capabilities.setCallHierarchyProvider(Boolean.TRUE); + } TextDocumentSyncOptions textDocumentSyncOptions = new TextDocumentSyncOptions(); textDocumentSyncOptions.setOpenClose(Boolean.TRUE); textDocumentSyncOptions.setSave(new SaveOptions(Boolean.TRUE)); diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JDTLanguageServer.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JDTLanguageServer.java index f63db46f83..e8203ab2a2 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JDTLanguageServer.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JDTLanguageServer.java @@ -74,6 +74,8 @@ import org.eclipse.jdt.ls.core.internal.managers.ProjectsManager; import org.eclipse.jdt.ls.core.internal.preferences.PreferenceManager; import org.eclipse.jdt.ls.core.internal.preferences.Preferences; +import org.eclipse.lsp4j.CallHierarchyItem; +import org.eclipse.lsp4j.CallHierarchyParams; import org.eclipse.lsp4j.CodeAction; import org.eclipse.lsp4j.CodeActionKind; import org.eclipse.lsp4j.CodeActionOptions; @@ -117,6 +119,7 @@ import org.eclipse.lsp4j.RenameParams; import org.eclipse.lsp4j.SelectionRange; import org.eclipse.lsp4j.SelectionRangeParams; +import org.eclipse.lsp4j.ResolveCallHierarchyItemParams; import org.eclipse.lsp4j.SignatureHelp; import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.lsp4j.TextDocumentIdentifier; @@ -904,6 +907,24 @@ public CompletableFuture> searchSymbols(SearchSymbolPara return computeAsyncWithClientProgress((monitor) -> WorkspaceSymbolHandler.search(params.getQuery(), params.maxResults, params.projectName, params.sourceOnly, monitor)); } + /* (non-Javadoc) + * @see org.eclipse.lsp4j.services.TextDocumentService#callHierarchy(org.eclipse.lsp4j.CallHierarchyParams) + */ + @Override + public CompletableFuture callHierarchy(CallHierarchyParams params) { + logInfo(">> textDocumentt/callHierarchy"); + return computeAsyncWithClientProgress((monitor) -> new CallHierarchyHandler().callHierarchy(params, monitor)); + } + + /* (non-Javadoc) + * @see org.eclipse.lsp4j.services.TextDocumentService#resolveCallHierarchy(org.eclipse.lsp4j.ResolveCallHierarchyItemParams) + */ + @Override + public CompletableFuture resolveCallHierarchy(ResolveCallHierarchyItemParams params) { + logInfo(">> callHierarchy/resolve"); + return computeAsyncWithClientProgress((monitor) -> new CallHierarchyResolveHandler().resolve(params, monitor)); + } + public void sendStatus(ServiceStatus serverStatus, String status) { if (client != null) { client.sendStatus(serverStatus, status); diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JsonRpcHelpers.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JsonRpcHelpers.java index 91a2163c6f..63e76ebf66 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JsonRpcHelpers.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/JsonRpcHelpers.java @@ -17,10 +17,13 @@ import org.eclipse.core.filebuffers.ITextFileBufferManager; import org.eclipse.core.filebuffers.LocationKind; import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.jdt.core.IBuffer; +import org.eclipse.jdt.core.IOpenable; +import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; @@ -33,6 +36,40 @@ private JsonRpcHelpers(){ /** * Convert line, column to a document offset. + * + * @param openable + * @param line + * @param column + * @return + */ + public static int toOffset(IOpenable openable, int line, int column) { + if (openable != null) { + boolean mustClose = false; + try { + if (!openable.isOpen()) { + openable.open(new NullProgressMonitor()); + mustClose = openable.isOpen(); + } + IBuffer buffer = openable.getBuffer(); + return toOffset(toDocument(buffer), line, column); + } catch (JavaModelException e) { + JavaLanguageServerPlugin.log(e); + } finally { + if (mustClose) { + try { + openable.close(); + } catch (JavaModelException e) { + JavaLanguageServerPlugin.logException("Error when closing openable: " + openable, e); + } + } + } + } + return -1; + } + + /** + * Convert line, column to a document offset. + * * @param buffer * @param line * @param column @@ -73,6 +110,41 @@ public static int[] toLine(IBuffer buffer, int offset){ return toLine(toDocument(buffer), offset); } + /** + * Converts an offset to line number and column in an openable. If the + * {@code openable} argument is {@link IOpenable#isOpen() not open}, this + * method tries to + * {@link IOpenable#open(org.eclipse.core.runtime.IProgressMonitor) open} it. + * + * @param buffer + * @param line + * @param column + * @return + */ + public static int[] toLine(IOpenable openable, int offset) { + Assert.isNotNull(openable, "openable"); + boolean mustClose = false; + try { + if (!openable.isOpen()) { + openable.open(new NullProgressMonitor()); + mustClose = openable.isOpen(); + } + IBuffer buffer = openable.getBuffer(); + return toLine(toDocument(buffer), offset); + } catch (JavaModelException e) { + JavaLanguageServerPlugin.log(e); + return null; + } finally { + if (mustClose) { + try { + openable.close(); + } catch (JavaModelException e) { + JavaLanguageServerPlugin.logException("Error when closing openable: " + openable, e); + } + } + } + } + /** * Convert the document offset to line number and column. * diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/ClientPreferences.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/ClientPreferences.java index 7a64e3f2cd..d6323bbe2c 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/ClientPreferences.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/ClientPreferences.java @@ -251,6 +251,7 @@ public boolean isHierarchicalDocumentSymbolSupported() { && capabilities.getTextDocument().getDocumentSymbol() != null && capabilities.getTextDocument().getDocumentSymbol().getHierarchicalDocumentSymbolSupport() != null && capabilities.getTextDocument().getDocumentSymbol().getHierarchicalDocumentSymbolSupport().booleanValue(); + //@formatter:on } public boolean isSemanticHighlightingSupported() { @@ -263,8 +264,8 @@ public boolean isSemanticHighlightingSupported() { /** * {@code true} if the client has explicitly set the - * {@code textDocument.documentSymbol.hierarchicalDocumentSymbolSupport} to - * {@code true} when initializing the LS. Otherwise, {@code false}. + * {@code textDocument.codeAction.codeActionLiteralSupport.codeActionKind.valueSet} + * value. Otherwise, {@code false}. */ public boolean isSupportedCodeActionKind(String kind) { //@formatter:off @@ -289,4 +290,9 @@ public boolean isDiagnosticTagSupported() { && capabilities.getTextDocument().getPublishDiagnostics().getTagSupport().booleanValue(); //@formatter:on } + + public boolean isCallHierarchyDynamicRegistered() { + return v3supported && isDynamicRegistrationSupported(capabilities.getTextDocument().getCallHierarchy()); + } + } diff --git a/org.eclipse.jdt.ls.target/org.eclipse.jdt.ls.tp.target b/org.eclipse.jdt.ls.target/org.eclipse.jdt.ls.tp.target index 62f192aaa5..f580d2a0f9 100644 --- a/org.eclipse.jdt.ls.target/org.eclipse.jdt.ls.tp.target +++ b/org.eclipse.jdt.ls.target/org.eclipse.jdt.ls.tp.target @@ -42,10 +42,10 @@ - - - - + + + + diff --git a/org.eclipse.jdt.ls.tests/projects/eclipse/hello/src/org/sample/CallHierarchy.java b/org.eclipse.jdt.ls.tests/projects/eclipse/hello/src/org/sample/CallHierarchy.java new file mode 100644 index 0000000000..e000aeaa4b --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/eclipse/hello/src/org/sample/CallHierarchy.java @@ -0,0 +1,60 @@ +package org.sample; + +public class CallHierarchy { + + @Deprecated + public static void main(String[] args) { + new Base(); + new Child().bar(); + System.out.println(Base.STATIC_FIELD); + } + + public static class Base { + + static int STATIC_FIELD = 100; + protected int protectedField = 200; + +/*nothing*/ + + public Base() { +/*should resolve to enclosing "method/constructor/initializer"*/ + } + + public void foo() { + + } + + public void bar() { + new Child(); + new Child(); + Thread.currentThread(); + Thread.currentThread(); + } + + protected void method_1() { + foo(); + bar(); + } + + } + + public static class Child extends Base { + + Child() { + super(); + foo(); + bar(); + method_1(); + protectedField = 300; + protectedField = 400; + } + + protected void method_2() { + method_1(); + method_1(); + method_1(); + } + + } + +} diff --git a/org.eclipse.jdt.ls.tests/projects/maven/salut/src/main/java/org/sample/CallHierarchy.java b/org.eclipse.jdt.ls.tests/projects/maven/salut/src/main/java/org/sample/CallHierarchy.java new file mode 100644 index 0000000000..627caa5fdb --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/maven/salut/src/main/java/org/sample/CallHierarchy.java @@ -0,0 +1,23 @@ +package org.sample; + +import org.apache.commons.lang3.builder.Builder; +import org.apache.commons.lang3.text.WordUtils; + +public class CallHierarchy { + + public static class FooBuilder implements Builder { + + public FooBuilder() { + new CallHierarchyOther.X(); + } + + @Override + public Object build() { + WordUtils.capitalize("a"); + WordUtils.capitalize("b"); + return null; + } + + } + +} diff --git a/org.eclipse.jdt.ls.tests/projects/maven/salut/src/main/java/org/sample/CallHierarchyOther.java b/org.eclipse.jdt.ls.tests/projects/maven/salut/src/main/java/org/sample/CallHierarchyOther.java new file mode 100644 index 0000000000..1388c0082e --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/maven/salut/src/main/java/org/sample/CallHierarchyOther.java @@ -0,0 +1,16 @@ +package org.sample; + +public class CallHierarchyOther { + + static { + new CallHierarchy.FooBuilder(); + new CallHierarchy.FooBuilder(); + new CallHierarchy.FooBuilder(); + } + + + @Deprecated public static class X { + + } + +} diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyHandlerTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyHandlerTest.java new file mode 100644 index 0000000000..5f72b7b3bc --- /dev/null +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyHandlerTest.java @@ -0,0 +1,218 @@ +/******************************************************************************* + * Copyright (c) 2019 TypeFox and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * TypeFox - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.handlers; + +import static java.util.Arrays.asList; +import static java.util.stream.Collectors.toList; +import static org.eclipse.lsp4j.CallHierarchyDirection.Incoming; +import static org.eclipse.lsp4j.CallHierarchyDirection.Outgoing; +import static org.eclipse.lsp4j.SymbolKind.Class; +import static org.eclipse.lsp4j.SymbolKind.Constructor; +import static org.eclipse.lsp4j.SymbolKind.Field; +import static org.eclipse.lsp4j.SymbolKind.Method; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.ls.core.internal.ClassFileUtil; +import org.eclipse.jdt.ls.core.internal.WorkspaceHelper; +import org.eclipse.jdt.ls.core.internal.hover.JavaElementLabels; +import org.eclipse.jdt.ls.core.internal.managers.AbstractProjectsManagerBasedTest; +import org.eclipse.lsp4j.CallHierarchyDirection; +import org.eclipse.lsp4j.CallHierarchyItem; +import org.eclipse.lsp4j.CallHierarchyParams; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.SymbolKind; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.junit.Before; +import org.junit.Test; + +/** + * @see CallHierarchyHandler + */ +public class CallHierarchyHandlerTest extends AbstractProjectsManagerBasedTest { + + @Before + public void setup() throws Exception { + importProjects(Arrays.asList("eclipse/hello", "maven/salut")); + } + + @Test + public void incoming_src_missing() throws Exception { + // Line 16 from `CallHierarchy` + //<|>/*nothing*/ + String uri = getUriFromSrcProject("org.sample.CallHierarchy"); + assertNull(getIncomings(uri, 13, 0, 1)); + } + + @Test + public void incoming_src_resolve_0() throws Exception { + // Line 15 from `CallHierarchy` + // protected int <|>protectedField = 200; + String uri = getUriFromSrcProject("org.sample.CallHierarchy"); + CallHierarchyItem item = getIncomings(uri, 14, 18, 0); + assertItem(item, "protectedField", Field, "", null, false, null); + } + + @Test + public void incoming_src_resolve_1() throws Exception { + // Line 26 from `CallHierarchy` + // public void <|>bar() { + String uri = getUriFromSrcProject("org.sample.CallHierarchy"); + CallHierarchyItem item = getIncomings(uri, 25, 16, 1); + assertItem(item, "bar()", Method, "void", 3, false, null); + + List calls = item.getCalls(); + assertItem(calls.get(0), "Child()", Constructor, "", null, false, asList(45)); + assertItem(calls.get(1), "main(String[])", Method, "void", null, true, asList(7)); + assertItem(calls.get(2), "method_1()", Method, "void", null, false, asList(35)); + } + + @Test + public void outgoing_src_resolve_0_enclosing() throws Exception { + // Line 20 from `CallHierarchy` + ///*should resolve to enclosing "method/constructor/initializer"*/ + String uri = getUriFromSrcProject("org.sample.CallHierarchy"); + CallHierarchyItem item = getIncomings(uri, 19, 0, 0); + assertItem(item, "Base()", Constructor, "", null, false, null); + } + + @Test + public void outgoing_src_resolve_2() throws Exception { + // Line 34 from `CallHierarchy` + // protected void <|>method_1() { + String uri = getUriFromSrcProject("org.sample.CallHierarchy"); + CallHierarchyItem item = getOutgoings(uri, 33, 19, 2); + assertItem(item, "method_1()", Method, "void", 2, false, null); + + List calls = item.getCalls(); + assertItem(calls.get(0), "foo()", Method, "void", 0, false, asList(34)); + assertItem(calls.get(1), "bar()", Method, "void", 2, false, asList(35)); + + List call1Calls = calls.get(1).getCalls(); + assertItem(call1Calls.get(0), "Child()", Constructor, "", null, false, asList(27, 28)); + assertItem(call1Calls.get(1), "currentThread()", Method, "Thread", null, false, asList(29, 30)); + } + + @Test + public void incoming_jar_resolve_2() throws Exception { + // Line 12 from `CallHierarchyOther` + // @Deprecated public static class <|>X { + String uri = getUriFromJarProject("org.sample.CallHierarchyOther"); + CallHierarchyItem item = getIncomings(uri, 11, 34, 2); + assertItem(item, "X", Class, "", 1, true, null); + + List calls = item.getCalls(); + assertItem(calls.get(0), "FooBuilder()", Constructor, "", 1, false, asList(10)); + + List call0Calls = calls.get(0).getCalls(); + assertItem(call0Calls.get(0), "{...}", Constructor, "", null, false, asList(5, 6, 7)); + } + + @Test + public void outgoing_jar_resolve_2() throws Exception { + // Line 15 from `CallHierarchy` + // public Object <|>build() { + String uri = getUriFromJarProject("org.sample.CallHierarchy"); + CallHierarchyItem item = getOutgoings(uri, 14, 18, 2); + assertItem(item, "build()", Method, "Object", 1, false, null); + + List calls = item.getCalls(); + assertItem(calls.get(0), "capitalize(String)", Method, "String", 1, false, asList(15, 16)); + + List call0Calls = calls.get(0).getCalls(); + assertItem(call0Calls.get(0), "capitalize(String, char...)", Method, "String", null, false, asList(369)); + + String jarUri = call0Calls.get(0).getUri(); + assertTrue(jarUri.startsWith("jdt://")); + assertTrue(jarUri.contains("org.apache.commons.lang3.text")); + assertTrue(jarUri.contains("WordUtils.class")); + } + + /** + * @param item + * to assert + * @param name + * the expected name + * @param kind + * the expected kind + * @param detail + * the expected detail without the ` : ` (declaration) suffix. + * @param calls + * the number of calls or {@code null} if expected non-defined calls. + * @param deprecated + * expected deprecated state + * @param callLocationStartLines + * {@code null}, if there are no call locations. Otherwise a list of + * expected start lines for the call locations + * @return the {@code item} + */ + static CallHierarchyItem assertItem(CallHierarchyItem item, String name, SymbolKind kind, String detail, Integer calls, boolean deprecated, List callLocationStartLines) { + assertNotNull(item); + assertEquals(name, item.getName()); + assertEquals(kind, item.getKind()); + if (detail.isEmpty()) { + assertEquals(detail, item.getDetail()); + } else { + assertEquals(JavaElementLabels.DECL_STRING + detail, item.getDetail()); + } + if (calls == null) { + assertNull(item.getCalls()); + } else { + assertEquals(calls.intValue(), item.getCalls().size()); + } + assertEquals(deprecated, item.getDeprecated()); + if (callLocationStartLines == null) { + assertNull(item.getCallLocations()); + } else { + List callLocations = item.getCallLocations(); + assertEquals(callLocationStartLines.size(), callLocations.size()); + List actualStartLines = callLocations.stream().map(loc -> loc.getRange().getStart().getLine()).collect(toList()); + assertEquals(callLocationStartLines, actualStartLines); + } + return item; + } + + static CallHierarchyItem getIncomings(String uri, int line, int character, int resolve) { + CallHierarchyParams params = createParams(uri, line, character, Incoming, resolve); + return new CallHierarchyHandler().callHierarchy(params, new NullProgressMonitor()); + } + + static CallHierarchyItem getOutgoings(String uri, int line, int character, int resolve) { + CallHierarchyParams params = createParams(uri, line, character, Outgoing, resolve); + return new CallHierarchyHandler().callHierarchy(params, new NullProgressMonitor()); + } + + static CallHierarchyParams createParams(String uri, int line, int character, CallHierarchyDirection direction, int resolve) { + CallHierarchyParams params = new CallHierarchyParams(); + params.setTextDocument(new TextDocumentIdentifier(uri)); + params.setPosition(new Position(line, character)); + params.setDirection(direction); + params.setResolve(resolve); + return params; + } + + static String getUriFromJarProject(String className) throws JavaModelException { + return ClassFileUtil.getURI(WorkspaceHelper.getProject("salut"), className); + } + + static String getUriFromSrcProject(String className) throws JavaModelException { + return ClassFileUtil.getURI(WorkspaceHelper.getProject("hello"), className); + } + +} diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyResolveHandlerTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyResolveHandlerTest.java new file mode 100644 index 0000000000..d312ce4180 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/CallHierarchyResolveHandlerTest.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (c) 2019 TypeFox and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * TypeFox - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.handlers; + +import static java.util.Arrays.asList; +import static org.eclipse.jdt.ls.core.internal.handlers.CallHierarchyHandlerTest.assertItem; +import static org.eclipse.jdt.ls.core.internal.handlers.CallHierarchyHandlerTest.getIncomings; +import static org.eclipse.jdt.ls.core.internal.handlers.CallHierarchyHandlerTest.getUriFromSrcProject; +import static org.eclipse.lsp4j.SymbolKind.Constructor; +import static org.eclipse.lsp4j.SymbolKind.Method; +import static org.junit.Assert.assertNull; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.ls.core.internal.managers.AbstractProjectsManagerBasedTest; +import org.eclipse.lsp4j.CallHierarchyDirection; +import org.eclipse.lsp4j.CallHierarchyItem; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.ResolveCallHierarchyItemParams; +import org.junit.Before; +import org.junit.Test; + +/** + * @see CallHierarchyResolveHandler + */ +public class CallHierarchyResolveHandlerTest extends AbstractProjectsManagerBasedTest { + + @Before + public void setup() throws Exception { + importProjects(Arrays.asList("eclipse/hello", "maven/salut")); + } + + @Test + public void cannot_resolve() { + CallHierarchyItem invalid = new CallHierarchyItem(); + invalid.setUri("file:///missing/Resource.java"); + invalid.setRange(new Range(new Position(0, 0), new Position(0, 0))); + CallHierarchyItem item = resolveIncoming(invalid, 1); + assertNull(item); + } + + @Test + public void resolve_incoming() throws Exception { + // Line 26 from `CallHierarchy` + // public void <|>bar() { + String uri = getUriFromSrcProject("org.sample.CallHierarchy"); + CallHierarchyItem item = getIncomings(uri, 25, 16, 0); + assertItem(item, "bar()", Method, "void", null, false, null); + + item = resolveIncoming(item, 1); + assertItem(item, "bar()", Method, "void", 3, false, null); + + List calls = item.getCalls(); + assertItem(calls.get(0), "Child()", Constructor, "", null, false, asList(45)); + assertItem(calls.get(1), "main(String[])", Method, "void", null, true, asList(7)); + assertItem(calls.get(2), "method_1()", Method, "void", null, false, asList(35)); + } + + private static CallHierarchyItem resolveIncoming(CallHierarchyItem toResolve, int resolve) { + ResolveCallHierarchyItemParams params = new ResolveCallHierarchyItemParams(); + params.setDirection(CallHierarchyDirection.Incoming); + params.setItem(toResolve); + params.setResolve(resolve); + return new CallHierarchyResolveHandler().resolve(params, new NullProgressMonitor()); + } + +} diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/DocumentSymbolHandlerTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/DocumentSymbolHandlerTest.java index 7fc05f988f..f054ece8ca 100644 --- a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/DocumentSymbolHandlerTest.java +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/handlers/DocumentSymbolHandlerTest.java @@ -159,7 +159,7 @@ public void testTypes_hierarchical() throws Exception { public void testSyntheticMember_hierarchical_noSourceAttached() throws Exception { String className = "foo.bar"; List symbols = asStream(internalGetHierarchicalSymbols(noSourceProject, monitor, className)).collect(Collectors.toList()); - assertHasHierarchicalSymbol("bar()", "bar", SymbolKind.Method, symbols); + assertHasHierarchicalSymbol("bar()", "bar", SymbolKind.Constructor, symbols); assertHasHierarchicalSymbol("add(int...) : int", "bar", SymbolKind.Method, symbols); }