diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 519a02d8..b909fd40 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ jagr = "org.sourcegrade:jagr-launcher:0.10.3" mockito = "org.mockito:mockito-core:5.14.1" junit = "org.junit.jupiter:junit-jupiter:5.11.2" asm = "org.ow2.asm:asm:9.7.1" +asmTree = "org.ow2.asm:asm-tree:9.7.1" dokkaKotlinAsJavaPlugin = "org.jetbrains.dokka:kotlin-as-java-plugin:1.9.20" dokkaBase = "org.jetbrains.dokka:dokka-base:1.9.20" [plugins] diff --git a/student/src/main/java/org/tudalgo/algoutils/student/annotation/ForceSignature.java b/student/src/main/java/org/tudalgo/algoutils/student/annotation/ForceSignature.java new file mode 100644 index 00000000..38b6aff7 --- /dev/null +++ b/student/src/main/java/org/tudalgo/algoutils/student/annotation/ForceSignature.java @@ -0,0 +1,73 @@ +package org.tudalgo.algoutils.student.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Forces the annotated type or member to be mapped to the specified one. + * Mappings must be 1:1, meaning multiple annotated members (or types) may not map to the same target and + * all members and types may be targeted by at most one annotation. + * + * @author Daniel Mangold + */ +@Target({ElementType.TYPE, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.METHOD}) +@Retention(RetentionPolicy.CLASS) +public @interface ForceSignature { + + /** + * The identifier of the annotated type / member. + * The value must be as follows: + * + * + * @return the type / member identifier + */ + String identifier(); + + /** + * The method descriptor as specified by + * Chapter 4.3 + * of the Java Virtual Machine Specification. + * If a value is set, it takes precedence over {@link #returnType()} and {@link #parameterTypes()}. + * + *

+ * Note: Setting this value has no effect for types or fields. + *

+ * + * @return the method's descriptor + */ + String descriptor() default ""; + + /** + * The class object specifying the method's return type. + * If a value is set, it will be overwritten if {@link #descriptor()} is also set. + * Default is no return type (void). + * + *

+ * Note: Setting this value has no effect for types or fields. + *

+ * + * @return the method's return type + */ + Class returnType() default void.class; + + /** + * An array of class objects specifying the method's parameter types. + * The classes need to be given in the same order as they are declared by the targeted method. + * If a value is set, it will be overwritten if {@link #descriptor()} is also set. + * Default is no parameters. + * + *

+ * Note: Setting this value has no effect for types or fields. + *

+ * + * @return the method's parameter types + */ + Class[] parameterTypes() default {}; +} diff --git a/tutor/build.gradle.kts b/tutor/build.gradle.kts index 5c096d2c..59a8870b 100644 --- a/tutor/build.gradle.kts +++ b/tutor/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { api(libs.jagr) api(libs.mockito) api(libs.asm) + api(libs.asmTree) testImplementation(libs.junit) dokkaPlugin(libs.dokkaKotlinAsJavaPlugin) } diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java new file mode 100644 index 00000000..b641acbf --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionClassNode.java @@ -0,0 +1,131 @@ +package org.tudalgo.algoutils.transform; + +import org.tudalgo.algoutils.transform.util.ClassHeader; +import org.tudalgo.algoutils.transform.util.FieldHeader; +import org.tudalgo.algoutils.transform.util.MethodHeader; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Handle; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.MethodNode; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; +import static org.objectweb.asm.Opcodes.ASM9; + +/** + * A class node for recording bytecode instructions of solution classes. + * @author Daniel Mangold + */ +public class SolutionClassNode extends ClassNode { + + private final String className; + private ClassHeader classHeader; + private final Map fields = new HashMap<>(); + private final Map methods = new HashMap<>(); + + /** + * Constructs a new {@link SolutionClassNode} instance. + * + * @param className the name of the solution class + */ + public SolutionClassNode(String className) { + super(Opcodes.ASM9); + this.className = className; + } + + public ClassHeader getClassHeader() { + return classHeader; + } + + /** + * Returns the mapping of field headers to field nodes for this solution class. + * + * @return the field header => field node mapping + */ + public Map getFields() { + return fields; + } + + /** + * Returns the mapping of method headers to method nodes for this solution class. + * + * @return the method header => method node mapping + */ + public Map getMethods() { + return methods; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + classHeader = new ClassHeader(access, name, signature, superName, interfaces); + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + FieldHeader fieldHeader = new FieldHeader(className, access, name, descriptor, signature); + FieldNode fieldNode = (FieldNode) super.visitField(access, name, descriptor, signature, value); + fields.put(fieldHeader, fieldNode); + return fieldNode; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodNode methodNode; + if ((access & ACC_SYNTHETIC) != 0 && name.startsWith("lambda$")) { // if method is lambda + methodNode = getMethodNode(access, name + "$solution", descriptor, signature, exceptions); + } else { + methodNode = getMethodNode(access, name, descriptor, signature, exceptions); + methods.put(new MethodHeader(className, access, name, descriptor, signature, exceptions), methodNode); + } + return methodNode; + } + + /** + * Constructs a new method node with the given information. + * The returned method node ensures that lambda methods of the solution class don't interfere + * with the ones defined in the submission class. + * + * @param access the method's modifiers + * @param name the method's name + * @param descriptor the method's descriptor + * @param signature the method's signature + * @param exceptions the method's declared exceptions + * @return a new {@link MethodNode} + */ + private MethodNode getMethodNode(int access, String name, String descriptor, String signature, String[] exceptions) { + return new MethodNode(ASM9, access, name, descriptor, signature, exceptions) { + @Override + public void visitMethodInsn(int opcodeAndSource, String owner, String name, String descriptor, boolean isInterface) { + super.visitMethodInsn(opcodeAndSource, + owner, + name + (name.startsWith("lambda$") ? "$solution" : ""), + descriptor, + isInterface); + } + + @Override + public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { + super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, Arrays.stream(bootstrapMethodArguments) + .map(o -> { + if (o instanceof Handle handle && handle.getName().startsWith("lambda$")) { + return new Handle(handle.getTag(), + handle.getOwner(), + handle.getName() + "$solution", + handle.getDesc(), + handle.isInterface()); + } else { + return o; + } + }) + .toArray()); + } + }; + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java new file mode 100644 index 00000000..b4cd8366 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SolutionMergingClassTransformer.java @@ -0,0 +1,101 @@ +package org.tudalgo.algoutils.transform; + +import org.tudalgo.algoutils.student.annotation.ForceSignature; +import org.tudalgo.algoutils.transform.util.TransformationContext; +import org.tudalgo.algoutils.transform.util.TransformationUtils; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Type; +import org.sourcegrade.jagr.api.testing.ClassTransformer; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A class transformer that allows logging, substitution and delegation of method invocations. + * This transformer uses two source sets: solution classes and submission classes. + *

+ * Solution classes + *

+ * Solution classes are compiled java classes located in the {@code resources/classes/} directory. + * Their class structure must match the one defined in the exercise sheet but they may define additional + * members such as fields and methods. + * Additional types (classes, interfaces, enums, etc.) may also be defined. + *
+ * The directory structure must match the one in the output / build directory. + * Due to limitations of {@link Class#getResourceAsStream(String)} the compiled classes cannot have + * the {@code .class} file extension, so {@code .bin} is used instead. + * For example, a class {@code MyClass} with an inner class {@code Inner} in package {@code my.package} + * would be compiled to {@code my/package/MyClass.class} and {@code my/package/MyClass$Inner.class}. + * In the solution classes directory they would be located at {@code resources/classes/my/package/MyClass.bin} + * and {@code resources/classes/my/package/MyClass$Inner.bin}, respectively. + *

+ * + * Submission classes + *

+ * Submission classes are the original java classes in the main module. + * They are compiled externally and processed one by one using {@link #transform(ClassReader, ClassWriter)}. + * In case classes or members are misnamed, this transformer will attempt to map them to the closest + * matching solution class / member. + * If both the direct and similarity matching approach fail, the intended target can be explicitly + * specified using the {@link ForceSignature} annotation. + *

+ *

+ * Implementation details: + *
    + *
  • + * Unless otherwise specified, this transformer and its tools use the internal name as specified by + * {@link Type#getInternalName()} when referring to class names. + *
  • + *
  • + * The term "descriptor" refers to the bytecode-level representation of types, such as + * field types, method return types or method parameter types as specified by + * Chapter 4.3 + * of the Java Virtual Machine Specification. + *
  • + *
+ * + * @see SubmissionClassVisitor + * @see SubmissionExecutionHandler + * @author Daniel Mangold + */ +public class SolutionMergingClassTransformer implements ClassTransformer { + + /** + * An object providing context throughout the transformation processing chain. + */ + private final TransformationContext transformationContext; + + /** + * Constructs a new {@link SolutionMergingClassTransformer} instance. + * + * @param projectPrefix the root package containing all submission classes, usually the sheet number + * @param availableSolutionClasses the list of solution class names (fully qualified) to use + */ + public SolutionMergingClassTransformer(String projectPrefix, List availableSolutionClasses) { + Map solutionClasses = new HashMap<>(); + Map submissionClasses = new ConcurrentHashMap<>(); + this.transformationContext = new TransformationContext(projectPrefix, solutionClasses, submissionClasses); + availableSolutionClasses.stream() + .map(s -> s.replace('.', '/')) + .forEach(className -> solutionClasses.put(className, TransformationUtils.readSolutionClass(className))); + } + + @Override + public String getName() { + return SolutionMergingClassTransformer.class.getSimpleName(); + } + + @Override + public int getWriterFlags() { + return ClassWriter.COMPUTE_MAXS; + } + + @Override + public void transform(ClassReader reader, ClassWriter writer) { + String submissionClassName = reader.getClassName(); + reader.accept(new SubmissionClassVisitor(writer, transformationContext, submissionClassName), 0); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java new file mode 100644 index 00000000..b5ca8254 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassInfo.java @@ -0,0 +1,298 @@ +package org.tudalgo.algoutils.transform; + +import org.tudalgo.algoutils.transform.util.*; +import kotlin.Pair; +import kotlin.Triple; +import org.objectweb.asm.*; +import org.tudalgo.algoutils.tutor.general.match.MatchingUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; + +import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; + +/** + * A class that holds information on a submission class. + * This class will attempt to find a corresponding solution class and map its members + * to the ones defined in the solution class. + * If no solution class can be found, for example because the submission class was added + * as a utility class, it will map its members to itself to remain usable. + * + * @author Daniel Mangold + */ +public class SubmissionClassInfo extends ClassVisitor { + + private final TransformationContext transformationContext; + private final String originalClassName; + private final String computedClassName; + private final Set, Map>> superClassMembers = new HashSet<>(); + private final ForceSignatureAnnotationProcessor fsAnnotationProcessor; + private final SolutionClassNode solutionClass; + + private String superClass; + private String[] interfaces; + + private ClassHeader submissionClassHeader; + + // Mapping of fields in submission => usable fields + private final Map fields = new HashMap<>(); + + // Mapping of methods in submission => usable methods + private final Map methods = new HashMap<>(); + + /** + * Constructs a new {@link SubmissionClassInfo} instance. + * + * @param transformationContext a {@link TransformationContext} object + * @param className the name of the submission class + * @param fsAnnotationProcessor a {@link ForceSignatureAnnotationProcessor} for the submission class + */ + public SubmissionClassInfo(TransformationContext transformationContext, + String className, + ForceSignatureAnnotationProcessor fsAnnotationProcessor) { + super(Opcodes.ASM9); + this.transformationContext = transformationContext; + this.originalClassName = className; + this.fsAnnotationProcessor = fsAnnotationProcessor; + + if (fsAnnotationProcessor.classIdentifierIsForced()) { + this.computedClassName = fsAnnotationProcessor.forcedClassIdentifier(); + } else { + // If not forced, get the closest matching solution class (at least 90% similarity) + this.computedClassName = transformationContext.solutionClasses() + .keySet() + .stream() + .map(s -> new Pair<>(s, MatchingUtils.similarity(originalClassName, s))) + .filter(pair -> pair.getSecond() >= 0.90) + .max(Comparator.comparing(Pair::getSecond)) + .map(Pair::getFirst) + .orElse(originalClassName); + } + this.solutionClass = transformationContext.solutionClasses().get(computedClassName); + } + + /** + * Returns the original class header. + * + * @return the original class header + */ + public ClassHeader getOriginalClassHeader() { + return submissionClassHeader; + } + + /** + * Returns the computed class name. + * The computed name is the name of the associated solution class, if one is present. + * If no solution class is present, the computed names equals the original submission class name. + * + * @return the computed class name + */ + public String getComputedClassName() { + return computedClassName; + } + + /** + * Returns the solution class associated with this submission class. + * + * @return an {@link Optional} object wrapping the associated solution class + */ + public Optional getSolutionClass() { + return Optional.ofNullable(solutionClass); + } + + /** + * Returns the original field headers for this class. + * + * @return the original field headers + */ + public Set getOriginalFieldHeaders() { + return fields.keySet(); + } + + /** + * Returns the computed field header for the given field name. + * The computed field header is the field header of the corresponding field in the solution class, + * if one is present. + * If no solution class is present, the computed field header equals the original field header + * in the submission class. + * + * @param name the field name + * @return the computed field header + */ + public FieldHeader getComputedFieldHeader(String name) { + return fields.entrySet() + .stream() + .filter(entry -> entry.getKey().name().equals(name)) + .findAny() + .map(Map.Entry::getValue) + .orElseThrow(); + } + + /** + * Return the original method headers for this class. + * + * @return the original method headers + */ + public Set getOriginalMethodHeaders() { + return methods.keySet(); + } + + /** + * Returns the computed method header for the given method signature. + * The computed method header is the method header of the corresponding method in the solution class, + * if one is present. + * If no solution class is present, the computed method header equals the original method header + * in the submission class. + * + * @param name the method name + * @param descriptor the method descriptor + * @return the computed method header + */ + public MethodHeader getComputedMethodHeader(String name, String descriptor) { + return methods.entrySet() + .stream() + .filter(entry -> entry.getKey().name().equals(name) && entry.getKey().descriptor().equals(descriptor)) + .findAny() + .map(Map.Entry::getValue) + .orElseThrow(); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + submissionClassHeader = new ClassHeader(access, name, signature, superName, interfaces); + resolveSuperClassMembers(superClassMembers, this.superClass = superName, this.interfaces = interfaces); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + FieldHeader submissionFieldHeader = new FieldHeader(originalClassName, access, name, descriptor, signature); + FieldHeader solutionFieldHeader; + if (fsAnnotationProcessor.fieldIdentifierIsForced(name)) { + solutionFieldHeader = fsAnnotationProcessor.forcedFieldHeader(name); + } else if (solutionClass != null) { + solutionFieldHeader = solutionClass.getFields() + .keySet() + .stream() + .map(fieldHeader -> new Pair<>(fieldHeader, MatchingUtils.similarity(name, fieldHeader.name()))) + .filter(pair -> pair.getSecond() >= 0.90) + .max(Comparator.comparing(Pair::getSecond)) + .map(Pair::getFirst) + .orElse(submissionFieldHeader); + } else { + solutionFieldHeader = submissionFieldHeader; + } + + fields.put(submissionFieldHeader, solutionFieldHeader); + return null; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodHeader submissionMethodHeader = new MethodHeader(originalClassName, access, name, descriptor, signature, exceptions); + if ((access & ACC_SYNTHETIC) != 0 && name.startsWith("lambda$")) { + methods.put(submissionMethodHeader, submissionMethodHeader); + return null; + } + + MethodHeader solutionMethodHeader; + if (fsAnnotationProcessor.methodSignatureIsForced(name, descriptor)) { + solutionMethodHeader = fsAnnotationProcessor.forcedMethodHeader(name, descriptor); + } else if (solutionClass != null) { + solutionMethodHeader = solutionClass.getMethods() + .keySet() + .stream() + .map(methodHeader -> new Triple<>(methodHeader, + MatchingUtils.similarity(name, methodHeader.name()), + MatchingUtils.similarity(descriptor, methodHeader.descriptor()))) + .filter(triple -> triple.getSecond() >= 0.90 && triple.getThird() >= 0.90) + .max(Comparator.comparing(Triple::getSecond).thenComparing(Triple::getThird)) + .map(Triple::getFirst) + .orElse(submissionMethodHeader); + } else { + solutionMethodHeader = submissionMethodHeader; + } + + methods.put(submissionMethodHeader, solutionMethodHeader); + return null; + } + + @Override + public void visitEnd() { + for (Triple, Map> triple : superClassMembers) { + triple.getSecond().forEach(fields::putIfAbsent); + triple.getThird().forEach(methods::putIfAbsent); + } + } + + /** + * Recursively resolves the members of superclasses and interfaces. + * + * @param superClassMembers a set for recording class members + * @param superClass the name of the superclass to process + * @param interfaces the names of the interfaces to process + */ + private void resolveSuperClassMembers(Set, Map>> superClassMembers, + String superClass, + String[] interfaces) { + resolveSuperClassMembers(superClassMembers, superClass); + if (interfaces != null) { + for (String interfaceName : interfaces) { + resolveSuperClassMembers(superClassMembers, interfaceName); + } + } + } + + /** + * Recursively resolves the members of the given class. + * + * @param superClassMembers a set for recording class members + * @param className the name of the class / interface to process + */ + private void resolveSuperClassMembers(Set, Map>> superClassMembers, + String className) { + if (className.startsWith(transformationContext.projectPrefix())) { + SubmissionClassInfo submissionClassInfo = transformationContext.getSubmissionClassInfo(className); + superClassMembers.add(new Triple<>(className, submissionClassInfo.fields, submissionClassInfo.methods)); + resolveSuperClassMembers(superClassMembers, submissionClassInfo.superClass, submissionClassInfo.interfaces); + } else { + try { + Class clazz = Class.forName(className.replace('/', '.')); + Map fieldHeaders = new HashMap<>(); + for (Field field : clazz.getDeclaredFields()) { + if ((field.getModifiers() & Modifier.PRIVATE) != 0) continue; + FieldHeader fieldHeader = new FieldHeader( + className, + field.getModifiers(), + field.getName(), + Type.getDescriptor(field.getType()), + null + ); + fieldHeaders.put(fieldHeader, fieldHeader); + } + Map methodHeaders = new HashMap<>(); + for (Method method : clazz.getDeclaredMethods()) { + if ((method.getModifiers() & Modifier.PRIVATE) != 0) continue; + MethodHeader methodHeader = new MethodHeader( + className, + method.getModifiers(), + method.getName(), + Type.getMethodDescriptor(method), + null, + Arrays.stream(method.getExceptionTypes()).map(Type::getInternalName).toArray(String[]::new) + ); + methodHeaders.put(methodHeader, methodHeader); + } + superClassMembers.add(new Triple<>(className, fieldHeaders, methodHeaders)); + if (clazz.getSuperclass() != null) { + resolveSuperClassMembers(superClassMembers, + Type.getInternalName(clazz.getSuperclass()), + Arrays.stream(clazz.getInterfaces()).map(Type::getInternalName).toArray(String[]::new)); + } + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java new file mode 100644 index 00000000..742a4e2b --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionClassVisitor.java @@ -0,0 +1,503 @@ +package org.tudalgo.algoutils.transform; + +import org.tudalgo.algoutils.transform.util.*; +import org.objectweb.asm.*; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.MethodNode; + +import java.util.*; +import java.util.stream.Collectors; + +import static org.tudalgo.algoutils.transform.util.TransformationUtils.*; +import static org.objectweb.asm.Opcodes.*; + +/** + * A class visitor merging a submission class with its corresponding solution class, should one exist. + * The heart piece of the {@link SolutionMergingClassTransformer} processing chain. + *
+ * Main features: + *
    + *
  • + * Method invocation logging
    + * Logs the parameter values the method was called with. + * This allows the user to verify that a method was called and also that it was called with + * the right parameters. + * If the target method is not static or a constructor, the object the method was invoked on + * is logged as well. + *
  • + *
  • + * Method substitution
    + * Allows for "replacement" of a method at runtime. + * While the method itself must still be invoked, it will hand over execution to the provided + * substitution. + * This can be useful when a method should always return a certain value, regardless of object state + * or for making a non-deterministic method (e.g., RNG) return deterministic values. + * Replacing constructors is currently not supported. + * Can be combined with invocation logging. + *
  • + *
  • + * Method delegation
    + * Will effectively "replace" the code of the original submission with the one from the solution. + * While the instructions from both submission and solution are present in the merged method, only + * one can be active at a time. + * This allows for improved unit testing by not relying on submission code transitively. + * If this mechanism is used and no solution class is associated with this submission class or + * the solution class does not contain a matching method, the submission code will be used + * as a fallback. + * Can be combined with invocation logging. + *
  • + *
+ * All of these options can be enabled / disabled via {@link SubmissionExecutionHandler}. + * + * @see SubmissionExecutionHandler + * @author Daniel Mangold + */ +class SubmissionClassVisitor extends ClassVisitor { + + private final boolean defaultTransformationsOnly; + private final TransformationContext transformationContext; + private final String className; + private final SubmissionClassInfo submissionClassInfo; + + private final Set visitedFields = new HashSet<>(); + private final Map solutionFieldNodes; + + private final Set visitedMethods = new HashSet<>(); + private final Map solutionMethodNodes; + + SubmissionClassVisitor(ClassVisitor classVisitor, + TransformationContext transformationContext, + String submissionClassName) { + super(ASM9, classVisitor); + this.transformationContext = transformationContext; + this.className = transformationContext.getSubmissionClassInfo(submissionClassName).getComputedClassName(); + this.submissionClassInfo = transformationContext.getSubmissionClassInfo(submissionClassName); + + Optional solutionClass = submissionClassInfo.getSolutionClass(); + if (solutionClass.isPresent()) { + this.defaultTransformationsOnly = false; + this.solutionFieldNodes = solutionClass.get().getFields(); + this.solutionMethodNodes = solutionClass.get().getMethods(); + } else { + System.err.printf("No corresponding solution class found for %s. Only applying default transformations.%n", submissionClassName); + this.defaultTransformationsOnly = true; + this.solutionFieldNodes = Collections.emptyMap(); + this.solutionMethodNodes = Collections.emptyMap(); + } + } + + /** + * Visits the header of the class, replacing it with the solution class' header, if one is present. + */ + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + submissionClassInfo.getSolutionClass() + .map(SolutionClassNode::getClassHeader) + .orElse(submissionClassInfo.getOriginalClassHeader()) + .visitClass(getDelegate(), version); +// .visitClass(getDelegate(), version, Type.getInternalName(SubmissionClassMetadata.class)); + } + + /** + * Visits a field of the submission class and transforms it if a solution class is present. + */ + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + if (defaultTransformationsOnly) { + return super.visitField(access, name, descriptor, signature, value); + } + + FieldHeader fieldHeader = submissionClassInfo.getComputedFieldHeader(name); + visitedFields.add(fieldHeader); + return fieldHeader.toFieldVisitor(getDelegate(), value); + } + + /** + * Visits a method of a submission class and transforms it. + * Enables invocation logging, substitution and, if a solution class is present, delegation. + */ + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodHeader methodHeader = submissionClassInfo.getComputedMethodHeader(name, descriptor); + visitedMethods.add(methodHeader); + boolean isStatic = (methodHeader.access() & ACC_STATIC) != 0; + boolean isConstructor = methodHeader.name().equals(""); + + // calculate length of locals array, including "this" if applicable + int submissionExecutionHandlerIndex = (Type.getArgumentsAndReturnSizes(methodHeader.descriptor()) >> 2) - (isStatic ? 1 : 0); + int methodHeaderIndex = submissionExecutionHandlerIndex + 1; + + // calculate default locals for frames + List parameterTypes = Arrays.stream(Type.getArgumentTypes(methodHeader.descriptor())) + .map(type -> switch (type.getSort()) { + case Type.BOOLEAN, Type.BYTE, Type.SHORT, Type.CHAR, Type.INT -> INTEGER; + case Type.FLOAT -> FLOAT; + case Type.LONG -> LONG; + case Type.DOUBLE -> DOUBLE; + default -> type.getInternalName(); + }) + .collect(Collectors.toList()); + if (!isStatic) { + parameterTypes.addFirst(isConstructor ? UNINITIALIZED_THIS : className); + } + Object[] fullFrameLocals = parameterTypes.toArray(); + + return new MethodVisitor(ASM9, methodHeader.toMethodVisitor(getDelegate())) { + @Override + public void visitCode() { + // if method is abstract or lambda, skip transformation + if ((methodHeader.access() & ACC_ABSTRACT) != 0 || + ((methodHeader.access() & ACC_SYNTHETIC) != 0 && methodHeader.name().startsWith("lambda$"))) { + super.visitCode(); + return; + } + + Label submissionExecutionHandlerVarLabel = new Label(); + Label methodHeaderVarLabel = new Label(); + Label substitutionCheckLabel = new Label(); + Label delegationCheckLabel = new Label(); + Label delegationCodeLabel = new Label(); + Label submissionCodeLabel = new Label(); + + // create SubmissionExecutionHandler$Internal instance and store in locals array + super.visitTypeInsn(NEW, SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName()); + super.visitInsn(DUP); + super.visitMethodInsn(INVOKESTATIC, + SubmissionExecutionHandler.INTERNAL_TYPE.getInternalName(), + "getInstance", + Type.getMethodDescriptor(SubmissionExecutionHandler.INTERNAL_TYPE), + false); + super.visitMethodInsn(INVOKESPECIAL, + SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), + "", + Type.getMethodDescriptor(Type.VOID_TYPE, SubmissionExecutionHandler.INTERNAL_TYPE), + false); + super.visitVarInsn(ASTORE, submissionExecutionHandlerIndex); + super.visitLabel(submissionExecutionHandlerVarLabel); + + // replicate method header in bytecode and store in locals array + buildMethodHeader(getDelegate(), methodHeader); + super.visitVarInsn(ASTORE, methodHeaderIndex); + super.visitLabel(methodHeaderVarLabel); + + super.visitFrame(F_APPEND, + 2, + new Object[] {SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), MethodHeader.INTERNAL_TYPE.getInternalName()}, + 0, + null); + + // check if invocation should be logged + super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + super.visitVarInsn(ALOAD, methodHeaderIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), + "logInvocation", + Type.getMethodDescriptor(Type.BOOLEAN_TYPE, MethodHeader.INTERNAL_TYPE), + false); + super.visitJumpInsn(IFEQ, isConstructor ? // jump to label if logInvocation(...) == false + defaultTransformationsOnly ? submissionCodeLabel : delegationCheckLabel : + substitutionCheckLabel); + + // intercept parameters + super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + super.visitVarInsn(ALOAD, methodHeaderIndex); + buildInvocation(Type.getArgumentTypes(methodHeader.descriptor())); + super.visitMethodInsn(INVOKEVIRTUAL, + SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), + "addInvocation", + Type.getMethodDescriptor(Type.VOID_TYPE, + MethodHeader.INTERNAL_TYPE, + Invocation.INTERNAL_TYPE), + false); + + // check if substitution exists for this method if not constructor (because waaay too complex right now) + if (!isConstructor) { + super.visitFrame(F_SAME, 0, null, 0, null); + super.visitLabel(substitutionCheckLabel); + super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + super.visitVarInsn(ALOAD, methodHeaderIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), + "useSubstitution", + Type.getMethodDescriptor(Type.BOOLEAN_TYPE, MethodHeader.INTERNAL_TYPE), + false); + super.visitJumpInsn(IFEQ, defaultTransformationsOnly ? submissionCodeLabel : delegationCheckLabel); // jump to label if useSubstitution(...) == false + + // get substitution and execute it + super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + super.visitVarInsn(ALOAD, methodHeaderIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), + "getSubstitution", + Type.getMethodDescriptor(Invocation.INTERNAL_TYPE, + MethodHeader.INTERNAL_TYPE), + false); + buildInvocation(Type.getArgumentTypes(methodHeader.descriptor())); + super.visitMethodInsn(INVOKEINTERFACE, + SubmissionExecutionHandler.MethodSubstitution.INTERNAL_TYPE.getInternalName(), + "execute", + Type.getMethodDescriptor(Type.getType(Object.class), + Invocation.INTERNAL_TYPE), + true); + Type returnType = Type.getReturnType(methodHeader.descriptor()); + if (returnType.getSort() == Type.ARRAY || returnType.getSort() == Type.OBJECT) { + super.visitTypeInsn(CHECKCAST, returnType.getInternalName()); + } else { + unboxType(getDelegate(), returnType); + } + super.visitInsn(returnType.getOpcode(IRETURN)); + } + + // if only default transformations are applied, skip delegation + if (!defaultTransformationsOnly) { + // check if call should be delegated to solution or not + super.visitFrame(F_SAME, 0, null, 0, null); + super.visitLabel(delegationCheckLabel); + super.visitVarInsn(ALOAD, submissionExecutionHandlerIndex); + super.visitVarInsn(ALOAD, methodHeaderIndex); + super.visitMethodInsn(INVOKEVIRTUAL, + SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getInternalName(), + "useSubmissionImpl", + Type.getMethodDescriptor(Type.BOOLEAN_TYPE, MethodHeader.INTERNAL_TYPE), + false); + super.visitJumpInsn(IFNE, submissionCodeLabel); // jump to label if useSubmissionImpl(...) == true + + // replay instructions from solution + super.visitFrame(F_CHOP, 2, null, 0, null); + super.visitLabel(delegationCodeLabel); + super.visitLocalVariable("submissionExecutionHandler", + SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getDescriptor(), + null, + submissionExecutionHandlerVarLabel, + delegationCodeLabel, + submissionExecutionHandlerIndex); + super.visitLocalVariable("methodHeader", + MethodHeader.INTERNAL_TYPE.getDescriptor(), + null, + methodHeaderVarLabel, + delegationCodeLabel, + methodHeaderIndex); + solutionMethodNodes.get(methodHeader).accept(getDelegate()); + + super.visitFrame(F_FULL, fullFrameLocals.length, fullFrameLocals, 0, new Object[0]); + super.visitLabel(submissionCodeLabel); + } else { + super.visitFrame(F_CHOP, 2, null, 0, null); + super.visitLabel(submissionCodeLabel); + super.visitLocalVariable("submissionExecutionHandler", + SubmissionExecutionHandler.Internal.INTERNAL_TYPE.getDescriptor(), + null, + submissionExecutionHandlerVarLabel, + submissionCodeLabel, + submissionExecutionHandlerIndex); + super.visitLocalVariable("methodHeader", + MethodHeader.INTERNAL_TYPE.getDescriptor(), + null, + methodHeaderVarLabel, + submissionCodeLabel, + methodHeaderIndex); + } + + // visit original code + super.visitCode(); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { + // skip transformation if only default transformations are applied or owner is not part of the submission + if (defaultTransformationsOnly || !owner.startsWith(transformationContext.projectPrefix())) { + super.visitFieldInsn(opcode, owner, name, descriptor); + } else { + FieldHeader fieldHeader = transformationContext.getSubmissionClassInfo(owner).getComputedFieldHeader(name); + super.visitFieldInsn(opcode, fieldHeader.owner(), fieldHeader.name(), fieldHeader.descriptor()); + } + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + // skip transformation if only default transformations are applied or owner is not part of the submission + if (defaultTransformationsOnly || !owner.startsWith(transformationContext.projectPrefix())) { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } else { + MethodHeader methodHeader = transformationContext.getSubmissionClassInfo(owner).getComputedMethodHeader(name, descriptor); + super.visitMethodInsn(opcode, methodHeader.owner(), methodHeader.name(), methodHeader.descriptor(), isInterface); + } + } + + /** + * Builds an {@link Invocation} in bytecode. + * + * @param argumentTypes an array of parameter types + */ + private void buildInvocation(Type[] argumentTypes) { + super.visitTypeInsn(NEW, Invocation.INTERNAL_TYPE.getInternalName()); + super.visitInsn(DUP); + if (!isStatic && !isConstructor) { + super.visitVarInsn(ALOAD, 0); + super.visitMethodInsn(INVOKESPECIAL, + Invocation.INTERNAL_TYPE.getInternalName(), + "", + "(Ljava/lang/Object;)V", + false); + } else { + super.visitMethodInsn(INVOKESPECIAL, + Invocation.INTERNAL_TYPE.getInternalName(), + "", + "()V", + false); + } + for (int i = 0; i < argumentTypes.length; i++) { + super.visitInsn(DUP); + // load parameter with opcode (ALOAD, ILOAD, etc.) for type and ignore "this", if it exists + super.visitVarInsn(argumentTypes[i].getOpcode(ILOAD), getLocalsIndex(argumentTypes, i) + (isStatic ? 0 : 1)); + boxType(getDelegate(), argumentTypes[i]); + super.visitMethodInsn(INVOKEVIRTUAL, + Invocation.INTERNAL_TYPE.getInternalName(), + "addParameter", + "(Ljava/lang/Object;)V", + false); + } + } + }; + } + + /** + * Adds all remaining fields and methods from the solution class that have not already + * been visited (e.g., lambdas). + */ + @Override + public void visitEnd() { + if (!defaultTransformationsOnly) { + // add missing fields + solutionFieldNodes.entrySet() + .stream() + .filter(entry -> !visitedFields.contains(entry.getKey())) + .map(Map.Entry::getValue) + .forEach(fieldNode -> fieldNode.accept(getDelegate())); + // add missing methods (including lambdas) + solutionMethodNodes.entrySet() + .stream() + .filter(entry -> !visitedMethods.contains(entry.getKey())) + .map(Map.Entry::getValue) + .forEach(methodNode -> methodNode.accept(getDelegate())); + } + + classMetadata(); + fieldMetadata(); + methodMetadata(); + + super.visitEnd(); + } + + private void classMetadata() { + ClassHeader classHeader = submissionClassInfo.getOriginalClassHeader(); + Label startLabel = new Label(); + Label endLabel = new Label(); + int maxStack = 0; + MethodVisitor mv = super.visitMethod(ACC_PUBLIC | ACC_STATIC, + "getOriginalClassHeader", + Type.getMethodDescriptor(classHeader.getType()), + null, + null); + + mv.visitLabel(startLabel); + maxStack += buildClassHeader(mv, classHeader); + mv.visitInsn(ARETURN); + mv.visitLabel(endLabel); + mv.visitLocalVariable("this", + Type.getObjectType(className).getDescriptor(), + null, + startLabel, + endLabel, + 0); + mv.visitMaxs(maxStack, 1); + } + + private void fieldMetadata() { + Set fieldHeaders = submissionClassInfo.getOriginalFieldHeaders(); + Label startLabel = new Label(); + Label endLabel = new Label(); + int maxStack, stackSize; + Type setType = Type.getType(Set.class); + MethodVisitor mv = super.visitMethod(ACC_PUBLIC | ACC_STATIC, + "getOriginalFieldHeaders", + Type.getMethodDescriptor(setType), + "()L%s<%s>;".formatted(setType.getInternalName(), Type.getDescriptor(FieldHeader.class)), + null); + + mv.visitLabel(startLabel); + mv.visitIntInsn(SIPUSH, fieldHeaders.size()); + mv.visitTypeInsn(ANEWARRAY, Type.getInternalName(Object.class)); + maxStack = stackSize = 1; + int i = 0; + for (FieldHeader fieldHeader : fieldHeaders) { + mv.visitInsn(DUP); + maxStack = Math.max(maxStack, ++stackSize); + mv.visitIntInsn(SIPUSH, i++); + maxStack = Math.max(maxStack, ++stackSize); + int stackSizeUsed = buildFieldHeader(mv, fieldHeader); + maxStack = Math.max(maxStack, stackSize + stackSizeUsed); + stackSize++; + mv.visitInsn(AASTORE); + stackSize -= 3; + } + mv.visitMethodInsn(INVOKESTATIC, + setType.getInternalName(), + "of", + Type.getMethodDescriptor(setType, Type.getType(Object[].class)), + true); + mv.visitInsn(ARETURN); + mv.visitLabel(endLabel); + mv.visitLocalVariable("this", + Type.getObjectType(className).getDescriptor(), + null, + startLabel, + endLabel, + 0); + mv.visitMaxs(maxStack, 1); + } + + private void methodMetadata() { + Set methodHeaders = submissionClassInfo.getOriginalMethodHeaders(); + Label startLabel = new Label(); + Label endLabel = new Label(); + int maxStack, stackSize; + Type setType = Type.getType(Set.class); + MethodVisitor mv = super.visitMethod(ACC_PUBLIC | ACC_STATIC, + "getOriginalMethodHeaders", + Type.getMethodDescriptor(setType), + "()L%s<%s>;".formatted(setType.getInternalName(), Type.getDescriptor(MethodHeader.class)), + null); + + mv.visitLabel(startLabel); + mv.visitIntInsn(SIPUSH, methodHeaders.size()); + mv.visitTypeInsn(ANEWARRAY, Type.getInternalName(Object.class)); + maxStack = stackSize = 1; + int i = 0; + for (MethodHeader methodHeader : methodHeaders) { + mv.visitInsn(DUP); + maxStack = Math.max(maxStack, ++stackSize); + mv.visitIntInsn(SIPUSH, i++); + maxStack = Math.max(maxStack, ++stackSize); + int stackSizeUsed = buildMethodHeader(mv, methodHeader); + maxStack = Math.max(maxStack, stackSize); + stackSize++; + mv.visitInsn(AASTORE); + stackSize -= 3; + } + mv.visitMethodInsn(INVOKESTATIC, + setType.getInternalName(), + "of", + Type.getMethodDescriptor(setType, Type.getType(Object[].class)), + true); + mv.visitInsn(ARETURN); + mv.visitLabel(endLabel); + mv.visitLocalVariable("this", + Type.getObjectType(className).getDescriptor(), + null, + startLabel, + endLabel, + 0); + mv.visitMaxs(maxStack, 1); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java new file mode 100644 index 00000000..513d9dda --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/SubmissionExecutionHandler.java @@ -0,0 +1,382 @@ +package org.tudalgo.algoutils.transform; + +import org.tudalgo.algoutils.transform.util.*; +import kotlin.Pair; +import org.objectweb.asm.Type; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.util.*; + +/** + * A singleton class to configure the way a submission is executed. + * This class can be used to + *
    + *
  • log method invocations
  • + *
  • delegate invocations to the solution / a pre-defined external class
  • + *
  • delegate invocations to a custom programmatically-defined method (e.g. lambdas)
  • + *
+ * By default, all method calls are delegated to the solution class, if one is present. + * To call the real method, delegation must be disabled before calling it. + * This can be done either explicitly using {@link #disableMethodDelegation} or implicitly using + * {@link #substituteMethod}. + *
+ * To use any of these features, the submission classes need to be transformed by {@link SolutionMergingClassTransformer}. + *

+ * An example test class could look like this: + *
+ * {@code
+ * public class ExampleTest {
+ *
+ *     private final SubmissionExecutionHandler executionHandler = SubmissionExecutionHandler.getInstance();
+ *
+ *     @BeforeAll
+ *     public static void start() {
+ *         Utils.transformSubmission(); // In case Jagr is not present
+ *     }
+ *
+ *     @BeforeEach
+ *     public void setup() {
+ *         // Pre-test setup, if necessary. Useful for substitution:
+ *         Method substitutedMethod = TestedClass.class.getDeclaredMethod("dependencyForTest");
+ *         executionHandler.substituteMethod(substitutedMethod, invocation -> "Hello world!");
+ *     }
+ *
+ *     @AfterEach
+ *     public void reset() {
+ *         // Optionally reset invocation logs, substitutions, etc.
+ *         executionHandler.resetMethodInvocationLogging();
+ *         executionHandler.resetMethodDelegation();
+ *         executionHandler.resetMethodSubstitution();
+ *     }
+ *
+ *     @Test
+ *     public void test() throws ReflectiveOperationException {
+ *         Method method = TestedClass.class.getDeclaredMethod("methodUnderTest");
+ *         executionHandler.disableDelegation(method); // Disable delegation, i.e., use the original implementation
+ *         ...
+ *     }
+ * }
+ * }
+ * 
+ * + * @see SolutionMergingClassTransformer + * @see SubmissionClassVisitor + * @author Daniel Mangold + */ +@SuppressWarnings("unused") +public class SubmissionExecutionHandler { + + public static final Type INTERNAL_TYPE = Type.getType(SubmissionExecutionHandler.class); + + private static SubmissionExecutionHandler instance; + + // declaring class => (method header => invocations) + private final Map>> methodInvocations = new HashMap<>(); + private final Map> methodSubstitutions = new HashMap<>(); + private final Map> methodDelegationAllowlist = new HashMap<>(); + + private SubmissionExecutionHandler() {} + + /** + * Returns the global {@link SubmissionExecutionHandler} instance. + * + * @return the global {@link SubmissionExecutionHandler} instance + * @throws IllegalStateException if no global instance is present, i.e., the project has not been + * transformed by {@link SolutionMergingClassTransformer} + */ + public static SubmissionExecutionHandler getInstance() { + if (instance == null) { + instance = new SubmissionExecutionHandler(); + } + return instance; + } + + // Submission class info + + /** + * Returns the original class header for the given submission class. + * + * @param clazz the submission class + * @return the original class header + */ + public static ClassHeader getOriginalClassHeader(Class clazz) { + try { + return (ClassHeader) MethodHandles.lookup() + .findStatic(clazz, "getOriginalClassHeader", MethodType.methodType(ClassHeader.class)) + .invokeExact(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + /** + * Returns the set of original field headers for the given submission class. + * + * @param clazz the submission class + * @return the set of original field headers + */ + @SuppressWarnings("unchecked") + public static Set getOriginalFieldHeaders(Class clazz) { + try { + return (Set) MethodHandles.lookup() + .findStatic(clazz, "getOriginalFieldHeaders", MethodType.methodType(Set.class)) + .invokeExact(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + /** + * Returns the set of original method headers for the given submission class. + * + * @param clazz the submission class + * @return the set of original method headers + */ + @SuppressWarnings("unchecked") + public static Set getOriginalMethodHeaders(Class clazz) { + try { + return (Set) MethodHandles.lookup() + .findStatic(clazz, "getOriginalMethodHeaders", MethodType.methodType(Set.class)) + .invokeExact(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + // Invocation logging + + /** + * Resets the logging of method invocations to log no invocations. + */ + public void resetMethodInvocationLogging() { + methodInvocations.clear(); + } + + /** + * Enables logging of method invocations for the given method. + * + * @param method the method to enable invocation logging for + */ + public void enableMethodInvocationLogging(Method method) { + enableMethodInvocationLogging(new MethodHeader(method)); + } + + /** + * Enables logging of method invocations for the given method. + * + * @param methodHeader a method header describing the method + */ + public void enableMethodInvocationLogging(MethodHeader methodHeader) { + methodInvocations.computeIfAbsent(methodHeader.owner(), k -> new HashMap<>()) + .putIfAbsent(methodHeader, new ArrayList<>()); + } + + /** + * Returns all logged invocations for the given method. + * + * @param method the method to get invocations of + * @return a list of invocations on the given method + */ + public List getInvocationsForMethod(Method method) { + return getInvocationsForMethod(new MethodHeader(method)); + } + + /** + * Returns all logged invocations for the given method. + * + * @param methodHeader a method header describing the method + * @return a list of invocations on the given method + */ + public List getInvocationsForMethod(MethodHeader methodHeader) { + return Optional.ofNullable(methodInvocations.get(methodHeader.owner())) + .map(map -> map.get(methodHeader)) + .map(Collections::unmodifiableList) + .orElse(null); + } + + // Method substitution + + /** + * Resets the substitution of methods. + */ + public void resetMethodSubstitution() { + methodSubstitutions.clear(); + } + + /** + * Substitute calls to the given method with the invocation of the given {@link MethodSubstitution}. + * In other words, instead of executing the instructions of either the original submission or the solution, + * this can be used to make the method do and return anything during runtime. + * + * @param method the method to substitute + * @param substitute the {@link MethodSubstitution} the method will be substituted with + */ + public void substituteMethod(Method method, MethodSubstitution substitute) { + substituteMethod(new MethodHeader(method), substitute); + } + + /** + * Substitute calls to the given method with the invocation of the given {@link MethodSubstitution}. + * In other words, instead of executing the instructions of either the original submission or the solution, + * this can be used to make the method do and return anything during runtime. + * + * @param methodHeader a method header describing the method + * @param substitute the {@link MethodSubstitution} the method will be substituted with + */ + public void substituteMethod(MethodHeader methodHeader, MethodSubstitution substitute) { + methodSubstitutions.computeIfAbsent(methodHeader.owner(), k -> new HashMap<>()) + .put(methodHeader, substitute); + } + + // Method delegation + + /** + * Resets the delegation of methods. + */ + public void resetMethodDelegation() { + methodDelegationAllowlist.clear(); + } + + /** + * Disables delegation to the solution for the given method. + * + * @param method the method to disable delegation for + */ + public void disableMethodDelegation(Method method) { + disableMethodDelegation(new MethodHeader(method)); + } + + /** + * Disables delegation to the solution for the given method. + * + * @param methodHeader a method header describing the method + */ + public void disableMethodDelegation(MethodHeader methodHeader) { + methodDelegationAllowlist.computeIfAbsent(methodHeader.owner(), k -> new HashMap<>()) + .put(methodHeader, true); + } + + /** + * This functional interface represents a substitution for a method. + * The functional method {@link #execute(Invocation)} is called with the original invocation's context. + * Its return value is also the value that will be returned by the substituted method. + */ + @FunctionalInterface + public interface MethodSubstitution { + + Type INTERNAL_TYPE = Type.getType(MethodSubstitution.class); + + /** + * DO NOT USE, THIS METHOD HAS NO EFFECT RIGHT NOW. + * TODO: implement constructor substitution + *

+ * Defines the behaviour of method substitution when the substituted method is a constructor. + * When a constructor method is substituted, either {@code super(...)} or {@code this(...)} must be called + * before calling {@link #execute(Invocation)}. + * This method returns a pair consisting of... + *
    + *
  1. the internal class name / owner of the target constructor and
  2. + *
  3. the values that are passed to the constructor of that class.
  4. + *
+ * The first pair entry must be either the original method's owner (for {@code this(...)}) or + * the superclass (for {@code super(...)}). + * The second entry is an array of parameter values for that constructor. + * Default behaviour assumes calling the constructor of {@link Object}, i.e., a class that has no superclass. + * + * @return a pair containing the target method's owner and arguments + */ + default Pair constructorBehaviour() { + return new Pair<>("java/lang/Object", new Object[0]); + } + + /** + * Defines the actions of the substituted method. + * + * @param invocation the context of an invocation + * @return the return value of the substituted method + */ + Object execute(Invocation invocation); + } + + /** + * Collection of methods injected into the bytecode of transformed methods. + */ + public final class Internal { + + public static final Type INTERNAL_TYPE = Type.getType(Internal.class); + + // Invocation logging + + /** + * Returns whether the calling method's invocation is logged, i.e. + * {@link #addInvocation(MethodHeader, Invocation)} may be called or not. + * Should only be used in bytecode transformations when intercepting method invocations. + * + * @param methodHeader a method header describing the method + * @return {@code true} if invocation logging is enabled for the given method, otherwise {@code false} + */ + public boolean logInvocation(MethodHeader methodHeader) { + return Optional.ofNullable(methodInvocations.get(methodHeader.owner())) + .map(map -> map.get(methodHeader)) + .isPresent(); + } + + /** + * Adds an invocation to the list of invocations for the calling method. + * Should only be used in bytecode transformations when intercepting method invocations. + * + * @param methodHeader a method header describing the method + * @param invocation the invocation on the method, i.e. the context it has been called with + */ + public void addInvocation(MethodHeader methodHeader, Invocation invocation) { + Optional.ofNullable(methodInvocations.get(methodHeader.owner())) + .map(map -> map.get(methodHeader)) + .ifPresent(list -> list.add(invocation)); + } + + // Method substitution + + /** + * Returns whether the given method has a substitute or not. + * Should only be used in bytecode transformations when intercepting method invocations. + * + * @param methodHeader a method header describing the method + * @return {@code true} if substitution is enabled for the given method, otherwise {@code false} + */ + public boolean useSubstitution(MethodHeader methodHeader) { + return Optional.ofNullable(methodSubstitutions.get(methodHeader.owner())) + .map(map -> map.containsKey(methodHeader)) + .orElse(false); + } + + /** + * Returns the substitute for the given method. + * Should only be used in bytecode transformations when intercepting method invocations. + * + * @param methodHeader a method header describing the method + * @return the substitute for the given method + */ + public MethodSubstitution getSubstitution(MethodHeader methodHeader) { + return Optional.ofNullable(methodSubstitutions.get(methodHeader.owner())) + .map(map -> map.get(methodHeader)) + .orElseThrow(); + } + + // Method delegation + + /** + * Returns whether the original instructions are used or not. + * Should only be used in bytecode transformations when intercepting method invocations. + * + * @param methodHeader a method header describing the method + * @return {@code true} if delegation is disabled for the given method, otherwise {@code false} + */ + public boolean useSubmissionImpl(MethodHeader methodHeader) { + return Optional.ofNullable(methodDelegationAllowlist.get(methodHeader.owner())) + .map(map -> map.get(methodHeader)) + .orElse(false); + } + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java new file mode 100644 index 00000000..5c702c0f --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ClassHeader.java @@ -0,0 +1,82 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Type; + +import java.util.Objects; + +/** + * A record holding information on the header of a class as declared in Java bytecode. + * + * @param access the class' modifiers + * @param name the class' name + * @param signature the class' signature, if using type parameters + * @param superName the class' superclass + * @param interfaces the class' interfaces + * @author Daniel Mangold + */ +public record ClassHeader(int access, String name, String signature, String superName, String[] interfaces) implements Header { + + private static final Type INTERNAL_TYPE = Type.getType(ClassHeader.class); + private static final Type[] INTERNAL_CONSTRUCTOR_TYPES = new Type[] { + Type.INT_TYPE, + Type.getType(String.class), + Type.getType(String.class), + Type.getType(String.class), + Type.getType(String[].class) + }; + + @Override + public Type getType() { + return INTERNAL_TYPE; + } + + @Override + public Type[] getConstructorParameterTypes() { + return INTERNAL_CONSTRUCTOR_TYPES; + } + + @Override + public Object getValue(String name) { + return switch (name) { + case "access" -> this.access; + case "name" -> this.name; + case "signature" -> this.signature; + case "superName" -> this.superName; + case "interfaces" -> this.interfaces; + default -> throw new IllegalArgumentException("Invalid name: " + name); + }; + } + + /** + * Visits the class header using the information stored in this record. + * + * @param delegate the class visitor to use + * @param version the class version (see {@link ClassVisitor#visit(int, int, String, String, String, String[])}) + * @param additionalInterfaces the internal names of additional interfaces this class should implement + */ + public void visitClass(ClassVisitor delegate, int version, String... additionalInterfaces) { + String[] interfaces; + if (this.interfaces == null) { + interfaces = additionalInterfaces; + } else { + interfaces = new String[this.interfaces.length + additionalInterfaces.length]; + System.arraycopy(this.interfaces, 0, interfaces, 0, this.interfaces.length); + System.arraycopy(additionalInterfaces, 0, interfaces, this.interfaces.length, additionalInterfaces.length); + } + + delegate.visit(version, this.access, this.name, this.signature, this.superName, interfaces); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ClassHeader that)) return false; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java new file mode 100644 index 00000000..8a3a9503 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/FieldHeader.java @@ -0,0 +1,80 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Type; + +import java.util.Objects; + +/** + * A record holding information on the header of a field as declared in java bytecode. + * + * @param owner the field's owner or declaring class + * @param access the field's modifiers + * @param name the field's name + * @param descriptor the field's descriptor / type + * @param signature the field's signature, if using type parameters + * @author Daniel Mangold + */ +public record FieldHeader(String owner, int access, String name, String descriptor, String signature) implements Header { + + private static final Type INTERNAL_TYPE = Type.getType(FieldHeader.class); + private static final Type[] INTERNAL_CONSTRUCTOR_TYPES = new Type[] { + Type.getType(String.class), + Type.INT_TYPE, + Type.getType(String.class), + Type.getType(String.class), + Type.getType(String.class) + }; + + @Override + public Type getType() { + return INTERNAL_TYPE; + } + + @Override + public Type[] getConstructorParameterTypes() { + return INTERNAL_CONSTRUCTOR_TYPES; + } + + @Override + public Object getValue(String name) { + return switch (name) { + case "owner" -> this.owner; + case "access" -> this.access; + case "name" -> this.name; + case "descriptor" -> this.descriptor; + case "signature" -> this.signature; + default -> throw new IllegalArgumentException("Invalid name: " + name); + }; + } + + /** + * Visits a field in the given class visitor using the information stored in this record. + * + * @param delegate the class visitor to use + * @param value an optional value for static fields + * (see {@link ClassVisitor#visitField(int, String, String, String, Object)}) + * @return the resulting {@link FieldVisitor} + */ + public FieldVisitor toFieldVisitor(ClassVisitor delegate, Object value) { + return delegate.visitField(access, name, descriptor, signature, value); + } + + /** + * Two instances of {@link FieldHeader} are considered equal if their names are equal. + * TODO: include owner and parent classes if possible + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof FieldHeader that)) return false; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ForceSignatureAnnotationProcessor.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ForceSignatureAnnotationProcessor.java new file mode 100644 index 00000000..71675c3f --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/ForceSignatureAnnotationProcessor.java @@ -0,0 +1,314 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.*; +import org.tudalgo.algoutils.student.annotation.ForceSignature; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A processor for the {@link ForceSignature} annotation. + * An instance of this class processes and holds information on a single class and its members. + * @author Daniel Mangold + */ +public class ForceSignatureAnnotationProcessor { + + private static final Type FORCE_SIGNATURE_TYPE = Type.getType(ForceSignature.class); + + /** + * The forced identifier of the class, if any. + */ + private String forcedClassIdentifier; + + /** + * A mapping of the actual field header to the forced one. + */ + private final Map forcedFieldsMapping = new HashMap<>(); + + /** + * A mapping of the actual method header to the forced one. + */ + private final Map forcedMethodsMapping = new HashMap<>(); + + /** + * Constructs a new {@link ForceSignatureAnnotationProcessor} instance and processes the + * {@link ForceSignature} annotation using the given class reader. + * + * @param reader the class reader to use for processing + */ + public ForceSignatureAnnotationProcessor(ClassReader reader) { + reader.accept(new ClassLevelVisitor(reader.getClassName()), 0); + } + + /** + * Whether the class identifier / name is forced. + * + * @return true, if forced, otherwise false + */ + public boolean classIdentifierIsForced() { + return forcedClassIdentifier != null; + } + + /** + * Returns the forced class identifier. + * + * @return the forced class identifier + */ + public String forcedClassIdentifier() { + return forcedClassIdentifier.replace('.', '/'); + } + + /** + * Whether the given field is forced. + * + * @param identifier the original identifier / name of the field + * @return true, if forced, otherwise false + */ + public boolean fieldIdentifierIsForced(String identifier) { + return forcedFieldHeader(identifier) != null; + } + + /** + * Returns the field header for a forced field. + * + * @param identifier the original identifier / name of the field + * @return the field header + */ + public FieldHeader forcedFieldHeader(String identifier) { + return forcedFieldsMapping.entrySet() + .stream() + .filter(entry -> identifier.equals(entry.getKey().name())) + .findAny() + .map(Map.Entry::getValue) + .orElse(null); + } + + /** + * Whether the given method is forced. + * + * @param identifier the original identifier / name of the method + * @param descriptor the original descriptor of the method + * @return true, if forced, otherwise false + */ + public boolean methodSignatureIsForced(String identifier, String descriptor) { + return forcedMethodHeader(identifier, descriptor) != null; + } + + /** + * Returns the method header for a forced method. + * + * @param identifier the original identifier / name of the method + * @param descriptor the original descriptor of the method + * @return the method header + */ + public MethodHeader forcedMethodHeader(String identifier, String descriptor) { + return forcedMethodsMapping.entrySet() + .stream() + .filter(entry -> identifier.equals(entry.getKey().name()) && descriptor.equals(entry.getKey().descriptor())) + .findAny() + .map(Map.Entry::getValue) + .orElse(null); + } + + /** + * A visitor for processing class-level annotations. + */ + private class ClassLevelVisitor extends ClassVisitor { + + private final String name; + + private ForceSignatureAnnotationVisitor annotationVisitor; + private final List fieldLevelVisitors = new ArrayList<>(); + private final List methodLevelVisitors = new ArrayList<>(); + + private ClassLevelVisitor(String name) { + super(Opcodes.ASM9); + this.name = name; + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if (descriptor.equals(FORCE_SIGNATURE_TYPE.getDescriptor())) { + return annotationVisitor = new ForceSignatureAnnotationVisitor(); + } else { + return null; + } + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + FieldLevelVisitor fieldLevelVisitor = new FieldLevelVisitor(this.name, access, name, descriptor, signature); + fieldLevelVisitors.add(fieldLevelVisitor); + return fieldLevelVisitor; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodLevelVisitor methodLevelVisitor = new MethodLevelVisitor(this.name, access, name, descriptor, signature, exceptions); + methodLevelVisitors.add(methodLevelVisitor); + return methodLevelVisitor; + } + + @Override + public void visitEnd() { + forcedClassIdentifier = annotationVisitor != null ? annotationVisitor.identifier : null; + + for (FieldLevelVisitor fieldLevelVisitor : fieldLevelVisitors) { + ForceSignatureAnnotationVisitor annotationVisitor = fieldLevelVisitor.annotationVisitor; + if (annotationVisitor == null) continue; + forcedFieldsMapping.put( + new FieldHeader(fieldLevelVisitor.owner, + fieldLevelVisitor.access, + fieldLevelVisitor.name, + fieldLevelVisitor.descriptor, + fieldLevelVisitor.signature), + new FieldHeader(fieldLevelVisitor.owner, + fieldLevelVisitor.access, + annotationVisitor.identifier, + fieldLevelVisitor.descriptor, + fieldLevelVisitor.signature) + ); + } + + for (MethodLevelVisitor methodLevelVisitor : methodLevelVisitors) { + ForceSignatureAnnotationVisitor annotationVisitor = methodLevelVisitor.annotationVisitor; + if (annotationVisitor == null) continue; + forcedMethodsMapping.put( + new MethodHeader(methodLevelVisitor.owner, + methodLevelVisitor.access, + methodLevelVisitor.name, + methodLevelVisitor.descriptor, + methodLevelVisitor.signature, + methodLevelVisitor.exceptions), + new MethodHeader(methodLevelVisitor.owner, + methodLevelVisitor.access, + annotationVisitor.identifier, + annotationVisitor.descriptor, + methodLevelVisitor.signature, + methodLevelVisitor.exceptions) + ); + } + } + } + + /** + * A field visitor for processing field-level annotations. + */ + private static class FieldLevelVisitor extends FieldVisitor { + + private final String owner; + private final int access; + private final String name; + private final String descriptor; + private final String signature; + private ForceSignatureAnnotationVisitor annotationVisitor; + + private FieldLevelVisitor(String owner, int access, String name, String descriptor, String signature) { + super(Opcodes.ASM9); + this.owner = owner; + this.access = access; + this.name = name; + this.descriptor = descriptor; + this.signature = signature; + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if (descriptor.equals(FORCE_SIGNATURE_TYPE.getDescriptor())) { + return annotationVisitor = new ForceSignatureAnnotationVisitor(); + } else { + return null; + } + } + } + + /** + * A method visitor for processing method-level annotations. + */ + private static class MethodLevelVisitor extends MethodVisitor { + + private final String owner; + private final int access; + private final String name; + private final String descriptor; + private final String signature; + private final String[] exceptions; + private ForceSignatureAnnotationVisitor annotationVisitor; + + private MethodLevelVisitor(String owner, int access, String name, String descriptor, String signature, String[] exceptions) { + super(Opcodes.ASM9); + this.owner = owner; + this.access = access; + this.name = name; + this.descriptor = descriptor; + this.signature = signature; + this.exceptions = exceptions; + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if (descriptor.equals(FORCE_SIGNATURE_TYPE.getDescriptor())) { + return annotationVisitor = new ForceSignatureAnnotationVisitor(); + } else { + return null; + } + } + } + + /** + * An annotation visitor for processing the actual {@link ForceSignature} annotation. + */ + private static class ForceSignatureAnnotationVisitor extends AnnotationVisitor { + + private String identifier; + private String descriptor; + private Type returnType; + private final List parameterTypes = new ArrayList<>(); + + ForceSignatureAnnotationVisitor() { + super(Opcodes.ASM9); + } + + @Override + public void visit(String name, Object value) { + switch (name) { + case "identifier" -> identifier = (String) value; + case "descriptor" -> descriptor = (String) value; + case "returnType" -> returnType = (Type) value; + } + } + + @Override + public AnnotationVisitor visitArray(String name) { + if (name.equals("parameterTypes")) { + return new ParameterTypesVisitor(); + } else { + return null; + } + } + + @Override + public void visitEnd() { + if ((descriptor == null || descriptor.isEmpty()) && returnType != null) { + descriptor = Type.getMethodDescriptor(returnType, parameterTypes.toArray(Type[]::new)); + } + } + + /** + * A specialized annotation visitor for visiting the values of {@link ForceSignature#parameterTypes()}. + */ + private class ParameterTypesVisitor extends AnnotationVisitor { + + private ParameterTypesVisitor() { + super(Opcodes.ASM9); + } + + @Override + public void visit(String name, Object value) { + parameterTypes.add((Type) value); + } + } + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java new file mode 100644 index 00000000..cfba416e --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Header.java @@ -0,0 +1,12 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.Type; + +public interface Header { + + Type getType(); + + Type[] getConstructorParameterTypes(); + + Object getValue(String name); +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java new file mode 100644 index 00000000..f26fbd8f --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/Invocation.java @@ -0,0 +1,183 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.Type; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * This class holds information about the context of an invocation. + * Context means the object a method was invoked on and the parameters it was invoked with. + */ +public class Invocation { + + public static final Type INTERNAL_TYPE = Type.getType(Invocation.class); + + private final Object instance; + private final List parameterValues = new ArrayList<>(); + + /** + * Constructs a new invocation. + */ + public Invocation() { + this(null); + } + + /** + * Constructs a new invocation. + * + * @param instance the object on which this invocation takes place + */ + public Invocation(Object instance) { + this.instance = instance; + } + + /** + * Returns the object the method was invoked on. + * + * @return the object the method was invoked on. + */ + public Object getInstance() { + return instance; + } + + /** + * Returns the list of parameter values the method was invoked with. + * + * @return the list of parameter values the method was invoked with. + */ + public List getParameters() { + return Collections.unmodifiableList(parameterValues); + } + + /** + * Returns the value of the parameter at the given index. + * + * @param index the parameter's index + * @return the parameter value + */ + @SuppressWarnings("unchecked") + public T getParameter(int index) { + return (T) parameterValues.get(index); + } + + /** + * Returns the value of the parameter at the given index, cast to the given class. + * + * @param index the parameter's index + * @param clazz the class the value will be cast to + * @return the parameter value, cast to the given class + */ + public T getParameter(int index, Class clazz) { + return clazz.cast(parameterValues.get(index)); + } + + /** + * Returns the value of the {@code boolean} parameter at the given index. + * + * @param index the parameter's index + * @return the parameter value + */ + public boolean getBooleanParameter(int index) { + return getParameter(index, Boolean.class); + } + + /** + * Returns the value of the {@code byte} parameter at the given index. + * + * @param index the parameter's index + * @return the parameter value + */ + public byte getByteParameter(int index) { + return getParameter(index, Byte.class); + } + + /** + * Returns the value of the {@code short} parameter at the given index. + * + * @param index the parameter's index + * @return the parameter value + */ + public short getShortParameter(int index) { + return getParameter(index, Short.class); + } + + /** + * Returns the value of the {@code char} parameter at the given index. + * + * @param index the parameter's index + * @return the parameter value + */ + public char getCharParameter(int index) { + return getParameter(index, Character.class); + } + + /** + * Returns the value of the {@code int} parameter at the given index. + * + * @param index the parameter's index + * @return the parameter value + */ + public int getIntParameter(int index) { + return getParameter(index, Integer.class); + } + + /** + * Returns the value of the {@code long} parameter at the given index. + * + * @param index the parameter's index + * @return the parameter value + */ + public long getLongParameter(int index) { + return getParameter(index, Long.class); + } + + /** + * Returns the value of the {@code float} parameter at the given index. + * + * @param index the parameter's index + * @return the parameter value + */ + public float getFloatParameter(int index) { + return getParameter(index, Float.class); + } + + /** + * Returns the value of the {@code double} parameter at the given index. + * + * @param index the parameter's index + * @return the parameter value + */ + public double getDoubleParameter(int index) { + return getParameter(index, Double.class); + } + + /** + * Adds a parameter value to the list of values. + * + * @param value the value to add + */ + public void addParameter(Object value) { + parameterValues.add(value); + } + + @Override + public String toString() { + return "Invocation{instance=%s, parameterValues=%s}".formatted(instance, parameterValues); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Invocation that = (Invocation) o; + return Objects.equals(parameterValues, that.parameterValues); + } + + @Override + public int hashCode() { + return Objects.hash(parameterValues); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java new file mode 100644 index 00000000..bcb94c7e --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/MethodHeader.java @@ -0,0 +1,106 @@ +package org.tudalgo.algoutils.transform.util; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Objects; + +/** + * A record holding information on the header of a method as declared in java bytecode. + * + * @param owner the method's owner or declaring class + * @param access the method's modifiers + * @param name the method's name + * @param descriptor the method's descriptor / parameter types + return type + * @param signature the method's signature, if using type parameters + * @param exceptions exceptions declared in the method's {@code throws} clause + * @author Daniel Mangold + */ +public record MethodHeader(String owner, int access, String name, String descriptor, String signature, String[] exceptions) implements Header { + + public static final Type INTERNAL_TYPE = Type.getType(MethodHeader.class); + public static final String INTERNAL_CONSTRUCTOR_DESCRIPTOR = Type.getMethodDescriptor(Type.VOID_TYPE, + Type.getType(String.class), + Type.INT_TYPE, + Type.getType(String.class), + Type.getType(String.class), + Type.getType(String.class), + Type.getType(String[].class)); + private static final Type[] INTERNAL_CONSTRUCTOR_TYPES = new Type[] { + Type.getType(String.class), + Type.INT_TYPE, + Type.getType(String.class), + Type.getType(String.class), + Type.getType(String.class), + Type.getType(String[].class) + }; + + @Override + public Type getType() { + return INTERNAL_TYPE; + } + + @Override + public Type[] getConstructorParameterTypes() { + return INTERNAL_CONSTRUCTOR_TYPES; + } + + @Override + public Object getValue(String name) { + return switch (name) { + case "owner" -> this.owner; + case "access" -> this.access; + case "name" -> this.name; + case "descriptor" -> this.descriptor; + case "signature" -> this.signature; + case "exceptions" -> this.exceptions; + default -> throw new IllegalArgumentException("Invalid name: " + name); + }; + } + + /** + * Constructs a new method header using the given method. + * + * @param method a java reflection method + */ + public MethodHeader(Method method) { + this(Type.getInternalName(method.getDeclaringClass()), + method.getModifiers(), + method.getName(), + Type.getMethodDescriptor(method), + null, + Arrays.stream(method.getExceptionTypes()) + .map(Type::getInternalName) + .toArray(String[]::new)); + } + + /** + * Visits a method in the given class visitor using the information stored in this record. + * + * @param delegate the class visitor to use + * @return the resulting {@link MethodVisitor} + */ + public MethodVisitor toMethodVisitor(ClassVisitor delegate) { + return delegate.visitMethod(access, name, descriptor, signature, exceptions); + } + + /** + * Two instances of {@link MethodHeader} are considered equal if their names and descriptors are equal. + * TODO: include owner and parent classes if possible + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MethodHeader that)) return false; + return Objects.equals(name, that.name) && Objects.equals(descriptor, that.descriptor); + } + + @Override + public int hashCode() { + return Objects.hash(name, descriptor); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java new file mode 100644 index 00000000..c2e3e1dc --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationContext.java @@ -0,0 +1,33 @@ +package org.tudalgo.algoutils.transform.util; + +import org.tudalgo.algoutils.transform.SolutionClassNode; +import org.tudalgo.algoutils.transform.SubmissionClassInfo; + +import java.util.Map; + +/** + * A record for holding context information for the transformation process. + * + * @param projectPrefix the root package for all submission classes + * @param solutionClasses a mapping of solution class names to their respective {@link SolutionClassNode} + * @param submissionClasses a mapping of submission class names to their respective {@link SubmissionClassInfo} + * @author Daniel Mangold + */ +public record TransformationContext( + String projectPrefix, + Map solutionClasses, + Map submissionClasses +) { + + /** + * Returns the {@link SubmissionClassInfo} for a given submission class name. + * If no mapping exists in {@link #submissionClasses}, will attempt to compute one. + * + * @param submissionClassName the submission class name + * @return the {@link SubmissionClassInfo} object + */ + public SubmissionClassInfo getSubmissionClassInfo(String submissionClassName) { + return submissionClasses.computeIfAbsent(submissionClassName, + className -> TransformationUtils.readSubmissionClass(this, className)); + } +} diff --git a/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java new file mode 100644 index 00000000..e73b6992 --- /dev/null +++ b/tutor/src/main/java/org/tudalgo/algoutils/transform/util/TransformationUtils.java @@ -0,0 +1,253 @@ +package org.tudalgo.algoutils.transform.util; + +import org.tudalgo.algoutils.transform.SolutionClassNode; +import org.tudalgo.algoutils.transform.SolutionMergingClassTransformer; +import org.tudalgo.algoutils.transform.SubmissionClassInfo; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +import java.io.IOException; +import java.io.InputStream; + +import static org.objectweb.asm.Opcodes.*; + +/** + * A collection of utility methods useful for bytecode transformations. + * @author Daniel Mangold + */ +public final class TransformationUtils { + + private TransformationUtils() {} + + /** + * Automatically box primitive types using the supplied {@link MethodVisitor}. + * If the given type is not a primitive type, this method does nothing. + * + * @param mv the {@link MethodVisitor} to use + * @param type the type of the value + */ + public static void boxType(MethodVisitor mv, Type type) { + switch (type.getSort()) { + case Type.BOOLEAN -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false); + case Type.BYTE -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;", false); + case Type.SHORT -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;", false); + case Type.CHAR -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Character", "valueOf", "(C)Ljava/lang/Character;", false); + case Type.INT -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false); + case Type.FLOAT -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Float", "valueOf", "(F)Ljava/lang/Float;", false); + case Type.LONG -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false); + case Type.DOUBLE -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;", false); + } + } + + /** + * Automatically unbox primitive types using the supplied {@link MethodVisitor}. + * If the given type is not a primitive type, this method does nothing. + * + * @param mv the {@link MethodVisitor} to use + * @param type the type of the value + */ + public static void unboxType(MethodVisitor mv, Type type) { + switch (type.getSort()) { + case Type.BOOLEAN -> { + mv.visitTypeInsn(CHECKCAST, "java/lang/Boolean"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z", false); + } + case Type.BYTE -> { + mv.visitTypeInsn(CHECKCAST, "java/lang/Byte"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Byte", "byteValue", "()B", false); + } + case Type.SHORT -> { + mv.visitTypeInsn(CHECKCAST, "java/lang/Short"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Short", "shortValue", "()S", false); + } + case Type.CHAR -> { + mv.visitTypeInsn(CHECKCAST, "java/lang/Character"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Character", "charValue", "()C", false); + } + case Type.INT -> { + mv.visitTypeInsn(CHECKCAST, "java/lang/Integer"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I", false); + } + case Type.FLOAT -> { + mv.visitTypeInsn(CHECKCAST, "java/lang/Float"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Float", "floatValue", "()F", false); + } + case Type.LONG -> { + mv.visitTypeInsn(CHECKCAST, "java/lang/Long"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false); + } + case Type.DOUBLE -> { + mv.visitTypeInsn(CHECKCAST, "java/lang/Double"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Double", "doubleValue", "()D", false); + } + } + } + + /** + * Calculates the true index of variables in the locals array. + * Variables with type long or double occupy two slots in the locals array, + * so the "expected" or "natural" index of these variables might be shifted. + * + * @param types the parameter types + * @param index the "natural" index of the variable + * @return the true index + */ + public static int getLocalsIndex(Type[] types, int index) { + int localsIndex = 0; + for (int i = 0; i < index; i++) { + localsIndex += (types[i].getSort() == Type.LONG || types[i].getSort() == Type.DOUBLE) ? 2 : 1; + } + return localsIndex; + } + + /** + * Builds a class header with bytecode instructions using the given method visitor and information + * stored in the given class header. + * Upon return, a reference to the newly created {@link ClassHeader} object is located at + * the top of the method visitor's stack. + * + * @param mv the method visitor to use + * @param classHeader the class header to replicate in bytecode + * @return the maximum stack size during the operation + */ + public static int buildClassHeader(MethodVisitor mv, ClassHeader classHeader) { + return buildHeader(mv, classHeader, "access", "name", "signature", "superName", "interfaces"); + } + + /** + * Builds a field header with bytecode instructions using the given method visitor and information + * stored in the given field header. + * Upon return, a reference to the newly created {@link FieldHeader} object is located at + * the top of the method visitor's stack. + * + * @param mv the method visitor to use + * @param fieldHeader the field header to replicate in bytecode + * @return the maximum stack size during the operation + */ + public static int buildFieldHeader(MethodVisitor mv, FieldHeader fieldHeader) { + return buildHeader(mv, fieldHeader, "owner", "access", "name", "descriptor", "signature"); + } + + /** + * Builds a method header with bytecode instructions using the given method visitor and information + * stored in the given method header. + * Upon return, a reference to the newly created {@link MethodHeader} object is located at + * the top of the method visitor's stack. + * + * @param mv the method visitor to use + * @param methodHeader the method header to replicate in bytecode + * @return the maximum stack size during the operation + */ + public static int buildMethodHeader(MethodVisitor mv, MethodHeader methodHeader) { + return buildHeader(mv, methodHeader, "owner", "access", "name", "descriptor", "signature", "exceptions"); + } + + /** + * Attempts to read and process a solution class from {@code resources/classes/}. + * + * @param className the name of the solution class + * @return the resulting {@link SolutionClassNode} object + */ + public static SolutionClassNode readSolutionClass(String className) { + ClassReader solutionClassReader; + String solutionClassFilePath = "/classes/%s.bin".formatted(className); + try (InputStream is = SolutionMergingClassTransformer.class.getResourceAsStream(solutionClassFilePath)) { + if (is == null) { + throw new IOException("No such resource: " + solutionClassFilePath); + } + solutionClassReader = new ClassReader(is); + } catch (IOException e) { + throw new RuntimeException(e); + } + SolutionClassNode solutionClassNode = new SolutionClassNode(className); + solutionClassReader.accept(solutionClassNode, 0); + return solutionClassNode; + } + + /** + * Attempts to read and process a submission class. + * + * @param transformationContext a {@link TransformationContext} object + * @param className the name of the submission class + * @return the resulting {@link SubmissionClassInfo} object + */ + public static SubmissionClassInfo readSubmissionClass(TransformationContext transformationContext, String className) { + ClassReader submissionClassReader; + String submissionClassFilePath = "/%s.class".formatted(className); + try (InputStream is = SolutionMergingClassTransformer.class.getResourceAsStream(submissionClassFilePath)) { + submissionClassReader = new ClassReader(is); + } catch (IOException e) { + return null; + } + SubmissionClassInfo submissionClassInfo = new SubmissionClassInfo( + transformationContext, + submissionClassReader.getClassName(), + new ForceSignatureAnnotationProcessor(submissionClassReader) + ); + submissionClassReader.accept(submissionClassInfo, 0); + return submissionClassInfo; + } + + /** + * Replicates the given header with bytecode instructions using the supplied method visitor. + * Upon return, a reference to the newly created header object is located at + * the top of the method visitor's stack. + *
+ * Note: The number of keys must equal the length of the array returned by + * {@link Header#getConstructorParameterTypes()}. + * Furthermore, the result of calling {@link Header#getValue(String)} with {@code keys[i]} must + * be assignable to the constructor parameter type at index {@code i}. + * + * @param mv the method visitor to use + * @param header the header object to replicate in bytecode + * @param keys the keys to get values for + * @return the maximum stack size during the operation + */ + private static int buildHeader(MethodVisitor mv, Header header, String... keys) { + Type headerType = header.getType(); + Type[] constructorParameterTypes = header.getConstructorParameterTypes(); + int maxStack, stackSize; + + mv.visitTypeInsn(NEW, header.getType().getInternalName()); + mv.visitInsn(DUP); + maxStack = stackSize = 2; + for (int i = 0; i < keys.length; i++) { + Object value = header.getValue(keys[i]); + if (constructorParameterTypes[i].equals(Type.getType(String[].class))) { + Object[] array = (Object[]) value; + if (array != null) { + mv.visitIntInsn(SIPUSH, array.length); + mv.visitTypeInsn(ANEWARRAY, Type.getInternalName(String.class)); + maxStack = Math.max(maxStack, ++stackSize); + for (int j = 0; j < array.length; j++) { + mv.visitInsn(DUP); + maxStack = Math.max(maxStack, ++stackSize); + mv.visitIntInsn(SIPUSH, j); + maxStack = Math.max(maxStack, ++stackSize); + mv.visitLdcInsn(array[j]); + maxStack = Math.max(maxStack, ++stackSize); + mv.visitInsn(AASTORE); + stackSize -= 3; + } + } else { + mv.visitInsn(ACONST_NULL); + maxStack = Math.max(maxStack, ++stackSize); + } + } else { + if (value != null) { + mv.visitLdcInsn(value); + } else { + mv.visitInsn(ACONST_NULL); + } + maxStack = Math.max(maxStack, ++stackSize); + } + } + mv.visitMethodInsn(INVOKESPECIAL, + headerType.getInternalName(), + "", + Type.getMethodDescriptor(Type.VOID_TYPE, constructorParameterTypes), + false); + return maxStack; + } +}