cleanup (inspection "Java | Class structure | Utility class is not 'final'")
[idea/community.git] / platform / util / src / com / intellij / util / ref / DebugReflectionUtil.java
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.util.ref;
3
4 import com.intellij.ReviseWhenPortedToJDK;
5 import com.intellij.openapi.util.Condition;
6 import com.intellij.openapi.util.Key;
7 import com.intellij.openapi.util.UserDataHolderEx;
8 import com.intellij.openapi.util.text.StringUtil;
9 import com.intellij.util.PairProcessor;
10 import com.intellij.util.ReflectionUtil;
11 import com.intellij.util.concurrency.AtomicFieldUpdater;
12 import com.intellij.util.containers.FList;
13 import com.intellij.util.containers.Queue;
14 import gnu.trove.THashMap;
15 import gnu.trove.TIntHashSet;
16 import gnu.trove.TObjectHashingStrategy;
17 import org.jetbrains.annotations.NotNull;
18 import org.jetbrains.annotations.Nullable;
19 import sun.misc.Unsafe;
20
21 import java.lang.ref.Reference;
22 import java.lang.reflect.Field;
23 import java.lang.reflect.Method;
24 import java.lang.reflect.Modifier;
25 import java.util.ArrayList;
26 import java.util.Collection;
27 import java.util.List;
28 import java.util.Map;
29
30 public final class DebugReflectionUtil {
31   private static final Map<Class, Field[]> allFields = new THashMap<>(new TObjectHashingStrategy<Class>() {
32     // default strategy seems to be too slow
33     @Override
34     public int computeHashCode(Class aClass) {
35       return aClass.getName().hashCode();
36     }
37
38     @Override
39     public boolean equals(Class o1, Class o2) {
40       return o1 == o2;
41     }
42   });
43   private static final Field[] EMPTY_FIELD_ARRAY = new Field[0];
44   private static final Method Unsafe_shouldBeInitialized = ReflectionUtil.getDeclaredMethod(Unsafe.class, "shouldBeInitialized", Class.class);
45
46   private static Field @NotNull [] getAllFields(@NotNull Class<?> aClass) {
47     Field[] cached = allFields.get(aClass);
48     if (cached == null) {
49       try {
50         Field[] declaredFields = aClass.getDeclaredFields();
51         List<Field> fields = new ArrayList<>(declaredFields.length + 5);
52         for (Field declaredField : declaredFields) {
53           declaredField.setAccessible(true);
54           Class<?> type = declaredField.getType();
55           if (isTrivial(type)) continue; // unable to hold references, skip
56           fields.add(declaredField);
57         }
58         Class<?> superclass = aClass.getSuperclass();
59         if (superclass != null) {
60           for (Field sup : getAllFields(superclass)) {
61             if (!fields.contains(sup)) {
62               fields.add(sup);
63             }
64           }
65         }
66         cached = fields.isEmpty() ? EMPTY_FIELD_ARRAY : fields.toArray(new Field[0]);
67       }
68       catch (IncompatibleClassChangeError | NoClassDefFoundError | SecurityException e) {
69         //this exception may be thrown because there are two different versions of org.objectweb.asm.tree.ClassNode from different plugins
70         //I don't see any sane way to fix it until we load all the plugins by the same classloader in tests
71         cached = EMPTY_FIELD_ARRAY;
72       }
73       catch (@ReviseWhenPortedToJDK("9") RuntimeException e) {
74         // field.setAccessible() can now throw this exception when accessing unexported module
75         if (e.getClass().getName().equals("java.lang.reflect.InaccessibleObjectException")) {
76           cached = EMPTY_FIELD_ARRAY;
77         }
78         else {
79           throw e;
80         }
81       }
82
83       allFields.put(aClass, cached);
84     }
85     return cached;
86   }
87
88   private static boolean isTrivial(@NotNull Class<?> type) {
89     return type.isPrimitive() || type == String.class || type == Class.class || type.isArray() && isTrivial(type.getComponentType());
90   }
91
92   private static boolean isInitialized(@NotNull Class<?> root) {
93     if (Unsafe_shouldBeInitialized == null) return false;
94     boolean isInitialized = false;
95     try {
96       isInitialized = !(Boolean)Unsafe_shouldBeInitialized.invoke(AtomicFieldUpdater.getUnsafe(), root);
97     }
98     catch (Exception e) {
99       e.printStackTrace();
100     }
101     return isInitialized;
102   }
103
104   private static final Key<Boolean> REPORTED_LEAKED = Key.create("REPORTED_LEAKED");
105
106   public static boolean walkObjects(int maxDepth,
107                                     @NotNull Map<Object, String> startRoots,
108                                     @NotNull final Class<?> lookFor,
109                                     @NotNull Condition<Object> shouldExamineValue,
110                                     @NotNull final PairProcessor<Object, ? super BackLink> leakProcessor) {
111     TIntHashSet visited = new TIntHashSet(100);
112     Queue<BackLink> toVisit = new Queue<>(100);
113
114     for (Map.Entry<Object, String> entry : startRoots.entrySet()) {
115       Object startRoot = entry.getKey();
116       final String description = entry.getValue();
117       toVisit.addLast(new BackLink(startRoot, null, null){
118         @NotNull
119         @Override
120         String print() {
121           return super.print() +" (from "+description+")";
122         }
123       });
124     }
125
126     while (true) {
127       if (toVisit.isEmpty()) return true;
128       final BackLink backLink = toVisit.pullFirst();
129       if (backLink.depth > maxDepth) continue;
130       Object value = backLink.value;
131       if (lookFor.isAssignableFrom(value.getClass()) && markLeaked(value) && !leakProcessor.process(value, backLink)) return false;
132
133       if (visited.add(System.identityHashCode(value))) {
134         queueStronglyReferencedValues(toVisit, value, shouldExamineValue, backLink);
135       }
136     }
137   }
138
139   private static void queueStronglyReferencedValues(@NotNull Queue<? super BackLink> queue,
140                                                     @NotNull Object root,
141                                                     @NotNull Condition<Object> shouldExamineValue,
142                                                     @NotNull BackLink backLink) {
143     Class<?> rootClass = root.getClass();
144     for (Field field : getAllFields(rootClass)) {
145       String fieldName = field.getName();
146       if (root instanceof Reference && ("referent".equals(fieldName) || "discovered".equals(fieldName))) continue; // do not follow weak/soft refs
147       Object value;
148       try {
149         value = field.get(root);
150       }
151       catch (IllegalArgumentException | IllegalAccessException e) {
152         throw new RuntimeException(e);
153       }
154
155       queue(value, field, backLink, queue, shouldExamineValue);
156     }
157     if (rootClass.isArray()) {
158       try {
159         for (Object value : (Object[])root) {
160           queue(value, null, backLink, queue, shouldExamineValue);
161         }
162       }
163       catch (ClassCastException ignored) {
164       }
165     }
166     // check for objects leaking via static fields. process initialized classes only
167     if (root instanceof Class && isInitialized((Class)root)) {
168         for (Field field : getAllFields((Class)root)) {
169           if ((field.getModifiers() & Modifier.STATIC) == 0) continue;
170           try {
171             Object value = field.get(null);
172             queue(value, field, backLink, queue, shouldExamineValue);
173           }
174           catch (IllegalAccessException ignored) {
175           }
176         }
177     }
178   }
179
180   private static void queue(Object value, Field field, @NotNull BackLink backLink, @NotNull Queue<? super BackLink> queue,
181                             @NotNull Condition<Object> shouldExamineValue) {
182     if (value == null || isTrivial(value.getClass())) return;
183     if (shouldExamineValue.value(value)) {
184       BackLink newBackLink = new BackLink(value, field, backLink);
185       queue.addLast(newBackLink);
186     }
187   }
188
189   private static boolean markLeaked(Object leaked) {
190     return !(leaked instanceof UserDataHolderEx) || ((UserDataHolderEx)leaked).replace(REPORTED_LEAKED, null, Boolean.TRUE);
191   }
192
193   public static class BackLink {
194     @NotNull private final Object value;
195     private final Field field;
196     private final BackLink backLink;
197     private final int depth;
198
199     BackLink(@NotNull Object value, @Nullable Field field, @Nullable BackLink backLink) {
200       this.value = value;
201       this.field = field;
202       this.backLink = backLink;
203       depth = backLink == null ? 0 : backLink.depth + 1;
204     }
205
206     @Override
207     public String toString() {
208       String result = "";
209       BackLink backLink = this;
210       while (backLink != null) {
211         String s = backLink.print();
212         result += s;
213         backLink = backLink.backLink;
214       }
215       return result;
216     }
217
218     @NotNull
219     String print() {
220       String valueStr;
221       Object value = this.value;
222       try {
223         valueStr = value instanceof FList
224                    ? "FList (size=" + ((FList)value).size() + ")" :
225                    value instanceof Collection ? "Collection (size=" + ((Collection)value).size() + ")" :
226                    String.valueOf(value);
227         valueStr = StringUtil.first(StringUtil.convertLineSeparators(valueStr, "\\n"), 200, true);
228       }
229       catch (Throwable e) {
230         valueStr = "(" + e.getMessage() + " while computing .toString())";
231       }
232       Field field = this.field;
233       String fieldName = field == null ? "?" : field.getDeclaringClass().getName() + "." + field.getName();
234       return "via '" + fieldName + "'; Value: '" + valueStr + "' of " + value.getClass() + "\n";
235     }
236   }
237 }