Skip to content
Closed
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Bazel is a high-performance build system, but it has significant shortcomings in
- **Dependency Resolution**: Build a complete Java dependency graph through Bazel CLI and BUILD file parsing
- **Real-time Sync**: Monitor BUILD file changes, automatically trigger incremental sync, and keep classpath consistent with the workspace
- **Smart Caching**: Persistent KV storage based on redb, distinguishing between fast and slow paths to reduce unnecessary Bazel invocations
- **PowerMock Support**: Automatic bytecode-level fix prevents PowerMock test methods from disappearing in Test Explorer after running individual tests

**Project Directory Structure:**

Expand Down Expand Up @@ -232,6 +233,7 @@ TEST_WORKSPACE=multi-module-project npm run e2e
| `simple-java-project` | Extension activation, basic completion, Greeter class resolution |
| `maven-deps-project` | External dependency completion (Guava, JUnit) |
| `multi-module-project` | Transitive dependency exports, resources |
| `powermock-repro-project` | PowerMock test tree preservation fix |

**Layered Testing Strategy:**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class BazelActivator implements BundleActivator {

private IResourceChangeListener invisibleProjectListener;
private ServiceRegistration<WeavingHook> weavingHookRegistration;
private ServiceRegistration<WeavingHook> powerMockPatcherRegistration;

@Override
public void start(BundleContext context) throws Exception {
Expand All @@ -38,6 +39,9 @@ public void start(BundleContext context) throws Exception {
weavingHookRegistration = context.registerService(
WeavingHook.class, new JDTUtilsPatcher(), null);

powerMockPatcherRegistration = context.registerService(
WeavingHook.class, new PowerMockRunnerPatcher(), null);

invisibleProjectListener = this::checkForInvisibleProjects;
ResourcesPlugin.getWorkspace().addResourceChangeListener(
invisibleProjectListener, IResourceChangeEvent.POST_CHANGE);
Expand All @@ -49,6 +53,10 @@ public void stop(BundleContext context) throws Exception {
weavingHookRegistration.unregister();
weavingHookRegistration = null;
}
if (powerMockPatcherRegistration != null) {
powerMockPatcherRegistration.unregister();
powerMockPatcherRegistration = null;
}
if (invisibleProjectListener != null) {
ResourcesPlugin.getWorkspace().removeResourceChangeListener(invisibleProjectListener);
invisibleProjectListener = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.bazel.jdt;

import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

public class PowerMockHelper {

private static final Logger LOG = Logger.getLogger(PowerMockHelper.class.getName());

private static final ConcurrentHashMap<String, Boolean> cache = new ConcurrentHashMap<>();

private static final String[] POWERMOCK_RUNNER_NAMES = {
"PowerMockRunner",
"org.powermock.modules.junit4.PowerMockRunner",
"PowerMockJUnit44Runner",
"org.powermock.modules.junit4.legacy.PowerMockJUnit44Runner"
};

private PowerMockHelper() {}

public static boolean isPowerMockRunner(String qualifiedName) {
if (qualifiedName == null || qualifiedName.isEmpty()) {
return false;
}
try {
return cache.computeIfAbsent(qualifiedName, PowerMockHelper::detectPowerMockRunner);
} catch (Exception e) {
LOG.log(Level.WARNING, "Error detecting PowerMock for " + qualifiedName, e);
return false;
}
}

static boolean detectPowerMockRunner(String qualifiedName) {
Boolean result = isPowerMockRunnerViaIType(qualifiedName);
if (result != null) {
LOG.fine("PowerMock detection via JDT API for " + qualifiedName + ": " + result);
return result;
}
boolean heuristic = isPowerMockByClassName(qualifiedName);
LOG.fine("PowerMock detection via heuristic for " + qualifiedName + ": " + heuristic);
return heuristic;
}

static Boolean isPowerMockRunnerViaIType(String qualifiedName) {
try {
org.eclipse.jdt.core.IJavaModel model = org.eclipse.jdt.core.JavaCore.create(
org.eclipse.core.resources.ResourcesPlugin.getWorkspace().getRoot());
org.eclipse.jdt.core.IJavaProject[] projects = model.getJavaProjects();

for (org.eclipse.jdt.core.IJavaProject project : projects) {
org.eclipse.jdt.core.IType type = project.findType(qualifiedName);
if (type == null) continue;

for (org.eclipse.jdt.core.IAnnotation annotation : type.getAnnotations()) {
String name = annotation.getElementName();
if (!"RunWith".equals(name) && !"org.junit.runner.RunWith".equals(name)) {
continue;
}
for (org.eclipse.jdt.core.IMemberValuePair pair : annotation.getMemberValuePairs()) {
if ("value".equals(pair.getMemberName())) {
String value = String.valueOf(pair.getValue());
for (String runner : POWERMOCK_RUNNER_NAMES) {
if (value.contains(runner)) {
return true;
}
}
}
}
return false;
}
return false;
}
} catch (Exception e) {
LOG.log(Level.FINE, "JDT lookup failed for " + qualifiedName, e);
}
return null;
}

static boolean isPowerMockByClassName(String qualifiedName) {
String simpleName = qualifiedName.contains(".")
? qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1)
: qualifiedName;
return simpleName.contains("PowerMock");
}

static void clearCache() {
cache.clear();
}

static int cacheSize() {
return cache.size();
}

static Boolean getCachedResult(String qualifiedName) {
return cache.get(qualifiedName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package com.bazel.jdt;

import java.util.logging.Level;
import java.util.logging.Logger;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.osgi.framework.hooks.weaving.WeavingHook;
import org.osgi.framework.hooks.weaving.WovenClass;

public class PowerMockRunnerPatcher implements WeavingHook, Opcodes {

private static final Logger LOG = Logger.getLogger(PowerMockRunnerPatcher.class.getName());

private static final String TARGET_BUNDLE = "com.microsoft.java.test.plugin";
static final String IS_TEST_CLASS = "isTestClass";
private static final String ITYPEBINDING_DESC_FRAGMENT = "ITypeBinding";
private static final String HELPER_INTERNAL = "com/bazel/jdt/PowerMockHelper";
private static final String ITYPEBINDING_INTERNAL = "org/eclipse/jdt/core/dom/ITypeBinding";

@Override
public void weave(WovenClass wovenClass) {
String bundleName = wovenClass.getBundleWiring().getBundle().getSymbolicName();
if (!TARGET_BUNDLE.equals(bundleName)) {
return;
}

byte[] original = wovenClass.getBytes();
if (!containsIsTestClassCall(original)) {
return;
}

try {
byte[] patched = patchTestSearchClass(original);
if (patched != null) {
wovenClass.getDynamicImports().add("com.bazel.jdt");
wovenClass.setBytes(patched);
LOG.info("Patched " + wovenClass.getClassName()
+ ": injected PowerMock guard at isTestClass call site");
}
} catch (Exception e) {
LOG.log(Level.WARNING,
"Failed to patch " + wovenClass.getClassName() + ", leaving class unmodified", e);
}
}

static boolean containsIsTestClassCall(byte[] classBytes) {
boolean[] found = {false};
ClassReader reader = new ClassReader(classBytes);
reader.accept(new ClassVisitor(ASM9) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
if (found[0]) return null;
return new MethodVisitor(ASM9) {
@Override
public void visitMethodInsn(int opcode, String owner, String mName,
String mDesc, boolean isInterface) {
if (IS_TEST_CLASS.equals(mName)
&& mDesc.contains(ITYPEBINDING_DESC_FRAGMENT)
&& mDesc.endsWith(")Z")) {
found[0] = true;
}
}
};
}
}, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
return found[0];
}

byte[] patchTestSearchClass(byte[] classBytes) {
ClassReader reader = new ClassReader(classBytes);
ClassWriter writer = new SafeClassWriter(reader,
ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
boolean[] patched = {false};

ClassVisitor visitor = new ClassVisitor(ASM9, writer) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
return new PowerMockGuardInjectingVisitor(mv, patched);
}
};

reader.accept(visitor, 0);
return patched[0] ? writer.toByteArray() : null;
}

static class PowerMockGuardInjectingVisitor extends MethodVisitor {
private final boolean[] patched;
private int lastAloadVar = -1;

PowerMockGuardInjectingVisitor(MethodVisitor mv, boolean[] patched) {
super(ASM9, mv);
this.patched = patched;
}

@Override
public void visitVarInsn(int opcode, int varIndex) {
if (opcode == ALOAD) {
lastAloadVar = varIndex;
}
super.visitVarInsn(opcode, varIndex);
}

@Override
public void visitMethodInsn(int opcode, String owner, String name,
String descriptor, boolean isInterface) {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);

if (IS_TEST_CLASS.equals(name)
&& descriptor.contains(ITYPEBINDING_DESC_FRAGMENT)
&& descriptor.endsWith(")Z")
&& lastAloadVar >= 0) {
injectPowerMockGuard(lastAloadVar);
patched[0] = true;
}
lastAloadVar = -1;
}

private void injectPowerMockGuard(int typeBindingVar) {
Label done = new Label();

mv.visitInsn(DUP);
mv.visitJumpInsn(IFEQ, done);

mv.visitVarInsn(ALOAD, typeBindingVar);
mv.visitMethodInsn(INVOKEINTERFACE, ITYPEBINDING_INTERNAL,
"getQualifiedName", "()Ljava/lang/String;", true);
mv.visitMethodInsn(INVOKESTATIC, HELPER_INTERNAL,
"isPowerMockRunner", "(Ljava/lang/String;)Z", false);
mv.visitJumpInsn(IFEQ, done);

mv.visitInsn(POP);
mv.visitInsn(ICONST_0);

mv.visitLabel(done);
}
}

private static class SafeClassWriter extends ClassWriter {
SafeClassWriter(ClassReader classReader, int flags) {
super(classReader, flags);
}

@Override
protected String getCommonSuperClass(String type1, String type2) {
try {
return super.getCommonSuperClass(type1, type2);
} catch (Exception e) {
return "java/lang/Object";
}
}
}
}
Loading
Loading