blob: b6ea11b414c987172f8c06391486349e7175a509 [file] [log] [blame]
// 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;
}
}
}