diff --git a/src/java.base/share/classes/java/lang/classfile/constantpool/ClassEntry.java b/src/java.base/share/classes/java/lang/classfile/constantpool/ClassEntry.java index 5017838d68eba7..70c370ef104759 100644 --- a/src/java.base/share/classes/java/lang/classfile/constantpool/ClassEntry.java +++ b/src/java.base/share/classes/java/lang/classfile/constantpool/ClassEntry.java @@ -104,8 +104,22 @@ default ConstantDesc constantValue() { * returned descriptor is never {@linkplain ClassDesc#isPrimitive() * primitive}. * + * @apiNote + * If only symbol equivalence is desired, {@link #equalsSymbol(ClassDesc) + * equalsSymbol} should be used. It requires reduced parsing and can + * improve {@code class} file reading performance. + * * @see ConstantPoolBuilder#classEntry(ClassDesc) * ConstantPoolBuilder::classEntry(ClassDesc) */ ClassDesc asSymbol(); + + /** + * {@return whether this entry describes the given reference type} Returns + * {@code false} if {@code desc} is primitive. + * + * @param desc the reference type + * @since 25 + */ + boolean equalsSymbol(ClassDesc desc); } diff --git a/src/java.base/share/classes/java/lang/classfile/constantpool/MethodTypeEntry.java b/src/java.base/share/classes/java/lang/classfile/constantpool/MethodTypeEntry.java index 5da36502678252..f69c0953fc523b 100644 --- a/src/java.base/share/classes/java/lang/classfile/constantpool/MethodTypeEntry.java +++ b/src/java.base/share/classes/java/lang/classfile/constantpool/MethodTypeEntry.java @@ -70,6 +70,19 @@ default ConstantDesc constantValue() { /** * {@return a symbolic descriptor for the {@linkplain #descriptor() method * type}} + * + * @apiNote + * If only symbol equivalence is desired, {@link #equalsSymbol(MethodTypeDesc) + * equalsSymbol} should be used. It requires reduced parsing and can + * improve {@code class} file reading performance. */ MethodTypeDesc asSymbol(); + + /** + * {@return whether this entry describes the given method type} + * + * @param desc the method type descriptor + * @since 25 + */ + boolean equalsSymbol(MethodTypeDesc desc); } diff --git a/src/java.base/share/classes/java/lang/classfile/constantpool/ModuleEntry.java b/src/java.base/share/classes/java/lang/classfile/constantpool/ModuleEntry.java index fd920aa12316f4..87a45fc3775e39 100644 --- a/src/java.base/share/classes/java/lang/classfile/constantpool/ModuleEntry.java +++ b/src/java.base/share/classes/java/lang/classfile/constantpool/ModuleEntry.java @@ -55,6 +55,19 @@ public sealed interface ModuleEntry extends PoolEntry /** * {@return a symbolic descriptor for the {@linkplain #name() module name}} + * + * @apiNote + * If only symbol equivalence is desired, {@link #equalsSymbol(ModuleDesc) + * equalsSymbol} should be used. It requires reduced parsing and can + * improve {@code class} file reading performance. */ ModuleDesc asSymbol(); + + /** + * {@return whether this entry describes the given module} + * + * @param desc the module descriptor + * @since 25 + */ + boolean equalsSymbol(ModuleDesc desc); } diff --git a/src/java.base/share/classes/java/lang/classfile/constantpool/PackageEntry.java b/src/java.base/share/classes/java/lang/classfile/constantpool/PackageEntry.java index ec56d0a4870cc1..9e8561c4a20933 100644 --- a/src/java.base/share/classes/java/lang/classfile/constantpool/PackageEntry.java +++ b/src/java.base/share/classes/java/lang/classfile/constantpool/PackageEntry.java @@ -58,6 +58,19 @@ public sealed interface PackageEntry extends PoolEntry /** * {@return a symbolic descriptor for the {@linkplain #name() package name}} + * + * @apiNote + * If only symbol equivalence is desired, {@link #equalsSymbol(PackageDesc) + * equalsSymbol} should be used. It requires reduced parsing and can + * improve {@code class} file reading performance. */ PackageDesc asSymbol(); + + /** + * {@return whether this entry describes the given package} + * + * @param desc the package descriptor + * @since 25 + */ + boolean equalsSymbol(PackageDesc desc); } diff --git a/src/java.base/share/classes/java/lang/classfile/constantpool/StringEntry.java b/src/java.base/share/classes/java/lang/classfile/constantpool/StringEntry.java index 8a0bbb4b0150d6..b49e74df40474e 100644 --- a/src/java.base/share/classes/java/lang/classfile/constantpool/StringEntry.java +++ b/src/java.base/share/classes/java/lang/classfile/constantpool/StringEntry.java @@ -56,7 +56,22 @@ public sealed interface StringEntry /** * {@return the string value for this entry} * + * @apiNote + * A {@code Utf8Entry} can be used directly as a {@link CharSequence} if + * {@code String} functionalities are not strictly desired. If only string + * equivalence is desired, {@link #equalsString(String) equalsString} should + * be used. Reduction of string processing can significantly improve {@code + * class} file reading performance. + * * @see ConstantPoolBuilder#stringEntry(String) */ String stringValue(); + + /** + * {@return whether this entry describes the same string as the provided string} + * + * @param value the string to compare to + * @since 25 + */ + boolean equalsString(String value); } diff --git a/src/java.base/share/classes/java/lang/classfile/constantpool/Utf8Entry.java b/src/java.base/share/classes/java/lang/classfile/constantpool/Utf8Entry.java index 1d885051b2b1a3..549c0b79fd71c1 100644 --- a/src/java.base/share/classes/java/lang/classfile/constantpool/Utf8Entry.java +++ b/src/java.base/share/classes/java/lang/classfile/constantpool/Utf8Entry.java @@ -84,4 +84,22 @@ public sealed interface Utf8Entry * @param s the string to compare to */ boolean equalsString(String s); + + /** + * {@return whether this entry describes the descriptor string of this + * field type} + * + * @param desc the field type + * @since 25 + */ + boolean equalsSymbol(ClassDesc desc); + + /** + * {@return whether this entry describes the descriptor string of this + * method type} + * + * @param desc the method type + * @since 25 + */ + boolean equalsSymbol(MethodTypeDesc desc); } diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/AbstractPoolEntry.java b/src/java.base/share/classes/jdk/internal/classfile/impl/AbstractPoolEntry.java index 3e50773d59ec2b..de8e7dee6e08c4 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/AbstractPoolEntry.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/AbstractPoolEntry.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * 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 @@ -32,6 +32,8 @@ import jdk.internal.access.JavaLangAccess; import jdk.internal.access.SharedSecrets; +import jdk.internal.constant.ClassOrInterfaceDescImpl; +import jdk.internal.constant.PrimitiveClassDescImpl; import jdk.internal.util.ArraysSupport; import jdk.internal.vm.annotation.Stable; @@ -73,11 +75,6 @@ static int hashClassFromDescriptor(int descriptorHash) { return hash1(PoolEntry.TAG_CLASS, descriptorHash); } - static boolean isArrayDescriptor(Utf8EntryImpl cs) { - // Do not throw out-of-bounds for empty strings - return !cs.isEmpty() && cs.charAt(0) == '['; - } - @SuppressWarnings("unchecked") public static T maybeClone(ConstantPoolBuilder cp, T entry) { if (cp.canWriteDirect(entry.constantPool())) @@ -438,6 +435,79 @@ public MethodTypeDesc methodTypeSymbol() { typeSym = ret; return ret; } + + @Override + public boolean equalsSymbol(ClassDesc desc) { + var sym = typeSym; + if (sym != null) { + return sym instanceof ClassDesc cd && cd.equals(desc); + } + + // In parsing, Utf8Entry is not even inflated by this point + // We can operate on the raw byte arrays, as all ascii are compatible + var ret = state == State.RAW + ? rawEqualsSym(desc) + : equalsString(desc.descriptorString()); + if (ret) + this.typeSym = desc; + return ret; + } + + private boolean rawEqualsSym(ClassDesc desc) { + int len = rawLen; + if (len < 1) { + return false; + } + int c = rawBytes[offset]; + if (len == 1) { + return desc instanceof PrimitiveClassDescImpl pd && pd.wrapper().basicTypeChar() == c; + } else if (c == 'L') { + return desc.isClassOrInterface() && equalsString(desc.descriptorString()); + } else if (c == '[') { + return desc.isArray() && equalsString(desc.descriptorString()); + } else { + return false; + } + } + + boolean mayBeArrayDescriptor() { + if (state == State.RAW) { + return rawLen > 0 && rawBytes[offset] == '['; + } else { + return charLen > 0 && charAt(0) == '['; + } + } + + @Override + public boolean equalsSymbol(MethodTypeDesc desc) { + var sym = typeSym; + if (sym != null) { + return sym instanceof MethodTypeDesc mtd && mtd.equals(desc); + } + + // In parsing, Utf8Entry is not even inflated by this point + // We can operate on the raw byte arrays, as all ascii are compatible + var ret = state == State.RAW + ? rawEqualsSym(desc) + : equalsString(desc.descriptorString()); + if (ret) + this.typeSym = desc; + return ret; + } + + private boolean rawEqualsSym(MethodTypeDesc desc) { + if (rawLen < 3) { + return false; + } + var bytes = rawBytes; + int index = offset; + int c = bytes[index] | (bytes[index + 1] << Byte.SIZE); + if ((desc.parameterCount() == 0) != (c == ('(' | (')' << Byte.SIZE)))) { + // heuristic - avoid inflation for no-arg status mismatch + return false; + } + return (c & 0xFF) == '(' && equalsString(desc.descriptorString()); + } } abstract static sealed class AbstractRefEntry extends AbstractPoolEntry { @@ -538,7 +608,7 @@ public ClassDesc asSymbol() { return sym; } - if (isArrayDescriptor(ref1)) { + if (ref1.mayBeArrayDescriptor()) { sym = ref1.fieldTypeSymbol(); // array, symbol already available } else { sym = ClassDesc.ofInternalName(asInternalName()); // class or interface @@ -546,6 +616,28 @@ public ClassDesc asSymbol() { return this.sym = sym; } + @Override + public boolean equalsSymbol(ClassDesc desc) { + var sym = this.sym; + if (sym != null) { + return sym.equals(desc); + } + + var ret = rawEqualsSymbol(desc); + if (ret) + this.sym = desc; + return ret; + } + + private boolean rawEqualsSymbol(ClassDesc desc) { + if (ref1.mayBeArrayDescriptor()) { + return desc.isArray() && ref1.equalsSymbol(desc); + } else { + return desc instanceof ClassOrInterfaceDescImpl coid + && ref1.equalsString(coid.internalName()); + } + } + @Override public boolean equals(Object o) { if (o == this) return true; @@ -571,7 +663,7 @@ public int hashCode() { if (hash != 0) return hash; - return this.hash = hashClassFromUtf8(isArrayDescriptor(ref1), ref1); + return this.hash = hashClassFromUtf8(ref1.mayBeArrayDescriptor(), ref1); } } @@ -596,6 +688,11 @@ public PackageDesc asSymbol() { return PackageDesc.ofInternalName(asInternalName()); } + @Override + public boolean equalsSymbol(PackageDesc desc) { + return ref1.equalsString(desc.internalName()); + } + @Override public boolean equals(Object o) { if (o == this) return true; @@ -627,6 +724,11 @@ public ModuleDesc asSymbol() { return ModuleDesc.of(asInternalName()); } + @Override + public boolean equalsSymbol(ModuleDesc desc) { + return ref1.equalsString(desc.name()); + } + @Override public boolean equals(Object o) { if (o == this) return true; @@ -983,6 +1085,11 @@ public MethodTypeDesc asSymbol() { return ref1.methodTypeSymbol(); } + @Override + public boolean equalsSymbol(MethodTypeDesc desc) { + return ref1.equalsSymbol(desc); + } + @Override public boolean equals(Object o) { if (o == this) return true; @@ -1016,6 +1123,11 @@ public String stringValue() { return ref1.toString(); } + @Override + public boolean equalsString(String value) { + return ref1.equalsString(value); + } + @Override public ConstantDesc constantValue() { return stringValue(); diff --git a/src/java.base/share/classes/jdk/internal/classfile/impl/SplitConstantPool.java b/src/java.base/share/classes/jdk/internal/classfile/impl/SplitConstantPool.java index 5ba81fc292750c..2ea6fa943a7a0b 100644 --- a/src/java.base/share/classes/jdk/internal/classfile/impl/SplitConstantPool.java +++ b/src/java.base/share/classes/jdk/internal/classfile/impl/SplitConstantPool.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * 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 @@ -497,7 +497,7 @@ AbstractPoolEntry.Utf8EntryImpl maybeCloneUtf8Entry(Utf8Entry entry) { @Override public AbstractPoolEntry.ClassEntryImpl classEntry(Utf8Entry nameEntry) { var ne = maybeCloneUtf8Entry(nameEntry); - return classEntry(ne, AbstractPoolEntry.isArrayDescriptor(ne)); + return classEntry(ne, ne.mayBeArrayDescriptor()); } AbstractPoolEntry.ClassEntryImpl classEntry(AbstractPoolEntry.Utf8EntryImpl ne, boolean isArray) { diff --git a/test/jdk/jdk/classfile/ConstantDescSymbolsTest.java b/test/jdk/jdk/classfile/ConstantDescSymbolsTest.java index b5d3ba5d584fbd..17ca7557fb5766 100644 --- a/test/jdk/jdk/classfile/ConstantDescSymbolsTest.java +++ b/test/jdk/jdk/classfile/ConstantDescSymbolsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 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 @@ -30,22 +30,25 @@ * @run junit ConstantDescSymbolsTest */ -import java.lang.classfile.constantpool.ConstantPoolBuilder; -import java.lang.constant.ClassDesc; -import java.lang.constant.DynamicConstantDesc; -import java.lang.constant.MethodHandleDesc; -import java.lang.constant.MethodTypeDesc; +import java.lang.classfile.constantpool.*; +import java.lang.constant.*; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.nio.charset.StandardCharsets; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Function; import java.util.function.Supplier; import java.lang.classfile.ClassFile; import java.util.stream.Stream; -import jdk.internal.classfile.impl.AbstractPoolEntry; +import jdk.internal.classfile.impl.Util; import jdk.internal.constant.ConstantUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.ThrowingConsumer; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import static java.lang.classfile.ClassFile.ACC_PUBLIC; @@ -165,4 +168,191 @@ void testConstantPoolBuilderClassOrInterfaceEntry(ClassDesc cd) { assertSame(utf8, ce.name(), "Reusing existing utf8 entry"); assertEquals(cd, ce.asSymbol(), "Symbol propagation on create with utf8"); } + + // a pool entry, suitable for testing lazy behaviors and has descriptive name + record PoolEntryCase

(String desc, Supplier

poolEntry) { + void test(ThrowingConsumer

job) { + try { + job.accept(poolEntry.get()); + } catch (Throwable e) { + Assertions.fail("Tested entry: " + desc, e); + } + } + } + + // Test pool entry <-> nominal descriptor, also the equals methods + record SymbolicTranslator(String name, BiFunction writer, BiPredicate tester, Function extractor) { + private P createUnboundEntry(T symbol) { + ConstantPoolBuilder cpb = ConstantPoolBuilder.of(); // Temp pool does not support some entries + return writer.apply(cpb, symbol); + } + + @SuppressWarnings("unchecked") + private P toBoundEntry(P unboundEntry) { + ConstantPoolBuilder cpb = (ConstantPoolBuilder) unboundEntry.constantPool(); + int index = unboundEntry.index(); + var bytes = ClassFile.of().build(cpb.classEntry(ClassDesc.of("Test")), cpb, _ -> {}); + return (P) ClassFile.of().parse(bytes).constantPool().entryByIndex(index); + } + + // Spawn entries to test from a nominal descriptor + public Stream> entriesSpawner(T original) { + return entriesSpawner(() -> this.createUnboundEntry(original)); + } + + // Spawn additional bound entries to test from an initial unbound entry + public Stream> entriesSpawner(Supplier

original) { + return Stream.of(new PoolEntryCase<>(original.get().toString(), original)) + .mapMulti((s, sink) -> { + sink.accept(s); // unbound + sink.accept(new PoolEntryCase<>(s.desc + "+lazy", () -> toBoundEntry(s.poolEntry.get()))); // bound + }); + } + + // Add extra stage of entry spawn to "inflate" entries via positive/negative tests + public PoolEntryCase

inflateByTest(PoolEntryCase

last, T arg, String msg) { + return new PoolEntryCase<>("+equalsSymbol(" + msg + ")", () -> { + var ret = last.poolEntry.get(); + tester.test(ret, arg); + return ret; + }); + } + + // Add extra stage of entry spawn to "inflate" entries via descriptor computation + // This should not be used if the pool entry may be invalid (i.e. throws IAE) + public PoolEntryCase

inflateByComputeSymbol(PoolEntryCase

last) { + return new PoolEntryCase<>(last.desc + "+asSymbol()", () -> { + var ret = last.poolEntry.get(); + extractor.apply(ret); + return ret; + }); + } + + @Override + public String toString() { + return name; // don't include lambda garbage in failure reports + } + } + + // Current supported conversions + static final SymbolicTranslator UTF8_STRING_TRANSLATOR = new SymbolicTranslator<>("Utf8", ConstantPoolBuilder::utf8Entry, Utf8Entry::equalsString, Utf8Entry::stringValue); + static final SymbolicTranslator UTF8_CLASS_TRANSLATOR = new SymbolicTranslator<>("FieldDescriptorUtf8", ConstantPoolBuilder::utf8Entry, Utf8Entry::equalsSymbol, Util::fieldTypeSymbol); + static final SymbolicTranslator UTF8_METHOD_TYPE_TRANSLATOR = new SymbolicTranslator<>("MethodDescriptorUtf8", ConstantPoolBuilder::utf8Entry, Utf8Entry::equalsSymbol, Util::methodTypeSymbol); + static final SymbolicTranslator CLASS_ENTRY_TRANSLATOR = new SymbolicTranslator<>("ClassEntry", ConstantPoolBuilder::classEntry, ClassEntry::equalsSymbol, ClassEntry::asSymbol); + static final SymbolicTranslator METHOD_TYPE_ENTRY_TRANSLATOR = new SymbolicTranslator<>("MethodTypeEntry", ConstantPoolBuilder::methodTypeEntry, MethodTypeEntry::equalsSymbol, MethodTypeEntry::asSymbol); + static final SymbolicTranslator STRING_ENTRY_TRANSLATOR = new SymbolicTranslator<>("StringEntry", ConstantPoolBuilder::stringEntry, StringEntry::equalsString, StringEntry::stringValue); + static final SymbolicTranslator PACKAGE_ENTRY_TRANSLATOR = new SymbolicTranslator<>("PackageEntry", ConstantPoolBuilder::packageEntry, PackageEntry::equalsSymbol, PackageEntry::asSymbol); + static final SymbolicTranslator MODULE_ENTRY_TRANSLATOR = new SymbolicTranslator<>("ModuleEntry", ConstantPoolBuilder::moduleEntry, ModuleEntry::equalsSymbol, ModuleEntry::asSymbol); + + static Stream equalityCases() { + return Stream.of( + Arguments.of(CLASS_ENTRY_TRANSLATOR, CD_Object, ClassDesc.ofInternalName("java/lang/Object")), // class or interface + Arguments.of(CLASS_ENTRY_TRANSLATOR, CD_Object.arrayType(), ClassDesc.ofDescriptor("[Ljava/lang/Object;")), // array + Arguments.of(UTF8_CLASS_TRANSLATOR, CD_int, ClassDesc.ofDescriptor("I")), // primitive + Arguments.of(UTF8_CLASS_TRANSLATOR, CD_Object, ClassDesc.ofInternalName("java/lang/Object")), // class or interface + Arguments.of(UTF8_CLASS_TRANSLATOR, CD_Object.arrayType(), ClassDesc.ofDescriptor("[Ljava/lang/Object;")), // array + Arguments.of(UTF8_STRING_TRANSLATOR, "Ab\u0000c", "Ab\u0000c"), + Arguments.of(UTF8_METHOD_TYPE_TRANSLATOR, MTD_void, MethodTypeDesc.ofDescriptor("()V")), + Arguments.of(UTF8_METHOD_TYPE_TRANSLATOR, MethodTypeDesc.of(CD_int, CD_Long), MethodTypeDesc.ofDescriptor("(Ljava/lang/Long;)I")), + Arguments.of(METHOD_TYPE_ENTRY_TRANSLATOR, MethodTypeDesc.of(CD_Object), MethodTypeDesc.ofDescriptor("()Ljava/lang/Object;")), + Arguments.of(STRING_ENTRY_TRANSLATOR, "Ape", new String("Ape".getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8)), + Arguments.of(PACKAGE_ENTRY_TRANSLATOR, PackageDesc.of("java.lang"), PackageDesc.ofInternalName("java/lang")), + Arguments.of(MODULE_ENTRY_TRANSLATOR, ModuleDesc.of("java.base"), ModuleDesc.of(new String("java.base".getBytes(StandardCharsets.US_ASCII), StandardCharsets.US_ASCII))) + ); + } + + @ParameterizedTest + @MethodSource("equalityCases") + void testEquality(SymbolicTranslator translator, T first, T alt) { + assertEquals(first, alt, "Bad test data"); + translator.entriesSpawner(first) + .>mapMulti((src, sink) -> { + sink.accept(src); + sink.accept(translator.inflateByTest(src, first, "first")); + sink.accept(translator.inflateByTest(src, alt, "alt")); + sink.accept(translator.inflateByComputeSymbol(src)); + }) + .forEach(scenario -> { + scenario.test(p -> { + var asSymbol = translator.extractor.apply(p); + assertEquals(first, asSymbol, "asSym vs first"); + assertEquals(alt, asSymbol, "asSym vs alt"); + }); + scenario.test(p -> assertTrue(translator.tester.test(p, first), "eqSym first")); + scenario.test(p -> assertTrue(translator.tester.test(p, alt), "eqSym alt")); + }); + } + + static Stream inequalityCases() { + return Stream.of( + Arguments.of(CLASS_ENTRY_TRANSLATOR, CD_Object, ClassDesc.ofInternalName("java/io/Object")), // class or interface + Arguments.of(CLASS_ENTRY_TRANSLATOR, CD_Object.arrayType(), ClassDesc.ofDescriptor("[Ljava/lang/String;")), // array + Arguments.of(UTF8_CLASS_TRANSLATOR, CD_int, ClassDesc.ofDescriptor("S")), // primitive + Arguments.of(UTF8_CLASS_TRANSLATOR, CD_Object, ClassDesc.ofInternalName("java/lang/String")), // class or interface + Arguments.of(UTF8_CLASS_TRANSLATOR, CD_Object.arrayType(), ClassDesc.ofDescriptor("[Ljava/lang/System;")), // array + Arguments.of(UTF8_STRING_TRANSLATOR, "Ab\u0000c", "Abdc"), + Arguments.of(UTF8_METHOD_TYPE_TRANSLATOR, MTD_void, MethodTypeDesc.ofDescriptor("()I")), + Arguments.of(UTF8_METHOD_TYPE_TRANSLATOR, MethodTypeDesc.of(CD_int, CD_Short), MethodTypeDesc.ofDescriptor("(Ljava/lang/Long;)I")), + Arguments.of(METHOD_TYPE_ENTRY_TRANSLATOR, MethodTypeDesc.of(CD_String), MethodTypeDesc.ofDescriptor("()Ljava/lang/Object;")), + Arguments.of(STRING_ENTRY_TRANSLATOR, "Cat", new String("Ape".getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8)), + Arguments.of(PACKAGE_ENTRY_TRANSLATOR, PackageDesc.of("java.lang"), PackageDesc.ofInternalName("java/util")), + Arguments.of(MODULE_ENTRY_TRANSLATOR, ModuleDesc.of("java.base"), ModuleDesc.of(new String("java.desktop".getBytes(StandardCharsets.US_ASCII), StandardCharsets.US_ASCII))) + ); + } + + @ParameterizedTest + @MethodSource("inequalityCases") + void testInequality(SymbolicTranslator translator, T obj, T bad) { + assertNotEquals(obj, bad, "Bad test data"); + translator.entriesSpawner(obj) + .>mapMulti((src, sink) -> { + sink.accept(src); + sink.accept(translator.inflateByTest(src, obj, "obj")); + sink.accept(translator.inflateByTest(src, bad, "bad")); + sink.accept(translator.inflateByComputeSymbol(src)); + }) + .forEach(scenario -> { + scenario.test(p -> { + var asSymbol = translator.extractor.apply(p); + assertEquals(obj, asSymbol, "asSym vs obj"); + assertNotEquals(bad, asSymbol, "asSym vs bad"); + }); + scenario.test(p -> assertTrue(translator.tester.test(p, obj), "eqSym obj")); + scenario.test(p -> assertFalse(translator.tester.test(p, bad), "eqSym bad")); + }); + } + + // Type hint function + private static

Supplier

badFactory(Function func) { + return () -> func.apply(ConstantPoolBuilder.of()); + } + + static Stream malformedCases() { + return Stream.of( + Arguments.of(CLASS_ENTRY_TRANSLATOR, badFactory(b -> b.classEntry(b.utf8Entry("java.lang.Object"))), CD_Object), // class or interface + Arguments.of(CLASS_ENTRY_TRANSLATOR, badFactory(b -> b.classEntry(b.utf8Entry("[Ljava/lang/String"))), CD_String.arrayType()), // array + Arguments.of(UTF8_CLASS_TRANSLATOR, badFactory(b -> b.utf8Entry("int")), ClassDesc.ofDescriptor("I")), // primitive + Arguments.of(UTF8_CLASS_TRANSLATOR, badFactory(b -> b.utf8Entry("Ljava/lang/String")), CD_String), // class or interface + Arguments.of(UTF8_CLASS_TRANSLATOR, badFactory(b -> b.utf8Entry("[Ljava/lang/String")), CD_String.arrayType()), // array + Arguments.of(METHOD_TYPE_ENTRY_TRANSLATOR, badFactory(b -> b.methodTypeEntry(b.utf8Entry("()"))), MTD_void), + Arguments.of(METHOD_TYPE_ENTRY_TRANSLATOR, badFactory(b -> b.methodTypeEntry(b.utf8Entry("(V)"))), MTD_void), + Arguments.of(UTF8_METHOD_TYPE_TRANSLATOR, badFactory(b -> b.utf8Entry("()Ljava/lang/String")), MethodTypeDesc.of(CD_String)), + Arguments.of(PACKAGE_ENTRY_TRANSLATOR, badFactory(b -> b.packageEntry(b.utf8Entry("java.lang"))), PackageDesc.of("java.lang")), + Arguments.of(MODULE_ENTRY_TRANSLATOR, badFactory(b -> b.moduleEntry(b.utf8Entry("java@base"))), ModuleDesc.of("java.base")) + ); + } + + @ParameterizedTest + @MethodSource("malformedCases") + void testMalformed(SymbolicTranslator translator, Supplier

factory, T target) { + translator.entriesSpawner(factory) + .>mapMulti((src, sink) -> { + sink.accept(src); + sink.accept(translator.inflateByTest(src, target, "target")); + }) + .forEach(scenario -> { + scenario.test(p -> assertThrows(IllegalArgumentException.class, () -> translator.extractor.apply(p), "asSym")); + scenario.test(p -> assertFalse(translator.tester.test(p, target), "eqSym")); + }); + } }