| // Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| package com.android.tools.r8.naming; |
| |
| import static com.android.tools.r8.naming.MappedRangeUtils.isInlineMappedRange; |
| import static com.android.tools.r8.utils.FunctionUtils.ignoreArgument; |
| |
| import com.android.tools.r8.naming.ClassNamingForNameMapper.MappedRange; |
| import com.android.tools.r8.naming.ClassNamingForNameMapper.MappedRangesOfName; |
| import com.android.tools.r8.naming.MemberNaming.FieldSignature; |
| import com.android.tools.r8.naming.MemberNaming.MethodSignature; |
| import com.android.tools.r8.naming.MemberNaming.Signature; |
| import com.android.tools.r8.naming.mappinginformation.MapVersionMappingInformation; |
| import com.android.tools.r8.naming.mappinginformation.MappingInformation; |
| import com.android.tools.r8.naming.mappinginformation.OutlineCallsiteMappingInformation; |
| import com.android.tools.r8.naming.mappinginformation.OutlineMappingInformation; |
| import com.android.tools.r8.naming.mappinginformation.RewriteFrameMappingInformation; |
| import com.android.tools.r8.naming.mappinginformation.RewriteFrameMappingInformation.ThrowsCondition; |
| import com.android.tools.r8.references.ArrayReference; |
| import com.android.tools.r8.references.ClassReference; |
| import com.android.tools.r8.references.MethodReference; |
| import com.android.tools.r8.references.Reference; |
| import com.android.tools.r8.references.TypeReference; |
| import com.android.tools.r8.utils.Box; |
| import com.android.tools.r8.utils.ChainableStringConsumer; |
| import com.android.tools.r8.utils.ConsumerUtils; |
| import com.android.tools.r8.utils.InternalOptions; |
| import com.android.tools.r8.utils.ListUtils; |
| import com.android.tools.r8.utils.SegmentTree; |
| import com.android.tools.r8.utils.ThrowingBiFunction; |
| import com.google.common.collect.Sets; |
| import it.unimi.dsi.fastutil.ints.Int2IntLinkedOpenHashMap; |
| import it.unimi.dsi.fastutil.ints.Int2IntSortedMap; |
| import it.unimi.dsi.fastutil.ints.Int2ReferenceMap; |
| import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import java.util.function.BiConsumer; |
| import java.util.function.Consumer; |
| |
| public class ComposingBuilder { |
| |
| private MapVersionMappingInformation currentMapVersion = null; |
| |
| /** |
| * When composing we store a view of the previously known mappings in committed and retain a |
| * current working set. When composing of a new map is finished we commit everything in current |
| * into the committed set. |
| * |
| * <p>The reason for not having just a single set is that we can have a circular mapping as |
| * follows: |
| * |
| * <pre> |
| * a -> b: |
| * ... |
| * b -> a: |
| * </pre> |
| * |
| * After composing our current view of a with the above, we could end up transforming 'a' into 'b' |
| * and then later transforming 'b' back into 'a' again. To ensure we do not mess up namings while |
| * composing classes and methods we resort to a working set and committed set. |
| */ |
| private final ComposingData committed = new ComposingData(); |
| |
| private ComposingData current; |
| private final InternalOptions options; |
| |
| public ComposingBuilder(InternalOptions options) { |
| this.options = options; |
| } |
| |
| public void compose(ClassNameMapper classNameMapper) throws MappingComposeException { |
| current = new ComposingData(); |
| MapVersionMappingInformation newMapVersionInfo = |
| classNameMapper.getFirstMapVersionInformation(); |
| if (newMapVersionInfo == null) { |
| throw new MappingComposeException( |
| "Composition of mapping files supported from map version 2.2."); |
| } |
| MapVersion newMapVersion = newMapVersionInfo.getMapVersion(); |
| // TODO(b/241763080): Should be stable map version. |
| if (newMapVersion.isLessThan(MapVersion.MAP_VERSION_EXPERIMENTAL) |
| || newMapVersion.isUnknown()) { |
| throw new MappingComposeException( |
| "Composition of mapping files supported from map version 2.2."); |
| } |
| if (currentMapVersion == null) { |
| currentMapVersion = newMapVersionInfo; |
| } else { |
| currentMapVersion = |
| newMapVersionInfo.compose(currentMapVersion).asMapVersionMappingInformation(); |
| } |
| for (ClassNamingForNameMapper classMapping : classNameMapper.getClassNameMappings().values()) { |
| compose(classNameMapper, classMapping); |
| } |
| committed.commit(current, classNameMapper); |
| } |
| |
| private void compose(ClassNameMapper classNameMapper, ClassNamingForNameMapper classMapping) |
| throws MappingComposeException { |
| String originalName = classMapping.originalName; |
| String renamedName = classMapping.renamedName; |
| ComposingClassBuilder composingClassBuilder = |
| new ComposingClassBuilder(originalName, renamedName, committed, current, options); |
| ComposingClassBuilder duplicateMapping = |
| current.classBuilders.put(renamedName, composingClassBuilder); |
| if (duplicateMapping != null) { |
| throw new MappingComposeException( |
| "Duplicate class mapping. Both '" |
| + duplicateMapping.getOriginalName() |
| + "' and '" |
| + originalName |
| + "' maps to '" |
| + renamedName |
| + "'."); |
| } |
| composingClassBuilder.compose(classNameMapper, classMapping); |
| } |
| |
| |
| @Override |
| public String toString() { |
| List<ComposingClassBuilder> classBuilders = new ArrayList<>(committed.classBuilders.values()); |
| classBuilders.sort(Comparator.comparing(ComposingClassBuilder::getOriginalName)); |
| StringBuilder sb = new StringBuilder(); |
| committed.preamble.forEach(preambleLine -> sb.append(preambleLine).append("\n")); |
| if (currentMapVersion != null) { |
| sb.append("# ").append(currentMapVersion.serialize()).append("\n"); |
| } |
| ChainableStringConsumer wrap = ChainableStringConsumer.wrap(sb::append); |
| for (ComposingClassBuilder classBuilder : classBuilders) { |
| classBuilder.write(wrap); |
| } |
| return sb.toString(); |
| } |
| |
| public static class ComposingData { |
| |
| /** |
| * A map of minified names to their class builders. When committing to a new minified name we |
| * destructively remove the previous minified mapping and replace it with the up-to-date one. |
| */ |
| private Map<String, ComposingClassBuilder> classBuilders = new HashMap<>(); |
| /** |
| * RewriteFrameInformation contains condition clauses that are bound to the residual program. As |
| * a result of that, we have to patch up the conditions when we compose new class mappings. |
| */ |
| private final List<RewriteFrameMappingInformation> rewriteFrameInformation = new ArrayList<>(); |
| /** Map of newly added outline call site informations which do not require any rewriting. */ |
| private Map<ClassTypeNameAndMethodName, OutlineCallsiteMappingInformation> |
| outlineCallsiteInformation = new HashMap<>(); |
| /** |
| * Map of updated outline definitions which has to be committed. The positions in the caller are |
| * fixed at this point since these are local to the method when rewriting. |
| */ |
| private final Map<ClassTypeNameAndMethodName, UpdateOutlineCallsiteInformation> |
| outlineSourcePositionsUpdated = new HashMap<>(); |
| |
| private final List<String> preamble = new ArrayList<>(); |
| |
| public void commit(ComposingData current, ClassNameMapper classNameMapper) |
| throws MappingComposeException { |
| preamble.addAll(classNameMapper.getPreamble()); |
| commitClassBuilders(current, classNameMapper); |
| commitRewriteFrameInformation(current, classNameMapper); |
| commitOutlineCallsiteInformation(current, classNameMapper); |
| } |
| |
| private void commitClassBuilders(ComposingData current, ClassNameMapper classNameMapper) |
| throws MappingComposeException { |
| Set<String> updatedClassBuilders = new HashSet<>(); |
| Map<String, ComposingClassBuilder> newClassBuilders = new HashMap<>(); |
| for (Entry<String, ComposingClassBuilder> newEntry : current.classBuilders.entrySet()) { |
| String renamedName = newEntry.getKey(); |
| ComposingClassBuilder classBuilder = newEntry.getValue(); |
| updatedClassBuilders.add(classBuilder.originalName); |
| ComposingClassBuilder existingBuilder = classBuilders.get(classBuilder.originalName); |
| if (existingBuilder != null) { |
| classBuilder = existingBuilder.commit(classBuilder); |
| } |
| newClassBuilders.put(renamedName, classBuilder); |
| } |
| for (Entry<String, ComposingClassBuilder> existingEntry : classBuilders.entrySet()) { |
| if (!updatedClassBuilders.contains(existingEntry.getKey())) { |
| ComposingClassBuilder classBuilder = existingEntry.getValue(); |
| ComposingClassBuilder duplicateMapping = |
| newClassBuilders.put(existingEntry.getKey(), classBuilder); |
| if (duplicateMapping != null) { |
| throw new MappingComposeException( |
| "Duplicate class mapping. Both '" |
| + classBuilder.getOriginalName() |
| + "' and '" |
| + classNameMapper.getClassNaming(existingEntry.getKey()).originalName |
| + "' maps to '" |
| + classBuilder.renamedName |
| + "'."); |
| } |
| } |
| } |
| classBuilders = newClassBuilders; |
| } |
| |
| private void commitRewriteFrameInformation( |
| ComposingData current, ClassNameMapper classNameMapper) { |
| // First update the existing frame information to have new class name mappings. |
| Map<String, String> inverse = classNameMapper.getObfuscatedToOriginalMapping().inverse; |
| for (RewriteFrameMappingInformation rewriteMappingInfo : rewriteFrameInformation) { |
| rewriteMappingInfo |
| .getConditions() |
| .forEach( |
| rewriteCondition -> { |
| ThrowsCondition throwsCondition = rewriteCondition.asThrowsCondition(); |
| if (throwsCondition != null) { |
| throwsCondition.setClassReferenceInternal( |
| mapTypeReference(inverse, throwsCondition.getClassReference()).asClass()); |
| } |
| }); |
| } |
| rewriteFrameInformation.addAll(current.rewriteFrameInformation); |
| } |
| |
| private void commitOutlineCallsiteInformation( |
| ComposingData current, ClassNameMapper classNameMapper) { |
| // To commit outline call site information, we take the previously committed and bring forward |
| // to a new mapping, and potentially rewrite source positions if available. |
| Map<ClassTypeNameAndMethodName, OutlineCallsiteMappingInformation> newOutlineCallsiteInfo = |
| new HashMap<>(); |
| Map<String, String> inverse = classNameMapper.getObfuscatedToOriginalMapping().inverse; |
| outlineCallsiteInformation.forEach( |
| (holderAndMethodNameOfOutline, outlineInfo) -> { |
| UpdateOutlineCallsiteInformation updateOutlineCallsiteInformation = |
| current.outlineSourcePositionsUpdated.get(holderAndMethodNameOfOutline); |
| String newMethodName = outlineInfo.getOutline().getMethodName(); |
| if (updateOutlineCallsiteInformation != null) { |
| // We have a callsite mapping that we need to update. |
| MappedRangeOriginalToMinifiedMap originalToMinifiedMap = |
| MappedRangeOriginalToMinifiedMap.build( |
| updateOutlineCallsiteInformation.newMappedRanges); |
| Int2IntSortedMap newPositionMap = new Int2IntLinkedOpenHashMap(); |
| outlineInfo |
| .getPositions() |
| .forEach( |
| (originalPosition, destination) -> |
| originalToMinifiedMap.visitMinified( |
| originalPosition, |
| newMinified -> newPositionMap.put(newMinified, destination))); |
| outlineInfo.setPositionsInternal(newPositionMap); |
| newMethodName = updateOutlineCallsiteInformation.newMethodName; |
| } |
| // Holder, return type or formals could have changed the outline descriptor. |
| MethodReference outline = outlineInfo.getOutline(); |
| ClassReference newHolder = |
| mapTypeReference(inverse, outline.getHolderClass()).asClass(); |
| outlineInfo.setOutlineInternal( |
| Reference.method( |
| newHolder, |
| newMethodName, |
| mapTypeReferences(inverse, outline.getFormalTypes()), |
| mapTypeReference(inverse, outline.getReturnType()))); |
| newOutlineCallsiteInfo.put( |
| new ClassTypeNameAndMethodName( |
| newHolder.getTypeName(), holderAndMethodNameOfOutline.getMethodName()), |
| outlineInfo); |
| }); |
| newOutlineCallsiteInfo.putAll(current.outlineCallsiteInformation); |
| outlineCallsiteInformation = newOutlineCallsiteInfo; |
| } |
| |
| public void addNewOutlineCallsiteInformation( |
| MethodReference outline, OutlineCallsiteMappingInformation outlineCallsiteInfo) { |
| outlineCallsiteInformation.put( |
| new ClassTypeNameAndMethodName( |
| outline.getHolderClass().getTypeName(), outline.getMethodName()), |
| outlineCallsiteInfo); |
| } |
| |
| public UpdateOutlineCallsiteInformation getUpdateOutlineCallsiteInformation( |
| String originalHolder, String originalMethodName, String newMethodName) { |
| return outlineSourcePositionsUpdated.computeIfAbsent( |
| new ClassTypeNameAndMethodName(originalHolder, originalMethodName), |
| ignore -> new UpdateOutlineCallsiteInformation(newMethodName)); |
| } |
| |
| private List<TypeReference> mapTypeReferences( |
| Map<String, String> typeNameMap, List<TypeReference> typeReferences) { |
| return ListUtils.map(typeReferences, typeRef -> mapTypeReference(typeNameMap, typeRef)); |
| } |
| |
| private TypeReference mapTypeReference( |
| Map<String, String> typeNameMap, TypeReference typeReference) { |
| if (typeReference == null || typeReference.isPrimitive()) { |
| return typeReference; |
| } |
| if (typeReference.isArray()) { |
| ArrayReference arrayReference = typeReference.asArray(); |
| return Reference.array( |
| mapTypeReference(typeNameMap, arrayReference.getBaseType()), |
| arrayReference.getDimensions()); |
| } else { |
| assert typeReference.isClass(); |
| String newTypeName = typeNameMap.get(typeReference.getTypeName()); |
| return newTypeName == null ? typeReference : Reference.classFromTypeName(newTypeName); |
| } |
| } |
| } |
| |
| private static class ClassTypeNameAndMethodName { |
| |
| private final String holderTypeName; |
| private final String methodName; |
| |
| public ClassTypeNameAndMethodName(String holderTypeName, String methodName) { |
| this.holderTypeName = holderTypeName; |
| this.methodName = methodName; |
| } |
| |
| public String getHolderTypeName() { |
| return holderTypeName; |
| } |
| |
| public String getMethodName() { |
| return methodName; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (!(o instanceof ClassTypeNameAndMethodName)) { |
| return false; |
| } |
| ClassTypeNameAndMethodName that = (ClassTypeNameAndMethodName) o; |
| return holderTypeName.equals(that.holderTypeName) && methodName.equals(that.methodName); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(holderTypeName, methodName); |
| } |
| } |
| |
| private static class UpdateOutlineCallsiteInformation { |
| |
| private List<MappedRange> newMappedRanges; |
| private final String newMethodName; |
| |
| private UpdateOutlineCallsiteInformation(String newMethodName) { |
| this.newMethodName = newMethodName; |
| } |
| |
| private void setNewMappedRanges(List<MappedRange> mappedRanges) { |
| newMappedRanges = mappedRanges; |
| } |
| } |
| |
| private static class MappedRangeOriginalToMinifiedMap { |
| |
| private final Int2ReferenceMap<List<Integer>> originalToMinified; |
| |
| private MappedRangeOriginalToMinifiedMap(Int2ReferenceMap<List<Integer>> originalToMinified) { |
| this.originalToMinified = originalToMinified; |
| } |
| |
| private static MappedRangeOriginalToMinifiedMap build(List<MappedRange> mappedRanges) { |
| Int2ReferenceMap<List<Integer>> positionMap = new Int2ReferenceOpenHashMap<>(); |
| for (MappedRange mappedRange : mappedRanges) { |
| Range originalRange = mappedRange.originalRange; |
| for (int position = originalRange.from; position <= originalRange.to; position++) { |
| // It is perfectly fine to have multiple minified ranges mapping to the same source, we |
| // just need to keep the additional information. |
| positionMap |
| .computeIfAbsent(position, ignoreArgument(ArrayList::new)) |
| .add(mappedRange.minifiedRange.from + (position - originalRange.from)); |
| } |
| } |
| return new MappedRangeOriginalToMinifiedMap(positionMap); |
| } |
| |
| public int lookupFirst(int originalPosition) { |
| List<Integer> minifiedPositions = originalToMinified.get(originalPosition); |
| return minifiedPositions == null ? 0 : minifiedPositions.get(0); |
| } |
| |
| public void visitMinified(int originalPosition, Consumer<Integer> consumer) { |
| List<Integer> minifiedPositions = originalToMinified.get(originalPosition); |
| if (minifiedPositions != null) { |
| minifiedPositions.forEach(consumer); |
| } |
| } |
| } |
| |
| public static class ComposingClassBuilder { |
| |
| private static final String INDENTATION = " "; |
| private static final int NO_RANGE_FROM = -1; |
| |
| private final String originalName; |
| private final String renamedName; |
| private final Map<FieldSignature, MemberNaming> fieldMembers = new HashMap<>(); |
| private final Map<MethodSignature, SegmentTree<List<MappedRange>>> methodsWithPosition = |
| new HashMap<>(); |
| private final Map<MethodSignature, MappedRange> methodsWithoutPosition = new HashMap<>(); |
| private final List<MappingInformation> additionalMappingInfo = new ArrayList<>(); |
| |
| private final ComposingData committed; |
| private final ComposingData current; |
| |
| private final ComposingClassBuilder committedPreviousClassBuilder; |
| private final InternalOptions options; |
| |
| private ComposingClassBuilder( |
| String originalName, |
| String renamedName, |
| ComposingData committed, |
| ComposingData current, |
| InternalOptions options) { |
| this.originalName = originalName; |
| this.renamedName = renamedName; |
| this.current = current; |
| this.committed = committed; |
| this.options = options; |
| committedPreviousClassBuilder = committed.classBuilders.get(originalName); |
| } |
| |
| public String getOriginalName() { |
| return originalName; |
| } |
| |
| public String getRenamedName() { |
| return renamedName; |
| } |
| |
| public void compose(ClassNameMapper classNameMapper, ClassNamingForNameMapper mapper) |
| throws MappingComposeException { |
| List<MappingInformation> newMappingInfo = mapper.getAdditionalMappingInfo(); |
| if (newMappingInfo != null) { |
| additionalMappingInfo.addAll(newMappingInfo); |
| } |
| composeFieldNamings(mapper, classNameMapper); |
| composeMethodNamings(mapper, classNameMapper); |
| } |
| |
| private void composeFieldNamings( |
| ClassNamingForNameMapper mapper, ClassNameMapper classNameMapper) { |
| Map<String, String> inverseClassMapping = |
| classNameMapper.getObfuscatedToOriginalMapping().inverse; |
| mapper.forAllFieldNaming( |
| fieldNaming -> { |
| MemberNaming fieldNamingToAdd = fieldNaming; |
| FieldSignature originalSignature = |
| fieldNaming.getOriginalSignature().asFieldSignature(); |
| FieldSignature residualSignature = |
| fieldNaming |
| .computeResidualSignature(type -> inverseClassMapping.getOrDefault(type, type)) |
| .asFieldSignature(); |
| if (committedPreviousClassBuilder != null) { |
| MemberNaming existingMemberNaming = |
| committedPreviousClassBuilder.fieldMembers.remove(originalSignature); |
| if (existingMemberNaming != null) { |
| fieldNamingToAdd = |
| new MemberNaming( |
| existingMemberNaming.getOriginalSignature(), residualSignature); |
| } |
| } |
| MemberNaming existing = fieldMembers.put(residualSignature, fieldNamingToAdd); |
| assert existing == null; |
| }); |
| } |
| |
| private void composeMethodNamings( |
| ClassNamingForNameMapper mapper, ClassNameMapper classNameMapper) |
| throws MappingComposeException { |
| Map<String, String> inverseClassMapping = |
| classNameMapper.getObfuscatedToOriginalMapping().inverse; |
| for (Entry<String, MappedRangesOfName> entry : mapper.mappedRangesByRenamedName.entrySet()) { |
| MappedRangesOfName mappedRangesOfName = entry.getValue(); |
| for (MappedRangesOfName rangesOfName : mappedRangesOfName.partitionOnMethodSignature()) { |
| MemberNaming memberNaming = rangesOfName.getMemberNaming(mapper); |
| List<MappedRange> newMappedRanges = rangesOfName.getMappedRanges(); |
| RangeBuilder minified = new RangeBuilder(); |
| RangeBuilder original = new RangeBuilder(); |
| for (MappedRange mappedRange : newMappedRanges) { |
| minified.addRange(mappedRange.minifiedRange); |
| original.addRange(mappedRange.originalRange); |
| // Register mapping information that is dependent on the residual naming to allow |
| // updating later on. |
| registerMappingInformationFromMappedRanges(mappedRange); |
| } |
| MethodSignature originalSignature = |
| memberNaming.getOriginalSignature().asMethodSignature(); |
| // Remove the existing entry if it exists. |
| List<MappedRange> existingMappedRanges = null; |
| if (committedPreviousClassBuilder != null) { |
| SegmentTree<List<MappedRange>> listSegmentTree = |
| committedPreviousClassBuilder.methodsWithPosition.get(originalSignature); |
| if (listSegmentTree != null) { |
| // Any new transformation can be lossy - for example, D8 only promises to keep line |
| // positions if the method is not throwing (and is not debug). Additionally, R8/PG |
| // emits `1:1:void foo() -> a` instead of `1:1:void foo():1:1 -> a`, so R8 must |
| // capture the preamble position by explicitly inserting 0 as original range. |
| Entry<Integer, List<MappedRange>> existingEntry = |
| listSegmentTree.findEntry(original.getStartOrNoRangeFrom()); |
| // We assume that all new minified ranges for a method are rewritten in the new map |
| // such that no previous existing positions exists. |
| if (existingEntry != null) { |
| listSegmentTree.removeSegment(existingEntry.getKey()); |
| existingMappedRanges = existingEntry.getValue(); |
| } else { |
| // The original can be discarded if it no longer exists or if the method is |
| // non-throwing. |
| if (original.hasValue() |
| && !original.isPreamble() |
| && !options.mappingComposeOptions().allowNonExistingOriginalRanges) { |
| throw new MappingComposeException( |
| "Could not find original starting position of '" |
| + minified.start |
| + "' which should be " |
| + original.start); |
| } |
| } |
| assert minified.hasValue(); |
| } else { |
| MappedRange existingMappedRange = |
| committedPreviousClassBuilder.methodsWithoutPosition.remove(originalSignature); |
| existingMappedRanges = |
| existingMappedRange == null |
| ? null |
| : Collections.singletonList(existingMappedRange); |
| } |
| } |
| List<MappedRange> composedRanges = |
| composeMappedRangesForMethod(existingMappedRanges, newMappedRanges); |
| MappedRange lastComposedRange = ListUtils.last(composedRanges); |
| MethodSignature residualSignature = |
| memberNaming |
| .computeResidualSignature(type -> inverseClassMapping.getOrDefault(type, type)) |
| .asMethodSignature(); |
| if (lastComposedRange.minifiedRange != null) { |
| methodsWithPosition |
| .computeIfAbsent(residualSignature, ignored -> new SegmentTree<>(false)) |
| .add( |
| minified.getStartOrNoRangeFrom(), |
| minified.getEndOrNoRangeFrom(), |
| composedRanges); |
| } else { |
| assert composedRanges.size() == 1; |
| methodsWithoutPosition.put(residualSignature, lastComposedRange); |
| } |
| } |
| } |
| } |
| |
| private void registerMappingInformationFromMappedRanges(MappedRange mappedRange) |
| throws MappingComposeException { |
| for (MappingInformation mappingInformation : mappedRange.getAdditionalMappingInformation()) { |
| if (mappingInformation.isRewriteFrameMappingInformation()) { |
| RewriteFrameMappingInformation rewriteFrameMappingInformation = |
| mappingInformation.asRewriteFrameMappingInformation(); |
| rewriteFrameMappingInformation |
| .getConditions() |
| .forEach( |
| condition -> { |
| if (condition.isThrowsCondition()) { |
| current.rewriteFrameInformation.add(rewriteFrameMappingInformation); |
| } |
| }); |
| } else if (mappingInformation.isOutlineCallsiteInformation()) { |
| OutlineCallsiteMappingInformation outlineCallsiteInfo = |
| mappingInformation.asOutlineCallsiteInformation(); |
| MethodReference outline = outlineCallsiteInfo.getOutline(); |
| if (outline == null) { |
| throw new MappingComposeException( |
| "Unable to compose outline call site information without outline key: " |
| + outlineCallsiteInfo.serialize()); |
| } |
| current.addNewOutlineCallsiteInformation(outline, outlineCallsiteInfo); |
| } |
| } |
| } |
| |
| private List<MappedRange> composeMappedRangesForMethod( |
| List<MappedRange> existingRanges, List<MappedRange> newRanges) |
| throws MappingComposeException { |
| assert !newRanges.isEmpty(); |
| if (existingRanges == null || existingRanges.isEmpty()) { |
| return newRanges; |
| } |
| // The new minified ranges may have ranges that range over positions that has additional |
| // information, such as inlinees: |
| // Existing: |
| // 1:2:void caller():14:15 -> x |
| // 3:3:void inlinee():42:42 -> x |
| // 3:3:void caller():16:16 -> x |
| // 4:10:void caller():17:23 -> x |
| // ... |
| // New mapping: |
| // 1:5:void x():1:5 -> y |
| // 6:10:void x():6:6 -> y |
| // ... |
| // It is important that we therefore split up the new ranges to map everything back to the |
| // existing original mappings: |
| // Resulting mapping: |
| // 1:2:void caller():14:15 -> y |
| // 3:3:void inlinee():42:42 -> y |
| // 3:3:void caller():16:16 -> y |
| // 4:5:void caller():17:18 -> y |
| // 6:10:void caller():19:19 -> y |
| // ... |
| Box<Range> originalRange = new Box<>(); |
| ExistingMapping mappedRangesForPosition = |
| getExistingMapping( |
| existingRanges, (start, end) -> originalRange.set(new Range(start, end))); |
| List<MappedRange> newComposedRanges = new ArrayList<>(); |
| ComputedOutlineInformation computedOutlineInformation = new ComputedOutlineInformation(); |
| for (int i = 0; i < newRanges.size(); i++) { |
| if (isInlineMappedRange(newRanges, i)) { |
| throw new MappingComposeException( |
| "Unexpected inline frame '" + existingRanges.get(i) + "' in composing map."); |
| } |
| MappedRange newRange = newRanges.get(i); |
| MappedRange lastExistingRange = ListUtils.last(existingRanges); |
| if (newRange.originalRange == null && newRange.minifiedRange == null) { |
| MappedRange newComposedRange = |
| new MappedRange( |
| null, lastExistingRange.signature, originalRange.get(), newRange.renamedName); |
| composeMappingInformation( |
| newComposedRange.getAdditionalMappingInformation(), |
| lastExistingRange.getAdditionalMappingInformation(), |
| info -> newComposedRange.addMappingInformation(info, ConsumerUtils.emptyConsumer())); |
| newComposedRanges.add(newComposedRange); |
| } else { |
| assert newRange.minifiedRange != null; |
| // First check if the original range matches the existing minified range. |
| List<MappedRange> existingMappedRanges = |
| mappedRangesForPosition.getMappedRangesForPosition( |
| newRange.getFirstPositionOfOriginalRange(NO_RANGE_FROM)); |
| if (existingMappedRanges == null) { |
| // If we cannot lookup the original position because it has been removed we compose with |
| // the existing method signature. |
| if (newRange.originalRange != null && newRange.originalRange.isPreamble()) { |
| newComposedRanges.add( |
| new MappedRange( |
| null, |
| lastExistingRange.signature, |
| originalRange.get(), |
| newRange.renamedName)); |
| continue; |
| } else { |
| throw new MappingComposeException( |
| "Unexpected missing original position for '" + newRange + "'."); |
| } |
| } |
| // We have found an existing range for the original position. |
| MappedRange lastExistingMappedRange = ListUtils.last(existingMappedRanges); |
| // If the existing mapped minified range is equal to the original range of the new range |
| // then we have a perfect mapping that we can translate directly. |
| if (lastExistingMappedRange.minifiedRange.equals(newRange.originalRange)) { |
| computeComposedMappedRange( |
| newComposedRanges, |
| newRange, |
| existingMappedRanges, |
| computedOutlineInformation, |
| newRange.minifiedRange.from, |
| newRange.minifiedRange.to); |
| } else { |
| // Otherwise, we have a situation where the minified range references over multiple |
| // existing ranges. We simply chop op when the range changes on the right hand side. To |
| // ensure we do not mess up the spans from the original range, we have to check if the |
| // current starting position is inside an original range, and then chop it off. |
| // Similarly, when writing the last block, we have to cut it off to match. |
| int lastStartingMinifiedFrom = newRange.minifiedRange.from; |
| for (int position = newRange.minifiedRange.from; |
| position <= newRange.minifiedRange.to; |
| position++) { |
| List<MappedRange> existingMappedRangesForPosition = |
| mappedRangesForPosition.getMappedRangesForPosition( |
| newRange.getOriginalLineNumber(position)); |
| MappedRange lastExistingMappedRangeForPosition = null; |
| if (existingMappedRangesForPosition != null) { |
| lastExistingMappedRangeForPosition = |
| ListUtils.last(existingMappedRangesForPosition); |
| } |
| if (lastExistingMappedRangeForPosition == null |
| || !lastExistingMappedRange.minifiedRange.equals( |
| lastExistingMappedRangeForPosition.minifiedRange)) { |
| // We have seen an existing range we have to compute a splitting for. |
| computeComposedMappedRange( |
| newComposedRanges, |
| newRange, |
| existingMappedRanges, |
| computedOutlineInformation, |
| lastStartingMinifiedFrom, |
| position - 1); |
| // Advance the last starting position to this point and advance the existing mapped |
| // ranges for this position. |
| lastStartingMinifiedFrom = position; |
| if (existingMappedRangesForPosition == null) { |
| if (!options.mappingComposeOptions().allowNonExistingOriginalRanges) { |
| throw new MappingComposeException( |
| "Unexpected missing original position for '" + newRange + "'."); |
| } |
| // We are at the first position of a hole. If we have existing ranges: |
| // 1:1:void foo():41:41 -> a |
| // 9:9:void foo():49:49 -> a |
| // We may have a new range that is: |
| // 21:29:void foo():1:9 |
| // We end up here at position 2 and we have already committed |
| // 21:21:void foo():41:41. |
| // We then introduce a "fake" normal range to simulate the result of retracing one |
| // after the other to end up with: |
| // 21:21:void foo():41:41 |
| // 22:28:void foo():2:8 |
| // 29:29:void foo():49:49. |
| int startOriginalPosition = newRange.getOriginalLineNumber(position); |
| Integer endOriginalPosition = |
| mappedRangesForPosition.getCeilingForPosition(position); |
| if (endOriginalPosition == null) { |
| endOriginalPosition = newRange.getLastPositionOfOriginalRange() + 1; |
| } |
| Range newOriginalRange = |
| new Range(startOriginalPosition, endOriginalPosition - 1); |
| MappedRange nonExistingMappedRange = |
| new MappedRange( |
| newOriginalRange, |
| lastExistingMappedRange.getOriginalSignature().asMethodSignature(), |
| newOriginalRange, |
| lastExistingMappedRange.renamedName); |
| nonExistingMappedRange.setResidualSignatureInternal( |
| lastExistingRange.getResidualSignature()); |
| lastExistingMappedRange = nonExistingMappedRange; |
| existingMappedRanges = Collections.singletonList(nonExistingMappedRange); |
| position += (endOriginalPosition - startOriginalPosition) - 1; |
| } else { |
| lastExistingMappedRange = lastExistingMappedRangeForPosition; |
| existingMappedRanges = existingMappedRangesForPosition; |
| } |
| } |
| } |
| computeComposedMappedRange( |
| newComposedRanges, |
| newRange, |
| existingMappedRanges, |
| computedOutlineInformation, |
| lastStartingMinifiedFrom, |
| newRange.minifiedRange.to); |
| } |
| } |
| } |
| MappedRange lastComposedRange = ListUtils.last(newComposedRanges); |
| if (computedOutlineInformation.seenOutlineMappingInformation != null) { |
| current |
| .getUpdateOutlineCallsiteInformation( |
| committedPreviousClassBuilder.getRenamedName(), |
| ListUtils.last(newRanges).signature.getName(), |
| lastComposedRange.getRenamedName()) |
| .setNewMappedRanges(newRanges); |
| lastComposedRange.addMappingInformation( |
| computedOutlineInformation.seenOutlineMappingInformation, |
| ConsumerUtils.emptyConsumer()); |
| } |
| if (!computedOutlineInformation.outlineCallsiteMappingInformationToPatchUp.isEmpty()) { |
| MappedRangeOriginalToMinifiedMap originalToMinifiedMap = |
| MappedRangeOriginalToMinifiedMap.build(newRanges); |
| List<OutlineCallsiteMappingInformation> outlineCallSites = |
| new ArrayList<>(computedOutlineInformation.outlineCallsiteMappingInformationToPatchUp); |
| outlineCallSites.sort(Comparator.comparing(mapping -> mapping.getOutline().toString())); |
| for (OutlineCallsiteMappingInformation outlineCallSite : outlineCallSites) { |
| Int2IntSortedMap positionMap = outlineCallSite.getPositions(); |
| for (Integer keyPosition : positionMap.keySet()) { |
| int keyPositionInt = keyPosition; |
| int originalDestination = positionMap.get(keyPositionInt); |
| int newDestination = originalToMinifiedMap.lookupFirst(originalDestination); |
| positionMap.put(keyPositionInt, newDestination); |
| } |
| lastComposedRange.addMappingInformation(outlineCallSite, ConsumerUtils.emptyConsumer()); |
| } |
| } |
| return newComposedRanges; |
| } |
| |
| public interface ExistingMapping { |
| |
| Integer getCeilingForPosition(int i); |
| |
| List<MappedRange> getMappedRangesForPosition(int i); |
| } |
| |
| /*** |
| * Builds a position to mapped ranges for mappings for looking up all mapped ranges for a given |
| * position. |
| */ |
| private ExistingMapping getExistingMapping( |
| List<MappedRange> existingRanges, BiConsumer<Integer, Integer> positions) { |
| TreeMap<Integer, List<MappedRange>> mappedRangesForPosition = new TreeMap<>(); |
| List<MappedRange> currentRangesForPosition = new ArrayList<>(); |
| int startExisting = NO_RANGE_FROM; |
| int endExisting = NO_RANGE_FROM; |
| boolean isCatchAll = false; |
| for (int i = 0; i < existingRanges.size(); i++) { |
| MappedRange mappedRange = existingRanges.get(i); |
| currentRangesForPosition.add(mappedRange); |
| if (!isInlineMappedRange(existingRanges, i)) { |
| if (startExisting == NO_RANGE_FROM) { |
| startExisting = mappedRange.getFirstPositionOfOriginalRange(NO_RANGE_FROM); |
| } |
| endExisting = Math.max(mappedRange.getLastPositionOfOriginalRange(), endExisting); |
| if (mappedRange.minifiedRange == null) { |
| mappedRangesForPosition.put(NO_RANGE_FROM, currentRangesForPosition); |
| } else if (mappedRange.minifiedRange.isCatchAll()) { |
| isCatchAll = true; |
| } else { |
| for (int position = mappedRange.minifiedRange.from; |
| position <= mappedRange.minifiedRange.to; |
| position++) { |
| mappedRangesForPosition.put(position, currentRangesForPosition); |
| } |
| } |
| currentRangesForPosition = new ArrayList<>(); |
| } |
| } |
| if (startExisting > NO_RANGE_FROM) { |
| positions.accept(startExisting, endExisting); |
| } |
| boolean finalIsCatchAll = isCatchAll; |
| List<MappedRange> finalCurrentRangesForPosition = currentRangesForPosition; |
| return new ExistingMapping() { |
| @Override |
| public Integer getCeilingForPosition(int i) { |
| return finalIsCatchAll ? i : mappedRangesForPosition.ceilingKey(i); |
| } |
| |
| @Override |
| public List<MappedRange> getMappedRangesForPosition(int i) { |
| return finalIsCatchAll ? finalCurrentRangesForPosition : mappedRangesForPosition.get(i); |
| } |
| }; |
| } |
| |
| private void computeComposedMappedRange( |
| List<MappedRange> newComposedRanges, |
| MappedRange newMappedRange, |
| List<MappedRange> existingMappedRanges, |
| ComputedOutlineInformation computedOutlineInformation, |
| int lastStartingMinifiedFrom, |
| int position) |
| throws MappingComposeException { |
| Range existingRange = existingMappedRanges.get(0).minifiedRange; |
| assert existingMappedRanges.stream().allMatch(x -> x.minifiedRange.equals(existingRange)); |
| Range newMinifiedRange = new Range(lastStartingMinifiedFrom, position); |
| boolean copyOriginalRange = existingRange.equals(newMappedRange.originalRange); |
| for (MappedRange existingMappedRange : existingMappedRanges) { |
| Range existingOriginalRange = existingMappedRange.originalRange; |
| Range newOriginalRange; |
| if (copyOriginalRange |
| || existingOriginalRange == null |
| || existingOriginalRange.span() == 1) { |
| newOriginalRange = existingOriginalRange; |
| } else { |
| // Find the window that the new range points to into the original range. |
| int existingMinifiedPos = newMappedRange.getOriginalLineNumber(lastStartingMinifiedFrom); |
| int newOriginalStart = existingMappedRange.getOriginalLineNumber(existingMinifiedPos); |
| if (newMappedRange.originalRange.span() == 1) { |
| newOriginalRange = new Range(newOriginalStart, newOriginalStart); |
| } else { |
| assert newMinifiedRange.span() <= existingOriginalRange.span(); |
| newOriginalRange = |
| new Range(newOriginalStart, newOriginalStart + newMinifiedRange.span() - 1); |
| } |
| } |
| MappedRange computedRange = |
| new MappedRange( |
| newMinifiedRange, |
| existingMappedRange.signature, |
| newOriginalRange, |
| newMappedRange.renamedName); |
| List<MappingInformation> mappingInformationToCompose = new ArrayList<>(); |
| existingMappedRange |
| .getAdditionalMappingInformation() |
| .forEach( |
| info -> { |
| if (info.isOutlineMappingInformation()) { |
| computedOutlineInformation.seenOutlineMappingInformation = |
| info.asOutlineMappingInformation(); |
| } else if (info.isOutlineCallsiteInformation()) { |
| computedOutlineInformation.outlineCallsiteMappingInformationToPatchUp.add( |
| info.asOutlineCallsiteInformation()); |
| } else { |
| mappingInformationToCompose.add(info); |
| } |
| }); |
| composeMappingInformation( |
| computedRange.getAdditionalMappingInformation(), |
| mappingInformationToCompose, |
| info -> computedRange.addMappingInformation(info, ConsumerUtils.emptyConsumer())); |
| newComposedRanges.add(computedRange); |
| } |
| } |
| |
| /*** |
| * Populates newMappingInformation with existingMappingInformation. |
| */ |
| private void composeMappingInformation( |
| List<MappingInformation> newMappingInformation, |
| List<MappingInformation> existingMappingInformation, |
| Consumer<MappingInformation> consumer) |
| throws MappingComposeException { |
| Set<MappingInformation> nonCompasableNewInfos = Sets.newIdentityHashSet(); |
| for (MappingInformation existingInfo : existingMappingInformation) { |
| boolean hasBeenComposed = false; |
| for (MappingInformation newInfo : newMappingInformation) { |
| if (newInfo.shouldCompose(existingInfo)) { |
| nonCompasableNewInfos.add(newInfo); |
| consumer.accept(newInfo.compose(existingInfo)); |
| hasBeenComposed = true; |
| } |
| } |
| if (!hasBeenComposed) { |
| consumer.accept(existingInfo); |
| } |
| } |
| newMappingInformation.forEach( |
| info -> { |
| if (!nonCompasableNewInfos.contains(info)) { |
| consumer.accept(info); |
| } |
| }); |
| } |
| |
| public void write(ChainableStringConsumer consumer) { |
| consumer.accept(originalName).accept(" -> ").accept(renamedName).accept(":\n"); |
| additionalMappingInfo.forEach(info -> consumer.accept("# " + info.serialize()).accept("\n")); |
| writeFields(consumer); |
| writeMethods(consumer); |
| } |
| |
| private void writeFields(ChainableStringConsumer consumer) { |
| ArrayList<MemberNaming> fieldNamings = new ArrayList<>(fieldMembers.values()); |
| fieldNamings.sort(Comparator.comparing(MemberNaming::getOriginalName)); |
| fieldNamings.forEach( |
| naming -> consumer.accept(INDENTATION).accept(naming.toString()).accept("\n")); |
| } |
| |
| private void writeMethods(ChainableStringConsumer consumer) { |
| Map<String, List<MappedRange>> signatureToMappedRanges = new HashMap<>(); |
| methodsWithoutPosition.forEach( |
| (ignored, mapped) -> { |
| consumer.accept(INDENTATION).accept(mapped.toString()).accept("\n"); |
| for (MappingInformation info : mapped.getAdditionalMappingInformation()) { |
| consumer.accept(INDENTATION).accept("# ").accept(info.serialize()).accept("\n"); |
| } |
| }); |
| methodsWithPosition |
| .values() |
| .forEach( |
| segmentTree -> { |
| segmentTree.visitSegments( |
| mappedRanges -> { |
| MethodSignature originalSignature = ListUtils.last(mappedRanges).signature; |
| List<MappedRange> put = |
| signatureToMappedRanges.put( |
| originalSignature.getName() + "_" + originalSignature, mappedRanges); |
| assert put == null; |
| }); |
| }); |
| ArrayList<String> strings = new ArrayList<>(signatureToMappedRanges.keySet()); |
| Collections.sort(strings); |
| for (String key : strings) { |
| signatureToMappedRanges |
| .get(key) |
| .forEach( |
| mappedRange -> { |
| consumer.accept(INDENTATION).accept(mappedRange.toString()).accept("\n"); |
| for (MappingInformation info : mappedRange.getAdditionalMappingInformation()) { |
| consumer.accept(INDENTATION).accept("# ").accept(info.serialize()).accept("\n"); |
| } |
| }); |
| } |
| } |
| |
| public ComposingClassBuilder commit(ComposingClassBuilder classBuilder) |
| throws MappingComposeException { |
| ComposingClassBuilder newClassBuilder = |
| new ComposingClassBuilder( |
| originalName, classBuilder.renamedName, committed, null, options); |
| composeMappingInformation( |
| classBuilder.additionalMappingInfo, |
| additionalMappingInfo, |
| newClassBuilder.additionalMappingInfo::add); |
| // Composed field namings and method namings should be freely composable by addition since |
| // any renaming/position change should have removed the existing committed mapping. |
| putAll( |
| newClassBuilder.fieldMembers, |
| fieldMembers, |
| classBuilder.fieldMembers, |
| (committed, add) -> { |
| assert committed == null; |
| return add; |
| }); |
| putAll( |
| newClassBuilder.methodsWithoutPosition, |
| methodsWithoutPosition, |
| classBuilder.methodsWithoutPosition, |
| (committed, add) -> { |
| if (committed != null && add != null) { |
| throw new MappingComposeException( |
| "Cannot compose duplicate methods without position in class '" |
| + renamedName |
| + "': '" |
| + committed |
| + "' and '" |
| + add); |
| } |
| return committed != null ? committed : add; |
| }); |
| putAll( |
| newClassBuilder.methodsWithPosition, |
| methodsWithPosition, |
| classBuilder.methodsWithPosition, |
| (committed, add) -> add); |
| return newClassBuilder; |
| } |
| |
| private <S extends Signature, V> void putAll( |
| Map<S, V> output, |
| Map<S, V> committed, |
| Map<S, V> toAdd, |
| ThrowingBiFunction<V, V, V, MappingComposeException> compose) |
| throws MappingComposeException { |
| assert output.isEmpty(); |
| output.putAll(committed); |
| for (Entry<S, V> kvEntry : toAdd.entrySet()) { |
| output.put( |
| kvEntry.getKey(), compose.apply(output.get(kvEntry.getKey()), kvEntry.getValue())); |
| } |
| } |
| |
| private static class RangeBuilder { |
| |
| private int start = Integer.MAX_VALUE; |
| private int end = Integer.MIN_VALUE; |
| |
| private void addRange(Range range) { |
| if (range != null) { |
| start = Math.min(start, range.from); |
| end = Math.max(end, range.to); |
| } |
| } |
| |
| private boolean hasValue() { |
| return start < Integer.MAX_VALUE; |
| } |
| |
| private int getStartOrNoRangeFrom() { |
| return hasValue() ? start : NO_RANGE_FROM; |
| } |
| |
| private int getEndOrNoRangeFrom() { |
| return hasValue() ? end : NO_RANGE_FROM; |
| } |
| |
| private boolean isPreamble() { |
| return hasValue() && start == end && start == 0; |
| } |
| } |
| |
| private static class ComputedOutlineInformation { |
| private final Set<OutlineCallsiteMappingInformation> |
| outlineCallsiteMappingInformationToPatchUp = Sets.newIdentityHashSet(); |
| private OutlineMappingInformation seenOutlineMappingInformation = null; |
| } |
| } |
| } |