blob: 4841274b93aeb0a4adcb503f2e7d7031b0a854fc [file] [log] [blame]
/*
* Copyright 2014-present Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.facebook.buck.tools.dxanalysis;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
public class MutabilityAnalyzer {
/**
* Deeply immutable "sorts" of types. See {@link org.objectweb.asm.Type#getSort()}.
*/
static final ImmutableSet<Integer> IMMUTABLE_TYPE_SORTS = ImmutableSet.of(
Type.BOOLEAN,
Type.BYTE,
Type.CHAR,
Type.DOUBLE,
Type.FLOAT,
Type.INT,
Type.LONG,
Type.SHORT);
/**
* External classes that are final and deeply immutable.
*/
private static final ImmutableSet<String> EXTERNAL_IMMUTABLE_CLASSES = ImmutableSet.of(
"java/lang/String",
"java/lang/Integer"
);
/**
* NonExternal classes that are non-final but have no inherent mutability.
*/
public static final ImmutableSet<String> EXTERNAL_IMMUTABLE_BASE_CLASSES = ImmutableSet.of(
"java/lang/Object",
"java/lang/Enum"
);
/**
* All classes under analysis.
*/
private final ImmutableMap<String, ClassNode> allClasses;
/**
* Any class that is definitely mutable. All subclasses become mutable as well.
* Superclasses are marked has having mutable descendents.
*/
private final Set<String> trulyMutableClasses;
/**
* Classes that have truly mutable descendents. Having a field of one of these types
* makes you truly mutable, but extending one does not.
*/
private final Set<String> classesWithMutableDescendents;
/**
* Only set once in go().
*/
@Nullable
private ImmutableSet<String> immutableClasses;
/**
* True iff we made progress during this round.
*/
private boolean madeProgress;
/**
* Log of messages.
*/
private List<String> log = new ArrayList<>();
public static MutabilityAnalyzer analyze(
ImmutableMap<String, ClassNode> allClasses) {
MutabilityAnalyzer analyzer = new MutabilityAnalyzer(allClasses);
analyzer.go();
return analyzer;
}
private MutabilityAnalyzer(ImmutableMap<String, ClassNode> allClasses) {
this.allClasses = allClasses;
trulyMutableClasses = new HashSet<>();
classesWithMutableDescendents = new HashSet<>();
}
public ImmutableList<String> getLog() {
return ImmutableList.copyOf(log);
}
public ImmutableSet<String> getImmutableClasses() {
return immutableClasses;
}
public boolean isTypeImmutable(String desc) {
Type type = Type.getType(desc);
if (IMMUTABLE_TYPE_SORTS.contains(type.getSort())) {
return true;
}
if (type.getSort() != Type.OBJECT) {
return false;
}
if (Sets.union(EXTERNAL_IMMUTABLE_CLASSES, immutableClasses)
.contains(type.getInternalName())) {
return true;
}
return false;
}
private void go() {
while (true) {
madeProgress = false;
for (ClassNode klass : allClasses.values()) {
analyzeClass(klass);
}
if (!madeProgress) {
break;
}
}
immutableClasses = ImmutableSet.copyOf(
Sets.difference(
allClasses.keySet(),
Sets.union(trulyMutableClasses, classesWithMutableDescendents)));
}
private void markClassTrulyMutable(String className) {
if (trulyMutableClasses.contains(className)) {
return;
}
if (!allClasses.containsKey(className)) {
throw new RuntimeException("Tried to mark external class '" + className + "' truly mutable");
}
trulyMutableClasses.add(className);
madeProgress = true;
markClassAsHavingMutableDescendents(className);
}
private void markClassAsHavingMutableDescendents(String className) {
if (classesWithMutableDescendents.contains(className)) {
return;
}
ClassNode klass = allClasses.get(className);
if (klass == null) {
return;
}
classesWithMutableDescendents.add(className);
madeProgress = true;
markClassAsHavingMutableDescendents(klass.superName);
for (String iface : klass.interfaces) {
markClassAsHavingMutableDescendents(iface);
}
}
private boolean superClassIsDefinitelyMutable(String superClassName) {
if (trulyMutableClasses.contains(superClassName)) {
return true;
}
if (EXTERNAL_IMMUTABLE_BASE_CLASSES.contains(superClassName)) {
return false;
}
if (!allClasses.containsKey(superClassName)) {
// All external classes are assumed to be mutable unless whitelisted.
return true;
}
// Assume internal classes are immutable until proven otherwise (in later rounds).
return false;
}
private boolean classIsDefinitelyMutable(ClassNode klass) {
if (superClassIsDefinitelyMutable(klass.superName)) {
log.add("Mutable parent: " + klass.name + " < " + klass.superName);
return true;
}
for (FieldNode field : klass.fields) {
if ((field.access & Opcodes.ACC_STATIC) != 0) {
continue;
}
if ((field.access & Opcodes.ACC_FINAL) == 0) {
log.add("Non-final field: " + klass.name + "#" + field.name + ":" + field.desc);
return true;
}
if (field.name.contains("$")) {
// Generated fields are assumed to be effectively immutable.
// This could, in principle, miss an issue like a static reference to a
// seemingly-immutable inner class object that maintains a hidden reference
// to its mutable outer object, but that seems unlikely.
continue;
}
Type type = Type.getType(field.desc);
if (IMMUTABLE_TYPE_SORTS.contains(type.getSort())) {
continue;
}
if (type.getSort() != Type.OBJECT) {
log.add("Odd sort: " + klass.name + "#" + field.name + ":" + field.desc);
return true;
}
if (allClasses.keySet().contains(type.getInternalName())) {
if (classesWithMutableDescendents.contains(type.getInternalName())) {
log.add("Internal mutable field: " + klass.name + "#" + field.name + ":" + field.desc);
return true;
}
} else {
if (!EXTERNAL_IMMUTABLE_CLASSES.contains(type.getInternalName())) {
log.add("External mutable field: " + klass.name + "#" + field.name + ":" + field.desc);
return true;
}
}
}
return false;
}
private void analyzeClass(ClassNode klass) {
if (trulyMutableClasses.contains(klass.name)) {
return;
}
if (classIsDefinitelyMutable(klass)) {
markClassTrulyMutable(klass.name);
}
}
}