mirror of
https://github.com/openjdk/jdk.git
synced 2026-01-28 12:09:14 +00:00
380 lines
14 KiB
Java
380 lines
14 KiB
Java
/*
|
|
* Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
/*
|
|
* @test
|
|
* @bug 8335935 8336588 8372047
|
|
* @summary Testing ClassFile transformations.
|
|
* @run junit TransformTests
|
|
*/
|
|
|
|
import java.lang.classfile.*;
|
|
import java.lang.classfile.attribute.AnnotationDefaultAttribute;
|
|
import java.lang.classfile.attribute.ConstantValueAttribute;
|
|
import java.lang.classfile.attribute.SourceDebugExtensionAttribute;
|
|
import java.lang.classfile.instruction.BranchInstruction;
|
|
import java.lang.classfile.instruction.ConstantInstruction;
|
|
import java.lang.classfile.instruction.LabelTarget;
|
|
import java.lang.constant.ClassDesc;
|
|
import java.lang.constant.MethodTypeDesc;
|
|
import java.lang.reflect.AccessFlag;
|
|
import java.net.URI;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.nio.file.Paths;
|
|
import java.util.HashSet;
|
|
import java.util.Set;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
|
import helpers.ByteArrayClassLoader;
|
|
import jdk.internal.classfile.impl.TransformImpl;
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
import static java.lang.classfile.ClassFile.*;
|
|
import static java.lang.constant.ConstantDescs.*;
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
|
|
/**
|
|
* TransformTests
|
|
*/
|
|
class TransformTests {
|
|
static final String testClassName = "TransformTests$TestClass";
|
|
static final Path testClassPath = Paths.get(URI.create(ArrayTest.class.getResource(testClassName + ".class").toString()));
|
|
static CodeTransform
|
|
foo2foo = swapLdc("foo", "foo"),
|
|
foo2bar = swapLdc("foo", "bar"),
|
|
bar2baz = swapLdc("bar", "baz"),
|
|
baz2quux = swapLdc("baz", "quux"),
|
|
baz2foo = swapLdc("baz", "foo");
|
|
|
|
static CodeTransform swapLdc(String x, String y) {
|
|
return (b, e) -> {
|
|
if (e instanceof ConstantInstruction ci && ci.constantValue().equals(x)) {
|
|
b.loadConstant(y);
|
|
}
|
|
else
|
|
b.with(e);
|
|
};
|
|
}
|
|
|
|
static ClassTransform transformCode(CodeTransform x) {
|
|
return (cb, ce) -> {
|
|
if (ce instanceof MethodModel mm) {
|
|
cb.transformMethod(mm, (mb, me) -> {
|
|
if (me instanceof CodeModel xm) {
|
|
mb.transformCode(xm, x);
|
|
}
|
|
else
|
|
mb.with(me);
|
|
});
|
|
}
|
|
else
|
|
cb.with(ce);
|
|
};
|
|
}
|
|
|
|
static String invoke(byte[] bytes) throws Exception {
|
|
return (String)
|
|
new ByteArrayClassLoader(AdaptCodeTest.class.getClassLoader(), testClassName, bytes)
|
|
.getMethod(testClassName, "foo")
|
|
.invoke(null);
|
|
}
|
|
|
|
@Test
|
|
void testSingleTransform() throws Exception {
|
|
|
|
byte[] bytes = Files.readAllBytes(testClassPath);
|
|
var cc = ClassFile.of();
|
|
ClassModel cm = cc.parse(bytes);
|
|
|
|
assertEquals(invoke(bytes), "foo");
|
|
assertEquals(invoke(cc.transformClass(cm, transformCode(foo2foo))), "foo");
|
|
assertEquals(invoke(cc.transformClass(cm, transformCode(foo2bar))), "bar");
|
|
}
|
|
|
|
@Test
|
|
void testSeq2() throws Exception {
|
|
|
|
byte[] bytes = Files.readAllBytes(testClassPath);
|
|
var cc = ClassFile.of();
|
|
ClassModel cm = cc.parse(bytes);
|
|
|
|
assertEquals(invoke(bytes), "foo");
|
|
ClassTransform transform = transformCode(foo2bar.andThen(bar2baz));
|
|
assertEquals(invoke(cc.transformClass(cm, transform)), "baz");
|
|
}
|
|
|
|
@Test
|
|
void testSeqN() throws Exception {
|
|
|
|
byte[] bytes = Files.readAllBytes(testClassPath);
|
|
var cc = ClassFile.of();
|
|
ClassModel cm = cc.parse(bytes);
|
|
|
|
assertEquals(invoke(bytes), "foo");
|
|
assertEquals(invoke(cc.transformClass(cm, transformCode(foo2bar.andThen(bar2baz).andThen(baz2foo)))), "foo");
|
|
assertEquals(invoke(cc.transformClass(cm, transformCode(foo2bar.andThen(bar2baz).andThen(baz2quux)))), "quux");
|
|
assertEquals(invoke(cc.transformClass(cm, transformCode(foo2foo.andThen(foo2bar).andThen(bar2baz)))), "baz");
|
|
}
|
|
|
|
/**
|
|
* Test to ensure class elements, such as field and
|
|
* methods defined with transform/with, are visible
|
|
* to next transforms.
|
|
*/
|
|
@Test
|
|
void testClassChaining() throws Exception {
|
|
var bytes = Files.readAllBytes(testClassPath);
|
|
var cf = ClassFile.of();
|
|
var cm = cf.parse(bytes);
|
|
var otherCm = cf.parse(cf.build(ClassDesc.of("Temp"), clb -> clb
|
|
.withMethodBody("baz", MTD_void, ACC_STATIC, CodeBuilder::return_)
|
|
.withField("baz", CD_long, ACC_STATIC)));
|
|
|
|
var methodBaz = otherCm.methods().getFirst();
|
|
var fieldBaz = otherCm.fields().getFirst();
|
|
|
|
ClassTransform transform1 = ClassTransform.endHandler(cb -> {
|
|
ClassBuilder ret;
|
|
ret = cb.withMethodBody("bar", MTD_void, ACC_STATIC, CodeBuilder::return_);
|
|
assertSame(cb, ret);
|
|
ret = cb.transformMethod(methodBaz, MethodTransform.ACCEPT_ALL);
|
|
assertSame(cb, ret);
|
|
ret = cb.withField("bar", CD_int, ACC_STATIC);
|
|
assertSame(cb, ret);
|
|
ret = cb.transformField(fieldBaz, FieldTransform.ACCEPT_ALL);
|
|
assertSame(cb, ret);
|
|
});
|
|
|
|
Set<String> methodNames = new HashSet<>();
|
|
Set<String> fieldNames = new HashSet<>();
|
|
ClassTransform transform2 = (cb, ce) -> {
|
|
if (ce instanceof MethodModel mm) {
|
|
methodNames.add(mm.methodName().stringValue());
|
|
}
|
|
if (ce instanceof FieldModel fm) {
|
|
fieldNames.add(fm.fieldName().stringValue());
|
|
}
|
|
cb.with(ce);
|
|
};
|
|
|
|
cf.transformClass(cm, transform1.andThen(transform2));
|
|
|
|
assertEquals(Set.of(INIT_NAME, "foo", "bar", "baz"), methodNames);
|
|
assertEquals(Set.of("bar", "baz"), fieldNames);
|
|
}
|
|
|
|
/**
|
|
* Test to ensure method elements, such as generated
|
|
* or transformed code, are visible to transforms.
|
|
*/
|
|
@Test
|
|
void testMethodChaining() throws Exception {
|
|
var mtd = MethodTypeDesc.of(CD_String);
|
|
|
|
var cf = ClassFile.of();
|
|
|
|
// withCode
|
|
var cm = cf.parse(cf.build(ClassDesc.of("Temp"), clb -> clb
|
|
.withMethod("baz", mtd, ACC_STATIC | ACC_NATIVE, _ -> {})));
|
|
|
|
MethodTransform transform1 = MethodTransform.endHandler(mb -> {
|
|
var ret = mb.withCode(cob -> cob.loadConstant("foo").areturn());
|
|
assertSame(mb, ret);
|
|
});
|
|
|
|
boolean[] sawWithCode = { false };
|
|
MethodTransform transform2 = (mb, me) -> {
|
|
if (me instanceof CodeModel) {
|
|
sawWithCode[0] = true;
|
|
}
|
|
mb.with(me);
|
|
};
|
|
|
|
cf.transformClass(cm, ClassTransform.transformingMethods(transform1.andThen(transform2)));
|
|
|
|
assertTrue(sawWithCode[0], "Code attribute generated not visible");
|
|
|
|
// transformCode
|
|
var outerCm = cf.parse(testClassPath);
|
|
var foo = outerCm.methods().stream()
|
|
.filter(m -> m.flags().has(AccessFlag.STATIC))
|
|
.findFirst().orElseThrow();
|
|
|
|
MethodTransform transform3 = MethodTransform.endHandler(mb -> {
|
|
var ret = mb.transformCode(foo.code().orElseThrow(), CodeTransform.ACCEPT_ALL);
|
|
assertSame(mb, ret);
|
|
});
|
|
|
|
boolean[] sawTransformCode = { false };
|
|
MethodTransform transform4 = (mb, me) -> {
|
|
if (me instanceof CodeModel) {
|
|
sawTransformCode[0] = true;
|
|
}
|
|
mb.with(me);
|
|
};
|
|
|
|
cf.transformClass(cm, ClassTransform.transformingMethods(transform3.andThen(transform4)));
|
|
|
|
assertTrue(sawTransformCode[0], "Code attribute transformed not visible");
|
|
}
|
|
|
|
/**
|
|
* Test to ensure code elements, such as code block
|
|
* begin and end labels, are visible to transforms.
|
|
*/
|
|
@Test
|
|
void testCodeChaining() throws Exception {
|
|
var bytes = Files.readAllBytes(testClassPath);
|
|
var cf = ClassFile.of();
|
|
var cm = cf.parse(bytes);
|
|
|
|
CodeTransform transform1 = new CodeTransform() {
|
|
@Override
|
|
public void atStart(CodeBuilder builder) {
|
|
builder.block(bcb -> {
|
|
bcb.loadConstant(9876L);
|
|
bcb.goto_(bcb.endLabel());
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void accept(CodeBuilder builder, CodeElement element) {
|
|
builder.with(element);
|
|
}
|
|
};
|
|
Set<Label> leaveLabels = new HashSet<>();
|
|
Set<Label> targetedLabels = new HashSet<>();
|
|
CodeTransform transform2 = (cb, ce) -> {
|
|
if (ce instanceof BranchInstruction bi) {
|
|
leaveLabels.add(bi.target());
|
|
}
|
|
if (ce instanceof LabelTarget lt) {
|
|
targetedLabels.add(lt.label());
|
|
}
|
|
cb.with(ce);
|
|
};
|
|
|
|
cf.transformClass(cm, ClassTransform.transformingMethods(MethodTransform
|
|
.transformingCode(transform1.andThen(transform2))));
|
|
|
|
leaveLabels.removeIf(targetedLabels::contains);
|
|
assertTrue(leaveLabels.isEmpty(), () -> "Some labels are not bounded: " + leaveLabels);
|
|
}
|
|
|
|
@Test
|
|
void testStateOrder() throws Exception {
|
|
var bytes = Files.readAllBytes(testClassPath);
|
|
var cf = ClassFile.of();
|
|
var cm = cf.parse(bytes);
|
|
|
|
int[] counter = {0};
|
|
|
|
enum TransformState { START, ONGOING, ENDED }
|
|
|
|
var ct = ClassTransform.ofStateful(() -> new ClassTransform() {
|
|
TransformState state = TransformState.START;
|
|
|
|
@Override
|
|
public void atStart(ClassBuilder builder) {
|
|
assertSame(TransformState.START, state);
|
|
builder.withField("f" + counter[0]++, CD_int, 0);
|
|
state = TransformState.ONGOING;
|
|
}
|
|
|
|
@Override
|
|
public void atEnd(ClassBuilder builder) {
|
|
assertSame(TransformState.ONGOING, state);
|
|
builder.withField("f" + counter[0]++, CD_int, 0);
|
|
state = TransformState.ENDED;
|
|
}
|
|
|
|
@Override
|
|
public void accept(ClassBuilder builder, ClassElement element) {
|
|
assertSame(TransformState.ONGOING, state);
|
|
builder.with(element);
|
|
}
|
|
});
|
|
|
|
cf.transformClass(cm, ct);
|
|
cf.transformClass(cm, ct.andThen(ct));
|
|
cf.transformClass(cm, ct.andThen(ct).andThen(ct));
|
|
}
|
|
|
|
public static class TestClass {
|
|
static public String foo() {
|
|
return "foo";
|
|
}
|
|
}
|
|
|
|
@Test
|
|
void testFilteringTransformChaining() {
|
|
var cf = ClassFile.of();
|
|
var clazz = cf.parse(cf.build(ClassDesc.of("Test"), clb -> clb
|
|
.withField("one", CD_int, fb -> fb.with(ConstantValueAttribute.of(1)))
|
|
.withField("two", CD_int, fb -> fb.with(ConstantValueAttribute.of(2)))
|
|
.withMethod("one", MTD_void, 0, mb -> mb.with(AnnotationDefaultAttribute.of(AnnotationValue.ofInt(1))).withCode(CodeBuilder::return_))
|
|
.withMethod("two", MTD_void, 0, mb -> mb.with(AnnotationDefaultAttribute.of(AnnotationValue.ofInt(2))).withCode(CodeBuilder::return_))));
|
|
|
|
AtomicBoolean oneFieldCalled = new AtomicBoolean(false);
|
|
var oneFieldTransform = new TransformImpl.ClassFieldTransform((fb, fe) -> {
|
|
if (fe instanceof ConstantValueAttribute cv) {
|
|
assertEquals(1, ((Integer) cv.constant().constantValue()), "Should only transform one");
|
|
}
|
|
oneFieldCalled.set(true);
|
|
fb.with(fe);
|
|
}, fm -> fm.fieldName().equalsString("one"));
|
|
AtomicBoolean twoFieldCalled = new AtomicBoolean(false);
|
|
var twoFieldTransform = new TransformImpl.ClassFieldTransform((fb, fe) -> {
|
|
if (fe instanceof ConstantValueAttribute cv) {
|
|
assertEquals(2, ((Integer) cv.constant().constantValue()), "Should only transform two");
|
|
}
|
|
twoFieldCalled.set(true);
|
|
fb.with(fe);
|
|
}, fm -> fm.fieldName().equalsString("two"));
|
|
cf.transformClass(clazz, oneFieldTransform.andThen(twoFieldTransform));
|
|
assertTrue(oneFieldCalled.get(), "Field one not transformed");
|
|
assertTrue(twoFieldCalled.get(), "Field two not transformed");
|
|
|
|
AtomicBoolean oneMethodCalled = new AtomicBoolean(false);
|
|
var oneMethodTransform = ClassTransform.transformingMethods(mm -> mm.methodName().equalsString("one"), (mb, me) -> {
|
|
if (me instanceof AnnotationDefaultAttribute ada) {
|
|
assertEquals(1, ((AnnotationValue.OfInt) ada.defaultValue()).intValue(), "Should only transform one");
|
|
}
|
|
oneMethodCalled.set(true);
|
|
mb.with(me);
|
|
});
|
|
AtomicBoolean twoMethodCalled = new AtomicBoolean(false);
|
|
var twoMethodTransform = ClassTransform.transformingMethods(mm -> mm.methodName().equalsString("two"), (mb, me) -> {
|
|
if (me instanceof AnnotationDefaultAttribute ada) {
|
|
assertEquals(2, ((AnnotationValue.OfInt) ada.defaultValue()).intValue(), "Should only transform two");
|
|
}
|
|
twoMethodCalled.set(true);
|
|
mb.with(me);
|
|
});
|
|
cf.transformClass(clazz, oneMethodTransform.andThen(twoMethodTransform));
|
|
assertTrue(oneMethodCalled.get(), "Method one not transformed");
|
|
assertTrue(twoMethodCalled.get(), "Method two not transformed");
|
|
}
|
|
}
|