blob: df6a224168a616fbef0412025c4e363cc286e7de [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.keepanno.ast;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
public class KeepBindings {
public static Builder builder() {
return new Builder();
}
private static final KeepBindings NONE_INSTANCE = new KeepBindings(Collections.emptyMap());
private final Map<KeepBindingSymbol, Binding> bindings;
private KeepBindings(Map<KeepBindingSymbol, Binding> bindings) {
assert bindings != null;
this.bindings = bindings;
}
public static KeepBindings none() {
return NONE_INSTANCE;
}
public Binding get(KeepBindingReference bindingReference) {
return bindings.get(bindingReference.getName());
}
public Binding get(KeepBindingSymbol bindingReference) {
return bindings.get(bindingReference);
}
public int size() {
return bindings.size();
}
public boolean isEmpty() {
return bindings.isEmpty();
}
public void forEach(BiConsumer<KeepBindingSymbol, KeepItemPattern> fn) {
bindings.forEach((name, binding) -> fn.accept(name, binding.getItem()));
}
@Override
public String toString() {
return "{"
+ bindings.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining(", "));
}
/**
* A unique binding.
*
* <p>The uniqueness / identity of a binding is critical as a binding denotes both the static
* structural constraint on an item and potentially the identity of a concrete match in the
* precondition of a rule.
*
* <p>In terms of proguard keep rules it provides the difference of structural constraints
* illustrated by:
*
* <pre>
* -keepclasswithmembers class *Foo { void bar(); void baz(); }
* </pre>
*
* and
*
* <pre>
* -keepclasswithmembers class *Foo { void bar(); }
* -keepclasswithmembers class *Foo { void baz(); }
* </pre>
*
* The former which only matches classes with the Foo suffix that have both bar and baz can be
* expressed with a binding of *Foo and the two method items expressed by referencing the shared
* class. Without the binding the targets will give rise to the latter rules which are independent
* of each other.
*
* <p>In terms of proguard keep rules it also provides the difference in back-referencing into
* preconditions:
*
* <pre>
* -if class *Foo -keep class *Foo { void <init>(...); }
* </pre>
*
* and
*
* <pre>
* -if class *Foo -keep class <1>Foo { void <init>(...); }
* </pre>
*
* The first case will keep all classes matching *Foo and there default constructors if any single
* live class matches. The second will keep the default constructors of the live classes that
* match.
*
* <p>This wrapper ensures that pattern equality does not imply binding equality.
*/
public static class Binding {
private final KeepItemPattern item;
public Binding(KeepItemPattern item) {
this.item = item;
}
public KeepItemPattern getItem() {
return item;
}
@Override
public boolean equals(Object obj) {
return this == obj;
}
@Override
public int hashCode() {
return System.identityHashCode(this);
}
@Override
public String toString() {
return item.toString();
}
}
public static class KeepBindingSymbol {
private final String hint;
private String suffix = "";
public KeepBindingSymbol(String hint) {
this.hint = hint;
}
private void setSuffix(String suffix) {
this.suffix = suffix;
}
@Override
public String toString() {
return hint + suffix;
}
@Override
public boolean equals(Object obj) {
return this == obj;
}
@Override
public int hashCode() {
return System.identityHashCode(this);
}
}
public static class Builder {
private final Map<String, KeepBindingSymbol> reserved = new HashMap<>();
private final Map<KeepBindingSymbol, KeepItemPattern> bindings = new IdentityHashMap<>();
public KeepBindingSymbol generateFreshSymbol(String hint) {
// Allocate a fresh non-forgeable symbol. The actual name is chosen at build time.
return new KeepBindingSymbol(hint);
}
public KeepBindingSymbol create(String name) {
KeepBindingSymbol symbol = new KeepBindingSymbol(name);
KeepBindingSymbol old = reserved.put(name, symbol);
if (old != null) {
throw new KeepEdgeException("Multiple bindings with name '" + name + "'");
}
return symbol;
}
public Builder addBinding(KeepBindingSymbol symbol, KeepItemPattern itemPattern) {
if (symbol == null || itemPattern == null) {
throw new KeepEdgeException("Invalid binding of '" + symbol + "'");
}
KeepItemPattern old = bindings.put(symbol, itemPattern);
if (old != null) {
throw new KeepEdgeException("Multiple definitions for binding '" + symbol + "'");
}
return this;
}
public KeepItemPattern getItemForBinding(KeepBindingSymbol symbol) {
return bindings.get(symbol);
}
@SuppressWarnings("ReferenceEquality")
public KeepBindings build() {
if (bindings.isEmpty()) {
return NONE_INSTANCE;
}
Map<KeepBindingSymbol, Binding> definitions = new HashMap<>(bindings.size());
for (KeepBindingSymbol symbol : bindings.keySet()) {
// The reserved symbols are a subset of all symbols. Those that are not yet reserved denote
// symbols that must be "unique" in the set of symbols, but that do not have a specific
// name. Now that all symbols are known we can give each of these a unique name.
KeepBindingSymbol defined = reserved.get(symbol.toString());
if (defined != symbol) {
// For each undefined symbol we try to use the "hint" as its name, if the name is already
// reserved for another symbol then we search for the first non-reserved name with an
// integer suffix.
int i = 0;
while (defined != null) {
symbol.setSuffix(Integer.toString(++i));
defined = reserved.get(symbol.toString());
}
reserved.put(symbol.toString(), symbol);
}
definitions.put(symbol, verifyAndCreateBinding(symbol));
}
return new KeepBindings(definitions);
}
private Binding verifyAndCreateBinding(KeepBindingSymbol bindingDefinitionSymbol) {
KeepItemPattern pattern = bindings.get(bindingDefinitionSymbol);
for (KeepBindingReference bindingReference : pattern.getBindingReferences()) {
// Currently, it is not possible to define mutually recursive items, so we only need
// to check against self.
if (bindingReference.getName().equals(bindingDefinitionSymbol)) {
throw new KeepEdgeException("Recursive binding for name '" + bindingReference + "'");
}
if (!bindings.containsKey(bindingReference.getName())) {
throw new KeepEdgeException(
"Undefined binding for binding '"
+ bindingReference.getName()
+ "' or type '"
+ (bindingReference.isClassType() ? "class" : "member")
+ "' referenced in binding of '"
+ bindingDefinitionSymbol
+ "'");
}
}
return new Binding(pattern);
}
}
}