| /* |
| * Copyright 2000-2014 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.openapi.progress.ProgressManager; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.psi.*; |
| import com.intellij.psi.util.PsiTreeUtil; |
| import com.intellij.psi.util.TypeConversionUtil; |
| 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.ProjectBytecodeAnalysis.LOG; |
| import static com.intellij.codeInspection.bytecodeAnalysis.Direction.*; |
| |
| /** |
| * @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; |
| |
| public static MessageDigest getMessageDigest() throws NoSuchAlgorithmException { |
| return MessageDigest.getInstance("MD5"); |
| } |
| |
| /** |
| * Converts an equation over asm keys into equation over small hash keys. |
| */ |
| @NotNull |
| static DirectionResultPair convert(@NotNull Equation<Key, Value> equation, @NotNull MessageDigest md) { |
| ProgressManager.checkCanceled(); |
| |
| Result<Key, Value> rhs = equation.rhs; |
| HResult result; |
| if (rhs instanceof Final) { |
| result = new HFinal(((Final<Key, Value>)rhs).value); |
| } else { |
| Pending<Key, Value> pending = (Pending<Key, Value>)rhs; |
| Set<Product<Key, Value>> sumOrigin = pending.sum; |
| HComponent[] components = new HComponent[sumOrigin.size()]; |
| int componentI = 0; |
| for (Product<Key, Value> prod : sumOrigin) { |
| HKey[] intProd = new HKey[prod.ids.size()]; |
| int idI = 0; |
| for (Key id : prod.ids) { |
| intProd[idI] = asmKey(id, md); |
| idI++; |
| } |
| HComponent intIdComponent = new HComponent(prod.value, intProd); |
| components[componentI] = intIdComponent; |
| componentI++; |
| } |
| result = new HPending(components); |
| } |
| return new DirectionResultPair(mkDirectionKey(equation.id.direction), result); |
| } |
| |
| /** |
| * 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()); |
| md.update(key.method.methodName.getBytes()); |
| md.update(key.method.methodDesc.getBytes()); |
| 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, mkDirectionKey(key.direction), key.stable); |
| } |
| |
| /** |
| * 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, mkDirectionKey(direction), true); |
| } |
| |
| @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()); |
| } |
| |
| @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()); |
| } |
| |
| @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 " + ((PsiClassType)psiType).getClassName()); |
| 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; |
| } |
| |
| |
| static int mkDirectionKey(Direction dir) { |
| if (dir == Out) { |
| return 0; |
| } |
| else if (dir == NullableOut) { |
| return 1; |
| } |
| else if (dir instanceof In) { |
| In in = (In)dir; |
| // nullity mask is 0/1 |
| return 2 + 8 * in.paramId() + in.nullityMask; |
| } |
| else { |
| InOut inOut = (InOut)dir; |
| return 4 + 8 * inOut.paramId() + inOut.valueId(); |
| } |
| } |
| |
| @NotNull |
| private static Direction extractDirection(int directionKey) { |
| if (directionKey == 0) { |
| return Out; |
| } |
| else if (directionKey == 1) { |
| return NullableOut; |
| } |
| else { |
| directionKey--; |
| int paramId = directionKey / 8; |
| int subDirection = directionKey % 8; |
| if (subDirection <= 2) { |
| return new In(paramId, subDirection - 1); |
| } |
| else { |
| return new InOut(paramId, Value.values()[subDirection - 3]); |
| } |
| } |
| } |
| |
| /** |
| * Given a PSI method and its primary HKey enumerate all contract keys for it. |
| */ |
| @NotNull |
| public static ArrayList<HKey> mkInOutKeys(@NotNull PsiMethod psiMethod, @NotNull HKey primaryKey) { |
| PsiParameter[] parameters = psiMethod.getParameterList().getParameters(); |
| ArrayList<HKey> keys = new ArrayList<HKey>(parameters.length * 2 + 1); |
| for (int i = 0; i < parameters.length; i++) { |
| PsiParameter parameter = parameters[i]; |
| PsiType parameterType = parameter.getType(); |
| if (parameterType instanceof PsiPrimitiveType) { |
| if (PsiType.BOOLEAN.equals(parameterType)) { |
| keys.add(primaryKey.updateDirection(mkDirectionKey(new InOut(i, Value.False)))); |
| keys.add(primaryKey.updateDirection(mkDirectionKey(new InOut(i, Value.True)))); |
| } |
| } else { |
| keys.add(primaryKey.updateDirection(mkDirectionKey(new InOut(i, Value.NotNull)))); |
| keys.add(primaryKey.updateDirection(mkDirectionKey(new InOut(i, Value.Null)))); |
| } |
| } |
| 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 |
| * @param arity arity of this method (hint for constructing @Contract annotations) |
| */ |
| public static void addMethodAnnotations(@NotNull HashMap<HKey, Value> solution, @NotNull MethodAnnotations methodAnnotations, @NotNull HKey methodKey, int arity) { |
| List<String> clauses = new ArrayList<String>(); |
| HashSet<HKey> notNulls = methodAnnotations.notNulls; |
| HashMap<HKey, String> contracts = methodAnnotations.contracts; |
| for (Map.Entry<HKey, Value> entry : solution.entrySet()) { |
| HKey key = entry.getKey().mkStable(); |
| Value value = entry.getValue(); |
| if (value == Value.Top || value == Value.Bot) { |
| continue; |
| } |
| Direction direction = extractDirection(key.dirKey); |
| if (value == Value.NotNull && direction == Out && methodKey.equals(key)) { |
| notNulls.add(key); |
| } |
| else if (direction instanceof InOut) { |
| HKey baseKey = key.mkBase(); |
| if (methodKey.equals(baseKey)) { |
| clauses.add(contractElement(arity, (InOut)direction, value)); |
| } |
| } |
| } |
| |
| if (!notNulls.contains(methodKey) && !clauses.isEmpty()) { |
| Collections.sort(clauses); |
| StringBuilder sb = new StringBuilder("\""); |
| StringUtil.join(clauses, ";", sb); |
| sb.append('"'); |
| contracts.put(methodKey, sb.toString().intern()); |
| } |
| } |
| |
| private static String contractValueString(@NotNull Value v) { |
| switch (v) { |
| case False: return "false"; |
| case True: return "true"; |
| case NotNull: return "!null"; |
| case Null: return "null"; |
| default: return "_"; |
| } |
| } |
| |
| private static String contractElement(int arity, InOut inOut, Value value) { |
| StringBuilder sb = new StringBuilder(); |
| for (int i = 0; i < arity; i++) { |
| Value currentValue = Value.Top; |
| if (i == inOut.paramIndex) { |
| currentValue = inOut.inValue; |
| } |
| if (i > 0) { |
| sb.append(','); |
| } |
| sb.append(contractValueString(currentValue)); |
| } |
| sb.append("->"); |
| sb.append(contractValueString(value)); |
| return sb.toString(); |
| } |
| |
| } |