├── settings.gradle.kts ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── shipkit.gradle ├── renovate.json ├── src ├── test │ └── java │ │ └── dev │ │ └── minco │ │ └── javatransformer │ │ ├── api │ │ ├── TestEnum.java │ │ ├── AnnotationWithDefault.java │ │ ├── MethodInfoTest.java │ │ ├── ClassPathTest.java │ │ ├── AnnotationTest.java │ │ ├── SourceCodeFragmentTest.java │ │ ├── TypeTest.java │ │ ├── JavaTransformerRuntimeTest.java │ │ └── JavaTransformerTest.java │ │ ├── transform │ │ ├── AnnotationInnerClassExample.java │ │ ├── InnerClassExample.java │ │ ├── codefragments │ │ │ ├── InsertBeforeTest.java │ │ │ └── InsertBeforeTest.java_patched │ │ ├── AnnotationWithEnums.java │ │ ├── innerpackage │ │ │ └── InnerClassReferencer.java │ │ ├── InnerClassReferencer.java │ │ └── CodeFragmentTesting.java │ │ └── internal │ │ ├── util │ │ ├── NodeUtilTest.java │ │ └── SplitterTest.java │ │ ├── SignatureTest.java │ │ ├── MethodNodeInfoTest.java │ │ ├── MethodDescriptorTest.java │ │ └── ResolutionContextTest.java └── main │ └── java │ └── dev │ └── minco │ └── javatransformer │ ├── api │ ├── Named.java │ ├── HasTypeVariable.java │ ├── ClassMember.java │ ├── code │ │ ├── ShouldNotBeCalledError.java │ │ ├── IntermediateValue.java │ │ ├── RETURN.java │ │ └── CodeFragment.java │ ├── Accessible.java │ ├── Transformer.java │ ├── TypeVariable.java │ ├── HasCodeFragment.java │ ├── Annotated.java │ ├── TransformationException.java │ ├── FieldInfo.java │ ├── Parameter.java │ ├── MethodInfo.java │ ├── ClassPath.java │ ├── ClassInfo.java │ ├── Annotation.java │ └── AccessFlags.java │ └── internal │ ├── package-info.java │ ├── asm │ ├── AsmUtil.java │ ├── DebugPrinter.java │ ├── FilteringClassWriter.java │ ├── AsmInstructions.java │ ├── CombinedValue.java │ ├── CombinedAnalyzer.java │ └── CombinedInterpreter.java │ ├── util │ ├── CodeFragmentUtil.java │ ├── StreamUtil.java │ ├── Joiner.java │ ├── LookupAccess.java │ ├── Splitter.java │ ├── CachingSupplier.java │ ├── DefineClass.java │ ├── TypeUtil.java │ ├── CollectionUtil.java │ ├── AnnotationParser.java │ ├── NodeUtil.java │ ├── Cloner.java │ └── JVMUtil.java │ ├── SimpleFieldInfo.java │ ├── javaparser │ ├── CompilationUnitInfo.java │ └── Expressions.java │ ├── SimpleMethodInfo.java │ ├── MethodDescriptor.java │ ├── Signature.java │ ├── JavaParserCodeFragmentGenerator.java │ ├── ClassPaths.java │ └── ByteCodeInfo.java ├── version.properties ├── .editorconfig ├── gradle.properties ├── properties.gradle ├── .gitignore ├── .gitattributes ├── gradlew.bat ├── .github └── workflows │ └── ci.yml ├── LICENSE └── gradlew /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "java-transformer" 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinimallyCorrect/JavaTransformer/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>MinimallyCorrect/.github:renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/api/TestEnum.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | public enum TestEnum { 4 | FIRST, 5 | SECOND, 6 | THIRD,; 7 | } 8 | -------------------------------------------------------------------------------- /version.properties: -------------------------------------------------------------------------------- 1 | #Version of the produced binaries. This file is intended to be checked-in. 2 | #It will be automatically bumped by release automation. 3 | version=1.10.* 4 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/api/Named.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | public interface Named { 4 | String getName(); 5 | 6 | void setName(String name); 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/transform/AnnotationInnerClassExample.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.transform; 2 | 3 | public @interface AnnotationInnerClassExample { 4 | enum TestEnum { 5 | ONE, 6 | TWO 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/transform/InnerClassExample.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.transform; 2 | 3 | public class InnerClassExample { 4 | public class Inner { 5 | 6 | } 7 | 8 | public class Inner2 { 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/api/HasTypeVariable.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import java.util.List; 4 | 5 | public interface HasTypeVariable { 6 | List getTypeVariables(); 7 | 8 | void setTypeVariables(List typeVariables); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/api/ClassMember.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import org.jetbrains.annotations.Contract; 4 | 5 | public interface ClassMember extends Annotated, Accessible, HasCodeFragment, Named { 6 | @Contract(pure = true) 7 | ClassInfo getClassInfo(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/api/code/ShouldNotBeCalledError.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api.code; 2 | 3 | class ShouldNotBeCalledError extends Error { 4 | ShouldNotBeCalledError() { 5 | super("This method should never be called at runtime - it should be converted into a return instruction"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | charset = utf-8 11 | indent_style = tab 12 | indent_size = 4 13 | max_line_length = 180 14 | 15 | [*.yml] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Classes in this package directly interact with specific code types (ASM or JavaParser currently) 3 | *

