mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-10 09:39:37 +07:00
462 lines
17 KiB
Java
462 lines
17 KiB
Java
/*
|
|
* Copyright 2000-2016 JetBrains s.r.o.
|
|
*
|
|
* 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.intellij.codeInspection.bytecodeAnalysis;
|
|
|
|
import com.intellij.codeInspection.dataFlow.MethodContract.ValueConstraint;
|
|
import com.intellij.codeInspection.dataFlow.StandardMethodContract;
|
|
import com.intellij.openapi.progress.ProgressManager;
|
|
import com.intellij.openapi.util.ThreadLocalCachedValue;
|
|
import com.intellij.openapi.vfs.CharsetToolkit;
|
|
import com.intellij.psi.*;
|
|
import com.intellij.psi.util.PsiTreeUtil;
|
|
import com.intellij.psi.util.TypeConversionUtil;
|
|
import one.util.streamex.StreamEx;
|
|
import org.jetbrains.annotations.NotNull;
|
|
import org.jetbrains.annotations.Nullable;
|
|
|
|
import java.security.MessageDigest;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.util.*;
|
|
|
|
import static com.intellij.codeInspection.bytecodeAnalysis.Direction.*;
|
|
import static com.intellij.codeInspection.bytecodeAnalysis.ProjectBytecodeAnalysis.LOG;
|
|
|
|
/**
|
|
* @author lambdamix
|
|
*/
|
|
public class BytecodeAnalysisConverter {
|
|
|
|
// how many bytes are taken from class fqn digest
|
|
public static final int CLASS_HASH_SIZE = 10;
|
|
// how many bytes are taken from signature digest
|
|
public static final int SIGNATURE_HASH_SIZE = 4;
|
|
public static final int HASH_SIZE = CLASS_HASH_SIZE + SIGNATURE_HASH_SIZE;
|
|
|
|
private static final ThreadLocalCachedValue<MessageDigest> HASHER_CACHE = new ThreadLocalCachedValue<MessageDigest>() {
|
|
@Override
|
|
public MessageDigest create() {
|
|
try {
|
|
return MessageDigest.getInstance("MD5");
|
|
} catch (NoSuchAlgorithmException exception) {
|
|
throw new RuntimeException(exception);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void init(MessageDigest value) {
|
|
value.reset();
|
|
}
|
|
};
|
|
|
|
public static MessageDigest getMessageDigest() {
|
|
return HASHER_CACHE.getValue();
|
|
}
|
|
|
|
/**
|
|
* Converts an equation over asm keys into equation over small hash keys.
|
|
*/
|
|
@NotNull
|
|
static DirectionResultPair convert(@NotNull Equation equation, @NotNull MessageDigest md) {
|
|
ProgressManager.checkCanceled();
|
|
|
|
Result rhs = equation.rhs;
|
|
HResult hResult;
|
|
if (rhs instanceof Final) {
|
|
hResult = new HFinal(((Final)rhs).value);
|
|
}
|
|
else if (rhs instanceof Pending) {
|
|
Pending pending = (Pending)rhs;
|
|
Set<Product> sumOrigin = pending.sum;
|
|
HComponent[] components = new HComponent[sumOrigin.size()];
|
|
int componentI = 0;
|
|
for (Product prod : sumOrigin) {
|
|
HKey[] intProd = new HKey[prod.ids.size()];
|
|
int idI = 0;
|
|
for (Key key : prod.ids) {
|
|
intProd[idI] = asmKey(key, md);
|
|
idI++;
|
|
}
|
|
HComponent intIdComponent = new HComponent(prod.value, intProd);
|
|
components[componentI] = intIdComponent;
|
|
componentI++;
|
|
}
|
|
hResult = new HPending(components);
|
|
} else {
|
|
Effects wrapper = (Effects)rhs;
|
|
Set<EffectQuantum> effects = wrapper.effects;
|
|
Set<HEffectQuantum> hEffects = new HashSet<>();
|
|
for (EffectQuantum effect : effects) {
|
|
if (effect == EffectQuantum.TopEffectQuantum) {
|
|
hEffects.add(HEffectQuantum.TopEffectQuantum);
|
|
}
|
|
else if (effect == EffectQuantum.ThisChangeQuantum) {
|
|
hEffects.add(HEffectQuantum.ThisChangeQuantum);
|
|
}
|
|
else if (effect instanceof EffectQuantum.ParamChangeQuantum) {
|
|
EffectQuantum.ParamChangeQuantum paramChangeQuantum = (EffectQuantum.ParamChangeQuantum)effect;
|
|
hEffects.add(new HEffectQuantum.ParamChangeQuantum(paramChangeQuantum.n));
|
|
}
|
|
else if (effect instanceof EffectQuantum.CallQuantum) {
|
|
EffectQuantum.CallQuantum callQuantum = (EffectQuantum.CallQuantum)effect;
|
|
hEffects.add(new HEffectQuantum.CallQuantum(asmKey(callQuantum.key, md), callQuantum.data, callQuantum.isStatic));
|
|
}
|
|
}
|
|
hResult = new HEffects(hEffects);
|
|
}
|
|
return new DirectionResultPair(equation.id.direction.asInt(), hResult);
|
|
}
|
|
|
|
/**
|
|
* Converts an asm method key to a small hash key (HKey)
|
|
*/
|
|
@NotNull
|
|
public static HKey asmKey(@NotNull Key key, @NotNull MessageDigest md) {
|
|
byte[] classDigest = md.digest(key.method.internalClassName.getBytes(CharsetToolkit.UTF8_CHARSET));
|
|
md.update(key.method.methodName.getBytes(CharsetToolkit.UTF8_CHARSET));
|
|
md.update(key.method.methodDesc.getBytes(CharsetToolkit.UTF8_CHARSET));
|
|
byte[] sigDigest = md.digest();
|
|
byte[] digest = new byte[HASH_SIZE];
|
|
System.arraycopy(classDigest, 0, digest, 0, CLASS_HASH_SIZE);
|
|
System.arraycopy(sigDigest, 0, digest, CLASS_HASH_SIZE, SIGNATURE_HASH_SIZE);
|
|
return new HKey(digest, key.direction.asInt(), key.stable, key.negated);
|
|
}
|
|
|
|
/**
|
|
* Converts a Psi method to a small hash key (HKey).
|
|
* Returns null if conversion is impossible (something is not resolvable).
|
|
*/
|
|
@Nullable
|
|
public static HKey psiKey(@NotNull PsiMethod psiMethod, @NotNull Direction direction, @NotNull MessageDigest md) {
|
|
final PsiClass psiClass = PsiTreeUtil.getParentOfType(psiMethod, PsiClass.class, false);
|
|
if (psiClass == null) {
|
|
return null;
|
|
}
|
|
byte[] classDigest = psiClassDigest(psiClass, md);
|
|
if (classDigest == null) {
|
|
return null;
|
|
}
|
|
byte[] sigDigest = methodDigest(psiMethod, md);
|
|
if (sigDigest == null) {
|
|
return null;
|
|
}
|
|
byte[] digest = new byte[HASH_SIZE];
|
|
System.arraycopy(classDigest, 0, digest, 0, CLASS_HASH_SIZE);
|
|
System.arraycopy(sigDigest, 0, digest, CLASS_HASH_SIZE, SIGNATURE_HASH_SIZE);
|
|
return new HKey(digest, direction.asInt(), true, false);
|
|
}
|
|
|
|
@Nullable
|
|
private static byte[] psiClassDigest(@NotNull PsiClass psiClass, @NotNull MessageDigest md) {
|
|
String descriptor = descriptor(psiClass, 0, false);
|
|
if (descriptor == null) {
|
|
return null;
|
|
}
|
|
return md.digest(descriptor.getBytes(CharsetToolkit.UTF8_CHARSET));
|
|
}
|
|
|
|
@Nullable
|
|
private static byte[] methodDigest(@NotNull PsiMethod psiMethod, @NotNull MessageDigest md) {
|
|
String descriptor = descriptor(psiMethod);
|
|
if (descriptor == null) {
|
|
return null;
|
|
}
|
|
return md.digest(descriptor.getBytes(CharsetToolkit.UTF8_CHARSET));
|
|
}
|
|
|
|
@Nullable
|
|
private static String descriptor(@NotNull PsiMethod psiMethod) {
|
|
StringBuilder sb = new StringBuilder();
|
|
final PsiClass psiClass = PsiTreeUtil.getParentOfType(psiMethod, PsiClass.class, false);
|
|
if (psiClass == null) {
|
|
return null;
|
|
}
|
|
PsiClass outerClass = psiClass.getContainingClass();
|
|
boolean isInnerClassConstructor = psiMethod.isConstructor() && (outerClass != null) && !psiClass.hasModifierProperty(PsiModifier.STATIC);
|
|
PsiParameter[] parameters = psiMethod.getParameterList().getParameters();
|
|
PsiType returnType = psiMethod.getReturnType();
|
|
|
|
sb.append(returnType == null ? "<init>" : psiMethod.getName());
|
|
sb.append('(');
|
|
|
|
String desc;
|
|
|
|
if (isInnerClassConstructor) {
|
|
desc = descriptor(outerClass, 0, true);
|
|
if (desc == null) {
|
|
return null;
|
|
}
|
|
sb.append(desc);
|
|
}
|
|
for (PsiParameter parameter : parameters) {
|
|
desc = descriptor(parameter.getType());
|
|
if (desc == null) {
|
|
return null;
|
|
}
|
|
sb.append(desc);
|
|
}
|
|
sb.append(')');
|
|
if (returnType == null) {
|
|
sb.append('V');
|
|
} else {
|
|
desc = descriptor(returnType);
|
|
if (desc == null) {
|
|
return null;
|
|
} else {
|
|
sb.append(desc);
|
|
}
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
@Nullable
|
|
private static String descriptor(@NotNull PsiClass psiClass, int dimensions, boolean full) {
|
|
PsiFile containingFile = psiClass.getContainingFile();
|
|
if (!(containingFile instanceof PsiClassOwner)) {
|
|
LOG.debug("containingFile was not resolved for " + psiClass.getQualifiedName());
|
|
return null;
|
|
}
|
|
PsiClassOwner psiFile = (PsiClassOwner)containingFile;
|
|
String packageName = psiFile.getPackageName();
|
|
String qname = psiClass.getQualifiedName();
|
|
if (qname == null) {
|
|
return null;
|
|
}
|
|
String className;
|
|
if (packageName.length() > 0) {
|
|
className = qname.substring(packageName.length() + 1).replace('.', '$');
|
|
} else {
|
|
className = qname.replace('.', '$');
|
|
}
|
|
StringBuilder sb = new StringBuilder();
|
|
for (int i = 0; i < dimensions; i++) {
|
|
sb.append('[');
|
|
}
|
|
if (full) {
|
|
sb.append('L');
|
|
}
|
|
if (packageName.length() > 0) {
|
|
sb.append(packageName.replace('.', '/'));
|
|
sb.append('/');
|
|
}
|
|
sb.append(className);
|
|
if (full) {
|
|
sb.append(';');
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
@Nullable
|
|
private static String descriptor(@NotNull PsiType psiType) {
|
|
int dimensions = 0;
|
|
psiType = TypeConversionUtil.erasure(psiType);
|
|
if (psiType instanceof PsiArrayType) {
|
|
PsiArrayType arrayType = (PsiArrayType)psiType;
|
|
psiType = arrayType.getDeepComponentType();
|
|
dimensions = arrayType.getArrayDimensions();
|
|
}
|
|
|
|
if (psiType instanceof PsiClassType) {
|
|
PsiClass psiClass = ((PsiClassType)psiType).resolve();
|
|
if (psiClass != null) {
|
|
return descriptor(psiClass, dimensions, true);
|
|
}
|
|
else {
|
|
LOG.debug("resolve was null for " + psiType.getCanonicalText());
|
|
return null;
|
|
}
|
|
}
|
|
else if (psiType instanceof PsiPrimitiveType) {
|
|
StringBuilder sb = new StringBuilder();
|
|
for (int i = 0; i < dimensions; i++) {
|
|
sb.append('[');
|
|
}
|
|
if (PsiType.VOID.equals(psiType)) {
|
|
sb.append('V');
|
|
}
|
|
else if (PsiType.BOOLEAN.equals(psiType)) {
|
|
sb.append('Z');
|
|
}
|
|
else if (PsiType.CHAR.equals(psiType)) {
|
|
sb.append('C');
|
|
}
|
|
else if (PsiType.BYTE.equals(psiType)) {
|
|
sb.append('B');
|
|
}
|
|
else if (PsiType.SHORT.equals(psiType)) {
|
|
sb.append('S');
|
|
}
|
|
else if (PsiType.INT.equals(psiType)) {
|
|
sb.append('I');
|
|
}
|
|
else if (PsiType.FLOAT.equals(psiType)) {
|
|
sb.append('F');
|
|
}
|
|
else if (PsiType.LONG.equals(psiType)) {
|
|
sb.append('J');
|
|
}
|
|
else if (PsiType.DOUBLE.equals(psiType)) {
|
|
sb.append('D');
|
|
}
|
|
return sb.toString();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Given a PSI method and its primary HKey enumerate all contract keys for it.
|
|
*
|
|
* @param psiMethod psi method
|
|
* @param primaryKey primary stable keys
|
|
* @return corresponding (stable!) keys
|
|
*/
|
|
@NotNull
|
|
public static ArrayList<HKey> mkInOutKeys(@NotNull PsiMethod psiMethod, @NotNull HKey primaryKey) {
|
|
PsiParameter[] parameters = psiMethod.getParameterList().getParameters();
|
|
ArrayList<HKey> keys = new ArrayList<>(parameters.length * 2 + 2);
|
|
keys.add(primaryKey);
|
|
for (int i = 0; i < parameters.length; i++) {
|
|
if (!(parameters[i].getType() instanceof PsiPrimitiveType)) {
|
|
keys.add(primaryKey.withDirection(new InOut(i, Value.NotNull)));
|
|
keys.add(primaryKey.withDirection(new InOut(i, Value.Null)));
|
|
keys.add(primaryKey.withDirection(new InThrow(i, Value.NotNull)));
|
|
keys.add(primaryKey.withDirection(new InThrow(i, Value.Null)));
|
|
} else if (PsiType.BOOLEAN.equals(parameters[i].getType())) {
|
|
keys.add(primaryKey.withDirection(new InOut(i, Value.True)));
|
|
keys.add(primaryKey.withDirection(new InOut(i, Value.False)));
|
|
keys.add(primaryKey.withDirection(new InThrow(i, Value.True)));
|
|
keys.add(primaryKey.withDirection(new InThrow(i, Value.False)));
|
|
}
|
|
}
|
|
return keys;
|
|
}
|
|
|
|
/**
|
|
* Given `solution` of all dependencies of a method with the `methodKey`, converts this solution into annotations.
|
|
*
|
|
* @param solution solution of equations
|
|
* @param methodAnnotations annotations to which corresponding solutions should be added
|
|
* @param methodKey a primary key of a method being analyzed. not it is stable
|
|
* @param arity arity of this method (hint for constructing @Contract annotations)
|
|
*/
|
|
public static void addMethodAnnotations(@NotNull Map<HKey, Value> solution, @NotNull MethodAnnotations methodAnnotations, @NotNull HKey methodKey, int arity) {
|
|
List<StandardMethodContract> contractClauses = new ArrayList<>();
|
|
Set<HKey> notNulls = methodAnnotations.notNulls;
|
|
Set<HKey> pures = methodAnnotations.pures;
|
|
Map<HKey, String> contracts = methodAnnotations.contractsValues;
|
|
|
|
for (Map.Entry<HKey, Value> entry : solution.entrySet()) {
|
|
// NB: keys from Psi are always stable, so we need to stabilize keys from equations
|
|
Value value = entry.getValue();
|
|
if (value == Value.Top || value == Value.Bot || (value == Value.Fail && !pures.contains(methodKey))) {
|
|
continue;
|
|
}
|
|
HKey key = entry.getKey().mkStable();
|
|
Direction direction = key.getDirection();
|
|
HKey baseKey = key.mkBase();
|
|
if (!methodKey.equals(baseKey)) {
|
|
continue;
|
|
}
|
|
if (value == Value.NotNull && direction == Out) {
|
|
notNulls.add(methodKey);
|
|
}
|
|
else if (value == Value.Pure && direction == Pure) {
|
|
pures.add(methodKey);
|
|
}
|
|
else if (direction instanceof ParamValueBasedDirection) {
|
|
contractClauses.add(contractElement(arity, (ParamValueBasedDirection)direction, value));
|
|
}
|
|
}
|
|
|
|
// no contract clauses for @NotNull methods
|
|
if (!notNulls.contains(methodKey) && !contractClauses.isEmpty()) {
|
|
Map<Boolean, List<StandardMethodContract>> partition =
|
|
StreamEx.of(contractClauses).partitioningBy(c -> c.getReturnValue() == ValueConstraint.THROW_EXCEPTION);
|
|
List<StandardMethodContract> failingContracts = squashContracts(partition.get(true));
|
|
List<StandardMethodContract> nonFailingContracts = squashContracts(partition.get(false));
|
|
// Sometimes "null,_->!null;!null,_->!null" contracts are inferred for some reason
|
|
// They are squashed to "_,_->!null" which is better expressed as @NotNull annotation
|
|
if(nonFailingContracts.size() == 1) {
|
|
StandardMethodContract contract = nonFailingContracts.get(0);
|
|
if(contract.getReturnValue() == ValueConstraint.NOT_NULL_VALUE && contract.isTrivial()) {
|
|
nonFailingContracts = Collections.emptyList();
|
|
notNulls.add(methodKey);
|
|
}
|
|
}
|
|
// Failing contracts go first
|
|
String result = StreamEx.of(failingContracts, nonFailingContracts)
|
|
.flatMap(list -> list.stream()
|
|
.map(Object::toString)
|
|
.map(str -> str.replace(" ", "")) // for compatibility with existing tests
|
|
.sorted())
|
|
.joining(";");
|
|
if(!result.isEmpty()) {
|
|
contracts.put(methodKey, '"'+result+'"');
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
@NotNull
|
|
private static List<StandardMethodContract> squashContracts(List<StandardMethodContract> contractClauses) {
|
|
// If there's a pair of contracts yielding the same value like "null,_->true", "!null,_->true"
|
|
// then trivial contract should be used like "_,_->true"
|
|
StandardMethodContract soleContract = StreamEx.ofPairs(contractClauses, (c1, c2) -> {
|
|
if (c1.getReturnValue() != c2.getReturnValue()) return null;
|
|
int idx = -1;
|
|
for (int i = 0; i < c1.arguments.length; i++) {
|
|
ValueConstraint left = c1.arguments[i];
|
|
ValueConstraint right = c2.arguments[i];
|
|
if (left == ValueConstraint.ANY_VALUE && right == ValueConstraint.ANY_VALUE) continue;
|
|
if (idx >= 0 || left == right || left != right.negate()) return null;
|
|
idx = i;
|
|
}
|
|
return c1;
|
|
}).nonNull().findFirst().orElse(null);
|
|
if(soleContract != null) {
|
|
Arrays.fill(soleContract.arguments, ValueConstraint.ANY_VALUE);
|
|
contractClauses = Collections.singletonList(soleContract);
|
|
}
|
|
return contractClauses;
|
|
}
|
|
|
|
public static void addEffectAnnotations(Map<HKey, Set<HEffectQuantum>> puritySolutions,
|
|
MethodAnnotations result,
|
|
HKey methodKey,
|
|
boolean constructor) {
|
|
for (Map.Entry<HKey, Set<HEffectQuantum>> entry : puritySolutions.entrySet()) {
|
|
Set<HEffectQuantum> effects = entry.getValue();
|
|
HKey key = entry.getKey().mkStable();
|
|
HKey baseKey = key.mkBase();
|
|
if (!methodKey.equals(baseKey)) {
|
|
continue;
|
|
}
|
|
if (effects.isEmpty() || (constructor && effects.size() == 1 && effects.contains(HEffectQuantum.ThisChangeQuantum))) {
|
|
// Pure constructor is allowed to change "this" object as this is a new object anyways
|
|
result.pures.add(methodKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static StandardMethodContract contractElement(int arity, ParamValueBasedDirection inOut, Value value) {
|
|
final ValueConstraint[] constraints = new ValueConstraint[arity];
|
|
Arrays.fill(constraints, ValueConstraint.ANY_VALUE);
|
|
constraints[inOut.paramIndex] = inOut.inValue.toValueConstraint();
|
|
return new StandardMethodContract(constraints, value.toValueConstraint());
|
|
}
|
|
|
|
}
|