Skip to content

Commit

Permalink
Add prototype for full project instrumentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Hapstyx committed Nov 15, 2024
1 parent 0482218 commit 4858461
Show file tree
Hide file tree
Showing 16 changed files with 2,553 additions and 0 deletions.
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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:
* <ul>
* <li>Types: the fully qualified name of the type (e.g., {@code java.lang.Object})</li>
* <li>Fields: the name / identifier of the field</li>
* <li>Constructors: Always {@code <init>}, regardless of the class</li>
* <li>Methods: the name / identifier of the method</li>
* </ul>
*
* @return the type / member identifier
*/
String identifier();

/**
* The method descriptor as specified by
* <a href="https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-4.html#jvms-4.3">Chapter 4.3</a>
* of the Java Virtual Machine Specification.
* If a value is set, it takes precedence over {@link #returnType()} and {@link #parameterTypes()}.
*
* <p>
* Note: Setting this value has no effect for types or fields.
* </p>
*
* @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).
*
* <p>
* Note: Setting this value has no effect for types or fields.
* </p>
*
* @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.
*
* <p>
* Note: Setting this value has no effect for types or fields.
* </p>
*
* @return the method's parameter types
*/
Class<?>[] parameterTypes() default {};
}
1 change: 1 addition & 0 deletions tutor/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies {
api(libs.jagr)
api(libs.mockito)
api(libs.asm)
api(libs.asmTree)
testImplementation(libs.junit)
dokkaPlugin(libs.dokkaKotlinAsJavaPlugin)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<FieldHeader, FieldNode> fields = new HashMap<>();
private final Map<MethodHeader, MethodNode> 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<FieldHeader, FieldNode> 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<MethodHeader, MethodNode> 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());
}
};
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <br><br>
* <b>Solution classes</b>
* <p>
* 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.
* <br>
* 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.
* </p>
*
* <b>Submission classes</b>
* <p>
* 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.
* </p>
* <br><br>
* Implementation details:
* <ul>
* <li>
* Unless otherwise specified, this transformer and its tools use the internal name as specified by
* {@link Type#getInternalName()} when referring to class names.
* </li>
* <li>
* 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
* <a href="https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-4.html#jvms-4.3">Chapter 4.3</a>
* of the Java Virtual Machine Specification.
* </li>
* </ul>
*
* @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<String> availableSolutionClasses) {
Map<String, SolutionClassNode> solutionClasses = new HashMap<>();
Map<String, SubmissionClassInfo> 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);
}
}
Loading

0 comments on commit 4858461

Please sign in to comment.