diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index e68ec1ab2..3e540539f 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -13,6 +13,10 @@ jobs: - uses: actions/checkout@v2 with: path: proguard-main + - uses: actions/checkout@v2.4.0 + with: + repository: heckej/proguard-core + path: proguard-core - uses: actions/setup-java@v1 with: java-version: 8 diff --git a/base/src/main/java/proguard/Configuration.java b/base/src/main/java/proguard/Configuration.java index 78cd9c0ec..43d6581bc 100644 --- a/base/src/main/java/proguard/Configuration.java +++ b/base/src/main/java/proguard/Configuration.java @@ -200,6 +200,12 @@ public class Configuration */ public boolean mergeInterfacesAggressively = false; + /** + * An optional output file for listing the lambda to lambdagroup mapping. + * An empty file name means the standard output. + */ + public File printLambdaGroupMapping; + /////////////////////////////////////////////////////////////////////////// // Obfuscation options. /////////////////////////////////////////////////////////////////////////// diff --git a/base/src/main/java/proguard/ConfigurationConstants.java b/base/src/main/java/proguard/ConfigurationConstants.java index b542d0542..6540ddd0c 100644 --- a/base/src/main/java/proguard/ConfigurationConstants.java +++ b/base/src/main/java/proguard/ConfigurationConstants.java @@ -66,6 +66,7 @@ public class ConfigurationConstants public static final String ASSUME_VALUES_OPTION = "-assumevalues"; public static final String ALLOW_ACCESS_MODIFICATION_OPTION = "-allowaccessmodification"; public static final String MERGE_INTERFACES_AGGRESSIVELY_OPTION = "-mergeinterfacesaggressively"; + public static final String PRINT_LAMBDAGROUP_MAPPING_OPTION = "-printlambdagroupmapping"; public static final String DONT_OBFUSCATE_OPTION = "-dontobfuscate"; public static final String PRINT_MAPPING_OPTION = "-printmapping"; diff --git a/base/src/main/java/proguard/ConfigurationParser.java b/base/src/main/java/proguard/ConfigurationParser.java index 80d7f83aa..4661fdc7a 100644 --- a/base/src/main/java/proguard/ConfigurationParser.java +++ b/base/src/main/java/proguard/ConfigurationParser.java @@ -190,6 +190,7 @@ public void parse(Configuration configuration) else if (ConfigurationConstants.ASSUME_VALUES_OPTION .startsWith(nextWord)) configuration.assumeValues = parseAssumeClassSpecificationArguments(configuration.assumeValues); else if (ConfigurationConstants.ALLOW_ACCESS_MODIFICATION_OPTION .startsWith(nextWord)) configuration.allowAccessModification = parseNoArgument(true); else if (ConfigurationConstants.MERGE_INTERFACES_AGGRESSIVELY_OPTION .startsWith(nextWord)) configuration.mergeInterfacesAggressively = parseNoArgument(true); + else if (ConfigurationConstants.PRINT_LAMBDAGROUP_MAPPING_OPTION .startsWith(nextWord)) configuration.printLambdaGroupMapping = parseOptionalFile(); else if (ConfigurationConstants.DONT_OBFUSCATE_OPTION .startsWith(nextWord)) configuration.obfuscate = parseNoArgument(false); else if (ConfigurationConstants.PRINT_MAPPING_OPTION .startsWith(nextWord)) configuration.printMapping = parseOptionalFile(); diff --git a/base/src/main/java/proguard/ConfigurationWriter.java b/base/src/main/java/proguard/ConfigurationWriter.java index 8b5067402..5d41c3291 100644 --- a/base/src/main/java/proguard/ConfigurationWriter.java +++ b/base/src/main/java/proguard/ConfigurationWriter.java @@ -124,6 +124,7 @@ public void write(Configuration configuration) throws IOException writeOption(ConfigurationConstants.OPTIMIZATION_PASSES, configuration.optimizationPasses); writeOption(ConfigurationConstants.ALLOW_ACCESS_MODIFICATION_OPTION, configuration.allowAccessModification); writeOption(ConfigurationConstants.MERGE_INTERFACES_AGGRESSIVELY_OPTION, configuration.mergeInterfacesAggressively); + writeOption(ConfigurationConstants.PRINT_LAMBDAGROUP_MAPPING_OPTION, configuration.printLambdaGroupMapping); writeOption(ConfigurationConstants.DONT_OBFUSCATE_OPTION, !configuration.obfuscate); writeOption(ConfigurationConstants.PRINT_MAPPING_OPTION, configuration.printMapping); diff --git a/base/src/main/java/proguard/ProGuard.java b/base/src/main/java/proguard/ProGuard.java index ba888afc5..695d4fb83 100644 --- a/base/src/main/java/proguard/ProGuard.java +++ b/base/src/main/java/proguard/ProGuard.java @@ -29,7 +29,6 @@ import proguard.configuration.ConfigurationLoggingAdder; import proguard.evaluation.IncompleteClassHierarchyException; import proguard.configuration.InitialStateInfo; -import proguard.io.ExtraDataEntryNameMap; import proguard.logging.Logging; import proguard.mark.Marker; import proguard.obfuscate.NameObfuscationReferenceFixer; @@ -39,6 +38,7 @@ import proguard.optimize.LineNumberTrimmer; import proguard.optimize.Optimizer; import proguard.optimize.gson.GsonOptimizer; +import proguard.optimize.kotlin.KotlinLambdaMerger; import proguard.optimize.peephole.LineNumberLinearizer; import proguard.pass.PassRunner; import proguard.preverify.*; @@ -424,6 +424,14 @@ private void shrink(boolean afterOptimizer) throws Exception } } + /** + * Reduce the size needed to represent Kotlin lambda's. + * The classes that are generated for lambda's with a same structure and from the same package are merged into a group. + */ + private void mergeKotlinLambdaClasses() throws Exception { + passRunner.run(new KotlinLambdaMerger(configuration), appView); + } + /** * Optimizes usages of the Gson library. @@ -452,6 +460,10 @@ private void optimize() throws Exception { shrink(true); } + if (optimizationPass == configuration.optimizationPasses - 2) + { + mergeKotlinLambdaClasses(); + } } } diff --git a/base/src/main/java/proguard/optimize/Optimizer.java b/base/src/main/java/proguard/optimize/Optimizer.java index 910bf71cc..49300745d 100644 --- a/base/src/main/java/proguard/optimize/Optimizer.java +++ b/base/src/main/java/proguard/optimize/Optimizer.java @@ -38,6 +38,7 @@ import proguard.optimize.evaluation.*; import proguard.optimize.evaluation.InstructionUsageMarker; import proguard.optimize.info.*; +import proguard.optimize.kotlin.KotlinLambdaMerger; import proguard.optimize.peephole.*; import proguard.pass.Pass; import proguard.util.*; @@ -63,6 +64,7 @@ public class Optimizer implements Pass private static final String CLASS_MERGING_VERTICAL = "class/merging/vertical"; private static final String CLASS_MERGING_HORIZONTAL = "class/merging/horizontal"; private static final String CLASS_MERGING_WRAPPER = "class/merging/wrapper"; + private static final String CLASS_MERGING_KOTLINLAMBDA = "class/merging/kotlinlambda"; private static final String FIELD_REMOVAL_WRITEONLY = "field/removal/writeonly"; private static final String FIELD_MARKING_PRIVATE = "field/marking/private"; private static final String FIELD_GENERALIZATION_CLASS = "field/generalization/class"; @@ -145,6 +147,7 @@ public class Optimizer implements Pass private boolean classMergingVertical; private boolean classMergingHorizontal; private boolean classMergingWrapper; + private boolean classMergingKotlinlambda; private boolean fieldRemovalWriteonly; private boolean fieldMarkingPrivate; private boolean fieldGeneralizationClass; @@ -195,11 +198,18 @@ public Optimizer(Configuration configuration) @Override - public void execute(AppView appView) throws IOException + public void execute(AppView appView) throws Exception { if (!moreOptimizationsPossible) { - return; + if (KotlinLambdaMerger.lambdaMergingDone) + { + return; + } + else + { + new KotlinLambdaMerger(configuration).execute(appView); + } } // Create a matcher for filtering optimizations. @@ -213,6 +223,7 @@ public void execute(AppView appView) throws IOException classMergingVertical = filter.matches(CLASS_MERGING_VERTICAL); classMergingHorizontal = filter.matches(CLASS_MERGING_HORIZONTAL); classMergingWrapper = filter.matches(CLASS_MERGING_WRAPPER); + classMergingKotlinlambda = filter.matches(CLASS_MERGING_KOTLINLAMBDA); fieldRemovalWriteonly = filter.matches(FIELD_REMOVAL_WRITEONLY); fieldMarkingPrivate = filter.matches(FIELD_MARKING_PRIVATE); fieldGeneralizationClass = filter.matches(FIELD_GENERALIZATION_CLASS); diff --git a/base/src/main/java/proguard/optimize/info/ProgramClassOptimizationInfo.java b/base/src/main/java/proguard/optimize/info/ProgramClassOptimizationInfo.java index 4254cb20f..bad188d3e 100644 --- a/base/src/main/java/proguard/optimize/info/ProgramClassOptimizationInfo.java +++ b/base/src/main/java/proguard/optimize/info/ProgramClassOptimizationInfo.java @@ -44,6 +44,8 @@ public class ProgramClassOptimizationInfo private volatile boolean mayBeMerged = true; private volatile Clazz wrappedClass; private volatile Clazz targetClass; + private volatile Clazz lambdaGroup; + private volatile int lambdaGroupClassId; public boolean isKept() @@ -250,6 +252,29 @@ public Clazz getTargetClass() } + public void setLambdaGroup(Clazz lambdaGroup) + { + this.lambdaGroup = lambdaGroup; + } + + + public Clazz getLambdaGroup() + { + return lambdaGroup; + } + + public void setLambdaGroupClassId(int classId) + { + this.lambdaGroupClassId = classId; + } + + + public int getLambdaGroupClassId() + { + return lambdaGroupClassId; + } + + /** * Merges in the given information of a class that is merged. */ diff --git a/base/src/main/java/proguard/optimize/kotlin/KotlinLambdaClassMerger.java b/base/src/main/java/proguard/optimize/kotlin/KotlinLambdaClassMerger.java new file mode 100644 index 000000000..afa690ea8 --- /dev/null +++ b/base/src/main/java/proguard/optimize/kotlin/KotlinLambdaClassMerger.java @@ -0,0 +1,125 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + * + * Copyright (c) 2002-2022 Guardsquare NV + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package proguard.optimize.kotlin; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import proguard.Configuration; +import proguard.classfile.*; +import proguard.classfile.attribute.Attribute; +import proguard.classfile.attribute.CodeAttribute; +import proguard.classfile.attribute.ExceptionsAttribute; +import proguard.classfile.attribute.LineNumberTableAttribute; +import proguard.classfile.attribute.visitor.AttributeVisitor; +import proguard.classfile.editor.ClassBuilder; +import proguard.classfile.editor.ClassEditor; +import proguard.classfile.editor.ConstantPoolEditor; +import proguard.classfile.editor.InstructionSequenceBuilder; +import proguard.classfile.instruction.*; +import proguard.classfile.instruction.visitor.InstructionVisitor; +import proguard.classfile.util.ClassSuperHierarchyInitializer; +import proguard.classfile.util.ClassUtil; +import proguard.classfile.visitor.*; +import proguard.io.ExtraDataEntryNameMap; +import proguard.optimize.info.ProgramClassOptimizationInfo; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class KotlinLambdaClassMerger implements ClassPoolVisitor +{ + + public static final String NAME_LAMBDA_GROUP = "LambdaGroup"; + private final ClassVisitor lambdaGroupVisitor; + private final ClassVisitor notMergedLambdaVisitor; + private final ClassVisitor mergedLambdaVisitor; + private final Configuration configuration; + private final ClassPool programClassPool; + private final ClassPool libraryClassPool; + private final ExtraDataEntryNameMap extraDataEntryNameMap; + private static final Logger logger = LogManager.getLogger(KotlinLambdaClassMerger.class); + + public KotlinLambdaClassMerger(final Configuration configuration, + final ClassPool programClassPool, + final ClassPool libraryClassPool, + final ClassVisitor lambdaGroupVisitor, + final ClassVisitor mergedLambdaVisitor, + final ClassVisitor notMergedLambdaVisitor, + final ExtraDataEntryNameMap extraDataEntryNameMap) + { + this.configuration = configuration; + this.programClassPool = programClassPool; + this.libraryClassPool = libraryClassPool; + this.lambdaGroupVisitor = lambdaGroupVisitor; + this.mergedLambdaVisitor = mergedLambdaVisitor; + this.notMergedLambdaVisitor = notMergedLambdaVisitor; + this.extraDataEntryNameMap = extraDataEntryNameMap; + } + + @Override + public void visitClassPool(ClassPool lambdaClassPool) + { + // don't merge lambda's if there is only one or zero + if (lambdaClassPool.size() < 2) + { + if (notMergedLambdaVisitor != null) { + lambdaClassPool.classesAccept(notMergedLambdaVisitor); + } + return; + } + + // choose a name for the lambda group + // ensure that the lambda group is in the same package as the classes of the class pool + String lambdaGroupName = getPackagePrefixOfClasses(lambdaClassPool) + NAME_LAMBDA_GROUP; + logger.trace("Creating lambda group with name {}", ClassUtil.externalClassName(lambdaGroupName)); + + // create a lambda group builder + KotlinLambdaGroupBuilder lambdaGroupBuilder = new KotlinLambdaGroupBuilder(lambdaGroupName, + this.configuration, + this.programClassPool, + this.libraryClassPool, + this.extraDataEntryNameMap, + this.mergedLambdaVisitor, + this.notMergedLambdaVisitor); + + // visit each lambda of this package to add their implementations to the lambda group + lambdaClassPool.classesAccept(lambdaGroupBuilder); + + ProgramClass lambdaGroup = lambdaGroupBuilder.build(); + + ProgramClassOptimizationInfo.setProgramClassOptimizationInfo(lambdaGroup); + ProgramClassOptimizationInfo optimizationInfo = + ProgramClassOptimizationInfo.getProgramClassOptimizationInfo(lambdaGroup); + optimizationInfo.setLambdaGroup(lambdaGroup); + + // let the lambda group visitor visit the newly created lambda group + this.lambdaGroupVisitor.visitProgramClass(lambdaGroup); + } + + private static String getPackagePrefixOfClasses(ClassPool classPool) + { + // Assume that all classes in the given class pool are in the same package. + String someClassName = classPool.classNames().next(); + return ClassUtil.internalPackagePrefix(someClassName); + } +} diff --git a/base/src/main/java/proguard/optimize/kotlin/KotlinLambdaEnclosingMethodUpdater.java b/base/src/main/java/proguard/optimize/kotlin/KotlinLambdaEnclosingMethodUpdater.java new file mode 100644 index 000000000..979cd13d3 --- /dev/null +++ b/base/src/main/java/proguard/optimize/kotlin/KotlinLambdaEnclosingMethodUpdater.java @@ -0,0 +1,319 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + * + * Copyright (c) 2002-2022 Guardsquare NV + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package proguard.optimize.kotlin; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import proguard.classfile.*; +import proguard.classfile.attribute.*; +import proguard.classfile.attribute.visitor.AttributeVisitor; +import proguard.classfile.editor.*; +import proguard.classfile.instruction.Instruction; +import proguard.classfile.kotlin.KotlinConstants; +import proguard.classfile.util.*; +import proguard.classfile.visitor.*; +import proguard.io.ExtraDataEntryNameMap; +import proguard.optimize.kotlin.visitor.ClassConstantReferenceUpdater; +import proguard.optimize.peephole.RetargetedInnerClassAttributeRemover; + +public class KotlinLambdaEnclosingMethodUpdater +implements ClassVisitor, + AttributeVisitor, + MemberVisitor +{ + + private final ClassPool programClassPool; + private final ClassPool libraryClassPool; + private final ProgramClass lambdaGroup; + private final int classId; + private final int arity; + private final String constructorDescriptor; + private final ExtraDataEntryNameMap extraDataEntryNameMap; + private boolean visitEnclosingMethodAttribute = false; + private boolean visitEnclosingMethod = false; + private boolean visitEnclosingCode = false; + private static final Logger logger = LogManager.getLogger(KotlinLambdaEnclosingMethodUpdater.class); + private Clazz currentLambdaClass; + private Clazz currentEnclosingClass; + private static final RetargetedInnerClassAttributeRemover retargetedInnerClassAttributeRemover = + new RetargetedInnerClassAttributeRemover(); + + public KotlinLambdaEnclosingMethodUpdater(ClassPool programClassPool, + ClassPool libraryClassPool, + ProgramClass lambdaClass, + ProgramClass lambdaGroup, + int classId, + int arity, + String constructorDescriptor, + ExtraDataEntryNameMap extraDataEntryNameMap) + { + this.programClassPool = programClassPool; + this.libraryClassPool = libraryClassPool; + this.currentLambdaClass = lambdaClass; + this.lambdaGroup = lambdaGroup; + this.classId = classId; + this.arity = arity; + this.constructorDescriptor = constructorDescriptor; + this.extraDataEntryNameMap = extraDataEntryNameMap; + } + + // Implementations for MemberVisitor + + @Override + public void visitProgramMethod(ProgramClass enclosingClass, ProgramMethod enclosingMethod) + { + // the given class must be the class that defines the lambda + // the given method must be the method where the lambda is defined + + if (visitEnclosingMethod) + { + return; + } + visitEnclosingMethod = true; + enclosingMethod.attributesAccept(enclosingClass, this); + visitEnclosingMethod = false; + } + + // Implementations for AttributeVisitor + + @Override + public void visitAnyAttribute(Clazz clazz, Attribute attribute) {} + + @Override + public void visitEnclosingMethodAttribute(Clazz lambdaClass, EnclosingMethodAttribute enclosingMethodAttribute) + { + // the given method must be the method where the lambda is defined + Clazz enclosingClass = enclosingMethodAttribute.referencedClass; + if (visitEnclosingMethodAttribute || enclosingClass == lambdaClass) + { + return; + } + + visitEnclosingMethodAttribute = true; + currentLambdaClass = lambdaClass; + currentEnclosingClass = enclosingClass; + + // Visit all methods of the enclosing class, assuming that those are the only methods that can contain + // references to this lambda class. + enclosingClass.methodsAccept(this); + + visitEnclosingMethodAttribute = false; + + // remove lambda class as inner class of its enclosing class + enclosingClass.attributeAccept(Attribute.INNER_CLASSES, + new InnerClassRemover(lambdaClass)); + + // remove all references to lambda class from the constant pool of its enclosing class + enclosingClass.accept(new ConstantPoolShrinker()); + + // ensure that the newly created lambda group is part of the resulting output + // as a dependency of this enclosing class + extraDataEntryNameMap.addExtraClassToClass(enclosingClass, this.lambdaGroup); + } + + @Override + public void visitCodeAttribute(Clazz enclosingClass, Method enclosingMethod, CodeAttribute codeAttribute) + { + // This attribute visitor should only be used for program classes. + try + { + visitCodeAttribute((ProgramClass) enclosingClass, (ProgramMethod) enclosingMethod, codeAttribute); + } + catch (ClassCastException exception) + { + logger.error("{} is incorrectly used to visit non-program class / method {} / {}", + this.getClass().getName(), enclosingClass, enclosingMethod); + } + } + + public void visitCodeAttribute(ProgramClass enclosingClass, + ProgramMethod enclosingMethod, + CodeAttribute codeAttribute) + { + // the given class must be the class that defines the lambda + // the given method must be the method where the lambda is defined + // the given code attribute must contain the original definition of the lambda: + // - load LambdaClass.INSTANCE + // - or instantiate LambdaClass() + if (!visitEnclosingMethod || visitEnclosingCode) + { + return; + } + visitEnclosingCode = true; + + CodeAttributeEditor codeAttributeEditor = new CodeAttributeEditor(); + BranchTargetFinder branchTargetFinder = new BranchTargetFinder(); + + InstructionSequencesReplacer replacer = createInstructionSequenceReplacer(branchTargetFinder, + codeAttributeEditor); + + codeAttribute.accept(enclosingClass, + enclosingMethod, + new PeepholeEditor(branchTargetFinder, + codeAttributeEditor, + replacer)); + visitEnclosingCode = false; + } + + private InstructionSequencesReplacer createInstructionSequenceReplacer(BranchTargetFinder branchTargetFinder, + CodeAttributeEditor codeAttributeEditor) + { + InstructionSequenceBuilder builder = new InstructionSequenceBuilder(programClassPool, libraryClassPool); + Instruction[][][] replacementPatterns = createReplacementPatternsForLambda(builder); + return new InstructionSequencesReplacer(builder.constants(), + replacementPatterns, + branchTargetFinder, + codeAttributeEditor); + } + + private Instruction[][][] createReplacementPatternsForLambda(InstructionSequenceBuilder builder) + { + Method initMethod = currentLambdaClass.findMethod(ClassConstants.METHOD_NAME_INIT, null); + Method specificInvokeMethod = KotlinLambdaGroupBuilder.getInvokeMethod((ProgramClass)currentLambdaClass, + false); + Method bridgeInvokeMethod = KotlinLambdaGroupBuilder.getInvokeMethod((ProgramClass)currentLambdaClass, + true); + return new Instruction[][][] + { + // Empty closure lambda's + { + // Lambda is 'instantiated' by referring to its static INSTANCE field + builder.getstatic(currentLambdaClass.getName(), + KotlinConstants.KOTLIN_OBJECT_INSTANCE_FIELD_NAME, + ClassUtil.internalTypeFromClassName(currentLambdaClass.getName())) + .__(), + + builder.new_(lambdaGroup) + .dup() + .iconst(classId) + .iconst(arity) + .invokespecial(lambdaGroup.getName(), + ClassConstants.METHOD_NAME_INIT, + "(II)V") + .__() + }, + { + // Lambda is explicitly instantiated + builder.new_(currentLambdaClass) + .dup() + .invokespecial(currentLambdaClass, initMethod) + .__(), + + builder.new_(lambdaGroup) + .dup() + .iconst(classId) + .iconst(arity) + .invokespecial(lambdaGroup.getName(), + ClassConstants.METHOD_NAME_INIT, + "(II)V") + .__() + }, + // Non-empty closure lambda's + { + // Lambda is explicitly instantiated with free variables as arguments (part 1) + builder.new_(currentLambdaClass) + .__(), + + builder.new_(lambdaGroup) + .__() + }, + { + builder.invokespecial(currentLambdaClass, initMethod) + .__(), + + builder.iconst(classId) + .iconst(arity) + .invokespecial(lambdaGroup.getName(), + ClassConstants.METHOD_NAME_INIT, + constructorDescriptor) + .__() + }, + // Direct invocation of named lambda's + { + builder.invokevirtual(currentLambdaClass, specificInvokeMethod) + .__(), + + builder.invokevirtual(lambdaGroup.getName(), + KotlinConstants.METHOD_NAME_LAMBDA_INVOKE, + specificInvokeMethod.getDescriptor(currentLambdaClass)) + .__() + }, + { + builder.invokevirtual(currentLambdaClass, bridgeInvokeMethod) + .__(), + + builder.invokevirtual(lambdaGroup.getName(), + KotlinConstants.METHOD_NAME_LAMBDA_INVOKE, + bridgeInvokeMethod.getDescriptor(currentLambdaClass)) + .__() + } + }; + } + + // Implementations for ClassVisitor + + @Override + public void visitAnyClass(Clazz clazz) {} + + @Override + public void visitProgramClass(ProgramClass programClass) + { + ClassReferenceFinder classReferenceFinder = new ClassReferenceFinder(this.currentLambdaClass); + programClass.constantPoolEntriesAccept(classReferenceFinder); + if (classReferenceFinder.classReferenceFound()) + { + if (currentEnclosingClass == null) + { + logger.warn("Lambda class {} is referenced by {}, while no enclosing class was linked to this " + + "lambda class.", + ClassUtil.externalClassName(currentLambdaClass.getName()), + ClassUtil.externalClassName(programClass.getName())); + } + else if (!currentEnclosingClass.equals(programClass) && !currentLambdaClass.equals(programClass)) + { + logger.warn("Lambda class {} is referenced by {}, which is not the enclosing class or the " + + "lambda class itself.", + ClassUtil.externalClassName(currentLambdaClass.getName()), + ClassUtil.externalClassName(programClass.getName())); + } + // This programClass references the lambda class, so any referencing instructions + // must be updated. + // TODO: consider whether this can be removed, given the fact that all constants are updated anyway + programClass.methodsAccept(this); + + // In some cases a class uses the lambda class as a type in a field or method descriptor + // All those constants must be updated as well. + programClass.constantPoolEntriesAccept(new MultiConstantVisitor( + new DescriptorTypeUpdater( + ClassUtil.internalTypeFromClassName(this.currentLambdaClass.getName()), + ClassUtil.internalTypeFromClassName(this.lambdaGroup.getName())), + new ClassConstantReferenceUpdater(this.currentLambdaClass, + this.lambdaGroup))); + + // remove any old links between lambda's and their inner classes + programClass.accept(KotlinLambdaEnclosingMethodUpdater.retargetedInnerClassAttributeRemover); + + // Remove any constants referring to the old lambda class. + programClass.accept(new ConstantPoolShrinker()); + this.extraDataEntryNameMap.addExtraClassToClass(programClass, this.lambdaGroup); + } + } +} diff --git a/base/src/main/java/proguard/optimize/kotlin/KotlinLambdaGroupBuilder.java b/base/src/main/java/proguard/optimize/kotlin/KotlinLambdaGroupBuilder.java new file mode 100644 index 000000000..35b546f88 --- /dev/null +++ b/base/src/main/java/proguard/optimize/kotlin/KotlinLambdaGroupBuilder.java @@ -0,0 +1,438 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + * + * Copyright (c) 2002-2022 Guardsquare NV + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package proguard.optimize.kotlin; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import proguard.Configuration; +import proguard.classfile.*; +import proguard.classfile.attribute.Attribute; +import proguard.classfile.attribute.visitor.*; +import proguard.classfile.editor.*; +import proguard.classfile.kotlin.KotlinConstants; +import proguard.classfile.util.ClassUtil; +import proguard.classfile.visitor.*; +import proguard.io.ExtraDataEntryNameMap; +import proguard.optimize.info.*; +import proguard.optimize.kotlin.attribute.visitor.InnerClassInfoClassConstantVisitor; +import proguard.optimize.kotlin.attribute.visitor.ModifiedAllInnerClassesInfoVisitor; +import proguard.optimize.kotlin.visitor.FieldRenamer; +import proguard.optimize.kotlin.visitor.MethodCopier; +import proguard.optimize.peephole.SameClassMethodInliner; +import proguard.preverify.CodePreverifier; +import proguard.util.ProcessingFlags; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * This ClassVisitor can be used to visit Kotlin lambda classes that should be merged into one lambda group. + */ +public class KotlinLambdaGroupBuilder implements ClassVisitor { + + public static final String FIELD_NAME_ID = "classId"; + public static final String FIELD_TYPE_ID = "I"; + public static final String METHOD_NAME_SUFFIX_INVOKE = "$invoke"; + protected static final int MAXIMUM_INLINED_INVOKE_METHOD_CODE_LENGTH = Integer.parseInt(System.getProperty("maximum.resulting.code.length", "65535")); + + private final ClassBuilder classBuilder; + private final Configuration configuration; + private final ClassPool programClassPool; + private final ClassPool libraryClassPool; + private final ClassVisitor mergedLambdaVisitor; + private final ClassVisitor notMergedLambdaVisitor; + private final Map invokeMethodBuilders; + private final InterfaceAdder interfaceAdder; + private final ExtraDataEntryNameMap extraDataEntryNameMap; + private final KotlinLambdaGroupInitUpdater initUpdater; + private static final Logger logger = LogManager.getLogger(KotlinLambdaGroupBuilder.class); + + /** + * Initialises a new Kotlin lambda group builder with the given name as the name for the lambda group to be built. + * @param lambdaGroupName a name for the new lambda group + * @param programClassPool a program class pool containing classes that can be referenced by the new lambda group + * @param libraryClassPool a library class pool containing classes that can be referenced by the new lambda group + */ + public KotlinLambdaGroupBuilder(final String lambdaGroupName, + final Configuration configuration, + final ClassPool programClassPool, + final ClassPool libraryClassPool, + final ExtraDataEntryNameMap extraDataEntryNameMap, + final ClassVisitor mergedLambdaVisitor, + final ClassVisitor notMergedLambdaVisitor) + { + this.classBuilder = getNewLambdaGroupClassBuilder(lambdaGroupName, + programClassPool, + libraryClassPool); + this.configuration = configuration; + this.programClassPool = programClassPool; + this.libraryClassPool = libraryClassPool; + this.invokeMethodBuilders = new HashMap<>(); + this.interfaceAdder = new InterfaceAdder(this.classBuilder.getProgramClass()); + this.extraDataEntryNameMap = extraDataEntryNameMap; + this.mergedLambdaVisitor = mergedLambdaVisitor; + this.notMergedLambdaVisitor = notMergedLambdaVisitor; + this.initUpdater = new KotlinLambdaGroupInitUpdater(programClassPool, libraryClassPool); + initialiseLambdaGroup(); + } + + private static ClassBuilder getNewLambdaGroupClassBuilder(String lambdaGroupName, + ClassPool programClassPool, + ClassPool libraryClassPool) + { + // The initial builder is used to set up the initial lambda group class + ClassBuilder initialBuilder = new ClassBuilder(VersionConstants.CLASS_VERSION_1_8, + AccessConstants.FINAL | AccessConstants.SUPER, + lambdaGroupName, + KotlinLambdaMerger.NAME_KOTLIN_LAMBDA); + ProgramClass lambdaGroup = initialBuilder.getProgramClass(); + lambdaGroup.accept(new ProgramClassOptimizationInfoSetter()); + + // The new builder receives the class pools, such that references can be added when necessary + return new ClassBuilder(lambdaGroup, programClassPool, libraryClassPool); + } + + private void initialiseLambdaGroup() + { + addIdField(); + } + + private void addIdField() + { + classBuilder.addAndReturnField(AccessConstants.PRIVATE, FIELD_NAME_ID, FIELD_TYPE_ID); + } + + private KotlinLambdaGroupInvokeMethodBuilder getInvokeMethodBuilder(int arity) + { + KotlinLambdaGroupInvokeMethodBuilder builder = this.invokeMethodBuilders.get(arity); + if (builder == null) + { + builder = new KotlinLambdaGroupInvokeMethodBuilder(arity, + this.classBuilder, + this.programClassPool, + this.libraryClassPool); + this.invokeMethodBuilders.put(arity, builder); + } + return builder; + } + + @Override + public void visitAnyClass(Clazz clazz) {} + + @Override + public void visitProgramClass(ProgramClass lambdaClass) { + if (!KotlinLambdaMerger.shouldMerge(lambdaClass)) + { + return; + } + + try + { + mergeLambdaClass(lambdaClass); + } + catch(Exception exception) + { + logger.error("Lambda class {} could not be merged: {}", + ClassUtil.externalClassName(lambdaClass.getName()), + exception); + if (this.notMergedLambdaVisitor != null) { + lambdaClass.accept(this.notMergedLambdaVisitor); + } + } + } + + private void mergeLambdaClass(ProgramClass lambdaClass) + { + KotlinLambdaMerger.ensureCanMerge(lambdaClass, programClassPool); + + ProgramClass lambdaGroup = this.classBuilder.getProgramClass(); + + // update optimisation info of lambda to show lambda has been merged or is going to be merged + ProgramClassOptimizationInfo optimizationInfo = ProgramClassOptimizationInfo.getProgramClassOptimizationInfo(lambdaClass); + optimizationInfo.setLambdaGroup(lambdaGroup); + optimizationInfo.setTargetClass(lambdaGroup); + + logger.info("Looking at inner lambda's of {}", ClassUtil.externalClassName(lambdaClass.getName())); + + // merge any inner lambda's before merging the current lambda + lambdaClass.attributeAccept(Attribute.INNER_CLASSES, + new ModifiedAllInnerClassesInfoVisitor( + new InnerClassInfoClassConstantVisitor( + new ClassConstantToClassVisitor( + new ClassNameFilter(lambdaClass.getName(), + (ClassVisitor)null, + this) + ), // don't revisit the current lambda + null))); + + // possibly, some inner lambda classes were not mentioned in the inner classes attribute of this lambda, + // so we have to find them by looking for their occurrence in this lambda class + // Normally, this should be redundant if all inner lambda classes are correctly mentioned as inner classes. + lambdaClass.constantPoolEntriesAccept(new ClassConstantToClassVisitor( + new ClassNameFilter(lambdaClass.getName(), + (ClassVisitor)null, + new ImplementedClassFilter( + programClassPool.getClass(KotlinLambdaMerger.NAME_KOTLIN_LAMBDA), + false, + new MultiClassVisitor(this, + new SimpleClassPrinter(true)), + null)))); + + canonicalizeLambdaClassFields(lambdaClass); + + // Add interfaces of lambda class to the lambda group + // TODO: ensure that only Function interfaces are added + lambdaClass.interfaceConstantsAccept(this.interfaceAdder); + + logger.info("Adding lambda {} to lambda group {}", + ClassUtil.externalClassName(lambdaClass.getName()), + ClassUtil.externalClassName(lambdaGroup.getName())); + + // First copy the constructors to the lambda group + Method initMethod = copyOrMergeLambdaInitIntoLambdaGroup(lambdaClass); + + String constructorDescriptor = initMethod.getDescriptor(lambdaGroup); + + // Then inline the specific invoke methods into the bridge invoke methods, within the lambda class + inlineLambdaInvokeMethods(lambdaClass); + + // and copy the bridge invoke methods to the lambda group + ProgramMethod copiedMethod = copyLambdaInvokeToLambdaGroup(lambdaClass); + int arity = ClassUtil.internalMethodParameterCount(copiedMethod.getDescriptor(lambdaGroup)); + if (arity == 1 && lambdaClass.extendsOrImplements(KotlinLambdaMerger.NAME_KOTLIN_FUNCTIONN) + && Objects.equals(copiedMethod.getDescriptor(lambdaGroup), KotlinLambdaGroupInvokeMethodBuilder.METHOD_TYPE_INVOKE_FUNCTIONN)) + { + arity = -1; + } + int lambdaClassId = getInvokeMethodBuilder(arity).addCallTo(copiedMethod); + + + // replace instantiation of lambda class with instantiation of lambda group with correct id + updateLambdaInstantiationSite(lambdaClass, lambdaClassId, arity, constructorDescriptor); + optimizationInfo.setLambdaGroupClassId(lambdaClassId); + SubclassRemover subclassRemover = new SubclassRemover(lambdaClass); + lambdaClass.getSuperClass().accept(subclassRemover); + lambdaClass.interfaceConstantsAccept(new ClassConstantToClassVisitor( + subclassRemover)); + if (this.mergedLambdaVisitor != null) + { + lambdaClass.accept(this.mergedLambdaVisitor); + } + } + + private void canonicalizeLambdaClassFields(ProgramClass lambdaClass) + { + FieldRenamer fieldRenamer = new FieldRenamer(true); + // Assumption: the only name clash of fields of different classes is + // for fields with the name "INSTANCE". + // We don't need these fields anyway, so we don't rename them. + // TODO: handle name clashes correctly - this happens also in the case of inner lambda's + // accessing their enclosing lambda class via a public field + String fieldNameRegularExpression = "!" + KotlinConstants.KOTLIN_OBJECT_INSTANCE_FIELD_NAME; + // Note: the order of the fields is not necessarily the order in which they are assigned + // For now, let's assume the order matches the order in which they are assigned. + lambdaClass.fieldsAccept(new MemberNameFilter(fieldNameRegularExpression, + fieldRenamer)); + } + + private void inlineMethodsInsideClass(ProgramClass lambdaClass) + { + lambdaClass.accept(new MultiClassVisitor( + new ProgramClassOptimizationInfoSetter(), + new AllMemberVisitor( + new ProgramMemberOptimizationInfoSetter()), + // Allow methods to be inlined + new AllMethodVisitor( + new AllAttributeVisitor( + new SameClassMethodInliner(configuration.microEdition, + configuration.android, + MAXIMUM_INLINED_INVOKE_METHOD_CODE_LENGTH, + configuration.allowAccessModification, + true, + null))))); + } + + private void inlineLambdaInvokeMethods(ProgramClass lambdaClass) + { + // Make the non-bridge invoke methods private, so they can be inlined. + lambdaClass.methodsAccept(new MemberNameFilter(KotlinConstants.METHOD_NAME_LAMBDA_INVOKE, + new MemberAccessFilter(0, AccessConstants.BRIDGE, + new MultiMemberVisitor( + new MemberAccessFlagCleaner(AccessConstants.PUBLIC), + new MemberAccessFlagSetter(AccessConstants.PRIVATE))))); + inlineMethodsInsideClass(lambdaClass); + } + + private ProgramMethod copyOrMergeLambdaInitIntoLambdaGroup(ProgramClass lambdaClass) + { + ProgramClass lambdaGroup = this.classBuilder.getProgramClass(); + logger.trace("Copying method of {} to lambda group {}", + ClassUtil.externalClassName(lambdaClass.getName()), + ClassUtil.externalClassName(this.classBuilder.getProgramClass().getName())); + ProgramMethod initMethod = (ProgramMethod)lambdaClass.findMethod(ClassConstants.METHOD_NAME_INIT, + null); + if (initMethod == null) + { + throw new NullPointerException("No method was found in lambda class " + lambdaClass); + } + logger.trace(" method of lambda class {}: {}", + ClassUtil.externalClassName(lambdaClass.getName()), + ClassUtil.externalFullMethodDescription(lambdaClass.getName(), + initMethod.getAccessFlags(), + initMethod.getName(lambdaClass), + initMethod.getDescriptor(lambdaClass))); + + String oldInitDescriptor = initMethod.getDescriptor(lambdaClass); + String newInitDescriptor = KotlinLambdaGroupInitUpdater.getNewInitMethodDescriptor(lambdaClass, initMethod); + + // Check whether an init method with this descriptor exists already + ProgramMethod existingInitMethod = (ProgramMethod)lambdaGroup.findMethod(ClassConstants.METHOD_NAME_INIT, + newInitDescriptor); + + if (existingInitMethod != null) + { + return existingInitMethod; + } + + initMethod.accept(lambdaClass, new MethodCopier(lambdaGroup, + ClassConstants.METHOD_NAME_INIT, + oldInitDescriptor, + AccessConstants.PUBLIC)); + ProgramMethod newInitMethod = (ProgramMethod)lambdaGroup.findMethod(ClassConstants.METHOD_NAME_INIT, + oldInitDescriptor); + + // Update the descriptor + // Add the necessary instructions to entirely new methods + newInitMethod.accept(lambdaGroup, this.initUpdater); + return newInitMethod; + } + + private ProgramMethod copyLambdaInvokeToLambdaGroup(ProgramClass lambdaClass) + { + logger.trace("Copying invoke method of {} to lambda group {}", + ClassUtil.externalClassName(lambdaClass.getName()), + ClassUtil.externalClassName(this.classBuilder.getProgramClass().getName())); + + // Note: the lambda class is expected to contain two invoke methods: + // - a bridge method that implements invoke()Ljava/lang/Object; for the Function0 interface + // - a specific method that contains the implementation of the lambda + // Assumption: the specific invoke method has been inlined into the bridge invoke method, such that + // copying the bridge method to th e lambda group is sufficient to retrieve the full implementation + ProgramMethod invokeMethod = getBridgeInvokeMethod(lambdaClass); + if (invokeMethod == null) + { + throw new NullPointerException("No invoke method was found in lambda class " + + ClassUtil.externalClassName(lambdaClass.getName())); + } + String newMethodName = createDerivedInvokeMethodName(lambdaClass); + invokeMethod.accept(lambdaClass, new MethodCopier(this.classBuilder.getProgramClass(), + newMethodName, + AccessConstants.PRIVATE)); + return (ProgramMethod) this.classBuilder.getProgramClass().findMethod(newMethodName, + invokeMethod.getDescriptor(lambdaClass)); + } + + private static String createDerivedInvokeMethodName(ProgramClass lambdaClass) + { + String shortClassName = ClassUtil.internalShortClassName(lambdaClass.getName()); + return shortClassName + METHOD_NAME_SUFFIX_INVOKE; + } + + /** + * Returns the bridge invoke method of the given class. + * If no bridge invoke method was found, but a non-bridge invoke method was found, then the non-bridge + * invoke method is returned. If no invoke method was found, then null is returned. + * @param lambdaClass the lambda class of which a (bridge) invoke method is to be returned + */ + private static ProgramMethod getBridgeInvokeMethod(ProgramClass lambdaClass) + { + // Assuming that all specific invoke methods have been inlined into the bridge invoke methods + // we can take the bridge invoke method (which overrides the invoke method of the FunctionX interface) + return getInvokeMethod(lambdaClass, true); + } + + public static ProgramMethod getInvokeMethod(ProgramClass lambdaClass, boolean isBridgeMethod) + { + ProgramMethod invokeMethod = null; + for (int methodIndex = 0; methodIndex < lambdaClass.u2methodsCount; methodIndex++) { + ProgramMethod method = lambdaClass.methods[methodIndex]; + if (method.getName(lambdaClass).equals(KotlinConstants.METHOD_NAME_LAMBDA_INVOKE)) + { + if ((isBridgeMethod && (method.u2accessFlags & AccessConstants.BRIDGE) != 0) || + (!isBridgeMethod && (method.u2accessFlags & AccessConstants.BRIDGE) == 0)) { + // we have found the bridge/non-bridge invoke method + return method; + } + invokeMethod = method; + } + } + return invokeMethod; + } + + /** + * Updates enclosing method of the given lambdaClass to instantiate the lambda group that is built by this builder. + * @param lambdaClass the lambda class of which the enclosing method must be updated + * @param lambdaClassId the id that is used for the given lambda class to identify its implementation in the lambda group + */ + private void updateLambdaInstantiationSite(ProgramClass lambdaClass, + int lambdaClassId, + int arity, + String constructorDescriptor) + { + logger.info("Updating instantiation of {} in enclosing method(s) to use id {}.", + ClassUtil.externalClassName(lambdaClass.getName()), lambdaClassId); + KotlinLambdaEnclosingMethodUpdater enclosingMethodUpdater = + new KotlinLambdaEnclosingMethodUpdater(this.programClassPool, + this.libraryClassPool, + lambdaClass, + this.classBuilder.getProgramClass(), + lambdaClassId, + arity, + constructorDescriptor, + this.extraDataEntryNameMap); + lambdaClass.attributeAccept(Attribute.ENCLOSING_METHOD, enclosingMethodUpdater); + + // Also update any references that would occur in other classes of the same package. + String regularExpression = ClassUtil.internalPackagePrefix(lambdaClass.getName()) + "*"; + regularExpression += ",!" + lambdaClass.getName(); + this.programClassPool.classesAccept(new ClassNameFilter(regularExpression, + enclosingMethodUpdater)); + } + + private void addInvokeMethods() + { + for (KotlinLambdaGroupInvokeMethodBuilder builder : this.invokeMethodBuilders.values()) + { + builder.build(); + } + } + + public ProgramClass build() + { + addInvokeMethods(); + ProgramClass lambdaGroup = this.classBuilder.getProgramClass(); + lambdaGroup.setProcessingFlags(lambdaGroup.getProcessingFlags() | ProcessingFlags.INJECTED); + lambdaGroup.accept(new AllMemberVisitor( + new AllAttributeVisitor( + new CodePreverifier(configuration.microEdition)))); + return lambdaGroup; + } +} diff --git a/base/src/main/java/proguard/optimize/kotlin/KotlinLambdaGroupInitUpdater.java b/base/src/main/java/proguard/optimize/kotlin/KotlinLambdaGroupInitUpdater.java new file mode 100644 index 000000000..13fe209c8 --- /dev/null +++ b/base/src/main/java/proguard/optimize/kotlin/KotlinLambdaGroupInitUpdater.java @@ -0,0 +1,147 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + * + * Copyright (c) 2002-2022 Guardsquare NV + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package proguard.optimize.kotlin; + + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import proguard.classfile.*; +import proguard.classfile.attribute.*; +import proguard.classfile.attribute.visitor.AttributeVisitor; +import proguard.classfile.constant.Utf8Constant; +import proguard.classfile.editor.*; +import proguard.classfile.instruction.Instruction; +import proguard.classfile.util.*; +import proguard.classfile.visitor.MemberVisitor; + +public class KotlinLambdaGroupInitUpdater implements AttributeVisitor, MemberVisitor { + + private final ClassPool programClassPool; + private final ClassPool libraryClassPool; + private static final Logger logger = LogManager.getLogger(KotlinLambdaGroupInitUpdater.class); + + public KotlinLambdaGroupInitUpdater(ClassPool programClassPool, + ClassPool libraryClassPool) + { + this.programClassPool = programClassPool; + this.libraryClassPool = libraryClassPool; + } + + @Override + public void visitProgramMethod(ProgramClass programClass, ProgramMethod programMethod) + { + logger.trace("Updating method {} in class {}", + ClassUtil.externalFullMethodDescription(programClass.getName(), + programMethod.getAccessFlags(), + programMethod.getName(programClass), + programMethod.getDescriptor(programClass)), + ClassUtil.externalClassName(programClass.getName())); + updateInitMethodDescriptor(programClass, programMethod); + programMethod.attributesAccept(programClass, this); + } + + private void updateInitMethodDescriptor(ProgramClass programClass, ProgramMethod programMethod) { + String newInitDescriptor = getNewInitMethodDescriptor(programClass, programMethod); + programMethod.u2descriptorIndex = new ConstantAdder(programClass).addConstant(programClass, + new Utf8Constant(newInitDescriptor)); + } + + @Override + public void visitCodeAttribute(Clazz clazz, Method method, CodeAttribute codeAttribute) + { + // This attribute visitor should only be used for program classes. + visitCodeAttribute((ProgramClass) clazz, (ProgramMethod) method, codeAttribute); + } + + public void visitCodeAttribute(ProgramClass programClass, ProgramMethod programMethod, CodeAttribute codeAttribute) + { + CodeAttributeEditor codeAttributeEditor = new CodeAttributeEditor(); + BranchTargetFinder branchTargetFinder = new BranchTargetFinder(); + + // The arity argument is the last of all and has a size of 1 byte. + // Note that the arguments start counting from 1, as the class itself is at byte 0. + // long and double arguments take 2 bytes. + // The classId and arity arguments are the 2 last, which take 1 byte each, as they are of type int. + int arityIndex = ClassUtil.internalMethodParameterSize(programMethod.getDescriptor(programClass), false) - 1; + + InstructionSequencesReplacer replacer = createInstructionSequenceReplacer(branchTargetFinder, + codeAttributeEditor, + arityIndex, + programClass); + + codeAttribute.accept(programClass, + programMethod, + new PeepholeEditor(branchTargetFinder, + codeAttributeEditor, + replacer)); + } + + public void visitAnyAttribute(Clazz clazz, Attribute attribute) {} + + public static String getNewInitMethodDescriptor(ProgramClass programClass, ProgramMethod programMethod) + { + String oldInitDescriptor = programMethod.getDescriptor(programClass); + return oldInitDescriptor.substring(0, oldInitDescriptor.length() - 2) + "II)V"; + } + + private InstructionSequencesReplacer createInstructionSequenceReplacer(BranchTargetFinder branchTargetFinder, + CodeAttributeEditor codeAttributeEditor, + int arityIndex, + ProgramClass lambdaGroup) + { + InstructionSequenceBuilder builder = new InstructionSequenceBuilder(programClassPool, libraryClassPool); + Instruction[][][] replacementPatterns = createReplacementPatternsForInit(builder, arityIndex, lambdaGroup); + return new InstructionSequencesReplacer(builder.constants(), + replacementPatterns, + branchTargetFinder, + codeAttributeEditor); + } + + private Instruction[][][] createReplacementPatternsForInit(InstructionSequenceBuilder builder, + int arityIndex, + ProgramClass lambdaGroup) + { + final int X = InstructionSequenceMatcher.X; + return new Instruction[][][] + { + { + builder.aload_0() + .iconst(X) + .invokespecial(KotlinLambdaMerger.NAME_KOTLIN_LAMBDA, + ClassConstants.METHOD_NAME_INIT, + "(I)V") + .__(), + builder.aload_0() + .iload(arityIndex - 1) + .putfield(lambdaGroup, // store the id in a field + lambdaGroup + .findField(KotlinLambdaGroupBuilder.FIELD_NAME_ID, + KotlinLambdaGroupBuilder.FIELD_TYPE_ID)) + .aload_0() + .iload(arityIndex) + .invokespecial(KotlinLambdaMerger.NAME_KOTLIN_LAMBDA, + ClassConstants.METHOD_NAME_INIT, + "(I)V") + .__() + } + }; + } +} diff --git a/base/src/main/java/proguard/optimize/kotlin/KotlinLambdaGroupInvokeMethodBuilder.java b/base/src/main/java/proguard/optimize/kotlin/KotlinLambdaGroupInvokeMethodBuilder.java new file mode 100644 index 000000000..8089bdd45 --- /dev/null +++ b/base/src/main/java/proguard/optimize/kotlin/KotlinLambdaGroupInvokeMethodBuilder.java @@ -0,0 +1,199 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + * + * Copyright (c) 2002-2022 Guardsquare NV + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package proguard.optimize.kotlin; + +import proguard.classfile.*; +import proguard.classfile.editor.*; +import proguard.classfile.instruction.Instruction; +import proguard.classfile.kotlin.KotlinConstants; +import proguard.classfile.util.ClassUtil; + +import java.util.ArrayList; +import java.util.List; + +public class KotlinLambdaGroupInvokeMethodBuilder { + + public static final String METHOD_ARGUMENT_TYPE_INVOKE = "Ljava/lang/Object;"; + public static final String METHOD_RETURN_TYPE_INVOKE = "Ljava/lang/Object;"; + public static final String METHOD_NAME_INVOKE = "invoke"; + public static final String METHOD_TYPE_INVOKE_FUNCTIONN = "([Ljava/lang/Object;)Ljava/lang/Object;"; + private final int arity; + private int caseIndexCounter = 0; + private final ClassBuilder classBuilder; + // tune the initial list size depending on the expected amount of lambda's that are merged + private final List methodsToBeCalled = new ArrayList<>(5); + private final ClassPool programClassPool; + private final ClassPool libraryClassPool; + + public KotlinLambdaGroupInvokeMethodBuilder(int arity, ClassBuilder classBuilder, ClassPool programClassPool, ClassPool libraryClassPool) + { + this.arity = arity; + this.classBuilder = classBuilder; + this.programClassPool = programClassPool; + this.libraryClassPool = libraryClassPool; + } + + /** + * Adds an invocation of the given method to the invoke method that is being built. + * @param methodToBeCalled a method that must be called by the invoke method. The method must belong to the + * lambda group that is being built by the class builder of this invoke method builder. + * @return the case index that uniquely identifies the call to the given method within the invoke method + */ + public int addCallTo(Method methodToBeCalled) + { + methodsToBeCalled.add(methodToBeCalled); + return getNewCaseIndex(); + } + + private int getNewCaseIndex() + { + int caseIndex = this.caseIndexCounter; + this.caseIndexCounter++; + return caseIndex; + } + + /** + * Returns the number of cases that have been added to the invoke method that is being built until now. + */ + public int getCaseIndexCounter() + { + return this.caseIndexCounter; + } + + private Instruction[] getInstructionsForCase(int caseIndex) + { + Method methodToBeCalled = this.methodsToBeCalled.get(caseIndex); + InstructionSequenceBuilder builder = new InstructionSequenceBuilder(this.classBuilder.getProgramClass(), + this.programClassPool, + this.libraryClassPool); + builder.invokevirtual(this.classBuilder.getProgramClass(), methodToBeCalled); // invoke the lambda implementation + if (methodDoesNotHaveReturnValue(methodToBeCalled)) { + // ensure there is a return value + builder.getstatic(KotlinConstants.NAME_KOTLIN_UNIT, KotlinConstants.KOTLIN_OBJECT_INSTANCE_FIELD_NAME, KotlinConstants.TYPE_KOTLIN_UNIT); + } + builder.areturn(); // return + return builder.instructions(); + } + + private boolean methodDoesNotHaveReturnValue(Method method) + { + String methodDescriptor = method.getDescriptor(this.classBuilder.getProgramClass()); + String returnType = ClassUtil.internalMethodReturnType(methodDescriptor); + return returnType.equals("V"); + } + + private CompactCodeAttributeComposer addLoadArgumentsInstructions(CompactCodeAttributeComposer composer) + { + // load the lambda group, so later on the correct implementation methods can be called on this lambda group + composer.aload_0(); + if (this.arity == -1) + { + return composer.aload_1(); + } + for (int argumentIndex = 1; argumentIndex <= this.arity; argumentIndex++) + { + composer.aload(argumentIndex); + } + return composer; + } + + /** + * Returns a code builder that builds the code for the invoke method that is being built. + */ + public ClassBuilder.CodeBuilder buildCodeBuilder() + { + return composer -> { + int cases = getCaseIndexCounter(); + if (cases == 0) + { + composer.getstatic(KotlinConstants.NAME_KOTLIN_UNIT, KotlinConstants.KOTLIN_OBJECT_INSTANCE_FIELD_NAME, KotlinConstants.TYPE_KOTLIN_UNIT); + composer.areturn(); + return; + } + + CompactCodeAttributeComposer.Label[] caseLabels = new CompactCodeAttributeComposer.Label[cases - 1]; + for (int caseIndex = 0; caseIndex < cases - 1; caseIndex++) + { + caseLabels[caseIndex] = composer.createLabel(); + } + CompactCodeAttributeComposer.Label defaultLabel = composer.createLabel(); + CompactCodeAttributeComposer.Label endOfMethodLabel = composer.createLabel(); + + addLoadArgumentsInstructions(composer); + + if (cases > 1) + { + // only add a switch when there is more than one case + // if composer.tableswitch() would be called with cases == 1, then the highCase would be lower than the lowCase + composer + .aload_0() + .getfield(this.classBuilder.getProgramClass(), + this.classBuilder.getProgramClass().findField(KotlinLambdaGroupBuilder.FIELD_NAME_ID, + KotlinLambdaGroupBuilder.FIELD_TYPE_ID)) + .tableswitch(defaultLabel, 0, cases - 2, caseLabels); + } + for (int caseIndex = 0; caseIndex < cases - 1; caseIndex++) + { + composer + .label(caseLabels[caseIndex]) + .appendInstructions(getInstructionsForCase(caseIndex)) + .goto_(endOfMethodLabel); + } + composer + .label(defaultLabel) + .appendInstructions(getInstructionsForCase(cases - 1)) + .label(endOfMethodLabel) + .areturn(); + }; + } + + private static String getMethodDescriptorForArity(int arity) + { + // arity -1 is used for implementations of FunctionN + if (arity == -1) + { + return METHOD_TYPE_INVOKE_FUNCTIONN; + } + StringBuilder descriptor = new StringBuilder("("); + for (int argumentIndex = 0; argumentIndex < arity; argumentIndex++) + { + descriptor.append(METHOD_ARGUMENT_TYPE_INVOKE); + } + descriptor.append(")").append(METHOD_RETURN_TYPE_INVOKE); + return descriptor.toString(); + } + + /** + * Adds a new invoke method to the class builder of this invoke method builder. + * If the new method has to replace an existing invoke method, ensure that the original invoke method + * has been removed before calling this method. + * @return the newly created invoke method, which has been added to the program class under construction + */ + public ProgramMethod build() + { + return classBuilder.addAndReturnMethod(AccessConstants.PUBLIC | AccessConstants.SYNTHETIC, + KotlinConstants.METHOD_NAME_LAMBDA_INVOKE, + getMethodDescriptorForArity(this.arity), + this.methodsToBeCalled.size() * 10, + this.buildCodeBuilder()); + + } +} diff --git a/base/src/main/java/proguard/optimize/kotlin/KotlinLambdaMerger.java b/base/src/main/java/proguard/optimize/kotlin/KotlinLambdaMerger.java new file mode 100644 index 000000000..97e7d30aa --- /dev/null +++ b/base/src/main/java/proguard/optimize/kotlin/KotlinLambdaMerger.java @@ -0,0 +1,437 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + * + * Copyright (c) 2002-2022 Guardsquare NV + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package proguard.optimize.kotlin; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import proguard.*; +import proguard.classfile.*; +import proguard.classfile.attribute.*; +import proguard.classfile.attribute.visitor.*; +import proguard.classfile.constant.FieldrefConstant; +import proguard.classfile.constant.visitor.*; +import proguard.classfile.editor.LineNumberTableAttributeTrimmer; +import proguard.classfile.instruction.visitor.InstructionCounter; +import proguard.classfile.kotlin.KotlinConstants; +import proguard.classfile.util.*; +import proguard.classfile.visitor.*; +import proguard.optimize.info.*; +import proguard.optimize.kotlin.visitor.FieldReferenceFinder; +import proguard.optimize.kotlin.visitor.MethodReferenceFinder; +import proguard.optimize.kotlin.visitor.PackageGrouper; +import proguard.optimize.peephole.*; +import proguard.pass.Pass; +import proguard.shrink.*; +import proguard.util.*; +import java.io.PrintWriter; + +/** + * This {@link Pass} provides the Kotlin Lambda Merging optimisation. + * It merges the implementations of eligible Kotlin Lambda classes into + * lambda groups per package. + * @author Joren Van Hecke + */ +public class KotlinLambdaMerger implements Pass +{ + + public static final String NAME_KOTLIN_LAMBDA = "kotlin/jvm/internal/Lambda"; + public static final String NAME_KOTLIN_FUNCTION = "kotlin/jvm/functions/Function"; + public static final String NAME_KOTLIN_FUNCTIONN = "kotlin/jvm/functions/FunctionN"; + + public static boolean lambdaMergingDone = false; + private static final Logger logger = LogManager.getLogger(KotlinLambdaMerger.class); + private final Configuration configuration; + + public KotlinLambdaMerger(Configuration configuration) + { + this.configuration = configuration; + } + + // Implementations for Pass + @Override + public void execute(AppView appView) throws Exception + { + if (lambdaMergingDone) + { + return; + } + // Remove old processing info + appView.programClassPool.classesAccept(new ClassCleaner()); + appView.libraryClassPool.classesAccept(new ClassCleaner()); + + // Get the Lambda class + Clazz kotlinLambdaClass = getKotlinLambdaClass(appView.programClassPool, appView.libraryClassPool); + if (kotlinLambdaClass == null) { + logger.trace("The Kotlin class '{}' is not found, so merging of Kotlin lambda classes is skipped.", + NAME_KOTLIN_LAMBDA); + } + else + { + // A class pool where the applicable lambda's will be stored + ClassPool lambdaClassPool = new ClassPool(); + ClassPool newProgramClassPool = new ClassPool(); + ClassPoolFiller newProgramClassPoolFiller = new ClassPoolFiller(newProgramClassPool); + + // find all lambda classes with an empty closure + // assume that the lambda classes have exactly 1 instance constructor, which has descriptor ()V + // (i.e. no arguments) if the closure is empty + appView.programClassPool.classesAccept(new MultiClassVisitor( + new ClassProcessingFlagFilter(ProcessingFlags.DONT_OPTIMIZE, + 0, + newProgramClassPoolFiller), + new ClassProcessingFlagFilter(0, + ProcessingFlags.DONT_OPTIMIZE, + new ImplementedClassFilter(kotlinLambdaClass, + false, + new ClassPoolFiller(lambdaClassPool), + newProgramClassPoolFiller)), + // add optimisation info to the lambda's, + // so that it can be filled out later + new ProgramClassOptimizationInfoSetter(), + new AllMemberVisitor( + new ProgramMemberOptimizationInfoSetter()))); + + // group the lambda's per package + PackageGrouper packageGrouper = new PackageGrouper(); + lambdaClassPool.classesAccept(packageGrouper); + + ClassPool lambdaGroupClassPool = new ClassPool(); + ClassPool notMergedLambdaClassPool = new ClassPool(); + ClassCounter mergedLambdaClassCounter = new ClassCounter(); + + // merge the lambda's per package + KotlinLambdaClassMerger merger = new KotlinLambdaClassMerger(this.configuration, + appView.programClassPool, + appView.libraryClassPool, + new MultiClassVisitor( + new ClassPoolFiller(lambdaGroupClassPool), + newProgramClassPoolFiller), + mergedLambdaClassCounter, + new MultiClassVisitor( + new ClassPoolFiller(notMergedLambdaClassPool), + newProgramClassPoolFiller), + appView.extraDataEntryNameMap); + packageGrouper.packagesAccept(merger); + + // Print out the mapping, if requested. + PrintWriter out = new PrintWriter(System.out, true); + if (configuration.printLambdaGroupMapping != null) + { + logger.trace("Printing lambda group mapping to [{}]...", + PrintWriterUtil.fileName(configuration.printLambdaGroupMapping)); + + PrintWriter mappingWriter = + PrintWriterUtil.createPrintWriter(configuration.printLambdaGroupMapping, out); + + try + { + // Print out lambda's that have been merged and their lambda groups. + lambdaClassPool.classesAcceptAlphabetically( + new LambdaGroupMappingPrinter(mappingWriter)); + } + finally + { + PrintWriterUtil.closePrintWriter(configuration.printMapping, + mappingWriter); + } + } + + // initialise the references from and to the newly created lambda groups and their enclosing classes + newProgramClassPool.classesAccept(new ClassInitializer(newProgramClassPool, appView.libraryClassPool)); + + // inline the helper invoke methods into the general invoke method + inlineMethodsInsideLambdaGroups(lambdaGroupClassPool); + + // remove the unused helper methods from the lambda groups + shrinkLambdaGroups(newProgramClassPool, appView.libraryClassPool, lambdaGroupClassPool); + + logger.info("Number of merged lambda classes: {}", mergedLambdaClassCounter.getCount()); + + logger.trace("Considered {} lambda classes for merging", lambdaClassPool.size()); + logger.trace("of which {} lambda classes were not merged.", notMergedLambdaClassPool.size()); + logger.trace("{} lambda group(s) created.", lambdaGroupClassPool.size()); + logger.trace("Size of original program class pool: {}", appView.programClassPool.size()); + logger.trace("Size of new program class pool: {}", newProgramClassPool.size()); + + appView.programClassPool.classesAccept(new ClassCleaner()); + appView.libraryClassPool.classesAccept(new ClassCleaner()); + newProgramClassPool.classesAccept(new ClassCleaner()); + appView.programClassPool.clear(); + newProgramClassPool.classesAccept(new ClassPoolFiller(appView.programClassPool)); + } + lambdaMergingDone = true; + } + + private void inlineMethodsInsideLambdaGroups(ClassPool lambdaGroupClassPool) + { + InstructionCounter methodInliningCounter = new InstructionCounter(); + + lambdaGroupClassPool.classesAccept(new MultiClassVisitor( + new ProgramClassOptimizationInfoSetter(), + new AllMemberVisitor( + new ProgramMemberOptimizationInfoSetter()), + new AllMethodVisitor( + new AllAttributeVisitor( + new SameClassMethodInliner(configuration.microEdition, + configuration.android, + configuration.allowAccessModification, + methodInliningCounter))))); + logger.debug("{} methods inlined inside lambda groups.", methodInliningCounter.getCount()); + } + + private void shrinkLambdaGroups(ClassPool programClassPool, + ClassPool libraryClassPool, + ClassPool lambdaGroupClassPool) + { + SimpleUsageMarker simpleUsageMarker = new SimpleUsageMarker(); + ClassUsageMarker classUsageMarker = new ClassUsageMarker(simpleUsageMarker); + + // make sure that the used methods of the lambda groups are marked as used + // by marking all classes and methods + libraryClassPool.classesAccept(classUsageMarker); + // but don't mark the lambda groups and their members in case they are not used, + // e.g. inlined helper invoke methods + programClassPool.classesAccept(new ClassNameFilter( + "**/" + KotlinLambdaClassMerger.NAME_LAMBDA_GROUP, + (ClassVisitor) null, + new MultiClassVisitor( + classUsageMarker, + new AllMemberVisitor( + classUsageMarker)))); + + lambdaGroupClassPool.classesAccept(new AllMemberVisitor( + new MultiMemberVisitor( + new MemberNameFilter(KotlinConstants.METHOD_NAME_LAMBDA_INVOKE, + classUsageMarker), + new MemberNameFilter(ClassConstants.METHOD_NAME_INIT, + classUsageMarker), + new MemberNameFilter(ClassConstants.METHOD_NAME_CLINIT, + classUsageMarker)))); + + + // ensure that the interfaces of the lambda group are not removed + lambdaGroupClassPool.classesAccept(new InterfaceUsageMarker( + classUsageMarker)); + + // mark the lambda groups themselves as used + // remove the unused parts of the lambda groups, such as the inlined invoke helper methods + // and make sure that the line numbers are updated + lambdaGroupClassPool.classesAccept(new MultiClassVisitor( + new UsedClassFilter(simpleUsageMarker, + new ClassShrinker(simpleUsageMarker)), + new LineNumberLinearizer(), + new AllAttributeVisitor(true, + new LineNumberTableAttributeTrimmer()))); + } + + private Clazz getKotlinLambdaClass(ClassPool programClassPool, ClassPool libraryClassPool) + { + Clazz kotlinLambdaClass = programClassPool.getClass(NAME_KOTLIN_LAMBDA); + if (kotlinLambdaClass == null) { + kotlinLambdaClass = libraryClassPool.getClass(NAME_KOTLIN_LAMBDA); + } + return kotlinLambdaClass; + } + + /** + * Returns the arity of the function interface that is implemented by a given class. + * @param programClass the class of which the arity must be determined + * @return the value following the base name "kotlin/jvm/functions/Function". This can be a number going from + * 0 to 22 or the character "N". For example, for a class implementing the interface + * "kotlin/jvm/functions/Function5" the value "5" is returned. + * @throws IllegalArgumentException if the given programClass does not implement a Kotlin function interface. + */ + public static String getArityFromInterface(ProgramClass programClass) throws IllegalArgumentException + { + for (int interfaceIndex = 0; interfaceIndex < programClass.u2interfacesCount; interfaceIndex++) + { + String interfaceName = programClass.getInterfaceName(interfaceIndex); + if (interfaceName.startsWith(NAME_KOTLIN_FUNCTION) && interfaceName.length() > NAME_KOTLIN_FUNCTION.length()) + { + return interfaceName.substring(NAME_KOTLIN_FUNCTION.length()); + } + } + throw new IllegalArgumentException("Class " + ClassUtil.externalClassName(programClass.getName()) + + " does not implement a Kotlin function interface."); + } + + /** + * Checks whether the given lambda class should still be merged. + * Returns true if the lambda class has not yet been merged and is allowed to be merged. + * @param lambdaClass the lambda class for which should be checked whether it should be merged + */ + public static boolean shouldMerge(ProgramClass lambdaClass) + { + ProgramClassOptimizationInfo optimizationInfo = + ProgramClassOptimizationInfo.getProgramClassOptimizationInfo(lambdaClass); + return (lambdaClass.getProcessingFlags() & (ProcessingFlags.DONT_OPTIMIZE | ProcessingFlags.DONT_SHRINK)) == 0 + && lambdaClass.extendsOrImplements(NAME_KOTLIN_LAMBDA) + // if optimisation info is null, then the lambda was not enqueued to be merged + && optimizationInfo != null + && optimizationInfo.getLambdaGroup() == null + && optimizationInfo.mayBeMerged(); + } + + /** + * Checks whether the given lambda class can be merged and throws an exception if it cannot be merged. + * @param lambdaClass the lambda class for which it should be checked whether it can be merged + * @param programClassPool the program class pool in which the lambda class is defined and used + * @throws IllegalArgumentException if the given lambda class cannot be merged + */ + public static void ensureCanMerge(ProgramClass lambdaClass, + ClassPool programClassPool) throws IllegalArgumentException + { + String externalClassName = ClassUtil.externalClassName(lambdaClass.getName()); + if (!lambdaClass.extendsOrImplements(NAME_KOTLIN_LAMBDA)) + { + throw new IllegalArgumentException("Class " + externalClassName + " cannot be merged in a Kotlin " + + "lambda group, because it is not a subclass of " + + ClassUtil.externalClassName(NAME_KOTLIN_LAMBDA) + "."); + } + else if (!lambdaClassHasExactlyOneInitConstructor(lambdaClass)) + { + throw new IllegalArgumentException("Lambda class " + externalClassName + " cannot be merged, because " + + "it has more than 1 constructor."); + } + else if (!lambdaClassHasNoBootstrapMethod(lambdaClass)) + { + throw new IllegalArgumentException("Lambda class " + externalClassName + " cannot be merged, because it " + + "contains a bootstrap method that would not be merged into the lambda group."); + } + else if (!lambdaClassHasNoAccessibleStaticMethods(lambdaClass)) + { + throw new IllegalArgumentException("Lambda class " + externalClassName + " cannot be merged, because it " + + "contains a static method that could be used outside the class itself."); + } + else if (!lambdaClassIsNotDirectlyInvoked(lambdaClass)) + { + throw new IllegalArgumentException("Lambda class " + externalClassName + " cannot be merged, because it is " + + "directly invoked with its specific invoke method."); + } + else if (!nonINSTANCEFieldsAreNotReferencedFromSamePackage(lambdaClass, programClassPool)) + { + throw new IllegalArgumentException("Lambda class " + externalClassName + " cannot be merged, because one of " + + "its fields, other than the 'INSTANCE' field is referenced by one of its " + + "inner classes."); + } + else if (!lambdaClassHasTotalMethodCodeSizeThatCanBeInlined(lambdaClass)) + { + throw new IllegalArgumentException("Lambda class " + externalClassName + " cannot be merged, because its " + + "methods are too big to be inlined."); + } + } + + private static boolean lambdaClassHasNoBootstrapMethod(ProgramClass lambdaClass) { + AttributeCounter attributeCounter = new AttributeCounter(); + lambdaClass.attributeAccept(Attribute.BOOTSTRAP_METHODS, attributeCounter); + return attributeCounter.getCount() == 0; + } + + private static boolean lambdaClassHasNoAccessibleStaticMethods(ProgramClass lambdaClass) + { + MethodCounter nonPrivateStaticMethodCounter = new MethodCounter(); + String regularExpression = "!" + ClassConstants.METHOD_NAME_CLINIT; + lambdaClass.methodsAccept(new MemberAccessFilter(AccessConstants.STATIC, AccessConstants.PRIVATE, + new MemberNameFilter(regularExpression, + new MultiMemberVisitor( + nonPrivateStaticMethodCounter, + new MemberVisitor() { + @Override + public void visitProgramMethod(ProgramClass programClass, + ProgramMethod programMethod) { + logger.trace("Lambda class {} contains a static method that cannot be merged: {}", + ClassUtil.externalClassName(lambdaClass.getName()), + ClassUtil.externalFullMethodDescription(lambdaClass.getName(), + programMethod.getAccessFlags(), + programMethod.getName(lambdaClass), + programMethod.getDescriptor(lambdaClass))); + } + })))); + return nonPrivateStaticMethodCounter.getCount() == 0; + } + + private static boolean lambdaClassHasTotalMethodCodeSizeThatCanBeInlined(ProgramClass lambdaClass) + { + String methodNameRegularExpression = "!" + ClassConstants.METHOD_NAME_INIT; + methodNameRegularExpression += ",!" + ClassConstants.METHOD_NAME_CLINIT; + CodeSizeCounter codeSizeCounter = new CodeSizeCounter(); + lambdaClass.methodsAccept(new MemberNameFilter(methodNameRegularExpression, + new AllAttributeVisitor( + codeSizeCounter))); + return codeSizeCounter.getCount() <= KotlinLambdaGroupBuilder.MAXIMUM_INLINED_INVOKE_METHOD_CODE_LENGTH; + } + + private static boolean lambdaClassHasExactlyOneInitConstructor(ProgramClass lambdaClass) + { + MethodCounter initMethodCounter = new MethodCounter(); + lambdaClass.methodsAccept(new MemberNameFilter(ClassConstants.METHOD_NAME_INIT, + initMethodCounter)); + if (initMethodCounter.getCount() != 1) + { + logger.trace("Lambda class {} has {} constructors.", + ClassUtil.externalClassName(lambdaClass.getName()), initMethodCounter.getCount()); + } + return initMethodCounter.getCount() == 1; + } + + private static boolean lambdaClassIsNotDirectlyInvoked(ProgramClass lambdaClass) + { + Method invokeMethod = KotlinLambdaGroupBuilder.getInvokeMethod(lambdaClass, false); + if (invokeMethod == null) + { + return true; + } + MethodReferenceFinder methodReferenceFinder = new MethodReferenceFinder(invokeMethod); + // TODO: move visitors to separate classes + lambdaClass.attributeAccept(Attribute.ENCLOSING_METHOD, new AttributeVisitor() { + @Override + public void visitEnclosingMethodAttribute(Clazz clazz, EnclosingMethodAttribute enclosingMethodAttribute) { + enclosingMethodAttribute.referencedClass.constantPoolEntriesAccept(methodReferenceFinder); + } + }); + return !methodReferenceFinder.methodReferenceFound(); + } + + private static boolean nonINSTANCEFieldsAreNotReferencedFromSamePackage(ProgramClass lambdaClass, + ClassPool programClassPool) + { + String regularExpression = ClassUtil.internalPackagePrefix(lambdaClass.getName()) + "*"; + String fieldRegularExpression = "!" + KotlinConstants.KOTLIN_OBJECT_INSTANCE_FIELD_NAME; + FieldReferenceFinder fieldReferenceFinder = new FieldReferenceFinder(lambdaClass, + fieldRegularExpression, + new ConstantVisitor() { + @Override + public void visitFieldrefConstant(Clazz clazz, FieldrefConstant fieldrefConstant) { + logger.trace("{} references non-INSTANCE field {} of lambda class {}.", + ClassUtil.externalClassName(clazz.getName()), + ClassUtil.externalFullFieldDescription(fieldrefConstant.referencedField.getAccessFlags(), + fieldrefConstant.getName(clazz), + fieldrefConstant.getType(clazz)), + ClassUtil.externalClassName(lambdaClass.getName())); + } + }); + programClassPool.classesAccept(new ClassNameFilter(regularExpression, + new ClassNameFilter(lambdaClass.getName(), (ClassVisitor)null, + new AllConstantVisitor( + fieldReferenceFinder)))); + return !fieldReferenceFinder.isFieldReferenceFound(); + } +} diff --git a/base/src/main/java/proguard/optimize/kotlin/LambdaGroupMappingPrinter.java b/base/src/main/java/proguard/optimize/kotlin/LambdaGroupMappingPrinter.java new file mode 100644 index 000000000..c06c947f5 --- /dev/null +++ b/base/src/main/java/proguard/optimize/kotlin/LambdaGroupMappingPrinter.java @@ -0,0 +1,77 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + * + * Copyright (c) 2002-2022 Guardsquare NV + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package proguard.optimize.kotlin; + +import proguard.classfile.Clazz; +import proguard.classfile.ProgramClass; +import proguard.classfile.util.ClassUtil; +import proguard.classfile.visitor.ClassVisitor; +import proguard.optimize.info.ProgramClassOptimizationInfo; + +import java.io.PrintWriter; + +/** + * This {@link ClassVisitor} prints out the merged lambda classes + * and the lambda groups into which they were merged, including their + * original arity and the class id in the resulting lambda group. + * + * @see proguard.optimize.kotlin.KotlinLambdaMerger + * + * @author Joren Van Hecke + */ +public class LambdaGroupMappingPrinter implements ClassVisitor { + + private final PrintWriter pw; + + /** + * Creates a new LambdaGroupMappingPrinter that prints to the given writer. + * @param printWriter the writer to which to print. + */ + public LambdaGroupMappingPrinter(PrintWriter printWriter) + { + this.pw = printWriter; + } + + + @Override + public void visitAnyClass(Clazz clazz) { } + + @Override + public void visitProgramClass(ProgramClass programClass) { + String name = programClass.getName(); + ProgramClassOptimizationInfo optimizationInfo = + ProgramClassOptimizationInfo.getProgramClassOptimizationInfo(programClass); + Clazz lambdaGroup = optimizationInfo.getLambdaGroup(); + if (lambdaGroup != null) + { + String lambdaGroupName = lambdaGroup.getName(); + // Print out the class to lambda group mapping. + pw.println(ClassUtil.externalClassName(name) + + " -> " + + ClassUtil.externalClassName(lambdaGroupName) + + " (arity " + + KotlinLambdaMerger.getArityFromInterface(programClass) + + ", case " + + optimizationInfo.getLambdaGroupClassId() + + ")"); + } + } +} diff --git a/base/src/main/java/proguard/optimize/kotlin/attribute/visitor/InnerClassInfoClassConstantVisitor.java b/base/src/main/java/proguard/optimize/kotlin/attribute/visitor/InnerClassInfoClassConstantVisitor.java new file mode 100644 index 000000000..4fbc35cd7 --- /dev/null +++ b/base/src/main/java/proguard/optimize/kotlin/attribute/visitor/InnerClassInfoClassConstantVisitor.java @@ -0,0 +1,51 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + * + * Copyright (c) 2002-2022 Guardsquare NV + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package proguard.optimize.kotlin.attribute.visitor; + +import proguard.classfile.Clazz; +import proguard.classfile.ProgramClass; +import proguard.classfile.attribute.InnerClassesInfo; +import proguard.classfile.attribute.visitor.InnerClassesInfoVisitor; +import proguard.classfile.constant.visitor.ConstantVisitor; + +public class InnerClassInfoClassConstantVisitor implements InnerClassesInfoVisitor { + + private final ConstantVisitor innerClassConstantVisitor; + private final ConstantVisitor outerClassConstantVisitor; + + public InnerClassInfoClassConstantVisitor(ConstantVisitor innerClassConstantVisitor, ConstantVisitor outerClassConstantVisitor) + { + this.innerClassConstantVisitor = innerClassConstantVisitor; + this.outerClassConstantVisitor = outerClassConstantVisitor; + } + + @Override + public void visitInnerClassesInfo(Clazz clazz, InnerClassesInfo innerClassesInfo) { + if (this.innerClassConstantVisitor != null) + { + innerClassesInfo.innerClassConstantAccept(clazz, this.innerClassConstantVisitor); + } + if (this.outerClassConstantVisitor != null) + { + innerClassesInfo.outerClassConstantAccept(clazz, this.outerClassConstantVisitor); + } + } +} diff --git a/base/src/main/java/proguard/optimize/kotlin/attribute/visitor/ModifiedAllInnerClassesInfoVisitor.java b/base/src/main/java/proguard/optimize/kotlin/attribute/visitor/ModifiedAllInnerClassesInfoVisitor.java new file mode 100644 index 000000000..199d9b653 --- /dev/null +++ b/base/src/main/java/proguard/optimize/kotlin/attribute/visitor/ModifiedAllInnerClassesInfoVisitor.java @@ -0,0 +1,47 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + * + * Copyright (c) 2002-2022 Guardsquare NV + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package proguard.optimize.kotlin.attribute.visitor; + +import proguard.classfile.Clazz; +import proguard.classfile.attribute.InnerClassesAttribute; +import proguard.classfile.attribute.visitor.AllInnerClassesInfoVisitor; +import proguard.classfile.attribute.visitor.InnerClassesInfoVisitor; + +/** + * This {@link AllInnerClassesInfoVisitor} revisits each {@link InnerClassesAttribute} everytime its amount of + * referenced classes has been modified in the meantime. + */ +public class ModifiedAllInnerClassesInfoVisitor extends AllInnerClassesInfoVisitor { + + public ModifiedAllInnerClassesInfoVisitor(InnerClassesInfoVisitor innerClassesInfoVisitor) { + super(innerClassesInfoVisitor); + } + + public void visitInnerClassesAttribute(Clazz clazz, InnerClassesAttribute innerClassesAttribute) + { + int originalClassesCount = -1; + while (originalClassesCount != innerClassesAttribute.u2classesCount) + { + originalClassesCount = innerClassesAttribute.u2classesCount; + super.visitInnerClassesAttribute(clazz, innerClassesAttribute); + } + } +} diff --git a/base/src/main/java/proguard/optimize/kotlin/visitor/ClassConstantReferenceUpdater.java b/base/src/main/java/proguard/optimize/kotlin/visitor/ClassConstantReferenceUpdater.java new file mode 100644 index 000000000..639010ca7 --- /dev/null +++ b/base/src/main/java/proguard/optimize/kotlin/visitor/ClassConstantReferenceUpdater.java @@ -0,0 +1,48 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + * + * Copyright (c) 2002-2022 Guardsquare NV + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package proguard.optimize.kotlin.visitor; + +import proguard.classfile.Clazz; +import proguard.classfile.constant.ClassConstant; +import proguard.classfile.constant.Constant; +import proguard.classfile.constant.visitor.ConstantVisitor; + +public class ClassConstantReferenceUpdater implements ConstantVisitor +{ + private final Clazz originalClass; + private final Clazz replacingClass; + public ClassConstantReferenceUpdater(Clazz originalClass, Clazz replacingClass) + { + this.originalClass = originalClass; + this.replacingClass = replacingClass; + } + + @Override + public void visitAnyConstant(Clazz clazz, Constant constant) {} + + @Override + public void visitClassConstant(Clazz clazz, ClassConstant classConstant) { + if (classConstant.referencedClass == originalClass) + { + classConstant.referencedClass = replacingClass; + } + } +} diff --git a/base/src/main/java/proguard/optimize/kotlin/visitor/FieldCopier.java b/base/src/main/java/proguard/optimize/kotlin/visitor/FieldCopier.java new file mode 100644 index 000000000..53a32e6ff --- /dev/null +++ b/base/src/main/java/proguard/optimize/kotlin/visitor/FieldCopier.java @@ -0,0 +1,99 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + * + * Copyright (c) 2002-2022 Guardsquare NV + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package proguard.optimize.kotlin.visitor; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import proguard.classfile.*; +import proguard.classfile.constant.FieldrefConstant; +import proguard.classfile.constant.visitor.ConstantVisitor; +import proguard.classfile.editor.ClassBuilder; +import proguard.classfile.editor.ClassEditor; +import proguard.classfile.visitor.MemberVisitor; + +public class FieldCopier implements MemberVisitor, ConstantVisitor { + + private final ClassBuilder classBuilder; + private final ClassEditor classEditor; + private final FieldRenamer fieldRenamer; + private ProgramField lastCopiedField; + private boolean hasCopiedField = false; + private static final Logger logger = LogManager.getLogger(FieldCopier.class); + + public FieldCopier(ClassBuilder builder, FieldRenamer renamer) + { + this.classBuilder = builder; + this.classEditor = new ClassEditor(builder.getProgramClass()); + this.fieldRenamer = renamer; + } + + @Override + public void visitProgramField(ProgramClass programClass, ProgramField programField) + { + String fieldName = programField.getName(programClass); + if (this.fieldRenamer != null) + { + fieldName = fieldRenamer.getNextFieldName(); + } + + String fieldDescriptor = programField.getDescriptor(programClass); + Field oldField = classBuilder.getProgramClass().findField(fieldName, null); + Field oldFieldSameDescriptor = classBuilder.getProgramClass().findField(fieldName, fieldDescriptor); + if (oldField != null && oldFieldSameDescriptor == null) + { + String oldFieldDescriptor = oldField.getDescriptor(classBuilder.getProgramClass()); + //logger.warn("Field " + fieldName + " already exists in class " + classBuilder.getProgramClass() + " with different descriptor: " + oldFieldDescriptor + " <-> " + fieldDescriptor + ". The field will be duplicated with different descriptors."); + // Merge the field types: generalise to a common super type + //fieldDescriptor = ClassConstants.TYPE_JAVA_LANG_OBJECT; + } + else if (oldFieldSameDescriptor != null) + { + this.classEditor.removeField(oldFieldSameDescriptor); + } + ProgramField copiedField = classBuilder.addAndReturnField(programField.u2accessFlags, fieldName, fieldDescriptor); + if (this.fieldRenamer != null) + { + this.fieldRenamer.visitProgramField(classBuilder.getProgramClass(), copiedField); + } + this.lastCopiedField = copiedField; + this.hasCopiedField = true; + } + + @Override + public void visitFieldrefConstant(Clazz clazz, FieldrefConstant fieldrefConstant) { + fieldrefConstant.referencedFieldAccept(this); + } + + public ProgramField getLastCopiedField() + { + return this.lastCopiedField; + } + + public boolean hasCopiedField() + { + return this.hasCopiedField; + } + + public void reset() + { + this.hasCopiedField = false; + } +} diff --git a/base/src/main/java/proguard/optimize/kotlin/visitor/FieldReferenceFinder.java b/base/src/main/java/proguard/optimize/kotlin/visitor/FieldReferenceFinder.java new file mode 100644 index 000000000..22c8ed7fe --- /dev/null +++ b/base/src/main/java/proguard/optimize/kotlin/visitor/FieldReferenceFinder.java @@ -0,0 +1,66 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + * + * Copyright (c) 2002-2022 Guardsquare NV + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package proguard.optimize.kotlin.visitor; + +import proguard.classfile.Clazz; +import proguard.classfile.constant.Constant; +import proguard.classfile.constant.FieldrefConstant; +import proguard.classfile.constant.visitor.ConstantVisitor; +import proguard.classfile.util.ClassUtil; +import proguard.util.ListParser; +import proguard.util.NameParser; +import proguard.util.StringMatcher; + +public class FieldReferenceFinder implements ConstantVisitor { + + private final Clazz referencedClass; + private final StringMatcher regularExpressionMatcher; + private final ConstantVisitor constantVisitor; + private boolean fieldReferenceFound = false; + + public FieldReferenceFinder(Clazz referencedClass, + String fieldNameRegularExpression, + ConstantVisitor constantVisitor) + { + this.referencedClass = referencedClass; + this.regularExpressionMatcher = new ListParser(new NameParser(null)).parse(fieldNameRegularExpression); + this.constantVisitor = constantVisitor; + } + + @Override + public void visitAnyConstant(Clazz clazz, Constant constant) {} + + @Override + public void visitFieldrefConstant(Clazz clazz, FieldrefConstant fieldrefConstant) + { + if (fieldrefConstant.referencedClass == referencedClass + && this.regularExpressionMatcher.matches(fieldrefConstant.getName(clazz))) + { + this.fieldReferenceFound = true; + fieldrefConstant.accept(clazz, this.constantVisitor); + } + } + + public boolean isFieldReferenceFound() + { + return this.fieldReferenceFound; + } +} diff --git a/base/src/main/java/proguard/optimize/kotlin/visitor/FieldRenamer.java b/base/src/main/java/proguard/optimize/kotlin/visitor/FieldRenamer.java new file mode 100644 index 000000000..ac4986acd --- /dev/null +++ b/base/src/main/java/proguard/optimize/kotlin/visitor/FieldRenamer.java @@ -0,0 +1,101 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + * + * Copyright (c) 2002-2022 Guardsquare NV + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package proguard.optimize.kotlin.visitor; + +import proguard.classfile.Clazz; +import proguard.classfile.Field; +import proguard.classfile.ProgramClass; +import proguard.classfile.ProgramField; +import proguard.classfile.constant.Constant; +import proguard.classfile.constant.Utf8Constant; +import proguard.classfile.constant.visitor.ConstantVisitor; +import proguard.classfile.visitor.MemberVisitor; + +import java.util.HashMap; +import java.util.Map; + +public class FieldRenamer implements MemberVisitor, ConstantVisitor { + + private final String newFieldNamePrefix; + private int newFieldNameIndex = 0; + private final boolean useDescriptorBasedNames; + private final Map descriptorIndex = new HashMap<>(); + private Field lastVisitedField; + private Clazz lastVisitedClass; + + public FieldRenamer(String newFieldNamePrefix) + { + this.newFieldNamePrefix = newFieldNamePrefix; + this.useDescriptorBasedNames = false; + } + + public FieldRenamer(boolean useDescriptorBasedNames) + { + this.newFieldNamePrefix = ""; + this.useDescriptorBasedNames = useDescriptorBasedNames; + } + + public void resetIndex() + { + this.newFieldNameIndex = 0; + } + + @Override + public void visitProgramField(ProgramClass programClass, ProgramField programField) + { + this.lastVisitedClass = programClass; + this.lastVisitedField = programField; + programClass.constantPoolEntryAccept(programField.u2nameIndex, this); + } + + @Override + public void visitAnyConstant(Clazz clazz, Constant constant) {} + + @Override + public void visitUtf8Constant(Clazz clazz, Utf8Constant utf8Constant) + { + String newName = getNextFieldName(); + utf8Constant.setString(newName); + this.newFieldNameIndex++; + String descriptor = this.lastVisitedField.getDescriptor(this.lastVisitedClass); + this.descriptorIndex.put(descriptor, this.descriptorIndex.getOrDefault(descriptor, 0) + 1); + + } + + public String getNextFieldName() + { + String newName; + if (useDescriptorBasedNames) + { + // This is non-logical behaviour: the method name suggests a globally correct next name would be + // returned, but here it depends on the previously visited field, while in practice + // we don't know whether the next field will have the same descriptor + String descriptor = this.lastVisitedField.getDescriptor(this.lastVisitedClass); + newName = descriptor.replace(";", "").replace("/", "").replace("[", "") + this.descriptorIndex.getOrDefault(descriptor, 0); + + } + else + { + newName = this.newFieldNamePrefix + (this.newFieldNameIndex + 1); + } + return newName; + } +} diff --git a/base/src/main/java/proguard/optimize/kotlin/visitor/MethodCopier.java b/base/src/main/java/proguard/optimize/kotlin/visitor/MethodCopier.java new file mode 100644 index 000000000..c17a17e37 --- /dev/null +++ b/base/src/main/java/proguard/optimize/kotlin/visitor/MethodCopier.java @@ -0,0 +1,179 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + * + * Copyright (c) 2002-2022 Guardsquare NV + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package proguard.optimize.kotlin.visitor; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import proguard.classfile.*; +import proguard.classfile.attribute.Attribute; +import proguard.classfile.attribute.CodeAttribute; +import proguard.classfile.attribute.visitor.AttributeVisitor; +import proguard.classfile.constant.ClassConstant; +import proguard.classfile.constant.Constant; +import proguard.classfile.constant.FieldrefConstant; +import proguard.classfile.constant.visitor.ConstantVisitor; +import proguard.classfile.editor.*; +import proguard.classfile.instruction.ConstantInstruction; +import proguard.classfile.instruction.Instruction; +import proguard.classfile.instruction.visitor.InstructionVisitor; +import proguard.classfile.visitor.MemberVisitor; + +import java.util.Objects; + +public class MethodCopier implements MemberVisitor, AttributeVisitor, InstructionVisitor +{ + private final ProgramClass destinationClass; + private final ClassBuilder classBuilder; + private final String newMethodNamePrefix; + private final String newMethodDescriptor; + private final int accessFlags; + private final ConstantAdder constantAdder; + private final CodeAttributeComposer codeAttributeComposer = new CodeAttributeComposer(); + private final ExceptionInfoAdder exceptionInfoAdder; + private int methodCounter = 0; + private final FieldRenamer fieldRenamer; + private final FieldCopier fieldCopier; + private static final Logger logger = LogManager.getLogger(MethodCopier.class); + + public MethodCopier(ProgramClass destinationClass, String newMethodNamePrefix, int accessFlags) + { + this(destinationClass, newMethodNamePrefix, null, accessFlags); + } + + public MethodCopier(ProgramClass destinationClass, String newMethodNamePrefix, String newMethodDescriptor, int accessFlags) + { + this(destinationClass, newMethodNamePrefix, newMethodDescriptor, accessFlags, null); + } + + public MethodCopier(ProgramClass destinationClass, String newMethodNamePrefix, String newMethodDescriptor, int accessFlags, FieldRenamer fieldRenamer) + { + this.destinationClass = destinationClass; + this.classBuilder = new ClassBuilder(destinationClass); + this.newMethodNamePrefix = newMethodNamePrefix; + this.newMethodDescriptor = newMethodDescriptor; + this.accessFlags = accessFlags; + this.fieldRenamer = fieldRenamer; + this.fieldCopier = new FieldCopier(this.classBuilder, this.fieldRenamer); + this.constantAdder = new ConstantAdder(destinationClass); + this.exceptionInfoAdder = new ExceptionInfoAdder(this.destinationClass, this.codeAttributeComposer); + } + + private int getNewMethodIndex() + { + int methodIndex = this.methodCounter; + this.methodCounter++; + return methodIndex; + } + + @Override + public void visitAnyMember(Clazz clazz, Member member) { + } + + @Override + public void visitProgramMethod(ProgramClass programClass, ProgramMethod programMethod) { + codeAttributeComposer.reset(); + if (this.fieldRenamer != null) + { + this.fieldRenamer.resetIndex(); + } + programMethod.attributesAccept(programClass, this); + int methodIndex = getNewMethodIndex(); + String newMethodName = newMethodNamePrefix; + if (methodIndex > 1) + { + logger.warn(methodIndex + " methods were visited by MethodCopier(" + destinationClass + ", " + newMethodNamePrefix +")."); + newMethodName += "$" + methodIndex; + } + String methodDescriptor = programMethod.getDescriptor(programClass); + if (this.newMethodDescriptor != null) + { + methodDescriptor = this.newMethodDescriptor; + } + ProgramMethod newMethod = classBuilder.addAndReturnMethod(accessFlags, newMethodName, methodDescriptor); + codeAttributeComposer.addCodeAttribute(this.destinationClass, newMethod); + } + + @Override + public void visitAnyAttribute(Clazz clazz, Attribute attribute) { + } + + @Override + public void visitCodeAttribute(Clazz clazz, Method method, CodeAttribute codeAttribute) { + codeAttributeComposer.beginCodeFragment(codeAttribute.u4codeLength); + // copy code and exceptions + codeAttribute.instructionsAccept(clazz, method, this); + codeAttribute.exceptionsAccept(clazz, method, this.exceptionInfoAdder); + codeAttribute.attributesAccept(clazz, method, this); + codeAttributeComposer.endCodeFragment(); + } + + @Override + public void visitAnyInstruction(Clazz clazz, Method method, CodeAttribute codeAttribute, int offset, Instruction instruction) { + // copy instruction + codeAttributeComposer.appendInstruction(offset, instruction); + } + + @Override + public void visitConstantInstruction(Clazz clazz, Method method, CodeAttribute codeAttribute, int offset, ConstantInstruction constantInstruction) { + // TODO: Replace references to the lambda class itself by references to the new lambda group. + // (WIP) + this.fieldCopier.reset(); + clazz.constantPoolEntryAccept(constantInstruction.constantIndex, new ConstantVisitor() { + + @Override + public void visitAnyConstant(Clazz clazz, Constant constant) {} + + @Override + public void visitFieldrefConstant(Clazz clazz, FieldrefConstant fieldrefConstant) + { + // TODO: replace lambda reference by lambda group reference + // Note: is it sufficient to only replace the class constant? + // or should the name of the class also be updated + if (Objects.equals(fieldrefConstant.referencedClass, clazz)) + { + // copy the field to the lambda group + fieldrefConstant.referencedFieldAccept(fieldCopier); + } + } + + @Override + public void visitClassConstant(Clazz clazz, ClassConstant classConstant) + { + if (Objects.equals(classConstant.referencedClass, clazz)) + { + logger.info("Class " + clazz + " references itself in a constant instruction: " + constantInstruction); + } + } + }); + if (this.fieldCopier.hasCopiedField()) + { + // add the necessary constants to the lambda group + constantInstruction.constantIndex = classBuilder.getConstantPoolEditor().addFieldrefConstant(destinationClass, fieldCopier.getLastCopiedField()); + } + else + { + // ensure the referenced constant is in the constant pool at the correct index + constantInstruction.constantIndex = this.constantAdder.addConstant(clazz, constantInstruction.constantIndex); + } + // copy instruction + codeAttributeComposer.appendInstruction(offset, constantInstruction); + } +} diff --git a/base/src/main/java/proguard/optimize/kotlin/visitor/MethodReferenceFinder.java b/base/src/main/java/proguard/optimize/kotlin/visitor/MethodReferenceFinder.java new file mode 100644 index 000000000..6a50de9e9 --- /dev/null +++ b/base/src/main/java/proguard/optimize/kotlin/visitor/MethodReferenceFinder.java @@ -0,0 +1,54 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + * + * Copyright (c) 2002-2022 Guardsquare NV + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package proguard.optimize.kotlin.visitor; + +import proguard.classfile.Clazz; +import proguard.classfile.Method; +import proguard.classfile.constant.AnyMethodrefConstant; +import proguard.classfile.constant.Constant; +import proguard.classfile.constant.visitor.ConstantVisitor; + +public class MethodReferenceFinder implements ConstantVisitor +{ + private final Method referencedMethod; + private boolean methodIsReferenced = false; + + public MethodReferenceFinder(Method referencedMethod) + { + this.referencedMethod = referencedMethod; + } + + public void visitAnyConstant(Clazz clazz, Constant constant) {} + + public void visitAnyMethodrefConstant(Clazz clazz, AnyMethodrefConstant anyMethodrefConstant) + { + if (anyMethodrefConstant.referencedMethod != null + && anyMethodrefConstant.referencedMethod.equals(referencedMethod)) + { + this.methodIsReferenced = true; + } + } + + public boolean methodReferenceFound() + { + return this.methodIsReferenced; + } +} diff --git a/base/src/main/java/proguard/optimize/kotlin/visitor/PackageGrouper.java b/base/src/main/java/proguard/optimize/kotlin/visitor/PackageGrouper.java new file mode 100644 index 000000000..171672f3b --- /dev/null +++ b/base/src/main/java/proguard/optimize/kotlin/visitor/PackageGrouper.java @@ -0,0 +1,89 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + * + * Copyright (c) 2002-2022 Guardsquare NV + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package proguard.optimize.kotlin.visitor; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import proguard.classfile.ClassPool; +import proguard.classfile.Clazz; +import proguard.classfile.util.ClassUtil; +import proguard.classfile.visitor.ClassPoolVisitor; +import proguard.classfile.visitor.ClassVisitor; + +import java.util.HashMap; +import java.util.Map; + +/** + * This {@link ClassVisitor} groups the visited classes per package, + * after which the classes can be visited per package. + * @author Joren Van Hecke + * @see proguard.optimize.kotlin.KotlinLambdaMerger + */ +public class PackageGrouper implements ClassVisitor { + + private final Map packageClassPools = new HashMap<>(); + private static final Logger logger = LogManager.getLogger(PackageGrouper.class); + + @Override + public void visitAnyClass(Clazz clazz) + { + String classPackageName = ClassUtil.internalPackageName(clazz.getName()); + if (!packageClassPools.containsKey(classPackageName)) + { + logger.info("New package found: {}", + ClassUtil.externalPackageName(ClassUtil.externalClassName(clazz.getName()))); + packageClassPools.put(classPackageName, new ClassPool()); + } + packageClassPools.get(classPackageName).addClass(clazz); + } + + public int size() + { + return packageClassPools.size(); + } + + public boolean containsPackage(String packageName) + { + return packageClassPools.containsKey(packageName); + } + + public Iterable packageNames() + { + return packageClassPools.keySet(); + } + + public void packagesAccept(ClassPoolVisitor classPoolVisitor) + { + for (ClassPool packageClassPool : packageClassPools.values()) + { + classPoolVisitor.visitClassPool(packageClassPool); + } + } + + public void packageAccept(String packageName, ClassPoolVisitor classPoolVisitor) + { + ClassPool packageClassPool = this.packageClassPools.get(packageName); + if (packageClassPool != null) + { + classPoolVisitor.visitClassPool(packageClassPool); + } + } +} diff --git a/base/src/main/java/proguard/optimize/peephole/SameClassMethodInliner.java b/base/src/main/java/proguard/optimize/peephole/SameClassMethodInliner.java new file mode 100644 index 000000000..c8842d427 --- /dev/null +++ b/base/src/main/java/proguard/optimize/peephole/SameClassMethodInliner.java @@ -0,0 +1,97 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + * + * Copyright (c) 2002-2022 Guardsquare NV + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package proguard.optimize.peephole; + +import proguard.classfile.Clazz; +import proguard.classfile.Method; +import proguard.classfile.ProgramClass; +import proguard.classfile.ProgramMethod; +import proguard.classfile.attribute.CodeAttribute; +import proguard.classfile.instruction.visitor.InstructionVisitor; +import proguard.optimize.info.MethodInvocationMarker; + +/** + * This AttributeVisitor inlines methods that are only invoked once, in the code attributes that it visits. + */ +public class SameClassMethodInliner extends MethodInliner { + + private Clazz startClass; + + public SameClassMethodInliner(boolean microEdition, + boolean android, + boolean allowAccessModification) + { + super(microEdition, android, allowAccessModification); + } + + public SameClassMethodInliner(boolean microEdition, + boolean android, + boolean allowAccessModification, + InstructionVisitor extraInlinedInvocationVisitor) + { + super(microEdition, android, allowAccessModification, extraInlinedInvocationVisitor); + } + + public SameClassMethodInliner(boolean microEdition, + boolean android, + int maxResultingCodeLength, + boolean allowAccessModification, + boolean usesOptimizationInfo, + InstructionVisitor extraInlinedInvocationVisitor) + { + super(microEdition, + android, + maxResultingCodeLength, + allowAccessModification, + usesOptimizationInfo, + extraInlinedInvocationVisitor); + } + + // Implementations for MethodInliner. + + @Override + public void visitCodeAttribute(Clazz clazz, Method method, CodeAttribute codeAttribute) + { + boolean startClassWasNull = this.startClass == null; + if (clazz == this.startClass || this.startClass == null) { + this.startClass = clazz; + super.visitCodeAttribute(clazz, method, codeAttribute); + } + if (startClassWasNull) + { + this.startClass = null; + } + } + + @Override + public void visitProgramMethod(ProgramClass programClass, ProgramMethod programMethod) + { + if (programClass == this.startClass) { + super.visitProgramMethod(programClass, programMethod); + } + } + + @Override + protected boolean shouldInline(Clazz clazz, Method method, CodeAttribute codeAttribute) + { + return clazz == this.startClass; + } +} diff --git a/base/src/test/kotlin/proguard/classfile/visitor/MethodCopierTest.kt b/base/src/test/kotlin/proguard/classfile/visitor/MethodCopierTest.kt new file mode 100644 index 000000000..82160ec9f --- /dev/null +++ b/base/src/test/kotlin/proguard/classfile/visitor/MethodCopierTest.kt @@ -0,0 +1,119 @@ +package proguard.classfile.visitor + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import proguard.classfile.AccessConstants +import proguard.classfile.ClassConstants +import proguard.classfile.VersionConstants +import proguard.classfile.attribute.visitor.AllAttributeVisitor +import proguard.classfile.editor.ClassBuilder +import proguard.classfile.editor.InstructionSequenceBuilder +import proguard.classfile.instruction.visitor.AllInstructionVisitor +import proguard.classfile.util.InstructionSequenceMatcher +import proguard.optimize.kotlin.visitor.MethodCopier +import testutils.MatchDetector + +class MethodCopierTest : FreeSpec({ + "Given a method and a target class" - { + val classBuilder = ClassBuilder( + VersionConstants.CLASS_VERSION_1_8, + AccessConstants.FINAL or AccessConstants.SUPER, + "Test", + ClassConstants.NAME_JAVA_LANG_OBJECT + ) + val testClass = classBuilder.programClass + val method = classBuilder.addAndReturnMethod( + AccessConstants.PUBLIC, + ClassConstants.METHOD_NAME_INIT, + ClassConstants.METHOD_TYPE_INIT, + 50 + ) { + it + .aload_0() + .iconst(0) + .invokespecial( + ClassConstants.NAME_JAVA_LANG_OBJECT, + ClassConstants.METHOD_NAME_INIT, + ClassConstants.METHOD_TYPE_INIT + ) + .return_() + } + val methodDescriptor = method.getDescriptor(testClass) + val methodName = method.getName(testClass) + val methodAccessFlags = AccessConstants.PUBLIC + val targetClass = ClassBuilder( + VersionConstants.CLASS_VERSION_1_8, + AccessConstants.PUBLIC, + "TargetClass", + ClassConstants.NAME_JAVA_LANG_OBJECT + ).programClass + "When the method is copied to the target class" - { + val methodCopier = MethodCopier( + targetClass, + ClassConstants.METHOD_NAME_INIT, + methodAccessFlags + ) + method.accept(testClass, methodCopier) + "Then the target class contains a method with the correct name and descriptor" { + targetClass.findMethod(methodName, methodDescriptor) shouldNotBe null + } + + "Then the copied method has the correct access modifiers" { + val copiedMethod = targetClass.findMethod(methodName, methodDescriptor) + copiedMethod.accessFlags shouldBe methodAccessFlags + } + + "Then the instructions of the copied method match those of the original method" { + val builder = InstructionSequenceBuilder() + builder + .aload_0() + .iconst(0) + .invokespecial( + ClassConstants.NAME_JAVA_LANG_OBJECT, + ClassConstants.METHOD_NAME_INIT, + ClassConstants.METHOD_TYPE_INIT + ) + .return_() + val matchDetector = MatchDetector(InstructionSequenceMatcher(builder.constants(), builder.instructions())) + val copiedMethod = targetClass.findMethod(methodName, methodDescriptor) + copiedMethod.accept( + targetClass, + AllAttributeVisitor( + AllInstructionVisitor( + matchDetector + ) + ) + ) + matchDetector.matchIsFound shouldBe true + } + } + + "When the method is copied to the target class with a new descriptor" - { + val newDescriptor = "(I)V" + val methodCopier = MethodCopier( + targetClass, + methodName, + newDescriptor, + methodAccessFlags + ) + method.accept(testClass, methodCopier) + "Then the target class contains a method with the correct name and descriptor" { + targetClass.findMethod(methodName, newDescriptor) shouldNotBe null + } + } + + "When the method is copied to the target class with a new name prefix" - { + val newNamePrefix = "copiedMethod" + val methodCopier = MethodCopier( + targetClass, + newNamePrefix, + methodAccessFlags + ) + method.accept(testClass, methodCopier) + "Then the target class contains a method with the correct name and descriptor" { + targetClass.findMethod(newNamePrefix, methodDescriptor) shouldNotBe null + } + } + } +}) diff --git a/base/src/test/kotlin/proguard/classfile/visitor/PackageGrouperTest.kt b/base/src/test/kotlin/proguard/classfile/visitor/PackageGrouperTest.kt new file mode 100644 index 000000000..9cf25b967 --- /dev/null +++ b/base/src/test/kotlin/proguard/classfile/visitor/PackageGrouperTest.kt @@ -0,0 +1,99 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + * + * Copyright (c) 2002-2021 Guardsquare NV + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package proguard.classfile.visitor + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.collections.shouldBeIn +import io.kotest.matchers.shouldBe +import proguard.classfile.ClassPool +import proguard.classfile.util.ClassUtil +import proguard.optimize.kotlin.visitor.PackageGrouper +import testutils.ClassPoolBuilder +import testutils.KotlinSource + +class PackageGrouperTest : FreeSpec({ + + val (programClassPool, _) = ClassPoolBuilder.fromSource( + KotlinSource( + "Test.kt", + """ + package app.package1 + fun main(index: Int) { + val lambda1 = { + println("Lambda1") + } + lambda1() + } + """.trimIndent() + ), + KotlinSource( + "Test2.kt", + """ + package app.package2 + fun main() { + val lambda2 = { println("Lambda2") } + lambda2() + } + """.trimIndent() + ) + ) + + "Given a PackageGrouper" - { + val grouper = PackageGrouper() + "When the grouper is applied to classes of different packages" - { + programClassPool.classesAccept(grouper) + "Then the amount packages of packages found by the grouper equals the amount of packages in the class pool" { + grouper.size() shouldBe 2 + } + "Then the grouper has found the packages that are in the class pool" { + grouper.containsPackage("app/package1") shouldBe true + grouper.containsPackage("app/package2") shouldBe true + } + "Then the grouper does not contain other packages, except for those from the class pool" { + grouper.packageNames().forEach { + it shouldBeIn arrayListOf("app/package2", "app/package1") + } + } + "Then the union of classes in the packages of the package grouper contain equals the class pool" { + val grouperCompleteClassPool = ClassPool() + grouper.packagesAccept { packageClassPool -> + packageClassPool.classes().forEach { clazz -> + programClassPool.contains(clazz) + grouperCompleteClassPool.addClass(clazz) + } + } + programClassPool.classes().forEach { clazz -> + grouperCompleteClassPool.contains(clazz) + } + } + "Then the classes of a package class pool belong to the respective package" { + grouper.packageNames().forEach { packageName -> + grouper.packageAccept(packageName) { packageClassPool -> + packageClassPool.classesAccept { clazz -> + ClassUtil.internalPackageName(clazz.name) shouldBe packageName + } + } + } + } + } + } +}) diff --git a/base/src/test/kotlin/proguard/optimize/kotlin/KotlinLambdaEnclosingMethodUpdaterTest.kt b/base/src/test/kotlin/proguard/optimize/kotlin/KotlinLambdaEnclosingMethodUpdaterTest.kt new file mode 100644 index 000000000..9bca3ea84 --- /dev/null +++ b/base/src/test/kotlin/proguard/optimize/kotlin/KotlinLambdaEnclosingMethodUpdaterTest.kt @@ -0,0 +1,212 @@ +package proguard.optimize.kotlin + +import io.kotest.core.spec.IsolationMode +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.collections.shouldNotContain +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import proguard.classfile.AccessConstants +import proguard.classfile.ClassConstants +import proguard.classfile.Clazz +import proguard.classfile.Method +import proguard.classfile.ProgramClass +import proguard.classfile.ProgramMethod +import proguard.classfile.VersionConstants +import proguard.classfile.attribute.Attribute +import proguard.classfile.attribute.CodeAttribute +import proguard.classfile.attribute.visitor.AllAttributeVisitor +import proguard.classfile.attribute.visitor.AttributeVisitor +import proguard.classfile.attribute.visitor.MultiAttributeVisitor +import proguard.classfile.constant.AnyMethodrefConstant +import proguard.classfile.constant.ClassConstant +import proguard.classfile.constant.Constant +import proguard.classfile.constant.visitor.ConstantVisitor +import proguard.classfile.editor.ClassBuilder +import proguard.classfile.editor.InstructionSequenceBuilder +import proguard.classfile.instruction.ConstantInstruction +import proguard.classfile.instruction.Instruction +import proguard.classfile.instruction.visitor.AllInstructionVisitor +import proguard.classfile.instruction.visitor.InstructionVisitor +import proguard.classfile.util.InstructionSequenceMatcher +import proguard.classfile.visitor.AllMemberVisitor +import proguard.io.ExtraDataEntryNameMap +import testutils.ClassPoolBuilder +import testutils.KotlinSource +import testutils.MatchDetector + +class KotlinLambdaEnclosingMethodUpdaterTest : FreeSpec({ + val (programClassPool, libraryClassPool) = ClassPoolBuilder.fromSource( + KotlinSource( + "Test.kt", + """ + fun main() { + val lambda1 = { println("Lambda1") } + val lambda2 = { println("Lambda2") } + val lambda3 = { println("Lambda3") } + lambda1() + lambda2() + lambda3() + } + """.trimIndent() + ) + ) + "Given a lambda class, a lambda group and an enclosing method updater" - { + val lambdaClass = programClassPool.getClass("TestKt\$main\$lambda1\$1") as ProgramClass + val enclosingClass = programClassPool.getClass("TestKt") as ProgramClass + println("Lambda class: $lambdaClass") + val lambdaGroupName = "LambdaGroup" + val lambdaGroup = ClassBuilder( + VersionConstants.CLASS_VERSION_1_8, + AccessConstants.FINAL or AccessConstants.SUPER, + lambdaGroupName, + KotlinLambdaMerger.NAME_KOTLIN_LAMBDA + ).programClass + val initMethod = ClassBuilder(lambdaGroup).addAndReturnMethod( + AccessConstants.PUBLIC, + ClassConstants.METHOD_NAME_INIT, + "(II)V" + ) + val classId = 0 + val arity = 1 + val constructorDescriptor = "(II)V" + val nameMapper = ExtraDataEntryNameMap() + val enclosingMethodUpdater = KotlinLambdaEnclosingMethodUpdater( + programClassPool, + libraryClassPool, + lambdaClass, + lambdaGroup, + classId, + arity, + constructorDescriptor, + nameMapper + ) + "When the enclosing method of the lambda class is updated" - { + lambdaClass.accept( + AllAttributeVisitor( + enclosingMethodUpdater + ) + ) + val referencedClasses = ArrayList() + val referencedClassNames = ArrayList() + enclosingClass.constantPoolEntriesAccept(object : ConstantVisitor { + override fun visitAnyConstant(clazz: Clazz?, constant: Constant?) {} + override fun visitClassConstant(clazz: Clazz?, classConstant: ClassConstant?) { + println("$clazz contains a ClassConstant referencing ${classConstant!!.referencedClass}") + referencedClasses.add(classConstant.referencedClass) + referencedClassNames.add(classConstant.getName(clazz)) + } + }) + "Then the enclosing class no longer references the lambda class" { + referencedClasses shouldNotContain lambdaClass + referencedClassNames shouldNotContain lambdaClass.name + } + "Then the enclosing class references the lambda group" { + referencedClassNames shouldContain lambdaGroupName + } + "Then the enclosing method initialises the lambda group with the correct arity and class id" { + val instructionSequenceBuilder = InstructionSequenceBuilder(programClassPool, libraryClassPool) + instructionSequenceBuilder + .new_(lambdaGroup) + .dup() + .iconst(classId) + .iconst(arity) + .invokespecial(lambdaGroup, initMethod) + val matchDetector = MatchDetector( + InstructionSequenceMatcher(instructionSequenceBuilder.constants(), instructionSequenceBuilder.instructions()) + ) + enclosingClass.accept( + AllMemberVisitor( + AllAttributeVisitor( + AllInstructionVisitor( + matchDetector + ) + ) + ) + ) + matchDetector.matchIsFound shouldBe true + } + } + "When a method is visited that does not use the lambda class" - { + val nonEnclosingMethod = enclosingClass.findMethod("main", "([Ljava/lang/String;)V") as ProgramMethod + val (originalInstructions, originalAttributes) = nonEnclosingMethod.getInstructionsAndAttributes(enclosingClass) + nonEnclosingMethod.accept(enclosingClass, enclosingMethodUpdater) + "Then the enclosing method updater does not modify this method" { + val (resultingInstructions, resultingAttributes) = nonEnclosingMethod.getInstructionsAndAttributes(enclosingClass) + resultingAttributes.forEach { originalAttributes shouldContain it } + originalAttributes.forEach { resultingAttributes shouldContain it } + resultingInstructions.forEach { originalInstructions shouldContain it } + originalInstructions.forEach { resultingInstructions shouldContain it } + } + "Then none of the instructions of this method references a method of the lambda group" { + nonEnclosingMethod.accept( + enclosingClass, + AllAttributeVisitor( + AllInstructionVisitor( + object : InstructionVisitor { + override fun visitAnyInstruction( + clazz: Clazz?, + method: Method?, + codeAttribute: CodeAttribute?, + offset: Int, + instruction: Instruction? + ) {} + override fun visitConstantInstruction( + clazz: Clazz?, + method: Method?, + codeAttribute: CodeAttribute?, + offset: Int, + constantInstruction: ConstantInstruction? + ) { + enclosingClass.constantPoolEntryAccept( + constantInstruction!!.constantIndex, + object : ConstantVisitor { + override fun visitAnyConstant(clazz: Clazz?, constant: Constant?) {} + override fun visitAnyMethodrefConstant( + clazz: Clazz?, + anyMethodrefConstant: AnyMethodrefConstant? + ) { + anyMethodrefConstant?.referencedClass shouldNotBe lambdaGroup + } + } + ) + } + } + ) + ) + ) + } + } + } +}) { + override fun isolationMode(): IsolationMode = IsolationMode.InstancePerLeaf +} + +fun ProgramMethod.getInstructionsAndAttributes(programClass: ProgramClass): Pair, List> { + val instructions = ArrayList() + val attributes = ArrayList() + this.accept( + programClass, + AllAttributeVisitor( + MultiAttributeVisitor( + object : AttributeVisitor { + override fun visitAnyAttribute(clazz: Clazz?, attribute: Attribute?) { + attributes.add(attribute!!) + } + }, + AllInstructionVisitor(object : InstructionVisitor { + override fun visitAnyInstruction( + clazz: Clazz?, + method: Method?, + codeAttribute: CodeAttribute?, + offset: Int, + instruction: Instruction? + ) { + instructions.add(instruction!!) + } + }) + ) + ) + ) + return Pair(instructions, attributes) +} diff --git a/base/src/test/kotlin/proguard/optimize/kotlin/KotlinLambdaGroupBuilderTest.kt b/base/src/test/kotlin/proguard/optimize/kotlin/KotlinLambdaGroupBuilderTest.kt new file mode 100644 index 000000000..a5c421e6d --- /dev/null +++ b/base/src/test/kotlin/proguard/optimize/kotlin/KotlinLambdaGroupBuilderTest.kt @@ -0,0 +1,98 @@ +package proguard.optimize.kotlin + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.ints.shouldBeGreaterThanOrEqual +import io.kotest.matchers.shouldBe +import proguard.Configuration +import proguard.classfile.AccessConstants +import proguard.classfile.ClassPool +import proguard.classfile.ProgramClass +import proguard.classfile.attribute.visitor.AllAttributeVisitor +import proguard.classfile.editor.InstructionSequenceBuilder +import proguard.classfile.instruction.visitor.AllInstructionVisitor +import proguard.classfile.util.ClassUtil +import proguard.classfile.util.InstructionSequenceMatcher +import proguard.classfile.visitor.AllMemberVisitor +import proguard.classfile.visitor.MemberAccessFilter +import proguard.classfile.visitor.MemberNameFilter +import proguard.io.ExtraDataEntryNameMap +import proguard.optimize.info.ProgramClassOptimizationInfo +import proguard.optimize.info.ProgramClassOptimizationInfoSetter +import testutils.* + +class KotlinLambdaGroupBuilderTest : FreeSpec({ + val (programClassPool, libraryClassPool) = ClassPoolBuilder.fromSource( + KotlinSource( + "Test.kt", + """ + fun main() { + val lambda1 = { println("Lambda1") } + val lambda2 = { println("Lambda2") } + val lambda3 = { println("Lambda3") } + lambda1() + lambda2() + lambda3() + } + """.trimIndent() + ) + ) + + "Given a lambda group builder and a lambda class" - { + val lambdaGroupName = "LambdaGroup" + val configuration = Configuration() + val entryMapper = ExtraDataEntryNameMap() + val mergedLambdaVisitor = null + val notMergedLambdaVisitor = null + val builder = KotlinLambdaGroupBuilder( + lambdaGroupName, + configuration, + programClassPool, + libraryClassPool, + entryMapper, + mergedLambdaVisitor, + notMergedLambdaVisitor + ) + val lambdaClassName = "TestKt\$main\$lambda1\$1" + val arity = 0 + val lambdaClass = programClassPool.getClass(lambdaClassName) as ProgramClass + lambdaClass.accept(ProgramClassOptimizationInfoSetter()) + val optimizationInfo = ProgramClassOptimizationInfo.getProgramClassOptimizationInfo(lambdaClass) + val capturedExecutionOutputBefore = captureExecutionOutput(programClassPool, + lambdaClassName) { + testClassBefore -> + val instance = testClassBefore.getDeclaredConstructor().newInstance() + testClassBefore.declaredMethods.single { it.name == "invoke" && it.isBridge } + .invoke(instance, null) + } + + "When the builder adds a lambda class to the lambda group under construction" - { + builder.visitProgramClass(lambdaClass) + val lambdaGroup = builder.build() + + "Then the optimization info lambda group of the lambda class refers to the lambda group" { + optimizationInfo.lambdaGroup shouldBe lambdaGroup + } + + "Then the optimization info target class of the lambda class refers to the lambda group" { + optimizationInfo.targetClass shouldBe lambdaGroup + } + + "Then the optimization info class id of the lambda class is 0 or greater" { + optimizationInfo.lambdaGroupClassId shouldBeGreaterThanOrEqual 0 + } + + + "Then the lambda group has the same output on invocation as the original lambda class" { + val capturedExecutionOutputAfter = captureExecutionOutput(ClassPool(lambdaGroup), + lambdaGroupName) { + testClassBefore -> + val instance = testClassBefore.getDeclaredConstructor().newInstance( + optimizationInfo.lambdaGroupClassId, arity) + testClassBefore.declaredMethods.single { it.name == "invoke" && it.isBridge } + .invoke(instance) + } + capturedExecutionOutputAfter shouldBe capturedExecutionOutputBefore + } + } + } +}) diff --git a/base/src/test/kotlin/proguard/optimize/kotlin/KotlinLambdaGroupInitUpdaterTest.kt b/base/src/test/kotlin/proguard/optimize/kotlin/KotlinLambdaGroupInitUpdaterTest.kt new file mode 100644 index 000000000..ce5989f5c --- /dev/null +++ b/base/src/test/kotlin/proguard/optimize/kotlin/KotlinLambdaGroupInitUpdaterTest.kt @@ -0,0 +1,104 @@ +package proguard.optimize.kotlin + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import proguard.classfile.AccessConstants +import proguard.classfile.ClassConstants +import proguard.classfile.ClassPool +import proguard.classfile.VersionConstants +import proguard.classfile.attribute.visitor.AllAttributeVisitor +import proguard.classfile.editor.ClassBuilder +import proguard.classfile.editor.InstructionSequenceBuilder +import proguard.classfile.instruction.visitor.AllInstructionVisitor +import proguard.classfile.util.ClassUtil +import proguard.classfile.util.InstructionSequenceMatcher +import testutils.MatchDetector + +class KotlinLambdaGroupInitUpdaterTest : FreeSpec({ + val lambdaGroupName = "LambdaGroup" + val classBuilder = ClassBuilder( + VersionConstants.CLASS_VERSION_1_8, + AccessConstants.FINAL or AccessConstants.SUPER, + lambdaGroupName, + KotlinLambdaMerger.NAME_KOTLIN_LAMBDA + ) + val initMethod = classBuilder.addAndReturnMethod( + AccessConstants.PUBLIC, + ClassConstants.METHOD_NAME_INIT, + ClassConstants.METHOD_TYPE_INIT, + 50 + ) { + it.aload_0() + .iconst(0) + .invokespecial(KotlinLambdaMerger.NAME_KOTLIN_LAMBDA, ClassConstants.METHOD_NAME_INIT, "(I)V") + .return_() + } + val classIdField = classBuilder.addAndReturnField(AccessConstants.PRIVATE, KotlinLambdaGroupBuilder.FIELD_NAME_ID, KotlinLambdaGroupBuilder.FIELD_TYPE_ID) + val lambdaGroup = classBuilder.programClass + + "Given a Kotlin Lambda Group Init Updater" - { + val initUpdater = KotlinLambdaGroupInitUpdater(ClassPool(lambdaGroup), ClassPool()) + "When the updater is applied to the method of a lambda group" - { + val initMethodDescriptor = initMethod.getDescriptor(lambdaGroup) + val argumentCountBefore = ClassUtil.internalMethodParameterCount(initMethodDescriptor, false) + val argumentSizeBefore = ClassUtil.internalMethodParameterSize(initMethodDescriptor, false) + initMethod.accept(lambdaGroup, initUpdater) + val newInitMethodDescriptor = initMethod.getDescriptor(lambdaGroup) + val argumentCountAfter = ClassUtil.internalMethodParameterCount(newInitMethodDescriptor, false) + val argumentSizeAfter = ClassUtil.internalMethodParameterSize(newInitMethodDescriptor, false) + "Then the init method contains two additional arguments" { + argumentCountAfter shouldBe argumentCountBefore + 2 + } + "Then the two additional arguments are each of size 1 byte" { + argumentSizeAfter shouldBe argumentSizeBefore + 2 + } + "Then the two additional arguments are of type int" { + ClassUtil.internalMethodParameterType(newInitMethodDescriptor, argumentCountAfter - 3) shouldBe "I" + ClassUtil.internalMethodParameterType(newInitMethodDescriptor, argumentCountAfter - 2) shouldBe "I" + } + + "Then the code of the method calls the super constructor with the arity argument" { + val classIdBuilder = InstructionSequenceBuilder(ClassPool(lambdaGroup), ClassPool()) + classIdBuilder + .aload_0() + .iload(InstructionSequenceMatcher.A) + .putfield(lambdaGroup, classIdField) + val classIdInstructionMatcher = InstructionSequenceMatcher(classIdBuilder.constants(), classIdBuilder.instructions()) + val classIdMatchDetector = MatchDetector(classIdInstructionMatcher, InstructionSequenceMatcher.A) + initMethod.accept( + lambdaGroup, + AllAttributeVisitor( + AllInstructionVisitor( + classIdMatchDetector + ) + ) + ) + classIdMatchDetector.matchIsFound shouldBe true + classIdMatchDetector.matchedArguments[0] shouldBe argumentSizeAfter - 2 + } + "Then the code of the method stores the classId argument in the classId field" { + val callBuilder = InstructionSequenceBuilder(ClassPool(lambdaGroup), ClassPool()) + callBuilder + .aload_0() + .iload(InstructionSequenceMatcher.A) + .invokespecial( + KotlinLambdaMerger.NAME_KOTLIN_LAMBDA, + ClassConstants.METHOD_NAME_INIT, + "(I)V" + ) + val callInstructionMatcher = InstructionSequenceMatcher(callBuilder.constants(), callBuilder.instructions()) + val callMatchDetector = MatchDetector(callInstructionMatcher, InstructionSequenceMatcher.A) + initMethod.accept( + lambdaGroup, + AllAttributeVisitor( + AllInstructionVisitor( + callMatchDetector + ) + ) + ) + callMatchDetector.matchIsFound shouldBe true + callMatchDetector.matchedArguments[0] shouldBe argumentSizeAfter - 1 + } + } + } +}) diff --git a/base/src/test/kotlin/proguard/optimize/kotlin/KotlinLambdaGroupInvokeMethodBuilderTest.kt b/base/src/test/kotlin/proguard/optimize/kotlin/KotlinLambdaGroupInvokeMethodBuilderTest.kt new file mode 100644 index 000000000..553303c61 --- /dev/null +++ b/base/src/test/kotlin/proguard/optimize/kotlin/KotlinLambdaGroupInvokeMethodBuilderTest.kt @@ -0,0 +1,76 @@ +package proguard.optimize.kotlin + +import io.kotest.core.spec.IsolationMode +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import proguard.classfile.AccessConstants +import proguard.classfile.ClassConstants +import proguard.classfile.ClassPool +import proguard.classfile.VersionConstants +import proguard.classfile.attribute.visitor.AllAttributeVisitor +import proguard.classfile.editor.ClassBuilder +import proguard.classfile.editor.InstructionSequenceBuilder +import proguard.classfile.instruction.visitor.AllInstructionVisitor +import proguard.classfile.util.InstructionSequenceMatcher +import testutils.MatchDetector + +class KotlinLambdaGroupInvokeMethodBuilderTest : FreeSpec({ + + val arity = 0 + "Given an invoke method builder, a target class and a method" - { + val classBuilder = ClassBuilder( + VersionConstants.CLASS_VERSION_1_8, + AccessConstants.PUBLIC, + "TargetClass", + ClassConstants.NAME_JAVA_LANG_OBJECT + ) + val method = classBuilder.addAndReturnMethod( + AccessConstants.PUBLIC, + ClassConstants.METHOD_NAME_INIT, + ClassConstants.METHOD_TYPE_INIT + ) + val targetClass = classBuilder.programClass + val invokeMethodBuilder = KotlinLambdaGroupInvokeMethodBuilder(arity, classBuilder, ClassPool(), ClassPool()) + "When a call to the method is added to the invoke method" - { + invokeMethodBuilder.addCallTo(method) + "Then the invoke method contains a call to the method" { + val invokeMethod = invokeMethodBuilder.build() + val matchingSequenceBuilder = InstructionSequenceBuilder().invokevirtual(targetClass, method) + val matchDetector = MatchDetector( + InstructionSequenceMatcher( + matchingSequenceBuilder.constants(), + matchingSequenceBuilder.instructions() + ) + ) + invokeMethod.accept( + targetClass, + AllAttributeVisitor( + AllInstructionVisitor( + matchDetector + ) + ) + ) + matchDetector.matchIsFound shouldBe true + } + } + + "When the invoke method is built" - { + val invokeMethod = invokeMethodBuilder.build() + targetClass.methods.forEach { + println("$it: ${it.getName(targetClass)}${it.getDescriptor(targetClass)}") + } + "Then the returned invoke method is not null" { + invokeMethod shouldNotBe null + } + "Then the target class contains an invoke method with the correct descriptor" { + targetClass.findMethod("invoke", "()Ljava/lang/Object;") shouldNotBe null + } + "Then the target class contains the returned invoke method" { + targetClass.findMethod("invoke", "()Ljava/lang/Object;") shouldBe invokeMethod + } + } + } +}) { + override fun isolationMode(): IsolationMode = IsolationMode.InstancePerLeaf +} diff --git a/base/src/test/kotlin/proguard/optimize/kotlin/KotlinLambdaMergerTest.kt b/base/src/test/kotlin/proguard/optimize/kotlin/KotlinLambdaMergerTest.kt new file mode 100644 index 000000000..ead49d90c --- /dev/null +++ b/base/src/test/kotlin/proguard/optimize/kotlin/KotlinLambdaMergerTest.kt @@ -0,0 +1,179 @@ +/* + * ProGuard -- shrinking, optimization, obfuscation, and preverification + * of Java bytecode. + * + * Copyright (c) 2002-2021 Guardsquare NV + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package proguard.optimize.kotlin + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.ints.shouldBeGreaterThan +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.mockk.* +import proguard.AppView +import proguard.Configuration +import proguard.classfile.* +import proguard.classfile.attribute.CodeAttribute +import proguard.classfile.attribute.visitor.AllAttributeVisitor +import proguard.classfile.constant.ClassConstant +import proguard.classfile.constant.Constant +import proguard.classfile.constant.visitor.ConstantVisitor +import proguard.classfile.editor.InstructionSequenceBuilder +import proguard.classfile.instruction.Instruction +import proguard.classfile.instruction.visitor.AllInstructionVisitor +import proguard.classfile.instruction.visitor.InstructionVisitor +import proguard.classfile.instruction.visitor.MultiInstructionVisitor +import proguard.classfile.util.InstructionSequenceMatcher +import proguard.classfile.visitor.AllMemberVisitor +import proguard.classfile.visitor.MemberAccessFilter +import proguard.classfile.visitor.MemberNameFilter +import proguard.io.ExtraDataEntryNameMap +import testutils.* +import java.io.* + +class KotlinLambdaMergerTest : FreeSpec({ + + val (programClassPool, libraryClassPool) = ClassPoolBuilder.fromSource( + KotlinSource( + "Test.kt", + """ + package app.package1 + fun main(index: Int) { + val lambda1 = { + println("Lambda1") + val lambda1a = { + println("Lambda1a") + val lambda1a1 = { + println("Lambda1a1") + } + lambda1a1() + } + lambda1a() + } + val lambda2 = { println("Lambda2") } + val lambda3 = { println("Lambda3") } + when (index) { + 0 -> lambda1() + 1 -> lambda2() + else -> lambda3() + } + } + """.trimIndent() + ), + KotlinSource( + "Test2.kt", + """ + package app.package2 + fun main() { + val lambda1 = { println("Lambda1") } + val lambda2 = { println("Lambda2") } + val lambda3 = { println("Lambda3") } + lambda1() + lambda2() + lambda3() + } + """.trimIndent() + ) + ) + + "Given a Kotlin Lambda Merger and entry name mapper" - { + val merger = KotlinLambdaMerger(Configuration()) + val nameMapper = ExtraDataEntryNameMap() + + class InstructionPrinter : InstructionVisitor { + override fun visitAnyInstruction( + clazz: Clazz, + method: Method, + codeAttribute: CodeAttribute, + offset: Int, + instruction: Instruction + ) { + println(instruction.toString(clazz, offset)) + } + } + + "When the merger is applied to the class pools" - { + val oldProgramClassPool = ClassPool(programClassPool) + val capturedExecutionOutputBefore = captureExecutionOutput(oldProgramClassPool, + "app.package2.Test2Kt") { + testClassBefore -> testClassBefore.declaredMethods.single { it.name == "main" && it.isSynthetic } + .invoke(null, arrayOf()) + } + + val appView = AppView(programClassPool, libraryClassPool, null, nameMapper) + merger.execute(appView) + val newProgramClassPool = appView.programClassPool + val newFullClassPool = appView.programClassPool.classes() union libraryClassPool.classes() + "Then the resulting program class pool contains less classes" { + oldProgramClassPool.size() shouldBeGreaterThan newProgramClassPool.size() + } + + "Then the program classes should only refer to classes that are in the class pool" { + val visitor = mockk() + val slot = slot() + every { + visitor.visitAnyConstant(any(), any()) + visitor.visitDoubleConstant(any(), any()) + visitor.visitDynamicConstant(any(), any()) + visitor.visitAnyRefConstant(any(), any()) + visitor.visitAnyMethodrefConstant(any(), any()) + visitor.visitFieldrefConstant(any(), any()) + visitor.visitFloatConstant(any(), any()) + visitor.visitIntegerConstant(any(), any()) + visitor.visitInterfaceMethodrefConstant(any(), any()) + visitor.visitInvokeDynamicConstant(any(), any()) + visitor.visitLongConstant(any(), any()) + visitor.visitMethodHandleConstant(any(), any()) + visitor.visitMethodrefConstant(any(), any()) + visitor.visitMethodTypeConstant(any(), any()) + visitor.visitModuleConstant(any(), any()) + visitor.visitNameAndTypeConstant(any(), any()) + visitor.visitPackageConstant(any(), any()) + visitor.visitPrimitiveArrayConstant(any(), any()) + visitor.visitStringConstant(any(), any()) + visitor.visitUtf8Constant(any(), any()) + } just Runs + every { + visitor.visitClassConstant(any(), capture(slot)) + } answers { + println("Visit class constant referring to ${slot.captured.referencedClass}") + newFullClassPool shouldContain slot.captured.referencedClass + } + newProgramClassPool.classes().forEach { + it.constantPoolEntriesAccept(visitor) + } + } + + "Then for each package with lambda's a lambda group has been created." { + newProgramClassPool.getClass("app/package1/LambdaGroup") shouldNotBe null + newProgramClassPool.getClass("app/package2/LambdaGroup") shouldNotBe null + } + + "Then the program output has not changed after optimisation" { + val capturedExecutionOutputAfter = captureExecutionOutput(newProgramClassPool, + "app.package2.Test2Kt") { + testClassBefore -> testClassBefore.declaredMethods.single { it.name == "main" && it.isSynthetic } + .invoke(null, arrayOf()) + } + capturedExecutionOutputAfter shouldBe capturedExecutionOutputBefore + } + } + } +}) diff --git a/base/src/test/kotlin/proguard/optimize/kotlin/LambdaGroupMappingPrinterTest.kt b/base/src/test/kotlin/proguard/optimize/kotlin/LambdaGroupMappingPrinterTest.kt new file mode 100644 index 000000000..bca709988 --- /dev/null +++ b/base/src/test/kotlin/proguard/optimize/kotlin/LambdaGroupMappingPrinterTest.kt @@ -0,0 +1,46 @@ +package proguard.optimize.kotlin + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import proguard.classfile.AccessConstants +import proguard.classfile.VersionConstants +import proguard.classfile.editor.ClassBuilder +import proguard.optimize.info.ProgramClassOptimizationInfo +import proguard.optimize.info.ProgramClassOptimizationInfoSetter +import java.io.ByteArrayOutputStream +import java.io.PrintWriter + +class LambdaGroupMappingPrinterTest : FreeSpec({ + "Given a lambda class and a lambda group" - { + val arity = 0 + val lambdaClassId = 0 + val lambdaClassName = "LambdaClass" + val lambdaGroupName = "LambdaGroup" + val lambdaClass = ClassBuilder( + VersionConstants.CLASS_VERSION_1_8, + AccessConstants.FINAL, + lambdaClassName, + KotlinLambdaMerger.NAME_KOTLIN_LAMBDA + ).addInterface("${KotlinLambdaMerger.NAME_KOTLIN_FUNCTION}$arity").programClass + val lambdaGroup = ClassBuilder( + VersionConstants.CLASS_VERSION_1_8, + AccessConstants.FINAL, + lambdaGroupName, + KotlinLambdaMerger.NAME_KOTLIN_LAMBDA + ).programClass + lambdaClass.accept(ProgramClassOptimizationInfoSetter()) + val optimizationInfo = ProgramClassOptimizationInfo.getProgramClassOptimizationInfo(lambdaClass) + optimizationInfo.lambdaGroup = lambdaGroup + optimizationInfo.lambdaGroupClassId = lambdaClassId + "When the mapping of the lambda class is printed" - { + val outputStream = ByteArrayOutputStream() + val mappingPrinter = LambdaGroupMappingPrinter(PrintWriter(outputStream, true)) + lambdaClass.accept(mappingPrinter) + "Then the correct lambda group, arity and case are printed" { + val mappingEntry = outputStream.toString() + val expectedMappingEntry = "$lambdaClassName -> $lambdaGroupName (arity $arity, case $lambdaClassId)${System.lineSeparator()}" + mappingEntry shouldBe expectedMappingEntry + } + } + } +}) diff --git a/base/src/test/kotlin/testutils/ClassPoolClassLoader.kt b/base/src/test/kotlin/testutils/ClassPoolClassLoader.kt new file mode 100644 index 000000000..fc5a37744 --- /dev/null +++ b/base/src/test/kotlin/testutils/ClassPoolClassLoader.kt @@ -0,0 +1,51 @@ +package testutils + +import proguard.classfile.ClassPool +import proguard.classfile.ProgramClass +import proguard.classfile.io.ProgramClassWriter +import proguard.classfile.util.ClassUtil.internalClassName +import proguard.classfile.visitor.ProgramClassFilter +import java.io.ByteArrayOutputStream +import java.io.DataOutputStream +import java.io.PrintStream + +/** + * A ClassLoader that can load classes from a ProGuardCORE ClassPool. + * Copyright: https://github.com/mrjameshamilton/klox/blob/master/src/main/kotlin/eu/jameshamilton/klox/util/ClassPoolClassLoader.kt + */ +class ClassPoolClassLoader(private val classPool: ClassPool) : ClassLoader() { + override fun findClass(name: String): Class<*> { + val clazz = classPool.getClass(internalClassName(name)) + val byteArrayOutputStream = ByteArrayOutputStream() + try { + clazz.accept(ProgramClassFilter(ProgramClassWriter(DataOutputStream(byteArrayOutputStream)))) + } catch (e: Exception) { + println("Exception for class $name: $e") + throw e + } + val bytes = byteArrayOutputStream.toByteArray() + if (clazz != null) return defineClass(name, bytes, 0, bytes.size) + return super.findClass(name) + } +} + + +fun captureExecutionOutput(programClassPool: ClassPool, programClassName: String, operation: (Class<*>) -> Unit): String { + val loader = ClassPoolClassLoader(programClassPool) + val loadedClass = loader.loadClass(programClassName) + val stdOutput = System.out + val capturedOutput = ByteArrayOutputStream() + val capturedOutputStream = PrintStream(capturedOutput) + System.setOut(capturedOutputStream) + try { + operation(loadedClass) + } catch (e: Exception) { + System.setOut(stdOutput) + println() + println("Exception: $e") + println("Output of method call:") + println(capturedOutput) + } + System.setOut(stdOutput) + return capturedOutput.toString() +} \ No newline at end of file diff --git a/base/src/test/kotlin/testutils/InstructionSequenceCollector.kt b/base/src/test/kotlin/testutils/InstructionSequenceCollector.kt new file mode 100644 index 000000000..4b386cd23 --- /dev/null +++ b/base/src/test/kotlin/testutils/InstructionSequenceCollector.kt @@ -0,0 +1,47 @@ +package testutils + +import proguard.classfile.Clazz +import proguard.classfile.Method +import proguard.classfile.attribute.CodeAttribute +import proguard.classfile.constant.Constant +import proguard.classfile.constant.visitor.ConstantVisitor +import proguard.classfile.editor.InstructionSequenceBuilder +import proguard.classfile.instruction.ConstantInstruction +import proguard.classfile.instruction.Instruction +import proguard.classfile.instruction.visitor.InstructionVisitor + +class InstructionSequenceCollector(val builder: InstructionSequenceBuilder) : InstructionVisitor { + override fun visitAnyInstruction( + clazz: Clazz, + method: Method, + codeAttribute: CodeAttribute, + offset: Int, + instruction: Instruction + ) { + builder.appendInstruction(instruction) + } + override fun visitConstantInstruction( + clazz: Clazz?, + method: Method?, + codeAttribute: CodeAttribute?, + offset: Int, + constantInstruction: ConstantInstruction? + ) { + val constantPoolEditor = builder.constantPoolEditor + var constantIndex = 0 + assert(constantInstruction != null) + assert(constantInstruction!!.constantIndex > 0) + clazz?.constantPoolEntryAccept( + constantInstruction.constantIndex, + object : ConstantVisitor { + override fun visitAnyConstant(clazz: Clazz?, constant: Constant?) { + constantIndex = constantPoolEditor.addConstant(constant) + } + } + ) + assert(constantIndex == 0) + val newConstantInstruction = ConstantInstruction().copy(constantInstruction) + newConstantInstruction.constantIndex = constantIndex + visitAnyInstruction(clazz!!, method!!, codeAttribute!!, offset, newConstantInstruction) + } +} diff --git a/base/src/test/kotlin/testutils/MatchDetector.kt b/base/src/test/kotlin/testutils/MatchDetector.kt new file mode 100644 index 000000000..0e722f8be --- /dev/null +++ b/base/src/test/kotlin/testutils/MatchDetector.kt @@ -0,0 +1,28 @@ +package testutils + +import proguard.classfile.Clazz +import proguard.classfile.Method +import proguard.classfile.attribute.CodeAttribute +import proguard.classfile.instruction.Instruction +import proguard.classfile.instruction.visitor.InstructionVisitor +import proguard.classfile.util.InstructionSequenceMatcher + +class MatchDetector(val matcher: InstructionSequenceMatcher, vararg val arguments: Int) : InstructionVisitor { + var matchIsFound = false + var matchedArguments = IntArray(arguments.size) + + override fun visitAnyInstruction( + clazz: Clazz, + method: Method, + codeAttribute: CodeAttribute, + offset: Int, + instruction: Instruction + ) { + println(instruction.toString(clazz, offset)) + instruction.accept(clazz, method, codeAttribute, offset, matcher) + if (matcher.isMatching()) { + matchIsFound = true + matchedArguments = matcher.matchedArguments(arguments) + } + } +} diff --git a/base/src/test/kotlin/testutils/RecursiveConstantCollector.kt b/base/src/test/kotlin/testutils/RecursiveConstantCollector.kt new file mode 100644 index 000000000..0b56e6b19 --- /dev/null +++ b/base/src/test/kotlin/testutils/RecursiveConstantCollector.kt @@ -0,0 +1,71 @@ +package testutils + +import proguard.classfile.Clazz +import proguard.classfile.constant.* +import proguard.classfile.constant.visitor.ConstantVisitor +import proguard.classfile.editor.ConstantPoolEditor + +class RecursiveConstantCollector(val constantPoolEditor: ConstantPoolEditor) : ConstantVisitor { + var firstAddedConstantIndex = 0 + override fun visitAnyConstant(clazz: Clazz?, constant: Constant?) { + val constantIndex = constantPoolEditor.addConstant(constant) + if (firstAddedConstantIndex == 0) + { + firstAddedConstantIndex = constantIndex + } + } + + override fun visitStringConstant(clazz: Clazz?, stringConstant: StringConstant?) { + visitAnyConstant(clazz, stringConstant) + clazz?.constantPoolEntryAccept(stringConstant?.u2stringIndex!!, this) + } + + override fun visitDynamicConstant(clazz: Clazz?, dynamicConstant: DynamicConstant?) { + visitAnyConstant(clazz, dynamicConstant) + clazz?.constantPoolEntryAccept(dynamicConstant?.nameAndTypeIndex!!, this) + clazz?.constantPoolEntryAccept(dynamicConstant?.bootstrapMethodAttributeIndex!!, this) + } + + override fun visitInvokeDynamicConstant(clazz: Clazz?, invokeDynamicConstant: InvokeDynamicConstant?) { + visitAnyConstant(clazz, invokeDynamicConstant) + clazz?.constantPoolEntryAccept(invokeDynamicConstant?.nameAndTypeIndex!!, this) + clazz?.constantPoolEntryAccept(invokeDynamicConstant?.bootstrapMethodAttributeIndex!!, this) + } + + override fun visitMethodHandleConstant(clazz: Clazz?, methodHandleConstant: MethodHandleConstant?) { + visitAnyConstant(clazz, methodHandleConstant) + clazz?.constantPoolEntryAccept(methodHandleConstant?.referenceIndex!!, this) + } + + override fun visitModuleConstant(clazz: Clazz?, moduleConstant: ModuleConstant?) { + visitAnyConstant(clazz, moduleConstant) + clazz?.constantPoolEntryAccept(moduleConstant?.u2nameIndex!!, this) + } + + override fun visitPackageConstant(clazz: Clazz?, packageConstant: PackageConstant?) { + visitAnyConstant(clazz, packageConstant) + clazz?.constantPoolEntryAccept(packageConstant?.u2nameIndex!!, this) + } + + override fun visitAnyRefConstant(clazz: Clazz?, refConstant: RefConstant?) { + visitAnyConstant(clazz, refConstant) + clazz?.constantPoolEntryAccept(refConstant?.nameAndTypeIndex!!, this) + clazz?.constantPoolEntryAccept(refConstant?.classIndex!!, this) + } + + override fun visitClassConstant(clazz: Clazz?, classConstant: ClassConstant?) { + visitAnyConstant(clazz, classConstant) + clazz?.constantPoolEntryAccept(classConstant?.u2nameIndex!!, this) + } + + override fun visitMethodTypeConstant(clazz: Clazz?, methodTypeConstant: MethodTypeConstant?) { + visitAnyConstant(clazz, methodTypeConstant) + clazz?.constantPoolEntryAccept(methodTypeConstant?.descriptorIndex!!, this) + } + + override fun visitNameAndTypeConstant(clazz: Clazz?, nameAndTypeConstant: NameAndTypeConstant?) { + visitAnyConstant(clazz, nameAndTypeConstant) + clazz?.constantPoolEntryAccept(nameAndTypeConstant?.u2descriptorIndex!!, this) + clazz?.constantPoolEntryAccept(nameAndTypeConstant?.u2nameIndex!!, this) + } +} diff --git a/settings.gradle b/settings.gradle index af28e14c7..16d615925 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,5 +20,6 @@ include 'gui' include 'gradle-plugin' include 'ant' include 'annotations' +includeBuild "../proguard-core" project(':gradle-plugin').name = 'gradle'