4 | * It's not recommended to work with these directly, but if you need to do something which the JavaTransformer API doesn't support this may be necessary. 5 | */ 6 | package dev.minco.javatransformer.internal; 7 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/transform/codefragments/InsertBeforeTest.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.transform.codefragments; 2 | 3 | @SuppressWarnings("ALL") 4 | public class InsertBeforeTest { 5 | public String toInsert() { 6 | return "success"; 7 | } 8 | 9 | public String testMethod() { 10 | throw new UnsupportedOperationException("test"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/api/Accessible.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import java.util.function.Function; 4 | 5 | public interface Accessible { 6 | AccessFlags getAccessFlags(); 7 | 8 | void setAccessFlags(AccessFlags accessFlags); 9 | 10 | default void accessFlags(Function c) { 11 | setAccessFlags(c.apply(getAccessFlags())); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/transform/AnnotationWithEnums.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.transform; 2 | 3 | import dev.minco.javatransformer.api.AnnotationWithDefault; 4 | import dev.minco.javatransformer.api.TestEnum; 5 | import dev.minco.javatransformer.api.code.CodeFragment; 6 | 7 | @AnnotationWithDefault(position = CodeFragment.InsertionPosition.AFTER, testEnum = TestEnum.SECOND) 8 | public class AnnotationWithEnums {} 9 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/api/AnnotationWithDefault.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import dev.minco.javatransformer.api.code.CodeFragment; 4 | 5 | public @interface AnnotationWithDefault { 6 | String value() default "value"; 7 | 8 | int index() default 1; 9 | 10 | CodeFragment.InsertionPosition position() default CodeFragment.InsertionPosition.BEFORE; 11 | 12 | TestEnum testEnum() default TestEnum.FIRST; 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/internal/util/NodeUtilTest.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.util; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | import com.github.javaparser.ast.expr.Name; 7 | 8 | public class NodeUtilTest { 9 | @Test 10 | public void testQualifiedName() throws Exception { 11 | Assert.assertEquals("java.lang.String", NodeUtil.qualifiedName(new Name("java.lang.String"))); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/transform/codefragments/InsertBeforeTest.java_patched: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.transform.codefragments; 2 | 3 | @SuppressWarnings("ALL") 4 | public class InsertBeforeTest { 5 | 6 | public String toInsert() { 7 | return "success"; 8 | } 9 | 10 | public String testMethod() { 11 | return "success"; 12 | throw new UnsupportedOperationException("test"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # The exports are needed due to https://github.com/diffplug/spotless/issues/834 2 | org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ 3 | --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ 4 | --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ 5 | --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ 6 | --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED 7 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/transform/innerpackage/InnerClassReferencer.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.transform.innerpackage; 2 | 3 | import dev.minco.javatransformer.transform.InnerClassExample; 4 | 5 | public class InnerClassReferencer { 6 | public InnerClassExample.Inner test1() { 7 | throw new UnsupportedOperationException(); 8 | } 9 | 10 | public InnerClassExample.Inner2 test2() { 11 | throw new UnsupportedOperationException(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/api/Transformer.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import java.util.Collection; 4 | 5 | public interface Transformer { 6 | /** 7 | * @param editor editor instance associated with a class 8 | */ 9 | void transform(ClassInfo editor); 10 | 11 | interface TargetedTransformer extends Transformer { 12 | /** 13 | * @return List of classes which this transformer will run on 14 | */ 15 | Collection getTargetClasses(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /properties.gradle: -------------------------------------------------------------------------------- 1 | group = "dev.minco" 2 | description = "Applies transformations to .java and .class files" 3 | 4 | ext.githubOwner = 'MinimallyCorrect' 5 | ext.githubProject = 'JavaTransformer' 6 | ext.tags = ['java-transformation', 'java-source', 'java-bytecode', 'java'] 7 | ext.licenses = ['LGPL-3.0'] 8 | 9 | ext.githubOwnerProject = "${ext.githubOwner}/${ext.githubProject}".toString() 10 | ext.githubUrl = "https://github.com/${ext.githubOwnerProject}".toString() 11 | ext.website = ext.githubUrl 12 | ext.vcsUrl = "${ext.githubUrl}.git".toString() 13 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/api/TypeVariable.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class TypeVariable { 7 | private final String name; 8 | private final Type bounds; 9 | 10 | public TypeVariable(String name) { 11 | this(name, Type.OBJECT); 12 | } 13 | 14 | public TypeVariable(String name, Type bounds) { 15 | this.name = name; 16 | this.bounds = bounds; 17 | } 18 | 19 | @Override 20 | public String toString() { 21 | return name + ':' + bounds.signatureElseDescriptor(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/internal/SignatureTest.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal; 2 | 3 | import lombok.val; 4 | 5 | import org.junit.Assert; 6 | import org.junit.Test; 7 | 8 | import dev.minco.javatransformer.api.TypeVariable; 9 | 10 | public class SignatureTest { 11 | @Test 12 | public void testClassSignature() { 13 | val sig = "Ljava/lang/Object;Ljava/util/Collection;"; 14 | val tvs = Signature.getTypeVariables(sig); 15 | Assert.assertArrayEquals(new TypeVariable[]{new TypeVariable("E")}, tvs.toArray()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /gradle/shipkit.gradle: -------------------------------------------------------------------------------- 1 | tasks.named("mincoGenerateChangelog") { 2 | version.value("" + project.version) 3 | githubUrl.value(project.ext["githubUrl"] as String) 4 | outputFile.fileValue(project.file("$buildDir/changelog.md")) 5 | fromRevision.value(project.ext["shipkit-auto-version.previous-tag"] as String) 6 | } 7 | 8 | tasks.named("githubRelease") { 9 | def genTask = tasks.named("mincoGenerateChangelog").get() 10 | dependsOn(genTask) 11 | repository = project.ext["githubOwnerProject"] as String 12 | changelog = genTask.outputFile.get().getAsFile() 13 | newTagRevision = System.getenv("GITHUB_SHA") 14 | githubToken = System.getenv("GITHUB_TOKEN") 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/api/HasCodeFragment.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import java.util.Collections; 4 | import java.util.List; 5 | 6 | import lombok.val; 7 | 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import dev.minco.javatransformer.api.code.CodeFragment; 11 | 12 | public interface HasCodeFragment { 13 | @Nullable 14 | default CodeFragment.Body getCodeFragment() { 15 | return null; 16 | } 17 | 18 | default List findFragments(Class fragmentType) { 19 | val fragment = getCodeFragment(); 20 | return fragment == null ? Collections.emptyList() : fragment.findFragments(fragmentType); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/api/Annotated.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import java.util.List; 4 | import java.util.stream.Collectors; 5 | 6 | public interface Annotated { 7 | List getAnnotations(); 8 | 9 | default List getAnnotations(Type t) { 10 | if (!t.isClassType()) 11 | throw new TransformationException("Type must be a class type: " + t); 12 | return getAnnotations(t.getClassName()); 13 | } 14 | 15 | default List getAnnotations(String fullAnnotationName) { 16 | return getAnnotations().stream() 17 | .filter((it) -> it.getType().getClassName().equals(fullAnnotationName)) 18 | .collect(Collectors.toList()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/api/MethodInfoTest.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | 6 | import lombok.val; 7 | 8 | import org.junit.Assert; 9 | import org.junit.Test; 10 | 11 | import dev.minco.javatransformer.internal.SimpleMethodInfo; 12 | 13 | public class MethodInfoTest { 14 | @Test 15 | public void testSimilar() throws Exception { 16 | val a = SimpleMethodInfo.of(new AccessFlags(AccessFlags.ACC_PUBLIC), Collections.emptyList(), Type.of("java.lang.String"), "name", new ArrayList<>()); 17 | val b = SimpleMethodInfo.of(new AccessFlags(AccessFlags.ACC_PUBLIC), Collections.emptyList(), Type.of("java.lang.String"), "name", new ArrayList<>()); 18 | Assert.assertTrue(a.similar(b)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/asm/AsmUtil.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.asm; 2 | 3 | import javax.annotation.Nonnull; 4 | import javax.annotation.Nullable; 5 | 6 | import lombok.NonNull; 7 | 8 | import org.objectweb.asm.ClassReader; 9 | import org.objectweb.asm.tree.ClassNode; 10 | 11 | public class AsmUtil { 12 | @Nonnull 13 | public static ClassNode getClassNode(@NonNull byte[] data, @Nullable Holder readerHolder) { 14 | ClassNode node = new ClassNode(); 15 | ClassReader reader = new ClassReader(data); 16 | reader.accept(node, ClassReader.EXPAND_FRAMES); 17 | 18 | if (readerHolder != null) 19 | readerHolder.value = reader; 20 | 21 | return node; 22 | } 23 | 24 | public static class Holder { 25 | public T value; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/api/TransformationException.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | /** 4 | * Thrown when parsing fails 5 | */ 6 | public class TransformationException extends RuntimeException { 7 | private static final long serialVersionUID = 0; 8 | 9 | public TransformationException() { 10 | super(); 11 | } 12 | 13 | public TransformationException(String message) { 14 | super(message); 15 | } 16 | 17 | public TransformationException(String message, Throwable cause) { 18 | super(message, cause); 19 | } 20 | 21 | public TransformationException(Throwable cause) { 22 | super(cause); 23 | } 24 | 25 | protected TransformationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 26 | super(message, cause, enableSuppression, writableStackTrace); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/api/FieldInfo.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import dev.minco.javatransformer.internal.SimpleFieldInfo; 4 | 5 | public interface FieldInfo extends ClassMember { 6 | static FieldInfo of(AccessFlags accessFlags, Type type, String name) { 7 | return SimpleFieldInfo.of(accessFlags, type, name); 8 | } 9 | 10 | Type getType(); 11 | 12 | void setType(Type type); 13 | 14 | default void setAll(FieldInfo info) { 15 | this.setName(info.getName()); 16 | this.setAccessFlags(info.getAccessFlags()); 17 | this.setType(info.getType()); 18 | } 19 | 20 | default boolean similar(FieldInfo other) { 21 | return other.getName().equals(this.getName()) && 22 | other.getType().similar(this.getType()); 23 | } 24 | 25 | @SuppressWarnings("MethodDoesntCallSuperMethod") 26 | default FieldInfo clone() { 27 | throw new UnsupportedOperationException(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build # 2 | ######### 3 | MANIFEST.MF 4 | dependency-reduced-pom.xml 5 | 6 | # Compiled # 7 | ############ 8 | bin 9 | build 10 | dist 11 | lib 12 | out 13 | run 14 | target 15 | *.com 16 | *.class 17 | *.dll 18 | *.exe 19 | *.o 20 | *.so 21 | 22 | # Databases # 23 | ############# 24 | *.db 25 | *.sql 26 | *.sqlite 27 | 28 | # Packages # 29 | ############ 30 | *.7z 31 | *.dmg 32 | *.gz 33 | *.iso 34 | *.rar 35 | *.tar 36 | *.zip 37 | 38 | # Repository # 39 | ############## 40 | .git 41 | 42 | # Logging # 43 | ########### 44 | /logs 45 | *.log 46 | 47 | # Misc # 48 | ######## 49 | *.bak 50 | 51 | # System # 52 | ########## 53 | .DS_Store 54 | ehthumbs.db 55 | Thumbs.db 56 | 57 | # Project # 58 | ########### 59 | .classpath 60 | .externalToolBuilders 61 | .gradle 62 | .idea 63 | .project 64 | .settings 65 | eclipse 66 | nbproject 67 | atlassian-ide-plugin.xml 68 | build.xml 69 | nb-configuration.xml 70 | *.iml 71 | *.ipr 72 | *.iws -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/util/CodeFragmentUtil.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.util; 2 | 3 | import lombok.NonNull; 4 | 5 | import dev.minco.javatransformer.api.code.CodeFragment; 6 | 7 | public final class CodeFragmentUtil { 8 | /** 9 | * Validates if an attempted insert is sensible 10 | * 11 | * @return true if the insertion would have no effect and can be skipped 12 | * @throws UnsupportedOperationException If the attempted insertion is not possible 13 | */ 14 | public static boolean validateInsert(@NonNull CodeFragment self, @NonNull CodeFragment codeFragment, 15 | @NonNull CodeFragment.InsertionPosition position, 16 | @NonNull CodeFragment.InsertionOptions insertionOptions) { 17 | if (self.equals(codeFragment)) { 18 | if (position == CodeFragment.InsertionPosition.OVERWRITE) 19 | return true; 20 | throw new UnsupportedOperationException("Can't insert a CodeFragment into itself"); 21 | } 22 | return false; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/util/StreamUtil.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.util; 2 | 3 | import java.io.InputStream; 4 | import java.util.Arrays; 5 | 6 | import lombok.SneakyThrows; 7 | 8 | public final class StreamUtil { 9 | @SneakyThrows 10 | public static byte[] readFully(InputStream is) { 11 | byte[] output = {}; 12 | int position = 0; 13 | while (true) { 14 | int bytesToRead; 15 | if (position >= output.length) { 16 | bytesToRead = output.length + 4096; 17 | if (output.length < position + bytesToRead) { 18 | output = Arrays.copyOf(output, position + bytesToRead); 19 | } 20 | } else { 21 | bytesToRead = output.length - position; 22 | } 23 | int bytesRead = is.read(output, position, bytesToRead); 24 | if (bytesRead < 0) { 25 | if (output.length != position) { 26 | output = Arrays.copyOf(output, position); 27 | } 28 | break; 29 | } 30 | position += bytesRead; 31 | } 32 | return output; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/util/Joiner.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.util; 2 | 3 | import java.util.Iterator; 4 | import java.util.stream.Stream; 5 | 6 | public interface Joiner { 7 | Joiner empty = parts -> { 8 | StringBuilder sb = new StringBuilder(); 9 | 10 | for (Object part : parts) { 11 | sb.append(part.toString()); 12 | } 13 | 14 | return sb.toString(); 15 | }; 16 | 17 | static Joiner on() { 18 | return empty; 19 | } 20 | 21 | static Joiner on(String join) { 22 | return parts -> { 23 | @SuppressWarnings("unchecked") 24 | Iterator i = (Iterator) parts.iterator(); 25 | 26 | if (!i.hasNext()) 27 | return ""; 28 | 29 | StringBuilder sb = new StringBuilder(i.next().toString()); 30 | 31 | while (i.hasNext()) 32 | sb.append(join).append(i.next().toString()); 33 | 34 | return sb.toString(); 35 | }; 36 | } 37 | 38 | String join(Iterable s); 39 | 40 | default String join(Stream s) { 41 | return join((Iterable) s::iterator); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/internal/util/SplitterTest.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.util; 2 | 3 | import java.util.ArrayList; 4 | 5 | import org.junit.Assert; 6 | import org.junit.Test; 7 | 8 | public class SplitterTest { 9 | @Test 10 | public void testSplitter() throws Exception { 11 | testSplitter('.', "a.b.c.d", "a", "b", "c", "d"); 12 | testSplitter('.', "a.b..c.d", "a", "b", "c", "d"); 13 | testSplitter('.', "a.b...c..d", "a", "b", "c", "d"); 14 | testSplitter('.', ".a.b...c..d", "a", "b", "c", "d"); 15 | testSplitter('.', "a.b...c..d.", "a", "b", "c", "d"); 16 | testSplitter('.', ".a.b...c..d.", "a", "b", "c", "d"); 17 | } 18 | 19 | private void testSplitter(char on, String input, String... expected) { 20 | Assert.assertArrayEquals(expected, toArray(Splitter.on(on).splitIterable(input))); 21 | } 22 | 23 | private String[] toArray(Iterable split) { 24 | ArrayList list = new ArrayList<>(); 25 | 26 | split.forEach(list::add); 27 | 28 | return list.toArray(new String[0]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Handle line endings automatically for files detected as text 2 | # and leave all files detected as binary untouched. 3 | * text=auto eol=lf 4 | 5 | # .bat must be crlf to work 6 | *.bat text eol=crlf 7 | 8 | # 9 | # The above will handle all files NOT found below 10 | # 11 | # These files are text and should be normalized (Convert crlf => lf) 12 | *.css text 13 | *.df text 14 | *.htm text 15 | *.html text 16 | *.java text 17 | *.js text 18 | *.json text 19 | *.jsp text 20 | *.jspf text 21 | *.properties text 22 | *.sh text 23 | *.sql text 24 | *.svg text 25 | *.tld text 26 | *.txt text 27 | *.xml text 28 | 29 | # These files are binary and should be left untouched 30 | # (binary is a macro for -text -diff) 31 | *.class binary 32 | *.dll binary 33 | *.ear binary 34 | *.gif binary 35 | *.ico binary 36 | *.jar binary 37 | *.jpg binary 38 | *.jpeg binary 39 | *.png binary 40 | *.so binary 41 | *.war binary -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/util/LookupAccess.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.util; 2 | 3 | import java.lang.invoke.MethodHandles; 4 | 5 | import lombok.SneakyThrows; 6 | import lombok.val; 7 | 8 | public class LookupAccess { 9 | private static final MethodHandles.Lookup IMPL_LOOKUP; 10 | 11 | @SneakyThrows 12 | public static MethodHandles.Lookup privateLookupFor(Class clazz) { 13 | if (IMPL_LOOKUP != null) { 14 | return IMPL_LOOKUP; 15 | } 16 | 17 | MethodHandles.Lookup lookup = MethodHandles.lookup(); 18 | 19 | Object privateLookupIn = MethodHandles.class.getMethod("privateLookupIn", Class.class, MethodHandles.Lookup.class) 20 | .invoke(null, clazz, lookup); 21 | return (MethodHandles.Lookup) privateLookupIn; 22 | } 23 | 24 | static { 25 | MethodHandles.Lookup lookup = null; 26 | try { 27 | val field = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP"); 28 | field.setAccessible(true); 29 | lookup = (MethodHandles.Lookup) field.get(null); 30 | } catch (Throwable t) { 31 | System.err.println("Failed to get MethodHandles.Lookup.IMPL_LOOKUP"); 32 | t.printStackTrace(); 33 | } 34 | IMPL_LOOKUP = lookup; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/transform/InnerClassReferencer.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.transform; 2 | 3 | import java.lang.invoke.MethodHandles; 4 | 5 | import dev.minco.javatransformer.api.code.CodeFragment; 6 | 7 | public class InnerClassReferencer { 8 | public InnerClassExample.Inner test1() { 9 | throw new UnsupportedOperationException(); 10 | } 11 | 12 | public InnerClassExample.Inner2 test2() { 13 | throw new UnsupportedOperationException(); 14 | } 15 | 16 | public AnnotationInnerClassExample.TestEnum test3() { 17 | return AnnotationInnerClassExample.TestEnum.ONE; 18 | } 19 | 20 | public AnnotationInnerClassExample.TestEnum test4() { 21 | return AnnotationInnerClassExample.TestEnum.TWO; 22 | } 23 | 24 | public MethodHandles.Lookup test5() { 25 | throw new UnsupportedOperationException(); 26 | } 27 | 28 | public java.lang.invoke.MethodHandles.Lookup test6() { 29 | throw new UnsupportedOperationException(); 30 | } 31 | 32 | public CodeFragment.InsertionOptions test7() { 33 | return CodeFragment.InsertionOptions.DEFAULT; 34 | } 35 | 36 | public CodeFragment.InsertionOptions test8() { 37 | return CodeFragment.InsertionOptions.DEFAULT; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/transform/CodeFragmentTesting.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.transform; 2 | 3 | import java.io.PrintStream; 4 | import java.util.function.Consumer; 5 | 6 | import dev.minco.javatransformer.api.code.RETURN; 7 | 8 | @SuppressWarnings({"FieldCanBeLocal", "unused"}) 9 | public class CodeFragmentTesting { 10 | private Consumer callback; 11 | 12 | public CodeFragmentTesting(Consumer callback) { 13 | this.callback = callback; 14 | } 15 | 16 | public void alternateMethodCallDemos() { 17 | System.getProperties(); 18 | Runtime.getRuntime().totalMemory(); 19 | } 20 | 21 | public void testMethodCallExpression() { 22 | System.out.println("1"); 23 | System.out.println("2"); 24 | System.out.println("3"); 25 | System.out.println("4"); 26 | } 27 | 28 | private void callbackCaller(PrintStream out, String parameter) { 29 | callback.accept(parameter); 30 | } 31 | 32 | public boolean testAbortEarly() { 33 | System.setProperty("finishedTestAbortEarly", "true"); 34 | return false; 35 | } 36 | 37 | private void aborter() { 38 | throw RETURN.BOOLEAN(true); 39 | } 40 | 41 | private void methodToInsert() { 42 | throw RETURN.VOID(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/asm/DebugPrinter.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.asm; 2 | 3 | import java.io.PrintWriter; 4 | import java.io.StringWriter; 5 | 6 | import org.objectweb.asm.tree.AbstractInsnNode; 7 | import org.objectweb.asm.tree.InsnList; 8 | import org.objectweb.asm.tree.MethodNode; 9 | import org.objectweb.asm.util.Printer; 10 | import org.objectweb.asm.util.Textifier; 11 | import org.objectweb.asm.util.TraceMethodVisitor; 12 | 13 | public final class DebugPrinter { 14 | public static void printByteCode(MethodNode m, String operation) { 15 | InsnList inList = m.instructions; 16 | System.out.println(m.name + " at stage " + operation + " maxLocals " + m.maxLocals + " maxStack " + m.maxStack); 17 | AbstractInsnNode current = m.instructions.getFirst(); 18 | AbstractInsnNode last = m.instructions.getLast(); 19 | 20 | Printer printer = new Textifier(); 21 | TraceMethodVisitor mp = new TraceMethodVisitor(printer); 22 | while (true) { 23 | current.accept(mp); 24 | if (current == last) 25 | break; 26 | current = current.getNext(); 27 | } 28 | 29 | StringWriter sw = new StringWriter(); 30 | printer.print(new PrintWriter(sw)); 31 | printer.getText().clear(); 32 | System.out.println(sw); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/util/Splitter.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.util; 2 | 3 | import java.io.File; 4 | import java.util.function.Supplier; 5 | import java.util.stream.Stream; 6 | 7 | /** 8 | * like Guava's Splitter but much smaller and with far less options 9 | * 10 | * Don't want to depend on guava as it's massive 11 | */ 12 | public interface Splitter { 13 | Splitter commaSplitter = on(','); 14 | Splitter pathSplitter = on(File.pathSeparatorChar); 15 | 16 | static Splitter on(char c) { 17 | return s -> CollectionUtil.stream(new Supplier() { 18 | int base = -1; 19 | 20 | @Override 21 | public String get() { 22 | while (base != 0) { 23 | int base = this.base; 24 | if (base == -1) 25 | base = 0; 26 | 27 | int next = s.indexOf(c, base); 28 | 29 | int nextBase = next + 1; 30 | 31 | if (next == -1) 32 | next = s.length(); 33 | 34 | String part = s.substring(base, next).trim(); 35 | 36 | this.base = nextBase; 37 | 38 | if (!part.isEmpty()) 39 | return part; 40 | } 41 | 42 | return null; 43 | } 44 | }); 45 | } 46 | 47 | Stream split(String s); 48 | 49 | default Iterable splitIterable(String s) { 50 | return split(s)::iterator; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/util/CachingSupplier.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.util; 2 | 3 | import java.util.Objects; 4 | import java.util.function.Supplier; 5 | 6 | import lombok.EqualsAndHashCode; 7 | import lombok.NonNull; 8 | import lombok.ToString; 9 | 10 | import org.jetbrains.annotations.Nullable; 11 | 12 | @EqualsAndHashCode 13 | @ToString 14 | public final class CachingSupplier implements Supplier { 15 | @NonNull 16 | private final Supplier wrapped; 17 | private transient T value; 18 | 19 | private CachingSupplier(@NonNull Supplier wrapped) { 20 | this.wrapped = wrapped; 21 | } 22 | 23 | public static CachingSupplier of(Supplier wrapped) { 24 | if (wrapped instanceof CachingSupplier) { 25 | return (CachingSupplier) wrapped; 26 | } 27 | return new CachingSupplier<>(wrapped); 28 | } 29 | 30 | @Override 31 | public T get() { 32 | T value = this.value; 33 | 34 | if (value == null) { 35 | synchronized (this) { 36 | value = this.value; 37 | if (value == null) { 38 | this.value = value = Objects.requireNonNull(wrapped.get()); 39 | } 40 | } 41 | } 42 | 43 | return value; 44 | } 45 | 46 | public void set(@Nullable T value) { 47 | this.value = value; 48 | } 49 | 50 | public boolean isCached() { 51 | return value != null; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/api/code/IntermediateValue.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api.code; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.NonNull; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.ToString; 7 | import lombok.With; 8 | 9 | import org.jetbrains.annotations.Nullable; 10 | 11 | import dev.minco.javatransformer.api.Type; 12 | 13 | @RequiredArgsConstructor 14 | @ToString 15 | @With 16 | public final class IntermediateValue { 17 | public final static UNKNOWN_CONSTANT UNKNOWN = new UNKNOWN_CONSTANT(); 18 | @NonNull 19 | public final Type type; 20 | /** 21 | * null indicates a constant value of null, not an unknown value 22 | *

23 | * an unknown value is represented by UNKNOWN_CONSTANT 24 | * 25 | * @see UNKNOWN_CONSTANT 26 | */ 27 | @Nullable 28 | public final Object constantValue; 29 | 30 | public final Location location; 31 | 32 | public enum LocationType { 33 | STACK, 34 | LOCAL 35 | } 36 | 37 | @AllArgsConstructor 38 | @ToString 39 | public static class Location { 40 | @NonNull 41 | public final LocationType type; 42 | public final int index; 43 | @Nullable 44 | public final String name; 45 | } 46 | 47 | public final static class UNKNOWN_CONSTANT { 48 | UNKNOWN_CONSTANT() {} 49 | 50 | @Override 51 | public String toString() { 52 | return "Unknown or non-constant value"; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/api/Parameter.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import java.util.Collections; 4 | import java.util.List; 5 | import java.util.function.Supplier; 6 | 7 | import lombok.AccessLevel; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.Getter; 10 | import lombok.NonNull; 11 | 12 | import org.jetbrains.annotations.Nullable; 13 | 14 | @EqualsAndHashCode(exclude = {"annotationSupplier", "name"}) 15 | @Getter 16 | public class Parameter implements Annotated { 17 | @Nullable 18 | public final String name; 19 | @NonNull 20 | public final Type type; 21 | @Getter(AccessLevel.NONE) 22 | private final Supplier> annotationSupplier; 23 | 24 | private Parameter(@NonNull Type type, @Nullable String name, Supplier> annotationSupplier) { 25 | this.type = type; 26 | this.name = name; 27 | this.annotationSupplier = annotationSupplier; 28 | } 29 | 30 | public static Parameter of(Type type, String name, @Nullable Supplier> annotationSupplier) { 31 | return new Parameter(type, name, annotationSupplier); 32 | } 33 | 34 | @Override 35 | public List getAnnotations() { 36 | if (annotationSupplier == null) 37 | return Collections.emptyList(); 38 | return annotationSupplier.get(); 39 | } 40 | 41 | public String toString() { 42 | return "Parameter(name=" + this.getName() + ", type=" + this.getType() + ")"; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/util/DefineClass.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.util; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.invoke.MethodHandles; 5 | import java.lang.invoke.MethodType; 6 | import java.security.AccessController; 7 | import java.security.PrivilegedAction; 8 | import java.security.ProtectionDomain; 9 | 10 | import lombok.SneakyThrows; 11 | 12 | public final class DefineClass { 13 | private static final MethodHandles.Lookup $L = LookupAccess.privateLookupFor(ClassLoader.class); 14 | private static final ProtectionDomain PROTECTION_DOMAIN = AccessController.doPrivileged((PrivilegedAction) DefineClass.class::getProtectionDomain); 15 | private static final MethodHandle defineClassHandle = getDefineClassHandle(); 16 | 17 | private static MethodHandle getDefineClassHandle() { 18 | try { 19 | return $L.findSpecial(ClassLoader.class, "defineClass", MethodType.methodType(Class.class, String.class, byte[].class, Integer.TYPE, Integer.TYPE, ProtectionDomain.class), ClassLoader.class); 20 | } catch (NoSuchMethodException | IllegalAccessException e) { 21 | e.printStackTrace(); 22 | } 23 | return null; 24 | } 25 | 26 | @SneakyThrows 27 | @SuppressWarnings("unchecked") 28 | public static Class defineClass(ClassLoader classLoader, String name, byte[] bytes) { 29 | return (Class) defineClassHandle.invokeExact(classLoader, name, bytes, 0, bytes.length, PROTECTION_DOMAIN); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/api/code/RETURN.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api.code; 2 | 3 | import org.jetbrains.annotations.Contract; 4 | 5 | @SuppressWarnings("unused") 6 | public class RETURN { 7 | @Contract(" -> fail") 8 | public static ShouldNotBeCalledError VOID() { 9 | throw new ShouldNotBeCalledError(); 10 | } 11 | 12 | @Contract("_ -> fail") 13 | public static ShouldNotBeCalledError OBJECT(T value) { 14 | throw new ShouldNotBeCalledError(); 15 | } 16 | 17 | @Contract("_ -> fail") 18 | public static ShouldNotBeCalledError BYTE(byte value) { 19 | throw new ShouldNotBeCalledError(); 20 | } 21 | 22 | @Contract("_ -> fail") 23 | public static ShouldNotBeCalledError SHORT(short value) { 24 | throw new ShouldNotBeCalledError(); 25 | } 26 | 27 | @Contract("_ -> fail") 28 | public static ShouldNotBeCalledError INT(int value) { 29 | throw new ShouldNotBeCalledError(); 30 | } 31 | 32 | @Contract("_ -> fail") 33 | public static ShouldNotBeCalledError LONG(long value) { 34 | throw new ShouldNotBeCalledError(); 35 | } 36 | 37 | @Contract("_ -> fail") 38 | public static ShouldNotBeCalledError BOOLEAN(boolean value) { 39 | throw new ShouldNotBeCalledError(); 40 | } 41 | 42 | @Contract("_ -> fail") 43 | public static ShouldNotBeCalledError FLOAT(float value) { 44 | throw new ShouldNotBeCalledError(); 45 | } 46 | 47 | @Contract("_ -> fail") 48 | public static ShouldNotBeCalledError DOUBLE(double value) { 49 | throw new ShouldNotBeCalledError(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/internal/MethodNodeInfoTest.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal; 2 | 3 | import java.util.HashMap; 4 | 5 | import lombok.val; 6 | 7 | import org.junit.Assert; 8 | import org.junit.Test; 9 | import org.objectweb.asm.tree.MethodNode; 10 | 11 | import dev.minco.javatransformer.api.AccessFlags; 12 | import dev.minco.javatransformer.api.MethodInfo; 13 | import dev.minco.javatransformer.api.Parameter; 14 | import dev.minco.javatransformer.api.Type; 15 | 16 | public class MethodNodeInfoTest { 17 | @Test 18 | public void testWrap() throws Exception { 19 | MethodNode node = new MethodNode(); 20 | node.access = 1; 21 | node.name = "test"; 22 | node.desc = "()Ljava/lang/String;"; 23 | 24 | ByteCodeInfo b = new ByteCodeInfo(null, "java.lang.String", new HashMap<>()); 25 | MethodInfo info = b.wrap(node); 26 | 27 | Assert.assertEquals("test", info.getName()); 28 | Assert.assertEquals("java.lang.String", info.getReturnType().getClassName()); 29 | Assert.assertEquals(AccessFlags.ACC_PUBLIC, info.getAccessFlags().access); 30 | 31 | info.setReturnType(new Type("Ljava/lang/Boolean;")); 32 | Assert.assertEquals("()Ljava/lang/Boolean;", node.desc); 33 | 34 | val parameters = info.getParameters(); 35 | parameters.add(Parameter.of(new Type("Ljava/lang/String;", null), "test", null)); 36 | info.setParameters(parameters); 37 | 38 | Assert.assertEquals("(Ljava/lang/String;)Ljava/lang/Boolean;", ((ByteCodeInfo.MethodNodeInfo) info).getDescriptor()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/SimpleFieldInfo.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal; 2 | 3 | import java.util.List; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | 8 | import dev.minco.javatransformer.api.AccessFlags; 9 | import dev.minco.javatransformer.api.Annotation; 10 | import dev.minco.javatransformer.api.ClassInfo; 11 | import dev.minco.javatransformer.api.FieldInfo; 12 | import dev.minco.javatransformer.api.Type; 13 | 14 | @Data 15 | @AllArgsConstructor 16 | public class SimpleFieldInfo implements FieldInfo { 17 | public AccessFlags accessFlags; 18 | public String name; 19 | public Type type; 20 | public List annotations; 21 | public ClassInfo owner; 22 | 23 | public SimpleFieldInfo(AccessFlags accessFlags, Type type, String name) { 24 | this.accessFlags = accessFlags; 25 | this.type = type; 26 | this.name = name; 27 | } 28 | 29 | public static FieldInfo of(AccessFlags accessFlags, Type type, String name) { 30 | return new SimpleFieldInfo(accessFlags, type, name); 31 | } 32 | 33 | public static String toString(FieldInfo info) { 34 | return info.getAccessFlags().toString() + ' ' + info.getType() + ' ' + info.getName(); 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return toString(this); 40 | } 41 | 42 | @Override 43 | public ClassInfo getClassInfo() { 44 | return null; 45 | } 46 | 47 | @Override 48 | @SuppressWarnings("MethodDoesntCallSuperMethod") 49 | public FieldInfo clone() { 50 | return of(accessFlags, type, name); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/api/ClassPathTest.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import java.nio.file.Paths; 4 | import java.util.Objects; 5 | 6 | import lombok.val; 7 | 8 | import org.junit.Assert; 9 | import org.junit.Test; 10 | 11 | public class ClassPathTest { 12 | @Test 13 | public void checkAddReturnsCorrectValue() { 14 | val classPath = ClassPath.of(); 15 | Assert.assertTrue("path should be added successfully", classPath.addPath(Paths.get("test"))); 16 | Assert.assertFalse("path should not be added successfully", classPath.addPath(Paths.get("test"))); 17 | Assert.assertFalse("path should not be added successfully", classPath.addPath(Paths.get("./test"))); 18 | Assert.assertFalse("path should not be added successfully", classPath.addPath(Paths.get("./asds/../test"))); 19 | } 20 | 21 | @Test 22 | public void addAfterInitialsied() { 23 | val classPath = ClassPath.of(); 24 | Assert.assertTrue("path should be added successfully", classPath.addPath(Paths.get("test"))); 25 | // trigger initialisation 26 | Objects.requireNonNull(classPath.iterator()); 27 | Assert.assertTrue("path should be added successfully", classPath.addPath(Paths.get("test2"))); 28 | } 29 | 30 | @Test 31 | public void checkClassesInClassPath() { 32 | val classPath = ClassPath.of(); 33 | boolean foundObject = false; 34 | for (ClassInfo clazz : classPath) { 35 | if (clazz.getName().equals("java.lang.Object")) { 36 | foundObject = true; 37 | break; 38 | } 39 | } 40 | Assert.assertTrue("Should find java.lang.Object in " + classPath, foundObject); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/javaparser/CompilationUnitInfo.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.javaparser; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import com.github.javaparser.ast.CompilationUnit; 7 | import com.github.javaparser.ast.body.TypeDeclaration; 8 | 9 | import dev.minco.javatransformer.api.ClassPath; 10 | import dev.minco.javatransformer.internal.SourceInfo; 11 | 12 | public final class CompilationUnitInfo { 13 | public static List getSourceInfos(CompilationUnit compilationUnit, ClassPath classPath) { 14 | List sourceInfos = new ArrayList<>(); 15 | String packageName = compilationUnit.getPackageDeclaration().map(it -> it.getNameAsString() + '.').orElse(""); 16 | getSourceInfos(compilationUnit.getTypes(), classPath, sourceInfos, packageName); 17 | return sourceInfos; 18 | } 19 | 20 | @SuppressWarnings({"unchecked", "deprecation"}) 21 | public static void getSourceInfos(Iterable> typeDeclarations, ClassPath classPath, List sourceInfos, String packageName) { 22 | for (TypeDeclaration typeDeclaration : typeDeclarations) { 23 | sourceInfos.add(new SourceInfo(() -> typeDeclaration, packageName + typeDeclaration.getName(), classPath)); 24 | // suppressed deprecation warning for now 25 | // https://github.com/javaparser/javaparser/issues/1472#issuecomment-424327421 26 | getSourceInfos(typeDeclaration.getChildNodesByType((Class>) (Object) TypeDeclaration.class), classPath, sourceInfos, packageName + typeDeclaration.getNameAsString() + '$'); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/api/AnnotationTest.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import java.util.Collections; 4 | import java.util.HashMap; 5 | 6 | import lombok.val; 7 | 8 | import org.junit.Assert; 9 | import org.junit.Test; 10 | 11 | public class AnnotationTest { 12 | @Test 13 | public void testAnnotationConversion() { 14 | val override = Annotation.of(Type.of("java.lang.Override")).toInstance(Override.class); 15 | Assert.assertEquals(Override.class, override.annotationType()); 16 | val suppressWarning = Annotation.of(Type.of("java.lang.SuppressWarnings"), new String[]{"unchecked"}).toInstance(SuppressWarnings.class); 17 | Assert.assertArrayEquals(new String[]{"unchecked"}, suppressWarning.value()); 18 | } 19 | 20 | @Test 21 | public void testAnnotationConversionWithDefaultValues() { 22 | val annotationWithDefault = Annotation.of(Type.of("dev.minco.javatransformer.api.AnnotationWithDefault"), "changed").toInstance(AnnotationWithDefault.class); 23 | Assert.assertEquals(1, annotationWithDefault.index()); 24 | Assert.assertEquals("changed", annotationWithDefault.value()); 25 | } 26 | 27 | @Test 28 | public void testAnnotationConversionWithDefaultValuesAndUnmodifiableMap() { 29 | val map = new HashMap(); 30 | map.put("value", "changed2"); 31 | map.put("index", 2); 32 | val annotationWithDefault2 = Annotation.of(Type.of("dev.minco.javatransformer.api.AnnotationWithDefault"), Collections.unmodifiableMap(map)).toInstance(AnnotationWithDefault.class); 33 | Assert.assertEquals(2, annotationWithDefault2.index()); 34 | Assert.assertEquals("changed2", annotationWithDefault2.value()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/api/MethodInfo.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | import dev.minco.javatransformer.internal.SimpleMethodInfo; 7 | 8 | public interface MethodInfo extends ClassMember, HasTypeVariable, Cloneable { 9 | static MethodInfo of(AccessFlags accessFlags, List typeVariables, Type returnType, String name, Parameter... parameters) { 10 | return of(accessFlags, typeVariables, returnType, name, Arrays.asList(parameters)); 11 | } 12 | 13 | static MethodInfo of(AccessFlags accessFlags, List typeVariables, Type returnType, String name, List parameters) { 14 | return SimpleMethodInfo.of(accessFlags, typeVariables, returnType, name, parameters); 15 | } 16 | 17 | Type getReturnType(); 18 | 19 | void setReturnType(Type returnType); 20 | 21 | List getParameters(); 22 | 23 | void setParameters(List parameters); 24 | 25 | default void setAll(MethodInfo info) { 26 | this.setName(info.getName()); 27 | this.setAccessFlags(info.getAccessFlags()); 28 | this.setReturnType(info.getReturnType()); 29 | this.setParameters(info.getParameters()); 30 | this.setTypeVariables(info.getTypeVariables()); 31 | } 32 | 33 | default boolean similar(MethodInfo other) { 34 | return other.getName().equals(this.getName()) && 35 | other.getReturnType().similar(this.getReturnType()) && 36 | (other.getParameters() == null || this.getParameters() == null || other.getParameters().equals(this.getParameters())); 37 | } 38 | 39 | @SuppressWarnings("MethodDoesntCallSuperMethod") 40 | default MethodInfo clone() { 41 | throw new UnsupportedOperationException(); 42 | } 43 | 44 | default boolean isConstructor() { 45 | return getName().equals(""); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/SimpleMethodInfo.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | import lombok.Data; 8 | 9 | import org.jetbrains.annotations.Nullable; 10 | 11 | import dev.minco.javatransformer.api.AccessFlags; 12 | import dev.minco.javatransformer.api.Annotation; 13 | import dev.minco.javatransformer.api.ClassInfo; 14 | import dev.minco.javatransformer.api.MethodInfo; 15 | import dev.minco.javatransformer.api.Parameter; 16 | import dev.minco.javatransformer.api.Type; 17 | import dev.minco.javatransformer.api.TypeVariable; 18 | 19 | @Data 20 | public class SimpleMethodInfo implements MethodInfo { 21 | public AccessFlags accessFlags; 22 | public List typeVariables; 23 | public Type returnType; 24 | public String name; 25 | public List parameters; 26 | public List annotations; 27 | @Nullable 28 | public ClassInfo classInfo; 29 | 30 | private SimpleMethodInfo(AccessFlags accessFlags, List typeVariables, Type returnType, String name, List parameters) { 31 | this.accessFlags = accessFlags; 32 | this.typeVariables = new ArrayList<>(typeVariables); 33 | this.returnType = returnType; 34 | this.name = name; 35 | this.parameters = new ArrayList<>(parameters); 36 | } 37 | 38 | public static SimpleMethodInfo of(AccessFlags accessFlags, List typeVariables, Type returnType, String name, List parameters) { 39 | return new SimpleMethodInfo(accessFlags, typeVariables, returnType, name, parameters); 40 | } 41 | 42 | public static String toString(MethodInfo info) { 43 | return info.getAccessFlags().toString() + ' ' + info.getReturnType() + ' ' + info.getName() + '(' + info.getParameters() + ')'; 44 | } 45 | 46 | public List getParameters() { 47 | return Collections.unmodifiableList(parameters); 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | return toString(this); 53 | } 54 | 55 | @Override 56 | @SuppressWarnings("MethodDoesntCallSuperMethod") 57 | public MethodInfo clone() { 58 | return of(accessFlags, typeVariables, returnType, name, parameters); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/api/SourceCodeFragmentTest.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import java.nio.file.Files; 4 | import java.nio.file.Path; 5 | import java.nio.file.Paths; 6 | import java.util.List; 7 | import java.util.concurrent.atomic.AtomicBoolean; 8 | import java.util.stream.Collectors; 9 | 10 | import lombok.SneakyThrows; 11 | import lombok.val; 12 | 13 | import org.junit.Assert; 14 | import org.junit.Rule; 15 | import org.junit.Test; 16 | import org.junit.rules.TemporaryFolder; 17 | 18 | import dev.minco.javatransformer.api.code.CodeFragment; 19 | 20 | public class SourceCodeFragmentTest { 21 | @Rule 22 | public TemporaryFolder folder = new TemporaryFolder(); 23 | 24 | @SneakyThrows 25 | @Test 26 | public void TestThing() { 27 | val input = Paths.get("src/test/java/"); 28 | val output = folder.newFolder("output").toPath(); 29 | val jt = new JavaTransformer(); 30 | jt.getClassPath().addPath(input); 31 | val hasRan = new AtomicBoolean(); 32 | val clazz = "dev.minco.javatransformer.transform.codefragments.InsertBeforeTest"; 33 | jt.addTransformer(clazz, (cI) -> { 34 | List methods = cI.getMethods().collect(Collectors.toList()); 35 | MethodInfo testMethod = methods.stream().filter((MethodInfo it) -> it.getName().equals("testMethod")).findFirst().get(); 36 | MethodInfo toInsert = methods.stream().filter((MethodInfo it) -> it.getName().equals("toInsert")).findFirst().get(); 37 | testMethod.getCodeFragment().insert(toInsert.getCodeFragment(), CodeFragment.InsertionPosition.BEFORE); 38 | hasRan.set(true); 39 | }); 40 | jt.transform(input, output); 41 | Assert.assertTrue("Must have transformed class", hasRan.get()); 42 | assertFilesEqual(input.resolve(clazz.replace(".", "/") + ".java_patched"), 43 | output.resolve(clazz.replace(".", "/") + ".java")); 44 | } 45 | 46 | @SneakyThrows 47 | private void assertFilesEqual(Path expect, Path actual) { 48 | String expectStr = new String(Files.readAllBytes(expect)) 49 | .replaceAll("[ \t]+", " ") 50 | .replace("\r", ""); 51 | String actualStr = new String(Files.readAllBytes(actual)).replaceAll("[ \t]+", " ") 52 | .replace("\r", ""); 53 | 54 | Assert.assertEquals(expectStr, actualStr); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/api/TypeTest.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import lombok.val; 4 | 5 | import org.junit.Assert; 6 | import org.junit.Test; 7 | 8 | public class TypeTest { 9 | @Test 10 | public void testTypeParameter() throws Exception { 11 | Type typeParameter = new Type("Ljava/lang/Object;", "TT;"); 12 | Assert.assertTrue(typeParameter.isClassType()); 13 | Assert.assertTrue(typeParameter.isTypeParameter()); 14 | Assert.assertFalse(typeParameter.isPrimitiveType()); 15 | 16 | Assert.assertEquals("T", typeParameter.getTypeParameterName()); 17 | Assert.assertEquals("java.lang.Object", typeParameter.getClassName()); 18 | } 19 | 20 | @Test 21 | public void testClassParameter() throws Exception { 22 | Type typeParameter = new Type("Ljava/lang/Object;", null); 23 | Assert.assertTrue(typeParameter.isClassType()); 24 | Assert.assertFalse(typeParameter.isTypeParameter()); 25 | Assert.assertFalse(typeParameter.isPrimitiveType()); 26 | 27 | Assert.assertEquals("java.lang.Object", typeParameter.getClassName()); 28 | } 29 | 30 | @Test 31 | public void testPrimitiveParameter() throws Exception { 32 | Type typeParameter = new Type("Z", null); 33 | Assert.assertFalse(typeParameter.isClassType()); 34 | Assert.assertFalse(typeParameter.isTypeParameter()); 35 | Assert.assertTrue(typeParameter.isPrimitiveType()); 36 | 37 | Assert.assertEquals("boolean", typeParameter.getPrimitiveTypeName()); 38 | } 39 | 40 | @Test 41 | public void testOf() throws Exception { 42 | testOf("java.lang.String", "Ljava/lang/String;"); 43 | testOf("nallar.test.Outer", "Lnallar/test/Outer;"); 44 | testOf("nallar.test.Outer.Inner", "Lnallar/test/Outer$Inner;"); 45 | testOf("BadNamingScheme.test.Outer", "LBadNamingScheme/test/Outer;"); 46 | testOf("BadNamingScheme.test.Outer.Inner", "LBadNamingScheme/test/Outer$Inner;"); 47 | testOf("BadNamingScheme.Test.Outer.Inner", "LBadNamingScheme$Test$Outer$Inner;"); 48 | } 49 | 50 | @Test 51 | public void testWithGenericType() { 52 | val arrayDequeCallable = Type.of("java.util.ArrayDeque").withTypeArgument(Type.of("java.util.Callable")); 53 | Assert.assertEquals("java.util.ArrayDeque", arrayDequeCallable.getClassName()); 54 | Assert.assertEquals("java.util.Callable", arrayDequeCallable.getTypeArguments().get(0).getClassName()); 55 | } 56 | 57 | private void testOf(String in, String expected) { 58 | Assert.assertEquals(expected, Type.of(in).descriptor); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/asm/FilteringClassWriter.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.asm; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import lombok.val; 7 | 8 | import org.objectweb.asm.ClassReader; 9 | import org.objectweb.asm.ClassWriter; 10 | 11 | public class FilteringClassWriter extends ClassWriter { 12 | public final Map filters = new HashMap<>(); 13 | 14 | public FilteringClassWriter(int flags) { 15 | super(flags); 16 | } 17 | 18 | public FilteringClassWriter(ClassReader classReader, int flags) { 19 | super(classReader, flags); 20 | } 21 | 22 | public static void addFilter(Map filters, String a, String b) { 23 | filters.put(a, b); 24 | val a2 = a.replace('.', '/'); 25 | val b2 = b.replace('.', '/'); 26 | filters.put(a2, b2); 27 | filters.put('L' + a2 + ';', 'L' + b2 + ';'); 28 | } 29 | 30 | @Override 31 | public int newClass(String value) { 32 | return super.newClass(replace(value)); 33 | } 34 | 35 | @Override 36 | public int newConst(Object value) { 37 | if (value instanceof String) { 38 | value = replace((String) value); 39 | } 40 | 41 | return super.newConst(value); 42 | } 43 | 44 | @Override 45 | public int newNameType(final String name, final String desc) { 46 | return super.newNameType(name, replace(desc)); 47 | } 48 | 49 | @Override 50 | public int newHandle(int tag, String owner, String name, String descriptor, boolean isInterface) { 51 | return super.newHandle(tag, replace(owner), name, descriptor, isInterface); 52 | } 53 | 54 | @Override 55 | @SuppressWarnings("deprecation") 56 | public int newHandle(int tag, String owner, String name, String desc) { 57 | return super.newHandle(tag, replace(owner), name, desc); 58 | } 59 | 60 | @Override 61 | public int newField(String owner, String name, String desc) { 62 | return super.newField(replace(owner), name, desc); 63 | } 64 | 65 | private String replace(String s) { 66 | val replaced = filters.get(s); 67 | return replaced == null ? s : replaced; 68 | } 69 | 70 | @Override 71 | public int newUTF8(String value) { 72 | return super.newUTF8(replace(value)); 73 | } 74 | 75 | @Override 76 | protected String getCommonSuperClass(final String a, final String b) { 77 | if ((a.indexOf('.') != -1 && !a.startsWith("java.")) || (b.indexOf('.') != -1 && !b.startsWith("java."))) 78 | throw new UnsupportedOperationException(); 79 | 80 | return super.getCommonSuperClass(a, b); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/api/ClassPath.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import java.nio.file.Path; 4 | import java.util.List; 5 | 6 | import javax.annotation.Nonnull; 7 | import javax.annotation.Nullable; 8 | 9 | import org.jetbrains.annotations.Contract; 10 | 11 | import dev.minco.javatransformer.internal.ClassPaths; 12 | 13 | public interface ClassPath extends Iterable { 14 | /** 15 | * Returns whether the given class name exists 16 | * 17 | * @param className class name in JLS format: {@code package1.package2.ClassName}, {@code package1.package2.ClassName$InnerClass} 18 | * @return true if the class exists 19 | */ 20 | @Contract(value = "null -> fail", pure = true) 21 | default boolean classExists(@Nonnull String className) { 22 | return getClassInfo(className) != null; 23 | } 24 | 25 | /** 26 | * Returns the class info for the given class 27 | * 28 | * This instance is only guaranteed to provide information required for type resolution. Other information such as method bodies and annotations may be stripped 29 | * 30 | * @param className class name in JLS format: {@code package1.package2.ClassName}, {@code package1.package2.ClassName$InnerClass} 31 | * @return ClassInfo for the given name, or null if not found 32 | */ 33 | @Contract(value = "null -> fail", pure = true) 34 | @Nullable 35 | ClassInfo getClassInfo(@Nonnull String className); 36 | 37 | /** 38 | * Adds the given paths to this {@link ClassPath} 39 | * 40 | * @param paths Paths to add 41 | */ 42 | default void addPaths(List paths) { 43 | for (Path extraPath : paths) 44 | addPath(extraPath); 45 | } 46 | 47 | /** 48 | * Adds a path to this {@link ClassPath} 49 | * 50 | * @param path Path to add 51 | * @return True if the path was added, false if it was already in this classpath 52 | * @throws UnsupportedOperationException if this {@link ClassPath} is immutable 53 | */ 54 | boolean addPath(Path path); 55 | 56 | /** 57 | * Checks if the given path is in this {@link ClassPath} 58 | * 59 | * @param path Path to check 60 | * @return True if this {@link ClassPath} contains the given path 61 | */ 62 | boolean hasPath(Path path); 63 | 64 | @Contract(pure = true) 65 | static @Nonnull ClassPath of(@Nonnull Path... paths) { 66 | return of(ClassPaths.SystemClassPath.SYSTEM_CLASS_PATH, paths); 67 | } 68 | 69 | @Contract(pure = true) 70 | static @Nonnull ClassPath of(@Nullable ClassPath parent, Path... paths) { 71 | return ClassPaths.of(parent, paths); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/internal/MethodDescriptorTest.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal; 2 | 3 | import org.junit.Assert; 4 | 5 | import dev.minco.javatransformer.api.Parameter; 6 | import dev.minco.javatransformer.api.Type; 7 | 8 | public class MethodDescriptorTest { 9 | @org.junit.Test 10 | public void testGetReturnType() throws Exception { 11 | MethodDescriptor d = new MethodDescriptor("()Ljava/lang/String;", null); 12 | Type t = d.getReturnType(); 13 | 14 | Assert.assertEquals("Ljava/lang/String;", t.descriptor); 15 | Assert.assertEquals(null, t.signature); 16 | Assert.assertEquals("java.lang.String", t.getClassName()); 17 | } 18 | 19 | @org.junit.Test 20 | public void testParameters() throws Exception { 21 | MethodDescriptor d = new MethodDescriptor("(Ljava/lang/Object;Ljava/util/ArrayList;)Ljava/lang/String;", "(TT;TA;)Ljava/lang/String;"); 22 | Parameter first = d.getParameters().get(0); 23 | Parameter second = d.getParameters().get(1); 24 | 25 | Assert.assertEquals("java.lang.Object", first.type.getClassName()); 26 | Assert.assertEquals("T", first.type.getTypeParameterName()); 27 | 28 | Assert.assertEquals("java.util.ArrayList", second.type.getClassName()); 29 | Assert.assertEquals("A", second.type.getTypeParameterName()); 30 | } 31 | 32 | @org.junit.Test 33 | public void testParametersWithTypeParamInSignature() throws Exception { 34 | MethodDescriptor d = new MethodDescriptor("(Ljava/lang/Class;)Ljava/lang/annotation/Annotation;", "(Ljava/lang/Class;)TT;"); 35 | Parameter first = d.getParameters().get(0); 36 | 37 | Assert.assertEquals("java.lang.Class", first.type.getClassName()); 38 | Assert.assertEquals("T", first.type.getTypeArguments().get(0).getTypeParameterName()); 39 | 40 | Assert.assertEquals("T", d.getReturnType().getTypeParameterName()); 41 | } 42 | 43 | @org.junit.Test 44 | public void testParametersWithTypeParam2() throws Exception { 45 | /* 46 | dev.minco.javatransformer.api.TransformationException: Failed to parse method parameters in unmodifiableMap: 47 | name: unmodifiableMap 48 | descriptor: (Ljava/util/Map;)Ljava/util/Map; 49 | signature:(Ljava/util/Map<+TK;+TV;>;)Ljava/util/Map; 50 | */ 51 | MethodDescriptor d = new MethodDescriptor("(Ljava/util/Map;)Ljava/util/Map;", "(Ljava/util/Map<+TK;+TV;>;)Ljava/util/Map;"); 52 | Parameter first = d.getParameters().get(0); 53 | 54 | Assert.assertEquals("java.util.Map", first.type.getClassName()); 55 | // TODO: +TK; = ? extends K 56 | /* 57 | Assert.assertEquals("T", first.type.getTypeArguments().get(0).getTypeParameterName()); 58 | 59 | Assert.assertEquals("T", d.getReturnType().getTypeParameterName()); 60 | */ 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/api/ClassInfo.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import java.util.List; 4 | import java.util.stream.Stream; 5 | 6 | import org.jetbrains.annotations.Contract; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | import dev.minco.javatransformer.internal.util.CollectionUtil; 10 | 11 | public interface ClassInfo extends ClassMember, HasTypeVariable { 12 | default void add(ClassMember member) { 13 | if (member instanceof MethodInfo) 14 | add((MethodInfo) member); 15 | else if (member instanceof FieldInfo) 16 | add((FieldInfo) member); 17 | else 18 | throw new TransformationException("Can't add member of type " + member.getClass().getCanonicalName() + " to " + this); 19 | } 20 | 21 | void add(MethodInfo method); 22 | 23 | void add(FieldInfo field); 24 | 25 | default void remove(ClassMember member) { 26 | if (member instanceof MethodInfo) 27 | remove((MethodInfo) member); 28 | else if (member instanceof FieldInfo) 29 | remove((FieldInfo) member); 30 | else 31 | throw new TransformationException("Can't remove member of type " + member.getClass().getCanonicalName() + " to " + this); 32 | } 33 | 34 | void remove(MethodInfo method); 35 | 36 | void remove(FieldInfo field); 37 | 38 | @Nullable 39 | Type getSuperType(); 40 | 41 | @Contract(pure = true) 42 | List getInterfaceTypes(); 43 | 44 | @Contract(pure = true) 45 | @Nullable 46 | default ClassMember get(ClassMember member) { 47 | if (member instanceof MethodInfo) 48 | return get((MethodInfo) member); 49 | else if (member instanceof FieldInfo) 50 | return get((FieldInfo) member); 51 | else 52 | throw new TransformationException("Can't get member of type " + member.getClass().getCanonicalName() + " in " + this); 53 | } 54 | 55 | @Contract(pure = true) 56 | @Nullable 57 | default MethodInfo get(MethodInfo like) { 58 | for (MethodInfo methodInfo : CollectionUtil.iterable(getMethods())) { 59 | if (like.similar(methodInfo)) 60 | return methodInfo; 61 | } 62 | 63 | return null; 64 | } 65 | 66 | @Contract(pure = true) 67 | @Nullable 68 | default FieldInfo get(FieldInfo like) { 69 | for (FieldInfo fieldInfo : CollectionUtil.iterable(getFields())) { 70 | if (like.similar(fieldInfo)) 71 | return fieldInfo; 72 | } 73 | 74 | return null; 75 | } 76 | 77 | @Contract(pure = true) 78 | default Type getType() { 79 | return Type.of(getName()); 80 | } 81 | 82 | @Contract(pure = true) 83 | Stream getMethods(); 84 | 85 | @Contract(pure = true) 86 | Stream getFields(); 87 | 88 | @Contract(pure = true) 89 | default Stream getConstructors() { 90 | return getMethods().filter(MethodInfo::isConstructor); 91 | } 92 | 93 | @Contract(pure = true) 94 | default Stream getMembers() { 95 | return Stream.concat(getFields(), getMethods()); 96 | } 97 | 98 | @Override 99 | default ClassInfo getClassInfo() { 100 | return this; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/util/TypeUtil.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.util; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.function.Supplier; 6 | import java.util.stream.Stream; 7 | 8 | import lombok.NonNull; 9 | import lombok.val; 10 | 11 | import org.jetbrains.annotations.Contract; 12 | import org.jetbrains.annotations.Nullable; 13 | 14 | import dev.minco.javatransformer.api.TransformationException; 15 | 16 | public final class TypeUtil { 17 | @Contract("null, _ -> null; !null, true -> _; !null, false -> !null") 18 | @Nullable 19 | public static List splitTypes(@Nullable final String signature, boolean isSignature) { 20 | if (signature == null) 21 | return null; 22 | 23 | val types = new ArrayList(); 24 | int pos = 0; 25 | while (pos < signature.length()) { 26 | val type = readType(signature, pos, isSignature); 27 | pos += type.length(); 28 | types.add(type); 29 | } 30 | 31 | if (isSignature && types.isEmpty()) 32 | return null; 33 | 34 | return types; 35 | } 36 | 37 | public static Stream readTypes(@NonNull String in, boolean isSignature) { 38 | return CollectionUtil.stream(new Supplier() { 39 | int pos = 0; 40 | 41 | @Nullable 42 | @Override 43 | public String get() { 44 | if (pos < in.length()) { 45 | String next = readType(in, pos, isSignature); 46 | pos += next.length(); 47 | return next; 48 | } 49 | return null; 50 | } 51 | }); 52 | } 53 | 54 | public static String readType(String in, int pos, boolean isSignature) { 55 | int startPos = pos; 56 | char c; 57 | String arrayLevel = ""; 58 | String name; 59 | while (pos < in.length()) 60 | switch (c = in.charAt(pos++)) { 61 | case 'Z': 62 | case 'C': 63 | case 'B': 64 | case 'S': 65 | case 'I': 66 | case 'F': 67 | case 'J': 68 | case 'D': 69 | case 'V': 70 | return arrayLevel + c; 71 | 72 | case '[': 73 | arrayLevel += '['; 74 | break; 75 | 76 | case 'T': 77 | int end = in.indexOf(';', pos); 78 | name = in.substring(pos, end); 79 | return arrayLevel + 'T' + name + ';'; 80 | case 'L': 81 | int start = pos; 82 | int genericCount = 0; 83 | while (pos < in.length()) 84 | switch (in.charAt(pos++)) { 85 | case ';': 86 | if (genericCount > 0) 87 | break; 88 | name = in.substring(start, pos); 89 | return arrayLevel + 'L' + name; 90 | case '<': 91 | if (!isSignature) 92 | throw new TransformationException("Illegal character '<' in descriptor: " + in); 93 | genericCount++; 94 | break; 95 | case '>': 96 | genericCount--; 97 | break; 98 | } 99 | break; 100 | default: 101 | throw new TransformationException("Unexpected character '" + c + "' in signature/descriptor '" + in + "' Searched section '" + in.substring(startPos, pos) + "'"); 102 | } 103 | 104 | throw new StringIndexOutOfBoundsException("Reached " + pos + " in " + in); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/util/CollectionUtil.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.util; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collection; 5 | import java.util.Iterator; 6 | import java.util.NoSuchElementException; 7 | import java.util.function.Supplier; 8 | import java.util.stream.Stream; 9 | import java.util.stream.StreamSupport; 10 | 11 | import javax.annotation.Nonnull; 12 | 13 | import lombok.RequiredArgsConstructor; 14 | import lombok.val; 15 | 16 | public final class CollectionUtil { 17 | @SafeVarargs 18 | @SuppressWarnings("varargs") 19 | public static Stream union(Collection... collections) { 20 | return union(Arrays.asList(collections)); 21 | } 22 | 23 | @SafeVarargs 24 | @SuppressWarnings({"varargs", "unchecked"}) 25 | public static Iterator union(Iterable... iterables) { 26 | Iterator[] iterators = (Iterator[]) new Iterator[iterables.length]; 27 | for (int i = 0; i < iterators.length; i++) { 28 | iterators[i] = iterables[i].iterator(); 29 | } 30 | return new IteratorIterator<>(iterators); 31 | } 32 | 33 | public static Stream union(Collection> collections) { 34 | return collections.stream().flatMap(x -> x == null ? Stream.empty() : x.stream()); 35 | } 36 | 37 | public static Stream stream(Supplier supplier) { 38 | return stream(iterable(supplier)); 39 | } 40 | 41 | public static Stream stream(Iterable iterable) { 42 | if (iterable instanceof Collection) 43 | return ((Collection) iterable).stream(); 44 | 45 | return StreamSupport.stream(iterable.spliterator(), false); 46 | } 47 | 48 | public static Iterable iterable(Stream supplier) { 49 | return supplier::iterator; 50 | } 51 | 52 | public static Iterable iterable(Supplier supplier) { 53 | return () -> new IteratorFromSupplier<>(supplier); 54 | } 55 | 56 | public static boolean equals(Collection a, Collection b, Comparer c) { 57 | if (a.size() != b.size()) 58 | return false; 59 | val bIterator = b.iterator(); 60 | for (val aa : a) { 61 | val bb = bIterator.next(); 62 | if (!c.compare(aa, bb)) 63 | return false; 64 | } 65 | return true; 66 | } 67 | 68 | @FunctionalInterface 69 | public interface Comparer { 70 | boolean compare(T a, T b); 71 | } 72 | 73 | private static class IteratorFromSupplier implements Iterator { 74 | private final Supplier supplier; 75 | private T next; 76 | 77 | public IteratorFromSupplier(Supplier supplier) { 78 | this.supplier = supplier; 79 | next = supplier.get(); 80 | } 81 | 82 | @Override 83 | public boolean hasNext() { 84 | return next != null; 85 | } 86 | 87 | @Override 88 | public T next() { 89 | if (next == null) 90 | throw new NoSuchElementException(); 91 | 92 | try { 93 | return next; 94 | } finally { 95 | next = supplier.get(); 96 | } 97 | } 98 | } 99 | 100 | @RequiredArgsConstructor 101 | private static class IteratorIterator implements Iterator { 102 | @Nonnull 103 | private final Iterator[] iterators; 104 | private int current; 105 | 106 | @Override 107 | public boolean hasNext() { 108 | while (current < iterators.length && !iterators[current].hasNext()) 109 | current++; 110 | 111 | return current < iterators.length; 112 | } 113 | 114 | @Override 115 | public T next() { 116 | if (!hasNext()) 117 | throw new NoSuchElementException(); 118 | 119 | return iterators[current].next(); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/util/AnnotationParser.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.util; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.stream.Collectors; 9 | 10 | import lombok.val; 11 | 12 | import org.objectweb.asm.ClassReader; 13 | import org.objectweb.asm.ClassVisitor; 14 | import org.objectweb.asm.Opcodes; 15 | import org.objectweb.asm.tree.AnnotationNode; 16 | 17 | import com.github.javaparser.ast.expr.AnnotationExpr; 18 | import com.github.javaparser.ast.expr.MarkerAnnotationExpr; 19 | import com.github.javaparser.ast.expr.MemberValuePair; 20 | import com.github.javaparser.ast.expr.NormalAnnotationExpr; 21 | import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr; 22 | 23 | import dev.minco.javatransformer.api.Annotation; 24 | import dev.minco.javatransformer.api.TransformationException; 25 | import dev.minco.javatransformer.api.Type; 26 | import dev.minco.javatransformer.internal.ResolutionContext; 27 | import dev.minco.javatransformer.internal.javaparser.Expressions; 28 | 29 | public final class AnnotationParser { 30 | public static List parseAnnotations(byte[] bytes) { 31 | ClassReader cr = new ClassReader(bytes); 32 | AnnotationVisitor cv = new AnnotationVisitor(); 33 | cr.accept(cv, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); 34 | 35 | return cv.annotations.stream().map(AnnotationParser::annotationFromAnnotationNode).collect(Collectors.toList()); 36 | } 37 | 38 | public static Annotation annotationFromAnnotationNode(AnnotationNode annotationNode) { 39 | return Annotation.of(new Type(annotationNode.desc), getAnnotationNodeValues(annotationNode)); 40 | } 41 | 42 | private static Map getAnnotationNodeValues(AnnotationNode annotationNode) { 43 | if (annotationNode.values == null) 44 | return Collections.emptyMap(); 45 | 46 | Map values = new HashMap<>(); 47 | for (int i = 0; i < annotationNode.values.size(); i += 2) { 48 | values.put((String) annotationNode.values.get(i), annotationNode.values.get(i + 1)); 49 | } 50 | return values; 51 | } 52 | 53 | public static Annotation annotationFromAnnotationExpr(AnnotationExpr annotationExpr, ResolutionContext context) { 54 | Type t = context.resolve(NodeUtil.qualifiedName(annotationExpr.getName())); 55 | if (annotationExpr instanceof SingleMemberAnnotationExpr) { 56 | return Annotation.of(t, Expressions.expressionToValue(((SingleMemberAnnotationExpr) annotationExpr).getMemberValue(), context)); 57 | } else if (annotationExpr instanceof NormalAnnotationExpr) { 58 | val map = new HashMap(); 59 | for (MemberValuePair memberValuePair : ((NormalAnnotationExpr) annotationExpr).getPairs()) { 60 | map.put(memberValuePair.getName().asString(), Expressions.expressionToValue(memberValuePair.getValue(), context)); 61 | } 62 | return Annotation.of(t, map); 63 | } else if (annotationExpr instanceof MarkerAnnotationExpr) { 64 | return Annotation.of(t); 65 | } 66 | throw new TransformationException("Unknown annotation type: " + annotationExpr.getClass().getCanonicalName()); 67 | } 68 | 69 | private static class AnnotationVisitor extends ClassVisitor { 70 | public final List annotations = new ArrayList<>(); 71 | 72 | public AnnotationVisitor() { 73 | super(Opcodes.ASM5); 74 | } 75 | 76 | @Override 77 | public org.objectweb.asm.AnnotationVisitor visitAnnotation(final String desc, final boolean visible) { 78 | AnnotationNode an = new AnnotationNode(desc); 79 | annotations.add(an); 80 | return an; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/util/NodeUtil.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.util; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.function.Consumer; 6 | import java.util.function.Function; 7 | 8 | import lombok.val; 9 | 10 | import com.github.javaparser.ast.Node; 11 | import com.github.javaparser.ast.NodeList; 12 | import com.github.javaparser.ast.expr.LambdaExpr; 13 | import com.github.javaparser.ast.expr.Name; 14 | import com.github.javaparser.ast.expr.ObjectCreationExpr; 15 | import com.github.javaparser.ast.nodeTypes.NodeWithTypeParameters; 16 | import com.github.javaparser.ast.type.TypeParameter; 17 | 18 | public final class NodeUtil { 19 | public static void forChildren(Node node, Consumer nodeConsumer) { 20 | forChildren(node, nodeConsumer, Node.class); 21 | } 22 | 23 | @SuppressWarnings("unchecked") 24 | public static void forChildren(Node node, Consumer nodeConsumer, Class ofClass) { 25 | for (Node child : node.getChildNodes()) { 26 | if (ofClass.isInstance(child)) 27 | nodeConsumer.accept((T) child); 28 | 29 | forChildren(child, nodeConsumer, ofClass); 30 | } 31 | } 32 | 33 | public static List getFromList(Node node, Function> getter) { 34 | List parameters = new ArrayList<>(); 35 | 36 | while (true) { 37 | if (node == null) 38 | return parameters; 39 | 40 | List extra = getter.apply(node); 41 | if (extra != null && !extra.isEmpty()) { 42 | parameters.addAll(extra); 43 | } 44 | 45 | node = node.getParentNode().orElse(null); 46 | } 47 | } 48 | 49 | public static List getFromSingle(Node node, Function getter) { 50 | List parameters = null; 51 | 52 | while (true) { 53 | if (node == null) 54 | return parameters; 55 | 56 | ResultType extra = getter.apply(node); 57 | if (extra != null) { 58 | if (parameters == null) { 59 | parameters = new ArrayList<>(); 60 | } 61 | 62 | parameters.add(extra); 63 | } 64 | 65 | node = node.getParentNode().orElse(null); 66 | } 67 | } 68 | 69 | public static List getTypeParameters(Node node) { 70 | return getFromList(node, NodeUtil::getTypeParametersOnly); 71 | } 72 | 73 | private static NodeList getTypeParametersOnly(Node node) { 74 | if (node instanceof NodeWithTypeParameters) { 75 | return ((NodeWithTypeParameters) node).getTypeParameters(); 76 | } 77 | 78 | return null; 79 | } 80 | 81 | @SuppressWarnings("unchecked") 82 | public static T getParentNode(Node node, Class target) { 83 | while (true) { 84 | node = node.getParentNode().orElse(null); 85 | if (node == null || target.isAssignableFrom(node.getClass())) { 86 | return (T) node; 87 | } 88 | } 89 | } 90 | 91 | public static String qualifiedName(Name name) { 92 | return name.asString(); 93 | } 94 | 95 | public static List findWithinMethodScope(Class clazz, Node node) { 96 | val list = new ArrayList(); 97 | findScopedLocals(clazz, node, list); 98 | return list; 99 | } 100 | 101 | @SuppressWarnings("unchecked") 102 | private static void findScopedLocals(Class clazz, Node node, List results) { 103 | for (Node childNode : node.getChildNodes()) { 104 | if (childNode instanceof LambdaExpr || childNode instanceof ObjectCreationExpr) { 105 | continue; 106 | } 107 | if (clazz.isAssignableFrom(childNode.getClass())) { 108 | results.add((T) childNode); 109 | } 110 | findScopedLocals(clazz, childNode, results); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/MethodDescriptor.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal; 2 | 3 | import java.util.List; 4 | 5 | import lombok.NonNull; 6 | import lombok.ToString; 7 | import lombok.val; 8 | 9 | import org.jetbrains.annotations.Nullable; 10 | import org.objectweb.asm.tree.MethodNode; 11 | 12 | import dev.minco.javatransformer.api.Parameter; 13 | import dev.minco.javatransformer.api.Type; 14 | import dev.minco.javatransformer.api.TypeVariable; 15 | 16 | @ToString 17 | public class MethodDescriptor { 18 | private final List typeVariables; 19 | private final List parameters; 20 | private final Type returnType; 21 | 22 | public MethodDescriptor(List typeVariables, List parameters, Type returnType) { 23 | this.typeVariables = typeVariables; 24 | this.parameters = parameters; 25 | this.returnType = returnType; 26 | } 27 | 28 | public MethodDescriptor(@NonNull String descriptor, @Nullable String signature) { 29 | this(descriptor, signature, null); 30 | } 31 | 32 | public MethodDescriptor(MethodNode node) { 33 | this(Signature.getTypeVariables(node.signature), Signature.getParameters(node), Signature.getReturnType(node.desc, node.signature)); 34 | } 35 | 36 | public MethodDescriptor(String descriptor, @Nullable String signature, @Nullable List parameterNames) { 37 | this(Signature.getTypeVariables(signature), Signature.getParameters(descriptor, signature, parameterNames, null, null), Signature.getReturnType(descriptor, signature)); 38 | } 39 | 40 | public MethodDescriptor withTypeVariables(List typeVariables) { 41 | return new MethodDescriptor(typeVariables, parameters, returnType); 42 | } 43 | 44 | public MethodDescriptor withParameters(List parameters) { 45 | return new MethodDescriptor(typeVariables, parameters, returnType); 46 | } 47 | 48 | public MethodDescriptor withReturnType(Type returnType) { 49 | return new MethodDescriptor(typeVariables, parameters, returnType); 50 | } 51 | 52 | public void saveTo(MethodNode node) { 53 | node.desc = getDescriptor(); 54 | node.signature = getSignature(); 55 | } 56 | 57 | public String getDescriptor() { 58 | StringBuilder desc = new StringBuilder("("); 59 | 60 | for (Parameter parameter : parameters) { 61 | desc.append(parameter.type.descriptor); 62 | } 63 | 64 | desc.append(")").append(returnType.descriptor); 65 | 66 | return desc.toString(); 67 | } 68 | 69 | @Nullable 70 | private String getSignature() { 71 | boolean any = false; 72 | StringBuilder signature = new StringBuilder(); 73 | 74 | val typeVariables = getTypeVariables(); 75 | 76 | if (!typeVariables.isEmpty()) { 77 | signature.append('<'); 78 | typeVariables.forEach(signature::append); 79 | signature.append('>'); 80 | } 81 | 82 | signature.append("("); 83 | 84 | for (Parameter parameter : parameters) { 85 | String generic = parameter.type.signature; 86 | if (generic == null) 87 | generic = parameter.type.descriptor; 88 | else 89 | any = true; 90 | 91 | signature.append(generic); 92 | } 93 | 94 | signature.append(")"); 95 | String generic = returnType.signature; 96 | if (generic == null) 97 | generic = returnType.descriptor; 98 | else 99 | any = true; 100 | 101 | signature.append(generic); 102 | 103 | if (any) 104 | return signature.toString(); 105 | 106 | return null; 107 | } 108 | 109 | public List getTypeVariables() { 110 | return this.typeVariables; 111 | } 112 | 113 | public List getParameters() { 114 | return this.parameters; 115 | } 116 | 117 | public Type getReturnType() { 118 | return this.returnType; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | name: CI 4 | 5 | on: 6 | push: 7 | branches: [ main ] 8 | pull_request: 9 | branches: [ main ] 10 | 11 | jobs: 12 | verify-gradle-wrapper: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3 16 | - uses: gradle/wrapper-validation-action@8d49e559aae34d3e0eb16cde532684bc9702762b # v1.0.6 17 | 18 | build: 19 | if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 20 | runs-on: ubuntu-latest 21 | strategy: 22 | matrix: 23 | jdk: [11, 15, 16] 24 | steps: 25 | - uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3 26 | - uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1 27 | with: 28 | path: | 29 | ~/.gradle/caches 30 | ~/.gradle/wrapper 31 | key: ${{ runner.os }}-jdk-${{ matrix.jdk }}-gradlewrapper-${{ hashFiles('**/gradle-wrapper.properties') }}-gradlescripts-${{ hashFiles('**/*.gradle*') }} 32 | restore-keys: | 33 | ${{ runner.os }}-jdk-${{ matrix.jdk }}-gradlewrapper-${{ hashFiles('**/gradle-wrapper.properties') }}-gradlescripts- 34 | - name: Set up JDK 35 | uses: actions/setup-java@3f07048e3d294f56e9b90ac5ea2c6f74e9ad0f98 # v3.10.0 36 | with: 37 | java-version: ${{ matrix.jdk }} 38 | distribution: adopt 39 | - run: ./gradlew build --stacktrace --warning-mode all 40 | 41 | release: 42 | runs-on: ubuntu-latest 43 | needs: [build, verify-gradle-wrapper] # build job must pass before we can release 44 | 45 | if: github.event_name == 'push' 46 | && github.ref == 'refs/heads/main' 47 | && github.repository == 'MinimallyCorrect/JavaTransformer' 48 | && !contains(toJSON(github.event.commits.*.message), '[skip ci]') 49 | && !contains(toJSON(github.event.commits.*.message), '[skip release]') 50 | 51 | steps: 52 | - name: Check out code 53 | uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3 54 | with: 55 | fetch-depth: '0' # https://github.com/shipkit/shipkit-changelog#fetch-depth-on-ci 56 | 57 | - uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1 58 | with: 59 | path: | 60 | ~/.gradle/caches 61 | ~/.gradle/wrapper 62 | key: releasing-${{ runner.os }}-jdk-${{ matrix.jdk }}-gradlewrapper-${{ hashFiles('**/gradle-wrapper.properties') }}-gradlescripts-${{ hashFiles('**/*.gradle*') }} 63 | 64 | - name: Set up Java 11 65 | uses: actions/setup-java@3f07048e3d294f56e9b90ac5ea2c6f74e9ad0f98 # v3.10.0 66 | with: 67 | java-version: 11 68 | distribution: adopt 69 | 70 | - name: Build and publish to github 71 | run: ./gradlew -P releasing build publish githubRelease -s 72 | env: 73 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 74 | DEPLOYMENT_REPO_URL_RELEASE: ${{secrets.DEPLOYMENT_REPO_URL_RELEASE}} 75 | DEPLOYMENT_REPO_URL_SNAPSHOT: ${{secrets.DEPLOYMENT_REPO_URL_SNAPSHOT}} 76 | DEPLOYMENT_REPO_USERNAME: ${{secrets.DEPLOYMENT_REPO_USERNAME}} 77 | DEPLOYMENT_REPO_PASSWORD: ${{secrets.DEPLOYMENT_REPO_PASSWORD}} 78 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/asm/AsmInstructions.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.asm; 2 | 3 | import lombok.val; 4 | 5 | import org.jetbrains.annotations.Nullable; 6 | import org.objectweb.asm.Opcodes; 7 | import org.objectweb.asm.tree.AbstractInsnNode; 8 | import org.objectweb.asm.tree.IntInsnNode; 9 | import org.objectweb.asm.tree.LdcInsnNode; 10 | 11 | import dev.minco.javatransformer.api.Type; 12 | import dev.minco.javatransformer.api.code.IntermediateValue; 13 | 14 | /** 15 | * asm is half an abstraction level above java bytecode, and very inconsistent. 16 | *

    17 | *
  • const instructions - no common interface to handle short versions for 0/1/-1/etc
  • 18 | *
  • var instructions - auto converts the 0/1/-1 to varinsnnode.
  • 19 | *
20 | *

21 | * THANKS!!!!! 22 | */ 23 | @SuppressWarnings("Duplicates") 24 | public final class AsmInstructions implements Opcodes { 25 | @Nullable 26 | public static Object getConstant(AbstractInsnNode insn) { 27 | switch (insn.getOpcode()) { 28 | case ACONST_NULL: 29 | return null; 30 | case DCONST_0: 31 | return 0d; 32 | case DCONST_1: 33 | return 1d; 34 | case FCONST_0: 35 | return 0f; 36 | case FCONST_1: 37 | return 1f; 38 | case FCONST_2: 39 | return 2f; 40 | case LCONST_0: 41 | return 0L; 42 | case LCONST_1: 43 | return 1L; 44 | case ICONST_M1: 45 | return -1; 46 | case ICONST_0: 47 | return 0; 48 | case ICONST_1: 49 | return 1; 50 | case ICONST_2: 51 | return 2; 52 | case ICONST_3: 53 | return 3; 54 | case ICONST_4: 55 | return 4; 56 | case ICONST_5: 57 | return 5; 58 | case SIPUSH: 59 | case BIPUSH: 60 | case NEWARRAY: 61 | return ((IntInsnNode) insn).operand; 62 | case LDC: 63 | return ((LdcInsnNode) insn).cst; 64 | } 65 | return IntermediateValue.UNKNOWN; 66 | } 67 | 68 | public static int getStoreInstructionForType(IntermediateValue iv) { 69 | val type = iv.type; 70 | if (type == null) 71 | throw new NullPointerException(); 72 | switch (type.getDescriptorType()) { 73 | case BYTE: 74 | case CHAR: 75 | case INT: 76 | case SHORT: 77 | case BOOLEAN: 78 | return ISTORE; 79 | case DOUBLE: 80 | return DSTORE; 81 | case FLOAT: 82 | return FSTORE; 83 | case LONG: 84 | return LSTORE; 85 | case VOID: 86 | throw new IllegalArgumentException(iv.toString()); 87 | case ARRAY: 88 | case CLASS: 89 | return ASTORE; 90 | case VALUE: 91 | case UNION: 92 | default: 93 | throw new UnsupportedOperationException(); 94 | } 95 | } 96 | 97 | public static int getLoadInstructionForType(IntermediateValue iv) { 98 | val type = iv.type; 99 | if (type == null) 100 | throw new NullPointerException(); 101 | switch (type.getDescriptorType()) { 102 | case BYTE: 103 | case CHAR: 104 | case INT: 105 | case SHORT: 106 | case BOOLEAN: 107 | return ILOAD; 108 | case DOUBLE: 109 | return DLOAD; 110 | case FLOAT: 111 | return FLOAD; 112 | case LONG: 113 | return LLOAD; 114 | case VOID: 115 | throw new IllegalArgumentException(iv.toString()); 116 | case ARRAY: 117 | case CLASS: 118 | return ALOAD; 119 | case VALUE: 120 | case UNION: 121 | default: 122 | throw new UnsupportedOperationException(); 123 | } 124 | } 125 | 126 | public static int getReturnInstructionForType(@Nullable Type type) { 127 | if (type == null) 128 | return RETURN; 129 | switch (type.getDescriptorType()) { 130 | case BYTE: 131 | case CHAR: 132 | case INT: 133 | case SHORT: 134 | case BOOLEAN: 135 | return IRETURN; 136 | case DOUBLE: 137 | return DRETURN; 138 | case FLOAT: 139 | return FRETURN; 140 | case LONG: 141 | return LRETURN; 142 | case VOID: 143 | return RETURN; 144 | case ARRAY: 145 | case CLASS: 146 | return ARETURN; 147 | case VALUE: 148 | case UNION: 149 | default: 150 | throw new UnsupportedOperationException(); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/internal/ResolutionContextTest.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collections; 5 | 6 | import lombok.val; 7 | 8 | import org.junit.Assert; 9 | import org.junit.Test; 10 | 11 | import com.github.javaparser.ast.type.TypeParameter; 12 | 13 | import dev.minco.javatransformer.api.ClassPath; 14 | import dev.minco.javatransformer.api.TransformationException; 15 | import dev.minco.javatransformer.api.Type; 16 | 17 | public class ResolutionContextTest { 18 | private ResolutionContext context() { 19 | return new ResolutionContext("org.example", Collections.emptyList(), Arrays.asList(new TypeParameter("A"), new TypeParameter("B")), ClassPath.of(), null); 20 | } 21 | 22 | @Test 23 | public void testExtractReal() throws Exception { 24 | Assert.assertEquals("ab", ResolutionContext.extractReal("ab")); 25 | } 26 | 27 | @Test 28 | public void testExtractGeneric() throws Exception { 29 | Assert.assertEquals("test", ResolutionContext.extractGeneric("test")); 30 | Assert.assertEquals(null, ResolutionContext.extractGeneric("test")); 31 | } 32 | 33 | @Test(expected = RuntimeException.class) 34 | public void testExtractGenericMismatched() throws Exception { 35 | ResolutionContext.extractGeneric("te>st<"); 36 | ResolutionContext.extractGeneric("test<"); 37 | ResolutionContext.extractGeneric("te>st"); 38 | } 39 | 40 | @Test 41 | public void testExtractPrimitive() { 42 | Type t = context().resolve("int"); 43 | Assert.assertNotNull(t); 44 | Assert.assertEquals(t.descriptor, "I"); 45 | t = context().resolve("boolean"); 46 | Assert.assertNotNull(t); 47 | Assert.assertEquals(t.descriptor, "Z"); 48 | } 49 | 50 | @Test 51 | public void testExtractPrimitiveArray() { 52 | Type t = context().resolve("int[]"); 53 | Assert.assertNotNull(t); 54 | Assert.assertEquals(t.descriptor, "[I"); 55 | t = context().resolve("int[][]"); 56 | Assert.assertNotNull(t); 57 | Assert.assertEquals(t.descriptor, "[[I"); 58 | t = context().resolve("boolean[]"); 59 | Assert.assertNotNull(t); 60 | Assert.assertEquals(t.descriptor, "[Z"); 61 | t = context().resolve("boolean[][]"); 62 | Assert.assertNotNull(t); 63 | Assert.assertEquals(t.descriptor, "[[Z"); 64 | } 65 | 66 | @Test(expected = TransformationException.class) 67 | public void testSanityCheckExpectedFailure() { 68 | ResolutionContext.sanityCheck(new Type("LDefaultPackageNotAllowed;")); 69 | } 70 | 71 | @Test 72 | public void testSanityCheckExpectedSuccess() { 73 | ResolutionContext.sanityCheck(new Type("Z")); 74 | ResolutionContext.sanityCheck(new Type("Ljava/lang/Object;", "TT;")); 75 | } 76 | 77 | @Test 78 | public void testMapWithArrayValue() { 79 | Type t = context().resolve("java.util.Hashtable"); 80 | Assert.assertNotNull(t); 81 | Assert.assertEquals("java.util.Hashtable", t.getClassName()); 82 | Assert.assertEquals("java.lang.Integer", t.getTypeArguments().get(0).getClassName()); 83 | Assert.assertEquals("long", t.getTypeArguments().get(1).getArrayContainedType().getPrimitiveTypeName()); 84 | Assert.assertEquals("long[]", t.getTypeArguments().get(1).getJavaName()); 85 | } 86 | 87 | @Test 88 | public void testTypeToJavaParsetType() { 89 | val qualifiedType = "java.util.Hashtable"; 90 | Type t = context().resolve(qualifiedType); 91 | Assert.assertNotNull(t); 92 | val javaParserType = ResolutionContext.typeToJavaParserType(t); 93 | Assert.assertEquals(qualifiedType, javaParserType.asString()); 94 | } 95 | 96 | @Test 97 | public void testSingleLengthTypeArgument() { 98 | Type t = context().resolve("java.util.Hashtable"); 99 | Assert.assertNotNull(t); 100 | Assert.assertEquals("A", t.getTypeArguments().get(0).getTypeParameterName()); 101 | Assert.assertEquals("B", t.getTypeArguments().get(1).getTypeParameterName()); 102 | } 103 | 104 | @Test 105 | public void testAutomaticGenericType() { 106 | Type t = context().resolve("java.util.Hashtable<>"); 107 | Assert.assertNotNull(t); 108 | Assert.assertEquals("java.util.Hashtable", t.getClassName()); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/api/code/CodeFragment.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api.code; 2 | 3 | import java.util.Collections; 4 | import java.util.List; 5 | 6 | import lombok.AllArgsConstructor; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.NonNull; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.ToString; 12 | import lombok.With; 13 | 14 | import dev.minco.javatransformer.api.TransformationException; 15 | import dev.minco.javatransformer.api.Type; 16 | 17 | @SuppressWarnings("serial") 18 | public interface CodeFragment { 19 | /* TODO: implement this 20 | * 21 | * fall-through/abort considerations: 22 | * 23 | * insertion before is always allowed 24 | * 25 | * overwrite is only allowed if can already fall through the fragment being overwritten, or can't fall through the fragment being inserted 26 | * 27 | * after is only allowed if execution can fall through 28 | * 29 | * input/output considerations: 30 | * 31 | * before - inputs can be none or the same. if same duplicate inputs? or allow changing inputs?. outputs must be none 32 | * 33 | * configurable?? by default duplicate, option to allow changing 34 | * 35 | * overwrite - inputs must be the same outputs must be the same 36 | * 37 | * after - inputs must be none, outputs must be none or same? */ 38 | 39 | void insert(@NonNull CodeFragment codeFragment, @NonNull InsertionPosition position, @NonNull InsertionOptions insertionOptions); 40 | 41 | default void insert(@NonNull CodeFragment codeFragment, @NonNull InsertionPosition position) { 42 | insert(codeFragment, position, new InsertionOptions()); 43 | } 44 | 45 | @SuppressWarnings("unchecked") 46 | default List findFragments(Class fragmentType) { 47 | if (fragmentType.isAssignableFrom(this.getClass())) 48 | return Collections.singletonList((T) this); 49 | return Collections.emptyList(); 50 | } 51 | 52 | ExecutionOutcome getExecutionOutcome(); 53 | 54 | @NonNull 55 | List getInputTypes(); 56 | 57 | @NonNull 58 | List getOutputTypes(); 59 | 60 | enum InsertionPosition { 61 | BEFORE, 62 | OVERWRITE, 63 | AFTER 64 | } 65 | 66 | interface MethodCall extends CodeFragment, HasContainingClassType, HasName {} 67 | 68 | interface FieldAccess extends CodeFragment, HasContainingClassType, HasName {} 69 | 70 | interface FieldLoad extends FieldAccess {} 71 | 72 | interface FieldStore extends FieldAccess {} 73 | 74 | interface Return extends CodeFragment {} 75 | 76 | interface New extends CodeFragment {} 77 | 78 | /** 79 | * Marker interface, the entire body of a method 80 | */ 81 | interface Body extends CodeFragment {} 82 | 83 | @FunctionalInterface 84 | interface HasContainingClassType { 85 | @NonNull 86 | Type getContainingClassType(); 87 | } 88 | 89 | @FunctionalInterface 90 | interface HasName { 91 | @NonNull 92 | String getName(); 93 | } 94 | 95 | @AllArgsConstructor 96 | @Getter 97 | @NoArgsConstructor 98 | @With 99 | class InsertionOptions { 100 | public static InsertionOptions DEFAULT = new InsertionOptions(); 101 | public boolean convertReturnToOutputTypes = true; 102 | public boolean convertReturnCallToReturnInstruction = true; 103 | public boolean eliminateDeadCode = true; 104 | } 105 | 106 | @RequiredArgsConstructor 107 | @ToString 108 | final class ExecutionOutcome { 109 | public final boolean canFallThrough; 110 | public final boolean canThrow; 111 | public final boolean canReturn; 112 | } 113 | 114 | class TypeMismatchException extends TransformationException { 115 | final Class expected; 116 | final Class actual; 117 | 118 | public TypeMismatchException(Class expected, @NonNull CodeFragment actual) { 119 | super("Expected CodeFragment of type " + expected + ". Actual type " + actual.getClass()); 120 | this.expected = expected; 121 | this.actual = actual.getClass(); 122 | } 123 | } 124 | 125 | class UnreachableInsertionException extends TransformationException { 126 | public UnreachableInsertionException(CodeFragment fragment, InsertionPosition position) { 127 | super("Can't insert into '" + fragment + "' at position " + position + " as the code would be unreachable"); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/api/Annotation.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import java.lang.reflect.InvocationHandler; 4 | import java.lang.reflect.Method; 5 | import java.lang.reflect.Modifier; 6 | import java.lang.reflect.Proxy; 7 | import java.util.Arrays; 8 | import java.util.Collections; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | import lombok.AccessLevel; 13 | import lombok.Data; 14 | import lombok.Getter; 15 | import lombok.NonNull; 16 | import lombok.RequiredArgsConstructor; 17 | import lombok.Setter; 18 | import lombok.SneakyThrows; 19 | import lombok.val; 20 | 21 | import org.jetbrains.annotations.Nullable; 22 | 23 | import dev.minco.javatransformer.internal.util.JVMUtil; 24 | 25 | @Data 26 | @RequiredArgsConstructor(access = AccessLevel.PROTECTED) 27 | public class Annotation { 28 | @NonNull 29 | public final Type type; 30 | @NonNull 31 | @Getter(AccessLevel.NONE) 32 | @Setter(AccessLevel.NONE) 33 | public final Map values; 34 | 35 | public static Annotation of(Type t, Map value) { 36 | return new Annotation(t, new HashMap<>(value)); 37 | } 38 | 39 | public static Annotation of(Type t, Object value) { 40 | val map = new HashMap(); 41 | map.put("value", value); 42 | return of(t, map); 43 | } 44 | 45 | public static Annotation of(Type t) { 46 | return of(t, Collections.emptyMap()); 47 | } 48 | 49 | @Nullable 50 | @SuppressWarnings("unchecked") 51 | public T get(String key, Class clazz) { 52 | val value = values.get(key); 53 | if (value == null) 54 | return null; 55 | if (clazz.isAssignableFrom(value.getClass())) 56 | return (T) value; 57 | if (clazz.isEnum()) { 58 | if (!(value instanceof String[])) 59 | throw new IllegalArgumentException("value for " + key + " is not a String[] so can't be mapped to enum. Actual type " + value.getClass().getName() + " value " + value); 60 | val array = ((String[]) value); 61 | val type = new Type(array[0]); 62 | if (!type.getClassName().endsWith(clazz.getName())) 63 | throw new IllegalArgumentException("value for " + key + " is of enum type " + type + " which does not match expected type " + clazz.getName() + " actual value " + Arrays.toString(array)); 64 | return (T) JVMUtil.searchEnum((Class>) clazz, array[1]); 65 | } 66 | throw new UnsupportedOperationException("Can't convert enum value of type " + value.getClass().getName() + " value " + value + " to " + clazz.getName()); 67 | } 68 | 69 | public void set(String key, Object value) { 70 | if (value != null) { 71 | val clazz = value.getClass(); 72 | if (clazz.isEnum()) 73 | value = new String[]{Type.of(clazz.getName()).descriptor, ((Enum) value).name()}; 74 | } 75 | values.put(key, value); 76 | } 77 | 78 | @SuppressWarnings("unchecked") 79 | @SneakyThrows 80 | public T toInstance(Class clazz) { 81 | if (!clazz.isAnnotation()) 82 | throw new IllegalArgumentException("Class " + clazz.getName() + " is not an annotation"); 83 | if (!clazz.getName().equals(type.getClassName())) 84 | throw new IllegalArgumentException("Type " + type + " can't be mapped to annotation class " + clazz); 85 | return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new AnnotationProxy(clazz)); 86 | } 87 | 88 | @RequiredArgsConstructor 89 | private final class AnnotationProxy implements InvocationHandler, java.lang.annotation.Annotation { 90 | final Class annotationType; 91 | 92 | public Class annotationType() { 93 | return annotationType; 94 | } 95 | 96 | @Override 97 | public String toString() { 98 | return '@' + annotationType.toString() + '(' + values.toString() + ')'; 99 | } 100 | 101 | @Override 102 | public int hashCode() { 103 | return toString().hashCode(); 104 | } 105 | 106 | @Override 107 | public boolean equals(Object other) { 108 | return other != null && other.getClass() == AnnotationProxy.class && toString().equals(other); 109 | } 110 | 111 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 112 | if (!Modifier.isPublic(method.getModifiers())) 113 | return method.invoke(this, args); 114 | val key = method.getName(); 115 | val returnType = method.getReturnType(); 116 | Object value = returnType.isPrimitive() ? values.get(key) : get(key, returnType); 117 | if (value == null) 118 | value = method.getDefaultValue(); 119 | if (value == null) 120 | return method.invoke(this, args); 121 | return value; 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/api/JavaTransformerRuntimeTest.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import java.io.PrintStream; 4 | import java.nio.file.Path; 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.concurrent.atomic.AtomicBoolean; 9 | import java.util.stream.Collectors; 10 | 11 | import lombok.val; 12 | 13 | import org.junit.Assert; 14 | import org.junit.Test; 15 | 16 | import dev.minco.javatransformer.api.code.CodeFragment; 17 | import dev.minco.javatransformer.api.code.IntermediateValue; 18 | import dev.minco.javatransformer.internal.ByteCodeInfo; 19 | import dev.minco.javatransformer.internal.asm.DebugPrinter; 20 | import dev.minco.javatransformer.transform.CodeFragmentTesting; 21 | 22 | public class JavaTransformerRuntimeTest { 23 | private static final List EXPECTED_METHOD_CALL_INPUTS = Arrays.asList("1", "2", "3", "4"); 24 | private static final int EXPECTED_METHOD_CALL_COUNT = 4; 25 | 26 | @Test 27 | public void testTransformRuntime() throws Exception { 28 | final Path input = JavaTransformer.pathFromClass(JavaTransformerTest.class); 29 | final String name = "dev.minco.javatransformer.transform.CodeFragmentTesting"; 30 | JavaTransformer transformer = new JavaTransformer(); 31 | 32 | val targetClass = this.getClass().getName(); 33 | AtomicBoolean check = new AtomicBoolean(false); 34 | 35 | transformer.addTransformer(name, c -> { 36 | Assert.assertEquals(name, c.getName()); 37 | check.set(true); 38 | c.accessFlags(it -> it.makeAccessible(true)); 39 | c.getAnnotations(); 40 | val fields = c.getFields().collect(Collectors.toList()); 41 | { 42 | Assert.assertEquals(1, fields.size()); 43 | val field = fields.get(0); 44 | Assert.assertEquals("callback", field.getName()); 45 | Assert.assertEquals("java.util.function.Consumer", field.getType().getJavaName()); 46 | } 47 | val interfaceTypes = c.getInterfaceTypes(); 48 | { 49 | Assert.assertTrue(interfaceTypes.isEmpty()); 50 | } 51 | c.getMembers(); 52 | c.getConstructors(); 53 | c.getMethods().forEach(it -> { 54 | val cf = it.getCodeFragment(); 55 | Assert.assertNotNull(it + " should have a CodeFragment", cf); 56 | switch (it.getName()) { 57 | case "testMethodCallExpression": 58 | val methodCalls = it.findFragments(CodeFragment.MethodCall.class); 59 | Assert.assertEquals(EXPECTED_METHOD_CALL_COUNT, methodCalls.size()); 60 | for (int i = 1; i <= EXPECTED_METHOD_CALL_COUNT; i++) { 61 | System.out.println("call " + i); 62 | val call = methodCalls.get(i - 1); 63 | Assert.assertEquals("println", call.getName()); 64 | Assert.assertEquals(PrintStream.class.getName(), call.getContainingClassType().getClassName()); 65 | val inputTypes = call.getInputTypes(); 66 | for (val inputType : inputTypes) 67 | Assert.assertEquals(IntermediateValue.LocationType.STACK, inputType.location.type); 68 | Assert.assertNotNull("Should find inputTypes for method call expression", inputTypes); 69 | Assert.assertEquals(2, inputTypes.size()); 70 | Assert.assertEquals(String.valueOf(i), inputTypes.get(1).constantValue); 71 | Assert.assertEquals("java.io.PrintStream", inputTypes.get(0).type.getClassName()); 72 | 73 | val callbackCaller = c.getMethods().filter(method -> method.getName().equals("callbackCaller")).findFirst().get(); 74 | val callbackCallerFragment = callbackCaller.getCodeFragment(); 75 | 76 | assert callbackCallerFragment != null; 77 | call.insert(callbackCallerFragment, CodeFragment.InsertionPosition.OVERWRITE); 78 | for (val inputType : callbackCallerFragment.getInputTypes()) 79 | Assert.assertEquals(IntermediateValue.LocationType.LOCAL, inputType.location.type); 80 | DebugPrinter.printByteCode(((ByteCodeInfo.MethodNodeInfo) it).node, "after insert callbackCallerFragment"); 81 | } 82 | break; 83 | case "testAbortEarly": 84 | val aborter = c.getMethods().filter(method -> method.getName().equals("aborter")).findFirst().get().getCodeFragment(); 85 | assert aborter != null; 86 | cf.insert(aborter, CodeFragment.InsertionPosition.BEFORE); 87 | break; 88 | } 89 | }); 90 | }); 91 | 92 | System.out.println("Transforming path '" + input + '\''); 93 | transformer.load(input); 94 | val clazz = transformer.defineClass(this.getClass().getClassLoader(), name); 95 | Assert.assertEquals(name, clazz.getName()); 96 | Assert.assertTrue("Transformer must process " + targetClass, check.get()); 97 | 98 | val list = new ArrayList(); 99 | val codeFragmentTesting = new CodeFragmentTesting(list::add); 100 | codeFragmentTesting.testMethodCallExpression(); 101 | Assert.assertEquals(EXPECTED_METHOD_CALL_INPUTS, list); 102 | 103 | val result = codeFragmentTesting.testAbortEarly(); 104 | Assert.assertTrue("testAbortEarly should return true after patch", result); 105 | Assert.assertEquals(null, System.getProperty("finishedTestAbortEarly")); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/Signature.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | import lombok.val; 8 | 9 | import org.jetbrains.annotations.Nullable; 10 | import org.objectweb.asm.tree.AnnotationNode; 11 | import org.objectweb.asm.tree.MethodNode; 12 | 13 | import dev.minco.javatransformer.api.Annotation; 14 | import dev.minco.javatransformer.api.Parameter; 15 | import dev.minco.javatransformer.api.TransformationException; 16 | import dev.minco.javatransformer.api.Type; 17 | import dev.minco.javatransformer.api.TypeVariable; 18 | import dev.minco.javatransformer.internal.util.AnnotationParser; 19 | import dev.minco.javatransformer.internal.util.CachingSupplier; 20 | import dev.minco.javatransformer.internal.util.TypeUtil; 21 | 22 | public final class Signature { 23 | private static String before(char c, String in) { 24 | int index = in.indexOf(c); 25 | 26 | if (index == -1) 27 | throw new TransformationException("Could not find '" + c + "' in '" + in + "'"); 28 | 29 | return in.substring(0, index); 30 | } 31 | 32 | private static String after(char c, String in) { 33 | int index = in.indexOf(c); 34 | 35 | if (index == -1) 36 | throw new TransformationException("Could not find '" + c + "' in '" + in + "'"); 37 | 38 | return in.substring(index + 1, in.length()); 39 | } 40 | 41 | static List getTypeVariables(@Nullable String signature) { 42 | if (signature == null) 43 | return Collections.emptyList(); 44 | 45 | if (signature.indexOf('(') != -1) { 46 | signature = before('(', signature); 47 | } 48 | String typeArguments = ResolutionContext.extractGeneric(signature); 49 | 50 | if (typeArguments == null) 51 | return Collections.emptyList(); 52 | 53 | val list = new ArrayList(); 54 | int pos = 0; 55 | int start = 0; 56 | val len = typeArguments.length(); 57 | 58 | // FIXME very broken needs some thorough unit tests 59 | while (pos < len) { 60 | char c = typeArguments.charAt(pos++); 61 | switch (c) { 62 | case ':': 63 | String name = typeArguments.substring(start, pos - 1); 64 | // TODO implicit object bound? 65 | int offset = typeArguments.charAt(pos) == ':' ? 1 : 0; 66 | String bounds = TypeUtil.readType(typeArguments, pos + offset, true); 67 | pos += bounds.length(); 68 | list.add(new TypeVariable(name, Type.ofSignature(bounds))); 69 | if (pos < len && typeArguments.charAt(pos) != ':') { 70 | start = pos; 71 | } 72 | } 73 | } 74 | 75 | return list; 76 | } 77 | 78 | static Type getReturnType(String descriptor, @Nullable String signature) { 79 | String returnDescriptor = after(')', descriptor); 80 | String returnSignature = null; 81 | 82 | if (signature != null) 83 | returnSignature = after(')', signature); 84 | 85 | return new Type(returnDescriptor, returnSignature); 86 | } 87 | 88 | static List getParameters(MethodNode node) { 89 | val parameterNames = new ArrayList(); 90 | if (node.parameters != null) 91 | for (val param : node.parameters) 92 | parameterNames.add(param.name); 93 | return getParameters(node.desc, node.signature, parameterNames, node.invisibleParameterAnnotations, node.visibleParameterAnnotations); 94 | } 95 | 96 | static List getParameters(String descriptor, @Nullable String signature, @Nullable List parameterNames, @Nullable List[] invisibleAnnotations, @Nullable List[] visibleAnnotations) { 97 | val parameters = new ArrayList(); 98 | 99 | List parameterTypes = Type.listOf(getParameters(descriptor), getParameters(signature)); 100 | 101 | for (int i = 0; i < parameterTypes.size(); i++) { 102 | String name = (parameterNames == null || i >= parameterNames.size()) ? null : parameterNames.get(i); 103 | CachingSupplier> annotationSupplier = null; 104 | 105 | if ((invisibleAnnotations != null && invisibleAnnotations.length > 0) || (visibleAnnotations != null && visibleAnnotations.length > 0)) { 106 | val j = i; 107 | annotationSupplier = CachingSupplier.of(() -> { 108 | val annotations = new ArrayList(); 109 | if (invisibleAnnotations != null && j < invisibleAnnotations.length) 110 | //noinspection ConstantConditions 111 | for (val node : invisibleAnnotations[j]) 112 | annotations.add(AnnotationParser.annotationFromAnnotationNode(node)); 113 | if (visibleAnnotations != null && j < visibleAnnotations.length) 114 | //noinspection ConstantConditions 115 | for (val node : visibleAnnotations[j]) 116 | annotations.add(AnnotationParser.annotationFromAnnotationNode(node)); 117 | return annotations; 118 | }); 119 | } 120 | parameters.add(Parameter.of(parameterTypes.get(i), name, annotationSupplier)); 121 | } 122 | 123 | return parameters; 124 | } 125 | 126 | @Nullable 127 | private static String getParameters(String descriptor) { 128 | if (descriptor == null) 129 | return null; 130 | return before(')', after('(', descriptor)); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/asm/CombinedValue.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.asm; 2 | 3 | import java.util.Collections; 4 | import java.util.Objects; 5 | import java.util.Set; 6 | 7 | import lombok.EqualsAndHashCode; 8 | import lombok.val; 9 | 10 | import org.jetbrains.annotations.Nullable; 11 | import org.objectweb.asm.Opcodes; 12 | import org.objectweb.asm.Type; 13 | import org.objectweb.asm.tree.AbstractInsnNode; 14 | import org.objectweb.asm.tree.InsnNode; 15 | import org.objectweb.asm.tree.analysis.Value; 16 | 17 | import dev.minco.javatransformer.api.code.IntermediateValue; 18 | 19 | @EqualsAndHashCode 20 | public class CombinedValue implements Value, Opcodes { 21 | public static final Type OBJECT_TYPE = Type.getObjectType("java/lang/Object"); 22 | public static final AbstractInsnNode POPPED_FROM_BOTTOM = new InsnNode(NOP) { 23 | @Override 24 | public String toString() { 25 | return "Popped from empty stack."; 26 | } 27 | }; 28 | public static final AbstractInsnNode PREFILLED = new InsnNode(NOP) { 29 | @Override 30 | public String toString() { 31 | return "Pre-filled value. Method parameter, this, or caught exception/NOP"; 32 | } 33 | }; 34 | private static final CombinedValue UNINITIALIZED_VALUE = new CombinedValue(null, Collections.emptySet()); 35 | private static final CombinedValue INT_VALUE = new CombinedValue(Type.INT_TYPE, Collections.emptySet()); 36 | private static final CombinedValue FLOAT_VALUE = new CombinedValue(Type.FLOAT_TYPE, Collections.emptySet()); 37 | private static final CombinedValue LONG_VALUE = new CombinedValue(Type.LONG_TYPE, Collections.emptySet()); 38 | private static final CombinedValue DOUBLE_VALUE = new CombinedValue(Type.DOUBLE_TYPE, Collections.emptySet()); 39 | private static final CombinedValue REFERENCE_VALUE = new CombinedValue(OBJECT_TYPE, Collections.emptySet()); 40 | /** 41 | * The instructions that can produce this value. For example, for the Java code below, the instructions that can produce the value of i at line 5 are the txo ISTORE instructions at line 1 and 3: 42 | * 43 | *

 44 | 	 * 1: i = 0;
 45 | 	 * 2: if (...) {
 46 | 	 * 3:   i = 1;
 47 | 	 * 4: }
 48 | 	 * 5: return i;
 49 | 	 * 
50 | * 51 | * This field is a set of {@link AbstractInsnNode} objects. 52 | */ 53 | public final Set insns; 54 | @Nullable 55 | private final Type type; 56 | 57 | protected CombinedValue(@Nullable final Type type, final Set insns) { 58 | this.type = type; 59 | this.insns = insns; 60 | } 61 | 62 | @Nullable 63 | @Deprecated 64 | public static CombinedValue of(@Nullable Type type) { 65 | return of(type, Collections.emptySet()); 66 | } 67 | 68 | @Nullable 69 | public static CombinedValue of(@Nullable Type type, AbstractInsnNode insn) { 70 | return of(type, Collections.singleton(insn)); 71 | } 72 | 73 | @Nullable 74 | public static CombinedValue of(@Nullable Type type, Set insns) { 75 | if (type != null && type.getSort() == Type.VOID && insns.size() == 1 && insns.contains(PREFILLED)) 76 | return null; 77 | if (!insns.isEmpty()) 78 | return new CombinedValue(type, insns); 79 | if (type == null) 80 | return CombinedValue.UNINITIALIZED_VALUE; 81 | switch (type.getSort()) { 82 | case Type.VOID: 83 | return null; 84 | case Type.BOOLEAN: 85 | case Type.CHAR: 86 | case Type.BYTE: 87 | case Type.SHORT: 88 | case Type.INT: 89 | return CombinedValue.INT_VALUE; 90 | case Type.FLOAT: 91 | return CombinedValue.FLOAT_VALUE; 92 | case Type.LONG: 93 | return CombinedValue.LONG_VALUE; 94 | case Type.DOUBLE: 95 | return CombinedValue.DOUBLE_VALUE; 96 | case Type.ARRAY: 97 | case Type.OBJECT: 98 | return type.getInternalName().equals("java/lang/Object") ? CombinedValue.REFERENCE_VALUE : new CombinedValue(type, insns); 99 | default: 100 | throw new IllegalArgumentException("Unhandled type" + type.getSort() + " " + type); 101 | } 102 | } 103 | 104 | @Nullable 105 | public Type getType() { 106 | return type; 107 | } 108 | 109 | public int getSize() { 110 | return type == Type.LONG_TYPE || type == Type.DOUBLE_TYPE ? 2 : 1; 111 | } 112 | 113 | public boolean isReference() { 114 | return type != null && (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY); 115 | } 116 | 117 | public boolean isInitialised() { 118 | return type != null; 119 | } 120 | 121 | @Nullable 122 | public Object getConstantValue() { 123 | val iterator = insns.iterator(); 124 | if (!iterator.hasNext()) 125 | return IntermediateValue.UNKNOWN; 126 | Object value = AsmInstructions.getConstant(iterator.next()); 127 | while (iterator.hasNext()) { 128 | Object newValue = AsmInstructions.getConstant(iterator.next()); 129 | if (!Objects.equals(newValue, value)) 130 | return IntermediateValue.UNKNOWN; 131 | } 132 | return value; 133 | } 134 | 135 | public String getDescriptor() { 136 | if (this == UNINITIALIZED_VALUE || type == null) { 137 | return "."; 138 | } else { 139 | return type.getDescriptor(); 140 | } 141 | } 142 | 143 | @Override 144 | public String toString() { 145 | return "type: " + getDescriptor() + " " + insns; 146 | } 147 | 148 | public boolean isPrefilled() { 149 | return insns != null && insns.contains(CombinedValue.PREFILLED); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/api/AccessFlags.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import java.util.ArrayList; 4 | import java.util.EnumSet; 5 | 6 | import lombok.val; 7 | 8 | import org.intellij.lang.annotations.MagicConstant; 9 | 10 | import com.github.javaparser.ast.Modifier; 11 | 12 | import dev.minco.javatransformer.internal.util.JVMUtil; 13 | 14 | public class AccessFlags { 15 | public static final int ACC_PUBLIC = 0x0001; // class, field, method 16 | public static final int ACC_PRIVATE = 0x0002; // class, field, method 17 | public static final int ACC_PROTECTED = 0x0004; // class, field, method 18 | public static final int ACC_STATIC = 0x0008; // field, method 19 | public static final int ACC_FINAL = 0x0010; // class, field, method, parameter 20 | public static final int ACC_SUPER = 0x0020; // class 21 | public static final int ACC_SYNCHRONIZED = 0x0020; // method 22 | public static final int ACC_VOLATILE = 0x0040; // field 23 | public static final int ACC_BRIDGE = 0x0040; // method 24 | public static final int ACC_VARARGS = 0x0080; // method 25 | public static final int ACC_TRANSIENT = 0x0080; // field 26 | public static final int ACC_NATIVE = 0x0100; // method 27 | public static final int ACC_INTERFACE = 0x0200; // class 28 | public static final int ACC_ABSTRACT = 0x0400; // class, method 29 | public static final int ACC_STRICT = 0x0800; // method 30 | public static final int ACC_SYNTHETIC = 0x1000; // class, field, method, parameter 31 | public static final int ACC_ANNOTATION = 0x2000; // class 32 | public static final int ACC_ENUM = 0x4000; // class(?) field inner 33 | public static final int ACC_MANDATED = 0x8000; // parameter 34 | @AccessFlagsConstant 35 | public final int access; 36 | 37 | public AccessFlags(@AccessFlagsConstant int access) { 38 | this.access = access; 39 | } 40 | 41 | public AccessFlags(Iterable modifiers) { 42 | this(accessFor(modifiers)); 43 | } 44 | 45 | @AccessFlagsConstant 46 | private static int accessFor(Iterable modifiers) { 47 | val modifierSet = EnumSet.noneOf(Modifier.Keyword.class); 48 | for (Modifier modifier : modifiers) { 49 | modifierSet.add(modifier.getKeyword()); 50 | } 51 | return accessFor(modifierSet); 52 | } 53 | 54 | @AccessFlagsConstant 55 | private static int accessFor(EnumSet modifiers) { 56 | int access = 0; 57 | if (modifiers.contains(Modifier.Keyword.PUBLIC)) 58 | access |= ACC_PUBLIC; 59 | if (modifiers.contains(Modifier.Keyword.PRIVATE)) 60 | access |= ACC_PRIVATE; 61 | if (modifiers.contains(Modifier.Keyword.PROTECTED)) 62 | access |= ACC_PROTECTED; 63 | if (modifiers.contains(Modifier.Keyword.STATIC)) 64 | access |= ACC_STATIC; 65 | if (modifiers.contains(Modifier.Keyword.FINAL)) 66 | access |= ACC_FINAL; 67 | if (modifiers.contains(Modifier.Keyword.SYNCHRONIZED)) 68 | access |= ACC_SYNCHRONIZED; 69 | if (modifiers.contains(Modifier.Keyword.VOLATILE)) 70 | access |= ACC_VOLATILE; 71 | if (modifiers.contains(Modifier.Keyword.TRANSIENT)) 72 | access |= ACC_TRANSIENT; 73 | if (modifiers.contains(Modifier.Keyword.NATIVE)) 74 | access |= ACC_NATIVE; 75 | if (modifiers.contains(Modifier.Keyword.ABSTRACT)) 76 | access |= ACC_ABSTRACT; 77 | if (modifiers.contains(Modifier.Keyword.STRICTFP)) 78 | access |= ACC_STRICT; 79 | return access; 80 | } 81 | 82 | public Modifier.Keyword[] toJavaParserModifierSet() { 83 | val modifiers = new ArrayList(); 84 | 85 | if (has(ACC_PUBLIC)) 86 | modifiers.add(Modifier.Keyword.PUBLIC); 87 | if (has(ACC_PRIVATE)) 88 | modifiers.add(Modifier.Keyword.PRIVATE); 89 | if (has(ACC_PROTECTED)) 90 | modifiers.add(Modifier.Keyword.PROTECTED); 91 | if (has(ACC_STATIC)) 92 | modifiers.add(Modifier.Keyword.STATIC); 93 | if (has(ACC_FINAL)) 94 | modifiers.add(Modifier.Keyword.FINAL); 95 | if (has(ACC_SYNCHRONIZED)) 96 | modifiers.add(Modifier.Keyword.SYNCHRONIZED); 97 | if (has(ACC_VOLATILE)) 98 | modifiers.add(Modifier.Keyword.VOLATILE); 99 | if (has(ACC_TRANSIENT)) 100 | modifiers.add(Modifier.Keyword.TRANSIENT); 101 | if (has(ACC_NATIVE)) 102 | modifiers.add(Modifier.Keyword.NATIVE); 103 | if (has(ACC_ABSTRACT)) 104 | modifiers.add(Modifier.Keyword.ABSTRACT); 105 | if (has(ACC_STRICT)) 106 | modifiers.add(Modifier.Keyword.STRICTFP); 107 | 108 | return modifiers.toArray(new Modifier.Keyword[0]); 109 | } 110 | 111 | @Override 112 | public String toString() { 113 | return "Access: " + access + " (" + JVMUtil.accessIntToString(access) + ")"; 114 | } 115 | 116 | @Override 117 | public boolean equals(Object o) { 118 | return o == this || (o instanceof AccessFlags && ((AccessFlags) o).access == access); 119 | } 120 | 121 | @Override 122 | public int hashCode() { 123 | return access; 124 | } 125 | 126 | public boolean has(@AccessFlagsConstant int flag) { 127 | return (access & flag) == flag; 128 | } 129 | 130 | public AccessFlags makeAccessible(boolean needsPublic) { 131 | return new AccessFlags(JVMUtil.makeAccess(access, needsPublic)); 132 | } 133 | 134 | public AccessFlags with(@AccessFlagsConstant int flag) { 135 | return new AccessFlags(access | flag); 136 | } 137 | 138 | public AccessFlags without(@AccessFlagsConstant int flag) { 139 | return new AccessFlags(access & ~flag); 140 | } 141 | 142 | @MagicConstant(flagsFromClass = AccessFlags.class) 143 | public @interface AccessFlagsConstant {} 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/util/Cloner.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.util; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | 7 | import lombok.NonNull; 8 | import lombok.val; 9 | 10 | import org.jetbrains.annotations.Contract; 11 | import org.jetbrains.annotations.Nullable; 12 | import org.objectweb.asm.tree.AbstractInsnNode; 13 | import org.objectweb.asm.tree.FieldNode; 14 | import org.objectweb.asm.tree.InsnList; 15 | import org.objectweb.asm.tree.LabelNode; 16 | import org.objectweb.asm.tree.MethodNode; 17 | 18 | import dev.minco.javatransformer.api.TransformationException; 19 | 20 | public final class Cloner { 21 | public static FieldNode clone(FieldNode other) { 22 | val node = new FieldNode(other.access, other.name, other.desc, other.signature, other.value); 23 | node.attrs = other.attrs; 24 | node.invisibleAnnotations = other.invisibleAnnotations; 25 | node.invisibleTypeAnnotations = other.invisibleTypeAnnotations; 26 | node.visibleAnnotations = other.visibleAnnotations; 27 | node.visibleTypeAnnotations = other.visibleTypeAnnotations; 28 | return node; 29 | } 30 | 31 | public static MethodNode clone(MethodNode other) { 32 | val node = new MethodNode(); 33 | node.desc = other.desc; 34 | node.signature = other.signature; 35 | node.access = other.access; 36 | node.name = other.name; 37 | node.attrs = other.attrs; 38 | node.maxLocals = other.maxLocals; 39 | node.maxStack = other.maxStack; 40 | node.annotationDefault = other.annotationDefault; 41 | node.exceptions = other.exceptions; 42 | node.instructions = other.instructions; 43 | node.invisibleAnnotations = other.invisibleAnnotations; 44 | node.invisibleLocalVariableAnnotations = other.invisibleLocalVariableAnnotations; 45 | node.invisibleParameterAnnotations = other.invisibleParameterAnnotations; 46 | node.invisibleTypeAnnotations = other.invisibleTypeAnnotations; 47 | node.visibleAnnotations = other.visibleAnnotations; 48 | node.visibleLocalVariableAnnotations = other.visibleLocalVariableAnnotations; 49 | node.visibleParameterAnnotations = other.visibleParameterAnnotations; 50 | node.visibleTypeAnnotations = other.visibleTypeAnnotations; 51 | node.localVariables = other.localVariables; 52 | node.tryCatchBlocks = other.tryCatchBlocks; 53 | return node; 54 | } 55 | 56 | public static MethodNode deepClone(MethodNode other) { 57 | val node = new MethodNode(); 58 | node.desc = other.desc; 59 | node.signature = other.signature; 60 | node.access = other.access; 61 | node.name = other.name; 62 | node.attrs = clone(other.attrs); 63 | node.maxLocals = other.maxLocals; 64 | node.maxStack = other.maxStack; 65 | node.annotationDefault = other.annotationDefault; 66 | node.exceptions = clone(other.exceptions); 67 | node.instructions = other.instructions; 68 | node.invisibleAnnotations = clone(other.invisibleAnnotations); 69 | node.invisibleLocalVariableAnnotations = clone(other.invisibleLocalVariableAnnotations); 70 | node.invisibleParameterAnnotations = clone(other.invisibleParameterAnnotations); 71 | node.invisibleTypeAnnotations = clone(other.invisibleTypeAnnotations); 72 | node.visibleAnnotations = clone(other.visibleAnnotations); 73 | node.visibleLocalVariableAnnotations = clone(other.visibleLocalVariableAnnotations); 74 | node.visibleParameterAnnotations = clone(other.visibleParameterAnnotations); 75 | node.visibleTypeAnnotations = clone(other.visibleTypeAnnotations); 76 | node.localVariables = clone(other.localVariables); 77 | node.tryCatchBlocks = clone(other.tryCatchBlocks); 78 | return node; 79 | } 80 | 81 | @Contract(value = "null -> null; !null -> !null", pure = true) 82 | @Nullable 83 | private static ArrayList clone(@Nullable List list) { 84 | return list == null ? null : new ArrayList<>(list); 85 | } 86 | 87 | @Contract(value = "null -> null; !null -> !null", pure = true) 88 | @Nullable 89 | @SuppressWarnings("unchecked") 90 | private static ArrayList[] clone(@Nullable List[] lists) { 91 | if (lists == null) 92 | return null; 93 | val newList = new List[lists.length]; 94 | for (int i = 0; i < lists.length; i++) 95 | newList[i] = clone(lists[i]); 96 | return (ArrayList[]) newList; 97 | } 98 | 99 | public static InsnList clone(InsnList list) { 100 | return clone(list, list.getFirst(), list.getLast()); 101 | } 102 | 103 | public static InsnList clone(@NonNull InsnList list, @Nullable AbstractInsnNode first, @Nullable AbstractInsnNode last) { 104 | val cloned = new InsnList(); 105 | if (first == null && last == null) 106 | return cloned; 107 | if (first == null || last == null) 108 | throw new NullPointerException(); 109 | 110 | val labels = new HashMap() { 111 | @Override 112 | public LabelNode get(Object key) { 113 | val result = super.get(key); 114 | if (result == null) 115 | throw new TransformationException("Label " + key + " referenced by copied instruction in " + list + " is out of bounds to be copied " + first + "->" + last); 116 | return result; 117 | } 118 | }; 119 | 120 | AbstractInsnNode insn = first; 121 | while (true) { 122 | if ((insn instanceof LabelNode)) 123 | labels.put((LabelNode) insn, new LabelNode()); 124 | if (insn == last) 125 | break; 126 | insn = insn.getNext(); 127 | } 128 | insn = first; 129 | while (true) { 130 | cloned.add(insn.clone(labels)); 131 | if (insn == last) 132 | break; 133 | insn = insn.getNext(); 134 | } 135 | 136 | return cloned; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/test/java/dev/minco/javatransformer/api/JavaTransformerTest.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.api; 2 | 3 | import java.io.PrintStream; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | import java.nio.file.Paths; 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.Collection; 10 | import java.util.Collections; 11 | import java.util.List; 12 | import java.util.concurrent.atomic.AtomicBoolean; 13 | import java.util.stream.Collectors; 14 | 15 | import lombok.val; 16 | 17 | import org.junit.Assert; 18 | import org.junit.Rule; 19 | import org.junit.Test; 20 | import org.junit.rules.TemporaryFolder; 21 | import org.junit.runner.RunWith; 22 | import org.junit.runners.Parameterized; 23 | import org.objectweb.asm.tree.ClassNode; 24 | 25 | import com.github.javaparser.JavaParser; 26 | 27 | import dev.minco.javatransformer.api.code.CodeFragment; 28 | 29 | @RunWith(Parameterized.class) 30 | public class JavaTransformerTest { 31 | private static final int EXPECTED_METHOD_CALL_COUNT = 4; 32 | private static final String[] extensions = new String[]{"java", "class"}; 33 | private final Path input; 34 | private final List extraPaths; 35 | @Rule 36 | public TemporaryFolder folder = new TemporaryFolder(); 37 | 38 | public JavaTransformerTest(List inputs) { 39 | inputs = new ArrayList<>(inputs); 40 | this.input = inputs.remove(0); 41 | this.extraPaths = inputs; 42 | } 43 | 44 | @Parameterized.Parameters 45 | public static Collection> paths() { 46 | return Arrays.asList(getClassPath(), getSourcePath(), getMixPath()); 47 | } 48 | 49 | private static List getClassPath() { 50 | return new ArrayList<>(Collections.singletonList(JavaTransformer.pathFromClass(JavaTransformerTest.class))); 51 | } 52 | 53 | private static List getSourcePath() { 54 | return new ArrayList<>(Arrays.asList(Paths.get("src/test/java/"), Paths.get("src/main/java"))); 55 | } 56 | 57 | private static List getMixPath() { 58 | return new ArrayList<>(Arrays.asList(Paths.get("src/test/java/"), JavaTransformer.pathFromClass((JavaTransformer.class)))); 59 | } 60 | 61 | private static boolean exists(Path p) { 62 | Path base = p.getParent(); 63 | String fileName = p.getFileName().toString(); 64 | return Arrays.stream(extensions).anyMatch(it -> Files.exists(base.resolve(fileName + it))); 65 | } 66 | 67 | @Test 68 | public void testSkipPackageInfo() throws Exception { 69 | Assert.assertNull("Should skip package-info.java", new JavaTransformer().transformBytes(null, "org/example/test/package-info.java", null)); 70 | } 71 | 72 | @SuppressWarnings("ResultOfMethodCallIgnored") 73 | @Test 74 | public void testTransform() throws Exception { 75 | Path output = folder.newFolder("output").toPath(); 76 | 77 | JavaTransformer transformer = new JavaTransformer(); 78 | transformer.getClassPath().addPaths(extraPaths); 79 | transformer.getClassPath().addPaths(Arrays.asList(JavaTransformer.pathFromClass(Assert.class), JavaTransformer.pathFromClass(ClassNode.class), JavaTransformer.pathFromClass(JavaParser.class))); 80 | 81 | val targetMethod = "testMethodCallExpression"; 82 | val targetClass = this.getClass().getName(); 83 | AtomicBoolean hasProcessedTargetClass = new AtomicBoolean(false); 84 | AtomicBoolean hasProcessedTargetMethod = new AtomicBoolean(false); 85 | 86 | transformer.addTransformer(c -> { 87 | System.out.println("Transforming class: " + c.getName() + " of type " + c.getClass().getSimpleName()); 88 | val isTarget = c.getName().equals(targetClass); 89 | if (isTarget) 90 | hasProcessedTargetClass.set(true); 91 | 92 | c.accessFlags(it -> it.makeAccessible(true)); 93 | c.getAnnotations().forEach(it -> { 94 | if (it.getType().getClassName().equals(AnnotationWithDefault.class.getName())) { 95 | val contract = it.toInstance(AnnotationWithDefault.class); 96 | Assert.assertEquals(TestEnum.SECOND, contract.testEnum()); 97 | } 98 | }); 99 | val fields = c.getFields().collect(Collectors.toList()); 100 | val interfaceTypes = c.getInterfaceTypes(); 101 | c.getMembers().collect(Collectors.toList()); 102 | c.getConstructors().collect(Collectors.toList()); 103 | c.getMethods().forEach(it -> { 104 | val rt = it.getReturnType(); 105 | Assert.assertNotNull(it + " should have a return type", rt); 106 | 107 | val cf = it.getCodeFragment(); 108 | 109 | if (!c.getInterfaceTypes().contains(Type.ANNOTATION)) { 110 | Assert.assertNotNull(it + " should have a CodeFragment", cf); 111 | } 112 | 113 | if (it.getName().equals(targetMethod)) { 114 | hasProcessedTargetMethod.set(true); 115 | val methodCalls = cf.findFragments(CodeFragment.MethodCall.class); 116 | Assert.assertEquals(EXPECTED_METHOD_CALL_COUNT, methodCalls.size()); 117 | for (int i = 1; i <= EXPECTED_METHOD_CALL_COUNT; i++) { 118 | val call = methodCalls.get(i - 1); 119 | Assert.assertEquals(PrintStream.class.getName(), call.getContainingClassType().getClassName()); 120 | val inputTypes = call.getInputTypes(); 121 | Assert.assertNotNull("Should find inputTypes for method call expression", inputTypes); 122 | Assert.assertEquals(2, inputTypes.size()); 123 | Assert.assertEquals(PrintStream.class.getName(), inputTypes.get(0).type.getClassName()); 124 | Assert.assertEquals(String.valueOf(i), inputTypes.get(1).constantValue); 125 | } 126 | } else if (cf != null) { 127 | val methodCalls = cf.findFragments(CodeFragment.MethodCall.class); 128 | for (val call : methodCalls) { 129 | Assert.assertNotNull(call.getContainingClassType()); 130 | val inputTypes = call.getInputTypes(); 131 | Assert.assertNotNull("Should find inputTypes for method call expression", inputTypes); 132 | } 133 | 134 | cf.insert(cf, CodeFragment.InsertionPosition.OVERWRITE); 135 | } 136 | }); 137 | 138 | if (isTarget) { 139 | Assert.assertEquals(5, fields.size()); 140 | val field = fields.get(0); 141 | Assert.assertEquals("EXPECTED_METHOD_CALL_COUNT", field.getName()); 142 | Assert.assertEquals("int", field.getType().getJavaName()); 143 | Assert.assertTrue(interfaceTypes.isEmpty()); 144 | } 145 | }); 146 | 147 | System.out.println("Transforming path '" + input + "' to '" + output + "'"); 148 | transformer.transform(input, output); 149 | 150 | Assert.assertTrue("Transformer must process " + targetClass, hasProcessedTargetClass.get()); 151 | Assert.assertTrue("Transformer must process " + targetMethod, hasProcessedTargetMethod.get()); 152 | 153 | Assert.assertTrue(exists(output.resolve("dev/minco/javatransformer/api/JavaTransformerTest."))); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/asm/CombinedAnalyzer.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.asm; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import lombok.val; 7 | 8 | import org.objectweb.asm.Opcodes; 9 | import org.objectweb.asm.Type; 10 | import org.objectweb.asm.tree.AbstractInsnNode; 11 | import org.objectweb.asm.tree.InsnList; 12 | import org.objectweb.asm.tree.JumpInsnNode; 13 | import org.objectweb.asm.tree.LabelNode; 14 | import org.objectweb.asm.tree.LookupSwitchInsnNode; 15 | import org.objectweb.asm.tree.MethodNode; 16 | import org.objectweb.asm.tree.TableSwitchInsnNode; 17 | import org.objectweb.asm.tree.TryCatchBlockNode; 18 | import org.objectweb.asm.tree.analysis.AnalyzerException; 19 | import org.objectweb.asm.tree.analysis.Interpreter; 20 | import org.objectweb.asm.tree.analysis.Value; 21 | 22 | public class CombinedAnalyzer implements Opcodes { 23 | private final Interpreter interpreter; 24 | private Frame[] frames; 25 | private boolean[] queued; 26 | private int[] queue; 27 | private int top; 28 | 29 | private CombinedAnalyzer(Interpreter interpreter) { 30 | this.interpreter = interpreter; 31 | } 32 | 33 | @SuppressWarnings("unchecked") 34 | public static Frame[] analyze(Interpreter interpreter, final String owner, final MethodNode m) throws AnalyzerException { 35 | if ((m.access & (ACC_ABSTRACT | ACC_NATIVE)) != 0 || m.instructions.size() == 0) { 36 | return (Frame[]) new Frame[0]; 37 | } 38 | return new CombinedAnalyzer<>(interpreter).analyze(owner, m); 39 | } 40 | 41 | @SuppressWarnings("unchecked") 42 | private Frame[] analyze(final String owner, final MethodNode m) throws AnalyzerException { 43 | int n = m.instructions.size(); 44 | InsnList insns = m.instructions; 45 | List[] handlers = (List[]) new List[n]; 46 | frames = (Frame[]) new Frame[n + 1]; 47 | queued = new boolean[n]; 48 | queue = new int[n]; 49 | top = 0; 50 | 51 | // computes exception handlers for each instruction 52 | for (int i = 0; i < m.tryCatchBlocks.size(); ++i) { 53 | TryCatchBlockNode tcb = m.tryCatchBlocks.get(i); 54 | int begin = insns.indexOf(tcb.start); 55 | int end = insns.indexOf(tcb.end); 56 | for (int j = begin; j < end; ++j) { 57 | List insnHandlers = handlers[j]; 58 | if (insnHandlers == null) { 59 | insnHandlers = new ArrayList<>(); 60 | handlers[j] = insnHandlers; 61 | } 62 | insnHandlers.add(tcb); 63 | } 64 | } 65 | 66 | // initializes the data structures for the control flow analysis 67 | Frame current = new Frame<>(m.maxLocals, m.maxStack); 68 | Frame handler = new Frame<>(m.maxLocals, m.maxStack); 69 | current.setReturn(interpreter.newValue(Type.getReturnType(m.desc))); 70 | Type[] args = Type.getArgumentTypes(m.desc); 71 | int local = 0; 72 | if ((m.access & ACC_STATIC) == 0) { 73 | Type ctype = Type.getObjectType(owner); 74 | current.setLocal(local++, interpreter.newValue(ctype)); 75 | } 76 | for (Type arg : args) { 77 | current.setLocal(local++, interpreter.newValue(arg)); 78 | if (arg.getSize() == 2) { 79 | current.setLocal(local++, interpreter.newValue(null)); 80 | } 81 | } 82 | while (local < m.maxLocals) { 83 | current.setLocal(local++, interpreter.newValue(null)); 84 | } 85 | merge(0, current); 86 | 87 | // control flow analysis 88 | while (top > 0) { 89 | int insn = queue[--top]; 90 | Frame f = frames[insn]; 91 | queued[insn] = false; 92 | 93 | AbstractInsnNode insnNode = null; 94 | try { 95 | insnNode = m.instructions.get(insn); 96 | int insnOpcode = insnNode.getOpcode(); 97 | int insnType = insnNode.getType(); 98 | 99 | if (insnType == AbstractInsnNode.LABEL || insnType == AbstractInsnNode.LINE || insnType == AbstractInsnNode.FRAME) { 100 | merge(insn + 1, f); 101 | } else { 102 | current.init(f).execute(insnNode, interpreter); 103 | 104 | if (insnNode instanceof JumpInsnNode) { 105 | JumpInsnNode j = (JumpInsnNode) insnNode; 106 | if (insnOpcode != GOTO && insnOpcode != JSR) { 107 | merge(insn + 1, current); 108 | } 109 | int jump = insns.indexOf(j.label); 110 | merge(jump, current); 111 | } else if (insnNode instanceof LookupSwitchInsnNode) { 112 | LookupSwitchInsnNode lsi = (LookupSwitchInsnNode) insnNode; 113 | int jump = insns.indexOf(lsi.dflt); 114 | merge(jump, current); 115 | for (LabelNode label : lsi.labels) { 116 | jump = insns.indexOf(label); 117 | merge(jump, current); 118 | } 119 | } else if (insnNode instanceof TableSwitchInsnNode) { 120 | TableSwitchInsnNode tsi = (TableSwitchInsnNode) insnNode; 121 | int jump = insns.indexOf(tsi.dflt); 122 | merge(jump, current); 123 | for (LabelNode label : tsi.labels) { 124 | jump = insns.indexOf(label); 125 | merge(jump, current); 126 | } 127 | } else if (insnOpcode != ATHROW && (insnOpcode < IRETURN || insnOpcode > RETURN)) { 128 | merge(insn + 1, current); 129 | } 130 | } 131 | 132 | List insnHandlers = handlers[insn]; 133 | if (insnHandlers != null) { 134 | for (TryCatchBlockNode tcb : insnHandlers) { 135 | Type type; 136 | if (tcb.type == null) { 137 | type = Type.getObjectType("java/lang/Throwable"); 138 | } else { 139 | type = Type.getObjectType(tcb.type); 140 | } 141 | int jump = insns.indexOf(tcb.handler); 142 | handler.init(f); 143 | handler.clearStack(); 144 | handler.push(interpreter.newValue(type)); 145 | merge(jump, handler); 146 | } 147 | } 148 | } catch (Exception e) { 149 | val errorNode = e instanceof AnalyzerException ? ((AnalyzerException) e).node : insnNode; 150 | val opCode = insnNode == null ? -1 : insnNode.getOpcode(); 151 | val message = "Error at instruction " + insn + " " + opCode + " " + e.getClass().getName() + ": " + e.getMessage(); 152 | throw new AnalyzerException(errorNode, message, e); 153 | } 154 | } 155 | 156 | return frames; 157 | } 158 | 159 | private void merge(final int insn, final Frame frame) throws AnalyzerException { 160 | Frame oldFrame = frames[insn]; 161 | boolean changes; 162 | 163 | if (oldFrame == null) { 164 | frames[insn] = new Frame<>(frame); 165 | changes = true; 166 | } else { 167 | changes = oldFrame.merge(frame, interpreter); 168 | } 169 | if (changes && insn < queued.length && !queued[insn]) { 170 | queued[insn] = true; 171 | queue[top++] = insn; 172 | } 173 | } 174 | 175 | public static class Frame extends org.objectweb.asm.tree.analysis.Frame { 176 | public Frame(int nLocals, int nStack) { 177 | super(nLocals, nStack); 178 | } 179 | 180 | public Frame(org.objectweb.asm.tree.analysis.Frame src) { 181 | super(src); 182 | } 183 | 184 | @Override 185 | @SuppressWarnings("unchecked") 186 | public V pop() { 187 | val top = getStackSize(); 188 | if (top == 0) { 189 | push((V) CombinedValue.of(null, CombinedValue.POPPED_FROM_BOTTOM)); 190 | } 191 | 192 | return super.pop(); 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/util/JVMUtil.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.util; 2 | 3 | import java.lang.reflect.Method; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | import lombok.val; 9 | 10 | import org.jetbrains.annotations.Contract; 11 | import org.jetbrains.annotations.Nullable; 12 | 13 | import dev.minco.javatransformer.api.AccessFlags; 14 | import dev.minco.javatransformer.api.AccessFlags.AccessFlagsConstant; 15 | import dev.minco.javatransformer.api.TransformationException; 16 | 17 | public final class JVMUtil { 18 | private static final Splitter dotSplitter = Splitter.on('.'); 19 | 20 | public static String getDescriptor(Class clazz) { 21 | if (clazz.isPrimitive()) { 22 | return descriptorToPrimitiveType(clazz.getSimpleName()); 23 | } 24 | return 'L' + clazz.getCanonicalName() + ';'; 25 | } 26 | 27 | public static String descriptorToPrimitiveType(String descriptor) { 28 | switch (descriptor) { 29 | case "B": 30 | return "byte"; 31 | case "C": 32 | return "char"; 33 | case "D": 34 | return "double"; 35 | case "F": 36 | return "float"; 37 | case "I": 38 | return "int"; 39 | case "J": 40 | return "long"; 41 | case "S": 42 | return "short"; 43 | case "V": 44 | return "void"; 45 | case "Z": 46 | return "boolean"; 47 | } 48 | 49 | throw new TransformationException("Invalid descriptor: " + descriptor); 50 | } 51 | 52 | public static String primitiveTypeToDescriptor(String primitive) { 53 | return primitiveTypeToDescriptor(primitive, false); 54 | } 55 | 56 | @Nullable 57 | @Contract("_, false -> !null") 58 | public static String primitiveTypeToDescriptor(String primitive, boolean allowMissing) { 59 | switch (primitive) { 60 | case "byte": 61 | return "B"; 62 | case "char": 63 | return "C"; 64 | case "double": 65 | return "D"; 66 | case "float": 67 | return "F"; 68 | case "int": 69 | return "I"; 70 | case "long": 71 | return "J"; 72 | case "short": 73 | return "S"; 74 | case "void": 75 | return "V"; 76 | case "boolean": 77 | return "Z"; 78 | } 79 | 80 | if (allowMissing) 81 | return null; 82 | 83 | throw new TransformationException("Invalid primitive type: " + primitive); 84 | } 85 | 86 | public static > T searchEnum(Class enumeration, String search) { 87 | for (T each : enumeration.getEnumConstants()) { 88 | if (each.name().equalsIgnoreCase(search)) { 89 | return each; 90 | } 91 | } 92 | throw new IllegalArgumentException("Can't find enum value with name " + search + " in " + enumeration); 93 | } 94 | 95 | public static String getParameterList(Method m) { 96 | List> parameterClasses = new ArrayList<>(Arrays.asList(m.getParameterTypes())); 97 | StringBuilder parameters = new StringBuilder(); 98 | for (Class clazz : parameterClasses) { 99 | parameters.append(getDescriptor(clazz)); 100 | } 101 | return parameters.toString(); 102 | } 103 | 104 | public static String accessIntToString(@AccessFlagsConstant int access) { 105 | StringBuilder result = new StringBuilder(); 106 | 107 | if (hasFlag(access, AccessFlags.ACC_PUBLIC)) 108 | result.append(" public"); 109 | 110 | if (hasFlag(access, AccessFlags.ACC_PRIVATE)) 111 | result.append(" private"); 112 | 113 | if (hasFlag(access, AccessFlags.ACC_PROTECTED)) 114 | result.append(" protected"); 115 | 116 | if (hasFlag(access, AccessFlags.ACC_STATIC)) 117 | result.append(" static"); 118 | 119 | if (hasFlag(access, AccessFlags.ACC_FINAL)) 120 | result.append(" final"); 121 | 122 | return result.toString().trim(); 123 | } 124 | 125 | @AccessFlagsConstant 126 | public static int accessStringToInt(String access) { 127 | int a = 0; 128 | for (String accessPart : Splitter.on(' ').splitIterable(access)) { 129 | switch (accessPart) { 130 | case "public": 131 | a |= AccessFlags.ACC_PUBLIC; 132 | break; 133 | case "protected": 134 | a |= AccessFlags.ACC_PROTECTED; 135 | break; 136 | case "private": 137 | a |= AccessFlags.ACC_PRIVATE; 138 | break; 139 | case "static": 140 | a |= AccessFlags.ACC_STATIC; 141 | break; 142 | case "synthetic": 143 | a |= AccessFlags.ACC_SYNTHETIC; 144 | break; 145 | case "final": 146 | a |= AccessFlags.ACC_FINAL; 147 | break; 148 | default: 149 | throw new TransformationException("Unknown access string " + access); 150 | } 151 | } 152 | return a; 153 | } 154 | 155 | public static String fileNameToClassName(String f) { 156 | f = removeFromEnd(f, ".class"); 157 | f = removeFromEnd(f, ".java"); 158 | val result = f.replace('\\', '.').replace('/', '.'); 159 | return result.isEmpty() ? "" : (result.charAt(0) == '.' ? result.substring(1) : result); 160 | } 161 | 162 | public static String classNameToFileName(String f) { 163 | return classNameToSlashName(f) + ".class"; 164 | } 165 | 166 | public static String classNameToSlashName(String f) { 167 | return f.replace('.', '/'); 168 | } 169 | 170 | public static String classNameToSlashName(Class returnClass) { 171 | return classNameToSlashName(returnClass.getName()); 172 | } 173 | 174 | private static String removeFromEnd(String s, String f) { 175 | return s.endsWith(f) ? s.substring(0, s.length() - f.length()) : s; 176 | } 177 | 178 | public static boolean hasFlag(int access, int flag) { 179 | return (access & flag) != 0; 180 | } 181 | 182 | @AccessFlagsConstant 183 | public static int replaceFlag(@AccessFlagsConstant int in, @AccessFlagsConstant int from, @AccessFlagsConstant int to) { 184 | if ((in & from) != 0) { 185 | in &= ~from; 186 | in |= to; 187 | } 188 | return in; 189 | } 190 | 191 | @AccessFlagsConstant 192 | public static int makeAccess(@AccessFlagsConstant int access, boolean makePublic) { 193 | access = makeAtLeastProtected(access); 194 | if (makePublic) { 195 | access = replaceFlag(access, AccessFlags.ACC_PROTECTED, AccessFlags.ACC_PUBLIC); 196 | } 197 | return access; 198 | } 199 | 200 | @AccessFlagsConstant 201 | public static int makeAtLeastProtected(@AccessFlagsConstant int access) { 202 | if (hasFlag(access, AccessFlags.ACC_PUBLIC) || hasFlag(access, AccessFlags.ACC_PROTECTED)) { 203 | // already protected or public 204 | return access; 205 | } 206 | if (hasFlag(access, AccessFlags.ACC_PRIVATE)) { 207 | // private -> protected 208 | return replaceFlag(access, AccessFlags.ACC_PRIVATE, AccessFlags.ACC_PROTECTED); 209 | } 210 | // not public, protected or private so must be package-local 211 | // change to public - protected doesn't include package-local. 212 | return access | AccessFlags.ACC_PUBLIC; 213 | } 214 | 215 | public static String classNameToJLSName(String className) { 216 | List parts = new ArrayList<>(); 217 | dotSplitter.splitIterable(className).forEach(parts::add); 218 | 219 | boolean possibleClass = true; 220 | for (int i = parts.size() - 1, size = i; i >= 0; i--) { 221 | String part = parts.get(i); 222 | 223 | boolean last = i == size; 224 | 225 | if (!last && !Character.isUpperCase(part.charAt(0))) { 226 | possibleClass = false; 227 | } 228 | 229 | if (!last) { 230 | parts.set(i, part + (possibleClass ? '$' : '/')); 231 | } 232 | } 233 | 234 | return Joiner.on().join(parts); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/JavaParserCodeFragmentGenerator.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal; 2 | 3 | import java.lang.reflect.Constructor; 4 | import java.util.ArrayList; 5 | import java.util.Collections; 6 | import java.util.List; 7 | import java.util.Objects; 8 | import java.util.stream.Collectors; 9 | 10 | import lombok.NonNull; 11 | import lombok.SneakyThrows; 12 | import lombok.val; 13 | 14 | import com.github.javaparser.ast.Node; 15 | import com.github.javaparser.ast.expr.Expression; 16 | import com.github.javaparser.ast.expr.MethodCallExpr; 17 | import com.github.javaparser.ast.expr.ThisExpr; 18 | import com.github.javaparser.ast.nodeTypes.NodeWithBlockStmt; 19 | import com.github.javaparser.ast.stmt.BlockStmt; 20 | import com.github.javaparser.ast.stmt.ExpressionStmt; 21 | import com.github.javaparser.ast.stmt.Statement; 22 | 23 | import dev.minco.javatransformer.api.MethodInfo; 24 | import dev.minco.javatransformer.api.TransformationException; 25 | import dev.minco.javatransformer.api.Type; 26 | import dev.minco.javatransformer.api.code.CodeFragment; 27 | import dev.minco.javatransformer.api.code.IntermediateValue; 28 | import dev.minco.javatransformer.internal.javaparser.Expressions; 29 | import dev.minco.javatransformer.internal.util.CachingSupplier; 30 | import dev.minco.javatransformer.internal.util.CodeFragmentUtil; 31 | import dev.minco.javatransformer.internal.util.NodeUtil; 32 | 33 | public class JavaParserCodeFragmentGenerator { 34 | static Class concreteImplementation(Class interfaceType) { 35 | if (interfaceType == CodeFragment.Body.class) 36 | return CallableDeclarationCodeFragment.class; 37 | if (interfaceType == CodeFragment.MethodCall.class) 38 | return MethodCall.class; 39 | throw new UnsupportedOperationException("No ASM implementation for " + interfaceType); 40 | } 41 | 42 | public abstract static class JavaParserCodeFragment implements CodeFragment { 43 | final SourceInfo.CallableDeclarationWrapper containingWrapper; 44 | 45 | protected Node getNode() { 46 | return getContainingBody(); 47 | } 48 | 49 | public JavaParserCodeFragment(SourceInfo.CallableDeclarationWrapper containingWrapper) { 50 | this.containingWrapper = containingWrapper; 51 | } 52 | 53 | @Override 54 | public ExecutionOutcome getExecutionOutcome() { 55 | // TODO: implement this? 56 | return new ExecutionOutcome(true, true, true); 57 | } 58 | 59 | @Override 60 | public void insert(@NonNull CodeFragment codeFragment, @NonNull InsertionPosition position, @NonNull InsertionOptions insertionOptions) { 61 | if (CodeFragmentUtil.validateInsert(this, codeFragment, position, insertionOptions)) { 62 | return; 63 | } 64 | 65 | if (!(codeFragment instanceof JavaParserCodeFragment)) { 66 | throw new UnsupportedOperationException("Can't insert source code into byte code"); 67 | } 68 | 69 | val cf = (JavaParserCodeFragment) codeFragment; 70 | Node currentNode = getNode(); 71 | Node insertNode = cf.getNode().clone(); 72 | 73 | SourceInfo.changeTypeContext(cf.containingWrapper.getContext(), containingWrapper.getContext(), insertNode); 74 | 75 | if (insertNode instanceof BlockStmt) { 76 | val statements = ((BlockStmt) insertNode).getStatements(); 77 | if (statements.size() == 0 && (position == InsertionPosition.BEFORE || position == InsertionPosition.AFTER)) { 78 | return; 79 | } 80 | if (statements.size() == 1) { 81 | insertNode = statements.get(0); 82 | } 83 | } 84 | 85 | if (!(insertNode instanceof Statement)) { 86 | if (insertNode instanceof Expression) { 87 | insertNode = new ExpressionStmt((Expression) insertNode); 88 | } else { 89 | throw new UnsupportedOperationException("TODO " + insertNode.getClass() + "\n" + insertNode); 90 | } 91 | } 92 | 93 | if (currentNode instanceof NodeWithBlockStmt) { 94 | currentNode = ((NodeWithBlockStmt) currentNode).getBody(); 95 | } 96 | 97 | switch (position) { 98 | case BEFORE: 99 | ((BlockStmt) currentNode).addStatement(0, (Statement) insertNode); 100 | break; 101 | case OVERWRITE: 102 | throw new UnsupportedOperationException(); 103 | case AFTER: 104 | ((BlockStmt) currentNode).addStatement((Statement) insertNode); 105 | break; 106 | } 107 | } 108 | 109 | private BlockStmt getContainingBody() { 110 | return Objects.requireNonNull(containingWrapper.getBody()); 111 | } 112 | 113 | @Override 114 | @SuppressWarnings({"JavaReflectionMemberAccess", "unchecked"}) 115 | @SneakyThrows 116 | public List findFragments(Class fragmentType) { 117 | if (fragmentType.isInstance(this)) 118 | return Collections.singletonList((T) this); 119 | 120 | val constructor = (Constructor) concreteImplementation(fragmentType).getDeclaredConstructors()[0]; 121 | val list = new ArrayList(); 122 | for (Node node : NodeUtil.findWithinMethodScope((Class) constructor.getParameterTypes()[1], getContainingBody())) { 123 | list.add(constructor.newInstance(containingWrapper, node)); 124 | } 125 | 126 | return list; 127 | } 128 | } 129 | 130 | public static class CallableDeclarationCodeFragment extends JavaParserCodeFragment implements CodeFragment.Body { 131 | public CallableDeclarationCodeFragment(SourceInfo.CallableDeclarationWrapper containingWrapper) { 132 | super(containingWrapper); 133 | } 134 | 135 | @NonNull 136 | @Override 137 | public List getInputTypes() { 138 | return containingWrapper.getParameters().stream().map(it -> new IntermediateValue(it.type, IntermediateValue.UNKNOWN, new IntermediateValue.Location(IntermediateValue.LocationType.LOCAL, -1, it.name))).collect(Collectors.toList()); 139 | } 140 | 141 | @NonNull 142 | @Override 143 | public List getOutputTypes() { 144 | val ret = containingWrapper.getReturnType(); 145 | if (ret.getDescriptorType() == Type.DescriptorType.VOID) 146 | return Collections.emptyList(); 147 | 148 | return Collections.singletonList(new IntermediateValue(ret, IntermediateValue.UNKNOWN, new IntermediateValue.Location(IntermediateValue.LocationType.LOCAL, -1, "return;"))); 149 | } 150 | } 151 | 152 | public static class MethodCall extends JavaParserCodeFragment implements CodeFragment.MethodCall { 153 | final MethodCallExpr expr; 154 | final CachingSupplier> inputTypes; 155 | final CachingSupplier methodInfo; 156 | 157 | public MethodCall(SourceInfo.CallableDeclarationWrapper containingWrapper, MethodCallExpr methodCallExpr) { 158 | super(containingWrapper); 159 | this.expr = methodCallExpr; 160 | inputTypes = CachingSupplier.of(() -> Expressions.getMethodCallInputTypes(expr, containingWrapper.getContext())); 161 | methodInfo = CachingSupplier.of(() -> { 162 | val context = containingWrapper.getContext(); 163 | val name = getName(); 164 | val scopeType = Expressions.expressionToType(expr.getScope().orElse(new ThisExpr()), context, true); 165 | val mi = context.resolveMethodCallType(scopeType, name, inputTypes); 166 | if (mi == null) { 167 | throw new TransformationException("Couldn't find method {" + name + "} on {" + scopeType + "} for {" + expr + "} with params {" + Expressions.getMethodCallInputTypes(expr, context) + "}"); 168 | } 169 | return mi; 170 | }); 171 | } 172 | 173 | @Override 174 | protected Node getNode() { 175 | return expr; 176 | } 177 | 178 | @NonNull 179 | @Override 180 | public List getInputTypes() { 181 | return Expressions.getMethodCallInputIVs(expr, containingWrapper.getContext(), inputTypes, methodInfo.get()); 182 | } 183 | 184 | @NonNull 185 | @Override 186 | public List getOutputTypes() { 187 | return Collections.emptyList(); 188 | } 189 | 190 | @NonNull 191 | @Override 192 | public Type getContainingClassType() { 193 | return methodInfo.get().getClassInfo().getType(); 194 | } 195 | 196 | @NonNull 197 | @Override 198 | public String getName() { 199 | return expr.getNameAsString(); 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/ClassPaths.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal; 2 | 3 | import java.io.File; 4 | import java.io.IOError; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.lang.management.ManagementFactory; 8 | import java.net.URI; 9 | import java.nio.file.FileSystems; 10 | import java.nio.file.FileVisitResult; 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.nio.file.Paths; 14 | import java.nio.file.SimpleFileVisitor; 15 | import java.nio.file.attribute.BasicFileAttributes; 16 | import java.util.ArrayList; 17 | import java.util.Arrays; 18 | import java.util.Collection; 19 | import java.util.Collections; 20 | import java.util.HashMap; 21 | import java.util.Iterator; 22 | import java.util.Map; 23 | import java.util.Objects; 24 | import java.util.function.Supplier; 25 | import java.util.stream.Collectors; 26 | import java.util.zip.ZipFile; 27 | 28 | import javax.annotation.Nonnull; 29 | import javax.annotation.Nullable; 30 | 31 | import lombok.SneakyThrows; 32 | import lombok.val; 33 | 34 | import org.jetbrains.annotations.NotNull; 35 | 36 | import com.github.javaparser.StaticJavaParser; 37 | import com.github.javaparser.ast.CompilationUnit; 38 | 39 | import dev.minco.javatransformer.api.ClassInfo; 40 | import dev.minco.javatransformer.api.ClassPath; 41 | import dev.minco.javatransformer.internal.asm.AsmUtil; 42 | import dev.minco.javatransformer.internal.javaparser.CompilationUnitInfo; 43 | import dev.minco.javatransformer.internal.util.CachingSupplier; 44 | import dev.minco.javatransformer.internal.util.CollectionUtil; 45 | import dev.minco.javatransformer.internal.util.JVMUtil; 46 | import dev.minco.javatransformer.internal.util.Splitter; 47 | import dev.minco.javatransformer.internal.util.StreamUtil; 48 | 49 | public final class ClassPaths { 50 | public static ClassPath of(ClassPath systemClassPath, Path... paths) { 51 | return new FileClassPath(systemClassPath, new ArrayList<>(Arrays.asList(paths))); 52 | } 53 | 54 | public static class SystemClassPath { 55 | public static final ClassPath SYSTEM_CLASS_PATH = makeSystemJarClassPath(); 56 | 57 | private static ClassPath makeSystemJarClassPath() { 58 | // only scan java/ files in boot class path 59 | // avoid JVM/JDK internals 60 | // TODO: self-test, if we can't load JDK classes with current asm version fall back to reflection 61 | try { 62 | val paths = Splitter.pathSplitter.split(ManagementFactory.getRuntimeMXBean().getBootClassPath()) 63 | .map(it -> Paths.get(it)).filter(it -> it.getFileName().toString().equals("rt.jar")) 64 | .collect(Collectors.toList()); 65 | return new FileClassPath(null, paths); 66 | } catch (UnsupportedOperationException ignored) { 67 | val fs = FileSystems.getFileSystem(URI.create("jrt:/")); 68 | return new FileClassPath(null, Collections.singletonList(fs.getPath("modules/java.base/"))); 69 | } 70 | } 71 | } 72 | 73 | private static abstract class ClassPathSolver implements ClassPath { 74 | @Nullable 75 | final ClassPath parent; 76 | 77 | ClassPathSolver(@Nullable ClassPath parent) { 78 | this.parent = parent; 79 | } 80 | 81 | static Path normalise(Path path) { 82 | return path.toAbsolutePath().normalize(); 83 | } 84 | } 85 | 86 | static class FileClassPath extends ClassPathSolver { 87 | private final Map entries = new HashMap<>(); 88 | private final Collection paths; 89 | private boolean initialised; 90 | 91 | public FileClassPath(@Nullable ClassPath parent, Collection paths) { 92 | super(parent); 93 | this.paths = paths; 94 | } 95 | 96 | @Nullable 97 | @Override 98 | public ClassInfo getClassInfo(@Nonnull String className) { 99 | Objects.requireNonNull(className); 100 | if (parent != null) { 101 | val p = parent.getClassInfo(className); 102 | if (p != null) 103 | return p; 104 | } 105 | if (!initialised) 106 | initialise(); 107 | return entries.get(className); 108 | } 109 | 110 | @Override 111 | public synchronized boolean addPath(Path path) { 112 | path = normalise(path); 113 | if (hasPath(path)) { 114 | return false; 115 | } 116 | paths.add(path); 117 | if (initialised) { 118 | loadPath(path); 119 | } 120 | return true; 121 | } 122 | 123 | @Override 124 | public boolean hasPath(Path path) { 125 | path = normalise(path); 126 | return paths.contains(path) || (parent != null && parent.hasPath(path)); 127 | } 128 | 129 | @NotNull 130 | @Override 131 | public Iterator iterator() { 132 | initialise(); 133 | if (parent == null) { 134 | return entries.values().iterator(); 135 | } 136 | return CollectionUtil.union(parent, entries.values()); 137 | } 138 | 139 | @SneakyThrows 140 | private void findPaths(String entryName, Supplier iss) { 141 | if (entryName.endsWith(".java")) 142 | try (val is = iss.get()) { 143 | findJavaPaths(is); 144 | } 145 | 146 | if (entryName.endsWith(".class")) { 147 | String name = JVMUtil.fileNameToClassName(entryName); 148 | entries.put(name, new ByteCodeInfo(CachingSupplier.of(() -> AsmUtil.getClassNode(StreamUtil.readFully(iss.get()), null)), name, Collections.emptyMap())); 149 | } 150 | } 151 | 152 | private void findJavaPaths(InputStream is) { 153 | val parsed = StaticJavaParser.parse(is); 154 | findJavaPaths(parsed); 155 | } 156 | 157 | private void findJavaPaths(CompilationUnit compilationUnit) { 158 | for (ClassInfo classInfo : CompilationUnitInfo.getSourceInfos(compilationUnit, this)) 159 | entries.put(classInfo.getName(), classInfo); 160 | } 161 | 162 | private synchronized void initialise() { 163 | if (!initialised) { 164 | for (Path path : paths) 165 | loadPath(path); 166 | initialised = true; 167 | } 168 | } 169 | 170 | @SneakyThrows 171 | private void loadPath(Path path) { 172 | if (Files.isDirectory(path)) 173 | Files.walkFileTree(path, new SimpleFileVisitor() { 174 | @SneakyThrows 175 | @Override 176 | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { 177 | val entryName = path.relativize(file).toString().replace(File.separatorChar, '/'); 178 | findPaths(entryName, () -> { 179 | try { 180 | return Files.newInputStream(file); 181 | } catch (IOException e) { 182 | throw new IOError(e); 183 | } 184 | }); 185 | return super.visitFile(file, attrs); 186 | } 187 | }); 188 | else if (Files.isRegularFile(path)) 189 | try (val zf = new ZipFile(path.toFile())) { 190 | val e$ = zf.entries(); 191 | while (e$.hasMoreElements()) { 192 | val ze = e$.nextElement(); 193 | val name = ze.getName(); 194 | findPaths(name, () -> { 195 | try { 196 | val zff = new ZipFile(path.toFile()); 197 | return zff.getInputStream(zff.getEntry(name)); 198 | } catch (IOException e) { 199 | throw new IOError(e); 200 | } 201 | }); 202 | } 203 | } 204 | } 205 | 206 | @Override 207 | public String toString() { 208 | return "FileClassPath{" + 209 | "entries.size()=" + entries.size() + 210 | ", paths=" + paths + 211 | ", initialised=" + initialised + 212 | ", parent=" + parent + 213 | '}'; 214 | } 215 | } 216 | 217 | /* 218 | @Getter 219 | @Setter 220 | private static class SimpleClassInfo implements ClassInfo { 221 | private Type superType; 222 | private List interfaceTypes; 223 | private String name; 224 | private AccessFlags accessFlags = new AccessFlags(0); 225 | private List methods = new ArrayList<>(); 226 | private List fields = new ArrayList<>(); 227 | private List annotations = new ArrayList<>(); 228 | 229 | @Override 230 | public void add(MethodInfo method) { 231 | methods.add(method); 232 | } 233 | 234 | @Override 235 | public void add(FieldInfo field) { 236 | fields.add(field); 237 | } 238 | 239 | @Override 240 | public void remove(MethodInfo method) { 241 | methods.remove(method); 242 | } 243 | 244 | @Override 245 | public void remove(FieldInfo field) { 246 | fields.remove(field); 247 | } 248 | } 249 | */ 250 | } 251 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | # Collect all arguments for the java command; 201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 202 | # shell script including quotes and variable substitutions, so put them in 203 | # double quotes to make sure that they get re-expanded; and 204 | # * put everything else in single quotes, so that it's not re-expanded. 205 | 206 | set -- \ 207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 208 | -classpath "$CLASSPATH" \ 209 | org.gradle.wrapper.GradleWrapperMain \ 210 | "$@" 211 | 212 | # Stop when "xargs" is not available. 213 | if ! command -v xargs >/dev/null 2>&1 214 | then 215 | die "xargs is not available" 216 | fi 217 | 218 | # Use "xargs" to parse quoted args. 219 | # 220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 221 | # 222 | # In Bash we could simply go: 223 | # 224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 225 | # set -- "${ARGS[@]}" "$@" 226 | # 227 | # but POSIX shell has neither arrays nor command substitution, so instead we 228 | # post-process each arg (as a line of input to sed) to backslash-escape any 229 | # character that might be a shell metacharacter, then use eval to reverse 230 | # that process (while maintaining the separation between arguments), and wrap 231 | # the whole thing up as a single "set" statement. 232 | # 233 | # This will of course break if any of these variables contains a newline or 234 | # an unmatched quote. 235 | # 236 | 237 | eval "set -- $( 238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 239 | xargs -n1 | 240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 241 | tr '\n' ' ' 242 | )" '"$@"' 243 | 244 | exec "$JAVACMD" "$@" 245 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/asm/CombinedInterpreter.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.asm; 2 | 3 | import java.util.Collections; 4 | import java.util.HashSet; 5 | import java.util.List; 6 | import java.util.Objects; 7 | 8 | import lombok.val; 9 | 10 | import org.jetbrains.annotations.Nullable; 11 | import org.objectweb.asm.Handle; 12 | import org.objectweb.asm.Opcodes; 13 | import org.objectweb.asm.Type; 14 | import org.objectweb.asm.tree.AbstractInsnNode; 15 | import org.objectweb.asm.tree.FieldInsnNode; 16 | import org.objectweb.asm.tree.IntInsnNode; 17 | import org.objectweb.asm.tree.InvokeDynamicInsnNode; 18 | import org.objectweb.asm.tree.LdcInsnNode; 19 | import org.objectweb.asm.tree.MethodInsnNode; 20 | import org.objectweb.asm.tree.MultiANewArrayInsnNode; 21 | import org.objectweb.asm.tree.TypeInsnNode; 22 | import org.objectweb.asm.tree.analysis.AnalyzerException; 23 | import org.objectweb.asm.tree.analysis.Interpreter; 24 | 25 | public class CombinedInterpreter extends Interpreter implements Opcodes { 26 | public CombinedInterpreter() { 27 | super(ASM5); 28 | } 29 | 30 | protected CombinedInterpreter(final int api) { 31 | super(api); 32 | } 33 | 34 | @Nullable 35 | @Override 36 | public CombinedValue unaryOperation(final AbstractInsnNode insn, 37 | final CombinedValue value) throws AnalyzerException { 38 | switch (insn.getOpcode()) { 39 | case INEG: 40 | case IINC: 41 | case L2I: 42 | case F2I: 43 | case D2I: 44 | case I2B: 45 | case I2C: 46 | case I2S: 47 | return CombinedValue.of(Type.INT_TYPE, insn); 48 | case FNEG: 49 | case I2F: 50 | case L2F: 51 | case D2F: 52 | return CombinedValue.of(Type.FLOAT_TYPE, insn); 53 | case LNEG: 54 | case I2L: 55 | case F2L: 56 | case D2L: 57 | return CombinedValue.of(Type.LONG_TYPE, insn); 58 | case DNEG: 59 | case I2D: 60 | case L2D: 61 | case F2D: 62 | return CombinedValue.of(Type.DOUBLE_TYPE, insn); 63 | case IFEQ: 64 | case IFNE: 65 | case IFLT: 66 | case IFGE: 67 | case IFGT: 68 | case IFLE: 69 | case TABLESWITCH: 70 | case LOOKUPSWITCH: 71 | case IRETURN: 72 | case LRETURN: 73 | case FRETURN: 74 | case DRETURN: 75 | case ARETURN: 76 | case PUTSTATIC: 77 | return null; 78 | case GETFIELD: 79 | return CombinedValue.of(Type.getType(((FieldInsnNode) insn).desc), insn); 80 | case NEWARRAY: 81 | switch (((IntInsnNode) insn).operand) { 82 | case T_BOOLEAN: 83 | return CombinedValue.of(Type.getType("[Z"), insn); 84 | case T_CHAR: 85 | return CombinedValue.of(Type.getType("[C"), insn); 86 | case T_BYTE: 87 | return CombinedValue.of(Type.getType("[B"), insn); 88 | case T_SHORT: 89 | return CombinedValue.of(Type.getType("[S"), insn); 90 | case T_INT: 91 | return CombinedValue.of(Type.getType("[I"), insn); 92 | case T_FLOAT: 93 | return CombinedValue.of(Type.getType("[F"), insn); 94 | case T_DOUBLE: 95 | return CombinedValue.of(Type.getType("[D"), insn); 96 | case T_LONG: 97 | return CombinedValue.of(Type.getType("[J"), insn); 98 | default: 99 | throw new AnalyzerException(insn, "Invalid array type"); 100 | } 101 | case ANEWARRAY: 102 | String desc = ((TypeInsnNode) insn).desc; 103 | return CombinedValue.of(Type.getType("[" + Type.getObjectType(desc)), insn); 104 | case ARRAYLENGTH: 105 | return CombinedValue.of(Type.INT_TYPE, insn); 106 | case ATHROW: 107 | return null; 108 | case CHECKCAST: 109 | desc = ((TypeInsnNode) insn).desc; 110 | return CombinedValue.of(Type.getObjectType(desc), Collections.singleton(insn)); 111 | case INSTANCEOF: 112 | return CombinedValue.of(Type.INT_TYPE, insn); 113 | case MONITORENTER: 114 | case MONITOREXIT: 115 | case IFNULL: 116 | case IFNONNULL: 117 | return null; 118 | default: 119 | throw new Error("Internal error."); 120 | } 121 | } 122 | 123 | @Nullable 124 | @Override 125 | public CombinedValue ternaryOperation(final AbstractInsnNode insn, final CombinedValue value1, final CombinedValue value2, final CombinedValue value3) throws AnalyzerException { 126 | return null; 127 | //return naryOperation(insn, Arrays.asList(value1, value2, value3)); 128 | } 129 | 130 | @Nullable 131 | @Override 132 | public CombinedValue naryOperation(final AbstractInsnNode insn, final List values) throws AnalyzerException { 133 | int opcode = insn.getOpcode(); 134 | if (opcode == MULTIANEWARRAY) { 135 | return CombinedValue.of(Type.getType(((MultiANewArrayInsnNode) insn).desc), insn); 136 | } else if (opcode == INVOKEDYNAMIC) { 137 | return CombinedValue.of(Type.getReturnType(((InvokeDynamicInsnNode) insn).desc), insn); 138 | } else { 139 | return CombinedValue.of(Type.getReturnType(((MethodInsnNode) insn).desc), insn); 140 | } 141 | } 142 | 143 | @Override 144 | public void returnOperation(final AbstractInsnNode insn, final CombinedValue value, final CombinedValue expected) {} 145 | 146 | @Nullable 147 | @Deprecated 148 | @Override 149 | public CombinedValue newValue(final Type type) { 150 | return CombinedValue.of(type, CombinedValue.PREFILLED); 151 | } 152 | 153 | @Nullable 154 | @Override 155 | public CombinedValue newOperation(final AbstractInsnNode insn) throws AnalyzerException { 156 | switch (insn.getOpcode()) { 157 | case ACONST_NULL: 158 | return CombinedValue.of(CombinedValue.OBJECT_TYPE, insn); 159 | case ICONST_M1: 160 | case ICONST_0: 161 | case ICONST_1: 162 | case ICONST_2: 163 | case ICONST_3: 164 | case ICONST_4: 165 | case ICONST_5: 166 | case BIPUSH: 167 | case SIPUSH: 168 | return CombinedValue.of(Type.INT_TYPE, insn); 169 | case LCONST_0: 170 | case LCONST_1: 171 | return CombinedValue.of(Type.LONG_TYPE, insn); 172 | case FCONST_0: 173 | case FCONST_1: 174 | case FCONST_2: 175 | return CombinedValue.of(Type.FLOAT_TYPE, insn); 176 | case DCONST_0: 177 | case DCONST_1: 178 | return CombinedValue.of(Type.DOUBLE_TYPE, insn); 179 | case LDC: 180 | Object cst = ((LdcInsnNode) insn).cst; 181 | if (cst instanceof Integer) { 182 | return CombinedValue.of(Type.INT_TYPE, insn); 183 | } else if (cst instanceof Float) { 184 | return CombinedValue.of(Type.FLOAT_TYPE, insn); 185 | } else if (cst instanceof Long) { 186 | return CombinedValue.of(Type.LONG_TYPE, insn); 187 | } else if (cst instanceof Double) { 188 | return CombinedValue.of(Type.DOUBLE_TYPE, insn); 189 | } else if (cst instanceof String) { 190 | return CombinedValue.of(Type.getObjectType("java/lang/String"), insn); 191 | } else if (cst instanceof Type) { 192 | int sort = ((Type) cst).getSort(); 193 | if (sort == Type.OBJECT || sort == Type.ARRAY) { 194 | return CombinedValue.of(Type.getObjectType("java/lang/Class"), insn); 195 | } else if (sort == Type.METHOD) { 196 | return CombinedValue.of(Type.getObjectType("java/lang/invoke/MethodType"), insn); 197 | } else { 198 | throw new IllegalArgumentException("Illegal LDC constant " + cst + " with unknown sort " + sort); 199 | } 200 | } else if (cst instanceof Handle) { 201 | return CombinedValue.of(Type.getObjectType("java/lang/invoke/MethodHandle"), insn); 202 | } else { 203 | throw new IllegalArgumentException("Illegal LDC constant " 204 | + cst); 205 | } 206 | case JSR: 207 | throw new UnsupportedOperationException("JSR not supported. Use JSRInlinerAdapter to inline JSR subroutines."); 208 | case GETSTATIC: 209 | return CombinedValue.of(Type.getType(((FieldInsnNode) insn).desc), insn); 210 | case NEW: 211 | return CombinedValue.of(Type.getObjectType(((TypeInsnNode) insn).desc), insn); 212 | default: 213 | throw new Error("Internal error."); 214 | } 215 | } 216 | 217 | @Override 218 | public CombinedValue copyOperation(final AbstractInsnNode insn, final CombinedValue value) throws AnalyzerException { 219 | // TODO: Is this right? SourceInterpreter does this, but isn't it more useful to keep the source as the one we copied from? 220 | // return CombinedValue.of(value.getType(), Collections.singleton(insn)); 221 | 222 | return value; 223 | } 224 | 225 | @Nullable 226 | @Override 227 | public CombinedValue binaryOperation(final AbstractInsnNode insn, final CombinedValue value1, final CombinedValue value2) 228 | throws AnalyzerException { 229 | switch (insn.getOpcode()) { 230 | case IALOAD: 231 | case BALOAD: 232 | case CALOAD: 233 | case SALOAD: 234 | case IADD: 235 | case ISUB: 236 | case IMUL: 237 | case IDIV: 238 | case IREM: 239 | case ISHL: 240 | case ISHR: 241 | case IUSHR: 242 | case IAND: 243 | case IOR: 244 | case IXOR: 245 | return CombinedValue.of(Type.INT_TYPE, insn); 246 | case FALOAD: 247 | case FADD: 248 | case FSUB: 249 | case FMUL: 250 | case FDIV: 251 | case FREM: 252 | return CombinedValue.of(Type.FLOAT_TYPE, insn); 253 | case LALOAD: 254 | case LADD: 255 | case LSUB: 256 | case LMUL: 257 | case LDIV: 258 | case LREM: 259 | case LSHL: 260 | case LSHR: 261 | case LUSHR: 262 | case LAND: 263 | case LOR: 264 | case LXOR: 265 | return CombinedValue.of(Type.LONG_TYPE, insn); 266 | case DALOAD: 267 | case DADD: 268 | case DSUB: 269 | case DMUL: 270 | case DDIV: 271 | case DREM: 272 | return CombinedValue.of(Type.DOUBLE_TYPE, insn); 273 | case AALOAD: 274 | return CombinedValue.of(CombinedValue.OBJECT_TYPE, insn); 275 | case LCMP: 276 | case FCMPL: 277 | case FCMPG: 278 | case DCMPL: 279 | case DCMPG: 280 | return CombinedValue.of(Type.INT_TYPE, insn); 281 | case IF_ICMPEQ: 282 | case IF_ICMPNE: 283 | case IF_ICMPLT: 284 | case IF_ICMPGE: 285 | case IF_ICMPGT: 286 | case IF_ICMPLE: 287 | case IF_ACMPEQ: 288 | case IF_ACMPNE: 289 | case PUTFIELD: 290 | return null; 291 | default: 292 | throw new Error("Internal error."); 293 | } 294 | } 295 | 296 | @Nullable 297 | @Override 298 | public CombinedValue merge(final CombinedValue v, final CombinedValue w) { 299 | if (v.equals(w)) { 300 | return v; 301 | } 302 | Type type = v.getType(); 303 | if (!Objects.equals(type, w.getType())) 304 | type = w.isReference() && v.isReference() ? CombinedValue.OBJECT_TYPE : null; 305 | 306 | val s = new HashSet(); 307 | s.addAll(v.insns); 308 | s.addAll(w.insns); 309 | return CombinedValue.of(type, s); 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/javaparser/Expressions.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal.javaparser; 2 | 3 | import java.lang.reflect.Array; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.stream.Collectors; 7 | 8 | import lombok.NonNull; 9 | import lombok.val; 10 | 11 | import org.jetbrains.annotations.Contract; 12 | import org.jetbrains.annotations.Nullable; 13 | 14 | import com.github.javaparser.ast.expr.ArrayCreationExpr; 15 | import com.github.javaparser.ast.expr.ArrayInitializerExpr; 16 | import com.github.javaparser.ast.expr.BinaryExpr; 17 | import com.github.javaparser.ast.expr.BooleanLiteralExpr; 18 | import com.github.javaparser.ast.expr.CastExpr; 19 | import com.github.javaparser.ast.expr.CharLiteralExpr; 20 | import com.github.javaparser.ast.expr.ClassExpr; 21 | import com.github.javaparser.ast.expr.EnclosedExpr; 22 | import com.github.javaparser.ast.expr.Expression; 23 | import com.github.javaparser.ast.expr.FieldAccessExpr; 24 | import com.github.javaparser.ast.expr.IntegerLiteralExpr; 25 | import com.github.javaparser.ast.expr.LambdaExpr; 26 | import com.github.javaparser.ast.expr.LongLiteralExpr; 27 | import com.github.javaparser.ast.expr.MethodCallExpr; 28 | import com.github.javaparser.ast.expr.NameExpr; 29 | import com.github.javaparser.ast.expr.NullLiteralExpr; 30 | import com.github.javaparser.ast.expr.ObjectCreationExpr; 31 | import com.github.javaparser.ast.expr.StringLiteralExpr; 32 | import com.github.javaparser.ast.expr.SuperExpr; 33 | import com.github.javaparser.ast.expr.ThisExpr; 34 | import com.github.javaparser.ast.nodeTypes.NodeWithScope; 35 | 36 | import dev.minco.javatransformer.api.AccessFlags; 37 | import dev.minco.javatransformer.api.MethodInfo; 38 | import dev.minco.javatransformer.api.TransformationException; 39 | import dev.minco.javatransformer.api.Type; 40 | import dev.minco.javatransformer.api.code.IntermediateValue; 41 | import dev.minco.javatransformer.internal.ResolutionContext; 42 | import dev.minco.javatransformer.internal.util.CachingSupplier; 43 | 44 | public final class Expressions { 45 | @Contract(value = "null, _, _ -> fail; _, null, _ -> fail; _, _, true -> !null", pure = true) 46 | @NonNull 47 | @SuppressWarnings({"Contract", "unchecked"}) // Not violated, @NonNull adds null checks 48 | public static Type expressionToType(@NonNull Expression e, @NonNull ResolutionContext context, boolean failIfUnknown) { 49 | if (e instanceof EnclosedExpr) { 50 | e = ((EnclosedExpr) e).getInner(); 51 | } 52 | 53 | if (e instanceof CastExpr) { 54 | return context.resolve(((CastExpr) e).getType()); 55 | } 56 | if (e instanceof StringLiteralExpr) { 57 | return Type.STRING; 58 | } 59 | if (e instanceof CharLiteralExpr) { 60 | return Type.CHAR; 61 | } else if (e instanceof BooleanLiteralExpr) { 62 | return Type.BOOLEAN; 63 | } else if (e instanceof IntegerLiteralExpr) { 64 | return Type.INT; 65 | } else if (e instanceof LongLiteralExpr) { 66 | return Type.LONG; 67 | } else if (e instanceof ClassExpr) { 68 | val ce = (ClassExpr) e; 69 | return Type.CLAZZ.withTypeArgument(context.resolve(ce.getType())); 70 | } else if (e instanceof ObjectCreationExpr) { 71 | val oce = (ObjectCreationExpr) e; 72 | // TODO: 73 | return context.resolve(oce.getType()); 74 | } else if (e instanceof BinaryExpr) { 75 | return expressionToType(((BinaryExpr) e).getLeft(), context, false); 76 | } else if (e instanceof NameExpr || e instanceof ThisExpr || e instanceof SuperExpr) { 77 | return context.resolveNameInExpressionContext(e.toString()); 78 | } else if (e instanceof ArrayCreationExpr) { 79 | val ace = (ArrayCreationExpr) e; 80 | return context.resolve(ace.createdType()); 81 | } else if (e instanceof FieldAccessExpr) { 82 | // NB some field accesses look like local var accesses and need handled separately 83 | // YAY INCONSISTENT SYNTAX! 84 | 85 | val scope = ((FieldAccessExpr) e).getScope(); 86 | Type scopeType = expressionToType(scope, context, false); 87 | 88 | val type = context.resolveFieldType(scopeType, ((FieldAccessExpr) e).getNameAsString()); 89 | if (type != Type.UNKNOWN) { 90 | return type; 91 | } 92 | return Type.STATIC_META_CLASS.withTypeArgument(context.resolve(e.toString())); 93 | } else if (e instanceof MethodCallExpr) { 94 | val mce = (MethodCallExpr) e; 95 | 96 | val scope = mce.getScope().orElse(new ThisExpr()); 97 | val scopeType = expressionToType(scope, context, false); 98 | val inputUsedTypes = CachingSupplier.of(() -> getMethodCallInputTypes(mce, context)); 99 | val method = context.resolveMethodCallType(scopeType, mce.getNameAsString(), inputUsedTypes); 100 | if (method != null) { 101 | val type = method.getReturnType(); 102 | if (type != Type.UNKNOWN) { 103 | if (type.isTypeParameter()) { 104 | val params = method.getParameters(); 105 | 106 | // TODO this isn't generic at all 107 | // needs to be able to recurse deeper 108 | if (scopeType.hasTypeArguments()) { 109 | val typeArgs = method.getClassInfo().getTypeVariables(); 110 | val scopeTypeArgs = scopeType.getTypeArguments(); 111 | for (int i = 0; i < typeArgs.size(); i++) { 112 | if (typeArgs.get(i).getName().equals(type.getTypeParameterName())) { 113 | return scopeTypeArgs.get(i); 114 | } 115 | } 116 | } 117 | 118 | for (int i = 0; i < params.size(); i++) { 119 | val pType = params.get(i).type; 120 | if (pType.isTypeParameter() && pType.getTypeParameterName().equals(type.getTypeParameterName())) { 121 | return inputUsedTypes.get().get(i); 122 | } 123 | if (!pType.hasTypeArguments()) { 124 | continue; 125 | } 126 | val tas = pType.getTypeArguments(); 127 | for (int j = 0; j < tas.size(); j++) { 128 | val ta = tas.get(i); 129 | if (ta.isTypeParameter() && ta.getTypeParameterName().equals(type.getTypeParameterName())) { 130 | return inputUsedTypes.get().get(i).getTypeArguments().get(j); 131 | } 132 | } 133 | } 134 | } 135 | 136 | return type; 137 | } 138 | } 139 | System.err.println("Couldn't find method {" + mce.getNameAsString() + "} in {" + scopeType + "}"); 140 | } else if (e instanceof LambdaExpr) { 141 | return Type.LAMBDA; 142 | } 143 | 144 | if (failIfUnknown) { 145 | if (e instanceof NodeWithScope) { 146 | System.err.println(((NodeWithScope) e).getScope().getClass()); 147 | } 148 | throw new TransformationException("Unknown type for expression {" + e + "}\nClass: " + e.getClass()); 149 | } 150 | 151 | return Type.UNKNOWN; 152 | } 153 | 154 | @Contract(value = "null, _, -> fail; _, null -> fail; _, _ -> !null", pure = true) 155 | @NonNull 156 | public static Object expressionToValue(@NonNull Expression e, @NonNull ResolutionContext context) { 157 | return expressionToValue(e, context, true); 158 | } 159 | 160 | @Contract(value = "null, _, _ -> fail; _, null, _ -> fail; _, _, true -> !null", pure = true) 161 | @Nullable 162 | @SuppressWarnings("Contract") // Not violated, @NonNull adds null checks 163 | public static Object expressionToValue(@NonNull Expression e, @NonNull ResolutionContext context, boolean failIfUnknown) { 164 | if (e instanceof StringLiteralExpr) { 165 | return ((StringLiteralExpr) e).getValue(); 166 | } else if (e instanceof BooleanLiteralExpr) { 167 | return ((BooleanLiteralExpr) e).getValue(); 168 | } else if (e instanceof ClassExpr) { 169 | return ((ClassExpr) e).getType().asString(); 170 | } else if (e instanceof ArrayInitializerExpr) { 171 | return arrayExpressionToArray((ArrayInitializerExpr) e, context, failIfUnknown); 172 | } else if (e instanceof FieldAccessExpr) { 173 | // needs to be a constant -> must be a field which is an enum constant or 174 | // a public static final field of constable type 175 | // TODO: handle public static final fields properly? currently treated as enum 176 | val fieldAccessExpr = (FieldAccessExpr) e; 177 | Type t = expressionToType(fieldAccessExpr, context, false); 178 | return new String[]{t.getDescriptor(), ((FieldAccessExpr) e).getNameAsString()}; 179 | } else if (e instanceof NullLiteralExpr) { 180 | return null; 181 | } 182 | if (failIfUnknown) 183 | throw new TransformationException("Unknown value: " + e + "\nClass: " + e.getClass()); 184 | return IntermediateValue.UNKNOWN; 185 | } 186 | 187 | @Contract(value = "null, _ -> fail; _, null -> fail; _, _ -> !null", pure = true) 188 | @NonNull 189 | public static IntermediateValue expressionToIntermediateValue(@NonNull Expression e, @NonNull ResolutionContext r) { 190 | Type type = expressionToType(e, r, true); 191 | val value = expressionToValue(e, r, false); 192 | return new IntermediateValue(type, value, new IntermediateValue.Location(IntermediateValue.LocationType.STACK, -1, "argument")); 193 | } 194 | 195 | @Contract(pure = true) 196 | @NonNull 197 | private static Object[] arrayExpressionToArray(ArrayInitializerExpr expr, ResolutionContext context, boolean failIfUnknown) { 198 | val values = expr.getValues(); 199 | if (values.isEmpty()) 200 | return new Object[0]; 201 | 202 | val results = values.stream().map(it -> expressionToValue(it, context, failIfUnknown)).collect(Collectors.toList()); 203 | return results.toArray((Object[]) Array.newInstance(results.get(0).getClass(), 0)); 204 | } 205 | 206 | @NonNull 207 | public static List getMethodCallInputIVs(MethodCallExpr expr, ResolutionContext context, CachingSupplier> inputTypes, MethodInfo methodInfo) { 208 | val parameters = methodInfo.getParameters(); 209 | 210 | val list = new ArrayList(); 211 | 212 | // this 213 | if (!methodInfo.getAccessFlags().has(AccessFlags.ACC_STATIC)) { 214 | list.add(new IntermediateValue(methodInfo.getClassInfo().getType(), IntermediateValue.UNKNOWN, 215 | new IntermediateValue.Location(IntermediateValue.LocationType.STACK, -1, "argument"))); 216 | } 217 | 218 | val args = expr.getArguments(); 219 | int maxArgs = parameters.size(); 220 | boolean applyVarargs = false; 221 | if (methodInfo.getAccessFlags().has(AccessFlags.ACC_VARARGS)) { 222 | val its = inputTypes.get(); 223 | if (its.size() == 0 || its.size() != maxArgs || !its.get(its.size() - 1).isArrayType()) { 224 | applyVarargs = true; 225 | maxArgs--; 226 | } 227 | } 228 | for (int i = 0; i < maxArgs; i++) { 229 | val it = args.get(i); 230 | list.add(new IntermediateValue(parameters.get(i).type, expressionToValue(it, context, false), 231 | new IntermediateValue.Location(IntermediateValue.LocationType.STACK, -1, "argument"))); 232 | } 233 | if (applyVarargs) { 234 | // TODO: varargs values always unknown 235 | list.add(new IntermediateValue(parameters.get(maxArgs).type, IntermediateValue.UNKNOWN, 236 | new IntermediateValue.Location(IntermediateValue.LocationType.STACK, -1, "argument"))); 237 | } 238 | 239 | return list; 240 | } 241 | 242 | @NonNull 243 | public static List getMethodCallInputTypes(MethodCallExpr expr, ResolutionContext context) { 244 | val list = new ArrayList(); 245 | 246 | for (val it : expr.getArguments()) { 247 | list.add(expressionToType(it, context, false)); 248 | } 249 | 250 | return list; 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/main/java/dev/minco/javatransformer/internal/ByteCodeInfo.java: -------------------------------------------------------------------------------- 1 | package dev.minco.javatransformer.internal; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.function.Supplier; 7 | import java.util.stream.Collectors; 8 | import java.util.stream.Stream; 9 | 10 | import lombok.Data; 11 | import lombok.Getter; 12 | import lombok.NonNull; 13 | import lombok.SneakyThrows; 14 | import lombok.val; 15 | 16 | import org.objectweb.asm.tree.ClassNode; 17 | import org.objectweb.asm.tree.FieldNode; 18 | import org.objectweb.asm.tree.MethodNode; 19 | import org.objectweb.asm.tree.analysis.Frame; 20 | 21 | import dev.minco.javatransformer.api.AccessFlags; 22 | import dev.minco.javatransformer.api.Annotation; 23 | import dev.minco.javatransformer.api.ClassInfo; 24 | import dev.minco.javatransformer.api.FieldInfo; 25 | import dev.minco.javatransformer.api.MethodInfo; 26 | import dev.minco.javatransformer.api.Parameter; 27 | import dev.minco.javatransformer.api.TransformationException; 28 | import dev.minco.javatransformer.api.Type; 29 | import dev.minco.javatransformer.api.TypeVariable; 30 | import dev.minco.javatransformer.api.code.CodeFragment; 31 | import dev.minco.javatransformer.internal.asm.CombinedAnalyzer; 32 | import dev.minco.javatransformer.internal.asm.CombinedInterpreter; 33 | import dev.minco.javatransformer.internal.asm.CombinedValue; 34 | import dev.minco.javatransformer.internal.asm.FilteringClassWriter; 35 | import dev.minco.javatransformer.internal.util.AnnotationParser; 36 | import dev.minco.javatransformer.internal.util.CachingSupplier; 37 | import dev.minco.javatransformer.internal.util.Cloner; 38 | import dev.minco.javatransformer.internal.util.CollectionUtil; 39 | 40 | @Data 41 | @SuppressWarnings("unchecked") 42 | public class ByteCodeInfo implements ClassInfo { 43 | private final Supplier node; 44 | @Getter(lazy = true) 45 | private final List annotations = getAnnotationsInternal(); 46 | public boolean hasChangedMethodControlFlow; 47 | @NonNull 48 | private String className; 49 | @NonNull 50 | private Map filters; 51 | 52 | @Override 53 | public String getName() { 54 | return className; 55 | } 56 | 57 | @Override 58 | public void setName(String name) { 59 | className = name; 60 | node.get().name = name.replace('.', '/'); 61 | } 62 | 63 | @Override 64 | public AccessFlags getAccessFlags() { 65 | return new AccessFlags(node.get().access); 66 | } 67 | 68 | @Override 69 | public void setAccessFlags(AccessFlags accessFlags) { 70 | node.get().access = accessFlags.access; 71 | } 72 | 73 | public void add(MethodInfo method) { 74 | MethodNode node; 75 | if (method instanceof MethodNodeInfo) { 76 | val orig = ((MethodNodeInfo) method); 77 | node = Cloner.clone(orig.node); 78 | FilteringClassWriter.addFilter(filters, orig.getClassInfo().getName(), getName()); 79 | } else { 80 | node = new MethodNode(); 81 | node.desc = "()V"; 82 | node.exceptions = new ArrayList<>(); 83 | MethodInfo info = new MethodNodeInfo(node); 84 | info.setAll(method); 85 | } 86 | this.node.get().methods.add(node); 87 | } 88 | 89 | public void add(FieldInfo field) { 90 | FieldNode node; 91 | if (field instanceof FieldNodeInfo) { 92 | node = Cloner.clone(((FieldNodeInfo) field).node); 93 | } else { 94 | node = new FieldNode(0, null, "V", null, null); 95 | val nodeInfo = new FieldNodeInfo(node); 96 | nodeInfo.setAll(field); 97 | } 98 | this.node.get().fields.add(node); 99 | } 100 | 101 | @Override 102 | public void remove(MethodInfo method) { 103 | MethodNodeInfo methodNodeInfo = !(method instanceof MethodNodeInfo) ? (MethodNodeInfo) get(method) : (MethodNodeInfo) method; 104 | 105 | if (methodNodeInfo == null) 106 | throw new TransformationException("Method " + method + " can not be removed as it is not present"); 107 | 108 | node.get().methods.remove(methodNodeInfo.node); 109 | } 110 | 111 | @Override 112 | public void remove(FieldInfo field) { 113 | FieldNodeInfo fieldNodeInfo = !(field instanceof FieldNodeInfo) ? (FieldNodeInfo) get(field) : (FieldNodeInfo) field; 114 | 115 | if (fieldNodeInfo == null) 116 | throw new TransformationException("Field " + field + " can not be removed as it is not present"); 117 | 118 | node.get().fields.remove(fieldNodeInfo.node); 119 | } 120 | 121 | @Override 122 | public Type getSuperType() { 123 | val superName = node.get().superName; 124 | if (superName == null) { 125 | return null; 126 | } 127 | return new Type("L" + superName + ";"); 128 | } 129 | 130 | @Override 131 | public List getInterfaceTypes() { 132 | return node.get().interfaces.stream().map((it) -> new Type("L" + it + ";")).collect(Collectors.toList()); 133 | } 134 | 135 | public Stream getMethods() { 136 | return node.get().methods.stream().map(MethodNodeInfo::new); 137 | } 138 | 139 | public Stream getFields() { 140 | return node.get().fields.stream().map(FieldNodeInfo::new); 141 | } 142 | 143 | private List getAnnotationsInternal() { 144 | // WAT: splitting this up from a single statement fixed the failure at runtime 145 | // was originally: 146 | // return CollectionUtil.union(node.get().invisibleAnnotations, node.get().visibleAnnotations).map(AnnotationParser::annotationFromAnnotationNode).collect(Collectors.toList()); 147 | // Was failing at runtime with 'java.lang.BootstrapMethodError: java.lang.IllegalAccessError:' with no message 148 | // Split up into multiple lines to try to help diagnose. 149 | val allAnnotationNodes = CollectionUtil.union(node.get().invisibleAnnotations, node.get().visibleAnnotations); 150 | //noinspection Convert2MethodRef 151 | val stream = allAnnotationNodes.map((it) -> AnnotationParser.annotationFromAnnotationNode(it)); 152 | return stream.collect(Collectors.toList()); 153 | } 154 | 155 | MethodNodeInfo wrap(MethodNode node) { 156 | return new MethodNodeInfo(node); 157 | } 158 | 159 | @Override 160 | public List getTypeVariables() { 161 | /* 162 | Test gets signature: 163 | 164 | */ 165 | return Signature.getTypeVariables(node.get().signature); 166 | } 167 | 168 | @Override 169 | public void setTypeVariables(List typeVariables) { 170 | throw new UnsupportedOperationException(); // TODO 171 | } 172 | 173 | public class FieldNodeInfo implements FieldInfo { 174 | final FieldNode node; 175 | private Type type; 176 | 177 | FieldNodeInfo(FieldNode node) { 178 | this.node = node; 179 | type = new Type(node.desc, node.signature); 180 | } 181 | 182 | @Override 183 | public String getName() { 184 | return node.name; 185 | } 186 | 187 | @Override 188 | public void setName(String name) { 189 | node.name = name; 190 | } 191 | 192 | @Override 193 | public AccessFlags getAccessFlags() { 194 | return new AccessFlags(node.access); 195 | } 196 | 197 | @Override 198 | public void setAccessFlags(AccessFlags accessFlags) { 199 | node.access = accessFlags.access; 200 | } 201 | 202 | @Override 203 | public Type getType() { 204 | return type; 205 | } 206 | 207 | @Override 208 | public void setType(Type type) { 209 | this.type = type; 210 | node.desc = type.descriptor; 211 | node.signature = type.signature; 212 | } 213 | 214 | @Override 215 | public List getAnnotations() { 216 | return CollectionUtil.union(node.invisibleAnnotations, node.visibleAnnotations).map(AnnotationParser::annotationFromAnnotationNode).collect(Collectors.toList()); 217 | } 218 | 219 | @Override 220 | public ClassInfo getClassInfo() { 221 | return ByteCodeInfo.this; 222 | } 223 | 224 | @Override 225 | public String toString() { 226 | return SimpleFieldInfo.toString(this); 227 | } 228 | 229 | @Override 230 | @SuppressWarnings("MethodDoesntCallSuperMethod") 231 | public FieldInfo clone() { 232 | return new FieldNodeInfo(Cloner.clone(node)); 233 | } 234 | } 235 | 236 | public class MethodNodeInfo implements MethodInfo { 237 | public final MethodNode node; 238 | private final CachingSupplier[]> stackFrames; 239 | private final CachingSupplier descriptor; 240 | private final CachingSupplier codeFragment; 241 | 242 | MethodNodeInfo(MethodNode node) { 243 | this.node = node; 244 | descriptor = CachingSupplier.of(() -> { 245 | try { 246 | return new MethodDescriptor(node); 247 | } catch (TransformationException e) { 248 | throw new TransformationException("Failed to parse method parameters in " + node.name + ':' + 249 | "\n\tname: " + node.name + 250 | "\n\tdescriptor: " + node.desc + 251 | "\n\tsignature:" + node.signature, e); 252 | } 253 | }); 254 | codeFragment = CachingSupplier.of(() -> new AsmCodeFragmentGenerator.MethodNodeInfoCodeFragment(this)); 255 | stackFrames = CachingSupplier.of(this::analyzeStackFrames); 256 | } 257 | 258 | @Override 259 | public AccessFlags getAccessFlags() { 260 | return new AccessFlags(node.access); 261 | } 262 | 263 | @Override 264 | public void setAccessFlags(AccessFlags accessFlags) { 265 | node.access = accessFlags.access; 266 | } 267 | 268 | @Override 269 | public String getName() { 270 | return node.name; 271 | } 272 | 273 | @Override 274 | public void setName(String name) { 275 | node.name = name; 276 | } 277 | 278 | @Override 279 | public Type getReturnType() { 280 | return descriptor.get().getReturnType(); 281 | } 282 | 283 | @Override 284 | public void setReturnType(Type returnType) { 285 | descriptor.set(descriptor.get().withReturnType(returnType)); 286 | descriptor.get().saveTo(node); 287 | } 288 | 289 | @Override 290 | public List getParameters() { 291 | return descriptor.get().getParameters(); 292 | } 293 | 294 | @Override 295 | public void setParameters(List parameters) { 296 | descriptor.set(descriptor.get().withParameters(parameters)); 297 | descriptor.get().saveTo(node); 298 | } 299 | 300 | public String getDescriptor() { 301 | return descriptor.get().getDescriptor(); 302 | } 303 | 304 | @Override 305 | public List getAnnotations() { 306 | return CollectionUtil.union(node.invisibleAnnotations, node.visibleAnnotations).map(AnnotationParser::annotationFromAnnotationNode).collect(Collectors.toList()); 307 | } 308 | 309 | @Override 310 | public ByteCodeInfo getClassInfo() { 311 | return ByteCodeInfo.this; 312 | } 313 | 314 | @Override 315 | public String toString() { 316 | return SimpleMethodInfo.toString(this); 317 | } 318 | 319 | @Override 320 | public List getTypeVariables() { 321 | return descriptor.get().getTypeVariables(); 322 | } 323 | 324 | @Override 325 | public void setTypeVariables(List typeVariables) { 326 | descriptor.set(descriptor.get().withTypeVariables(typeVariables)); 327 | descriptor.get().saveTo(node); 328 | } 329 | 330 | @Override 331 | @SuppressWarnings("MethodDoesntCallSuperMethod") 332 | public MethodInfo clone() { 333 | return new MethodNodeInfo(Cloner.clone(node)); 334 | } 335 | 336 | @Override 337 | public @NonNull CodeFragment.Body getCodeFragment() { 338 | return codeFragment.get(); 339 | } 340 | 341 | public Frame[] getStackFrames() { 342 | return stackFrames.get(); 343 | } 344 | 345 | @SneakyThrows 346 | private Frame[] analyzeStackFrames() { 347 | return CombinedAnalyzer.analyze(new CombinedInterpreter(), getClassInfo().getNode().get().name, node); 348 | } 349 | 350 | public void markCodeDirty() { 351 | stackFrames.set(null); 352 | hasChangedMethodControlFlow = true; 353 | } 354 | } 355 | } 356 | --------------------------------------------------------------------------------