Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -151,34 +151,39 @@ private void addTestItemArgs(List<String> arguments) throws CoreException {
method.getParameters().length > 0) {
final ICompilationUnit unit = method.getCompilationUnit();
if (unit == null) {
throw new CoreException(new Status(IStatus.ERROR, JUnitPlugin.PLUGIN_ID, IStatus.ERROR,
"Cannot get compilation unit of method" + method.getElementName(), null)); //$NON-NLS-1$
}
final CompilationUnit root = (CompilationUnit) TestSearchUtils.parseToAst(unit,
false /*fromCache*/, new NullProgressMonitor());
final MethodDeclaration methodDeclaration = ASTNodeSearchUtil.getMethodDeclarationNode(method, root);
if (methodDeclaration == null) {
throw new CoreException(new Status(IStatus.ERROR, JUnitPlugin.PLUGIN_ID, IStatus.ERROR,
// binary method
if (method.getDeclaringType() == null) {
throw new CoreException(new Status(IStatus.ERROR, JUnitPlugin.PLUGIN_ID, IStatus.ERROR,
"Cannot get compilation unit of method" + method.getElementName(), null)); //$NON-NLS-1$
}
} else {
final CompilationUnit root = (CompilationUnit) TestSearchUtils.parseToAst(unit,
false /* fromCache */, new NullProgressMonitor());
final MethodDeclaration methodDeclaration = ASTNodeSearchUtil.getMethodDeclarationNode(method,
root);
if (methodDeclaration == null) {
throw new CoreException(new Status(IStatus.ERROR, JUnitPlugin.PLUGIN_ID, IStatus.ERROR,
"Cannot get method declaration of method" + method.getElementName(), null)); //$NON-NLS-1$
}
}

final List<String> parameters = new LinkedList<>();
for (final Object obj : methodDeclaration.parameters()) {
if (obj instanceof SingleVariableDeclaration) {
final ITypeBinding paramTypeBinding = ((SingleVariableDeclaration) obj)
.getType().resolveBinding();
if (paramTypeBinding == null) {
throw new CoreException(new Status(IStatus.ERROR, JUnitPlugin.PLUGIN_ID, IStatus.ERROR,
"Cannot set set argument for method" + methodDeclaration.toString(), null));
} else if (paramTypeBinding.isPrimitive()) {
parameters.add(paramTypeBinding.getQualifiedName());
} else {
parameters.add(paramTypeBinding.getBinaryName());
final List<String> parameters = new LinkedList<>();
for (final Object obj : methodDeclaration.parameters()) {
if (obj instanceof SingleVariableDeclaration) {
final ITypeBinding paramTypeBinding = ((SingleVariableDeclaration) obj).getType()
.resolveBinding();
if (paramTypeBinding == null) {
throw new CoreException(new Status(IStatus.ERROR, JUnitPlugin.PLUGIN_ID, IStatus.ERROR,
"Cannot set set argument for method" + methodDeclaration.toString(), null));
} else if (paramTypeBinding.isPrimitive()) {
parameters.add(paramTypeBinding.getQualifiedName());
} else {
parameters.add(paramTypeBinding.getBinaryName());
}
}
}
}
if (parameters.size() > 0) {
testName += "(" + String.join(",", parameters) + ")";
if (parameters.size() > 0) {
testName += "(" + String.join(",", parameters) + ")";
}
}
}
arguments.add(method.getDeclaringType().getFullyQualifiedName() + ':' + testName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,33 @@
import com.microsoft.java.test.plugin.util.TestItemUtils;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.core.BinaryMember;
import org.eclipse.jdt.internal.core.BinaryMethod;
import org.eclipse.jdt.internal.core.BinaryType;
import org.eclipse.jdt.internal.core.PackageFragmentRoot;
import org.eclipse.jdt.internal.core.manipulation.JavaElementLabelsCore;
import org.eclipse.jdt.ls.core.internal.JDTUtils;
import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin;
import org.eclipse.jdt.ls.core.internal.ProjectUtils;
Comment on lines +29 to 36
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file now relies on several org.eclipse.jdt.internal.* types (BinaryMember, BinaryMethod, BinaryType, PackageFragmentRoot). Elsewhere in this codebase, files that touch internal Eclipse APIs are annotated with @SuppressWarnings("restriction") (e.g., TestSearchUtils, TestItemUtils). Consider adding the same suppression (or refactoring to public JDT APIs) to avoid Tycho/Eclipse "Discouraged access" build warnings or failures.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.io.ByteArrayInputStream;
import java.io.InputStream;

/**
* Builder class to build {@link com.microsoft.java.test.plugin.model.JavaTestItem}
Expand All @@ -36,6 +54,7 @@ public class JavaTestItemBuilder {
private IJavaElement element;
private TestLevel level;
private TestKind kind;
private String displayName;

public JavaTestItemBuilder setJavaElement(IJavaElement element) {
this.element = element;
Expand All @@ -52,34 +71,124 @@ public JavaTestItemBuilder setKind(TestKind kind) {
return this;
}

public JavaTestItemBuilder setDisplayName(String displayName) {
this.displayName = displayName;
return this;
}

public JavaTestItem build() throws JavaModelException {
if (this.element == null || this.level == null || this.kind == null) {
throw new IllegalArgumentException("Failed to build Java test item due to missing arguments");
}

final String displayName;
String uri = null;
if (this.element instanceof IJavaProject) {
final IJavaProject javaProject = (IJavaProject) this.element;
final IProject project = javaProject.getProject();
if (ProjectUtils.isVisibleProject(project)) {
displayName = project.getName();
if (this.displayName == null) {
if (this.element instanceof IJavaProject) {
final IJavaProject javaProject = (IJavaProject) this.element;
final IProject project = javaProject.getProject();
if (ProjectUtils.isVisibleProject(project)) {
displayName = project.getName();
} else {
final IPath realPath = ProjectUtils.getProjectRealFolder(project);
displayName = realPath.lastSegment();
uri = realPath.toFile().toURI().toString();
}
} else if (this.element instanceof IPackageFragment &&
((IPackageFragment) this.element).isDefaultPackage()) {
displayName = DEFAULT_PACKAGE_NAME;
final IResource resource = getResource((IPackageFragment) this.element);
if (resource == null || !resource.exists()) {
return null;
}
uri = JDTUtils.getFileURI(resource);
} else {
final IPath realPath = ProjectUtils.getProjectRealFolder(project);
displayName = realPath.lastSegment();
uri = realPath.toFile().toURI().toString();
displayName = JavaElementLabelsCore.getElementLabel(this.element, JavaElementLabelsCore.ALL_DEFAULT);
}
} else if (this.element instanceof IPackageFragment && ((IPackageFragment) this.element).isDefaultPackage()) {
displayName = DEFAULT_PACKAGE_NAME;
} else {
displayName = JavaElementLabelsCore.getElementLabel(this.element, JavaElementLabelsCore.ALL_DEFAULT);
}
Range range = null;
final String fullName = TestItemUtils.parseFullName(this.element, this.level, this.kind);
if (uri == null) {
uri = JDTUtils.getFileURI(this.element.getResource());
IResource resource = this.element.getResource();
if (resource == null && this.element instanceof IPackageFragment) {
resource = getResource((IPackageFragment) this.element);
}
if (resource == null || !resource.exists()) {
return null;
}
if (element instanceof BinaryMember) {
final String[] sources = new String[1];
final int[] lines = {-1};
final IClassFile classFile = ((BinaryMember) element).getClassFile();
try (InputStream is = new ByteArrayInputStream(classFile.getBytes())) {
final ClassReader cr = new ClassReader(is);
if (element instanceof BinaryType) {
cr.accept(new ClassVisitor(Opcodes.ASM9) {
@Override
public void visitSource(String source, String debug) {
sources[0] = source;
}
}, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES);
} else if (element instanceof BinaryMethod) {
final String methodDescriptor = ((BinaryMethod) element).getSignature();
cr.accept(new ClassVisitor(Opcodes.ASM9) {
@Override
public void visitSource(String source, String debug) {
sources[0] = source;
}

public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
if (name.equals(element.getElementName()) && descriptor.equals(methodDescriptor)) {
return new MethodVisitor(Opcodes.ASM9) {
Comment on lines +131 to +142
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When matching a BinaryMethod in ASM, BinaryMethod#getSignature() returns a JDT method signature, which typically does not match ASM's JVM descriptor string (visitMethod's descriptor). This comparison (descriptor.equals(methodDescriptor)) will often fail, preventing line number extraction for binary methods. Consider converting the JDT signature into a JVM descriptor (or matching by name + parameter types using the classfile model) before comparing.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

@Override
public void visitLineNumber(int line, Label start) {
if (lines[0] < 0) {
lines[0] = line;
if (line > 0) {
lines[0]--;
}
}
}
};
}
return null;
}
}, ClassReader.SKIP_FRAMES);
}
} catch (Exception e) {
JavaLanguageServerPlugin.logException(e);
}
if (sources[0] != null) {
final IPackageFragment packageFragment = (IPackageFragment) element
.getAncestor(IJavaElement.PACKAGE_FRAGMENT);
if (packageFragment != null) {
final String packageName = packageFragment.getElementName();
final IJavaProject project = element.getJavaProject();
final String packagePath = packageName.replace('.', '/');
for (final IClasspathEntry entry : project.getRawClasspath()) {
if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
final IPath sourceFolderPath = entry.getPath();
final IPath fullPath = sourceFolderPath.append(packagePath).append(sources[0]);
final IResource candidate = ResourcesPlugin.getWorkspace().getRoot()
.findMember(fullPath);
if (candidate != null && candidate.exists()) {
resource = candidate;
break;
}
}
}
}
}
if (lines[0] > 0) {
final Position line = new Position(lines[0] - 1, 0);
Comment on lines +182 to +183
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The line-number-to-Position conversion appears off by one: visitLineNumber decrements lines[0] and then later new Position(lines[0] - 1, 0) subtracts again. This can shift ranges up by an extra line and also causes line 1 to be dropped entirely (lines[0] becomes 0, failing the lines[0] > 0 check). Use a single 1-based→0-based conversion at one point (preferably when constructing the Position).

Suggested change
if (lines[0] > 0) {
final Position line = new Position(lines[0] - 1, 0);
if (lines[0] >= 0) {
final Position line = new Position(lines[0], 0);

Copilot uses AI. Check for mistakes.
range = new Range(line, line);
}
}
if (uri == null) {
uri = JDTUtils.getFileURI(resource);
}
}
Range range = null;
if (this.level == TestLevel.CLASS || this.level == TestLevel.METHOD) {
if (range == null && (this.level == TestLevel.CLASS || this.level == TestLevel.METHOD)) {
range = TestItemUtils.parseTestItemRange(this.element);
}

Expand All @@ -89,4 +198,22 @@ public JavaTestItem build() throws JavaModelException {

return result;
}

private IResource getResource(IPackageFragment packageFragment) {
if (packageFragment == null) {
return null;
}
IResource resource = packageFragment.getResource();
if (resource == null) {
final IJavaElement e = packageFragment.getParent();
if (e instanceof PackageFragmentRoot) {
final PackageFragmentRoot root = (PackageFragmentRoot) e;
resource = root.getResource();
if (resource == null) {
resource = root.resource(root);
}
}
}
return resource;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,9 @@ public Set<IType> findTestItemsInContainer(IJavaElement element, IProgressMonito

return types;
}

@Override
public String getDisplayName(IMethodBinding methodBinding) {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.IMemberValuePairBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.Modifier;
Expand All @@ -40,9 +41,11 @@ public class JUnit5TestSearcher extends BaseFrameworkSearcher {

protected static final String DISPLAY_NAME_ANNOTATION_JUNIT5 = "org.junit.jupiter.api.DisplayName";

protected static final String SPOCK_FEATURE_METADATA = "org.spockframework.runtime.model.FeatureMetadata";

public JUnit5TestSearcher() {
super();
this.testMethodAnnotations = new String[] { JUNIT_PLATFORM_TESTABLE };
this.testMethodAnnotations = new String[] { JUNIT_PLATFORM_TESTABLE, SPOCK_FEATURE_METADATA };
}

@Override
Expand Down Expand Up @@ -133,4 +136,19 @@ public Set<IType> findTestItemsInContainer(IJavaElement element, IProgressMonito
}
return types;
}

@Override
public String getDisplayName(IMethodBinding methodBinding) {
for (final IAnnotationBinding annotation : methodBinding.getAnnotations()) {
if (matchesName(annotation.getAnnotationType(), SPOCK_FEATURE_METADATA)) {
final IMemberValuePairBinding[] pairs = annotation.getDeclaredMemberValuePairs();
for (final IMemberValuePairBinding pair : pairs) {
if ("name".equals(pair.getName()) && (pair.getValue() instanceof String)) {
return (String) pair.getValue();
}
}
}
}
return null;
}
}
Loading