Merge remote-tracking branch 'origin/master' into develar/is
[idea/community.git] / java / java-analysis-impl / src / com / intellij / codeInspection / ex / EntryPointsManagerBase.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.codeInspection.ex;
17
18 import com.intellij.ToolExtensionPoints;
19 import com.intellij.codeInsight.AnnotationUtil;
20 import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
21 import com.intellij.codeInspection.reference.*;
22 import com.intellij.openapi.application.ApplicationManager;
23 import com.intellij.openapi.components.PersistentStateComponent;
24 import com.intellij.openapi.components.ServiceManager;
25 import com.intellij.openapi.components.State;
26 import com.intellij.openapi.extensions.ExtensionPoint;
27 import com.intellij.openapi.extensions.ExtensionPointListener;
28 import com.intellij.openapi.extensions.Extensions;
29 import com.intellij.openapi.extensions.PluginDescriptor;
30 import com.intellij.openapi.extensions.impl.ExtensionPointImpl;
31 import com.intellij.openapi.project.Project;
32 import com.intellij.openapi.util.Comparing;
33 import com.intellij.openapi.util.JDOMExternalizableStringList;
34 import com.intellij.openapi.util.text.StringUtil;
35 import com.intellij.profile.codeInspection.InspectionProfileManager;
36 import com.intellij.psi.*;
37 import com.intellij.util.containers.ContainerUtil;
38 import com.intellij.util.ui.UIUtil;
39 import com.intellij.util.xmlb.SkipDefaultsSerializationFilter;
40 import com.intellij.util.xmlb.XmlSerializer;
41 import com.intellij.util.xmlb.annotations.*;
42 import org.jdom.Element;
43 import org.jetbrains.annotations.NonNls;
44 import org.jetbrains.annotations.NotNull;
45 import org.jetbrains.annotations.Nullable;
46
47 import java.util.*;
48
49 @State(name = "EntryPointsManager")
50 public abstract class EntryPointsManagerBase extends EntryPointsManager implements PersistentStateComponent<Element> {
51   @NonNls private static final String[] STANDARD_ANNOS = {
52     "javax.ws.rs.*",
53   };
54   private static final String PATTERN_SUFFIX = ".*";
55
56   // null means uninitialized
57   private volatile List<String> ADDITIONAL_ANNOS;
58
59   public Collection<String> getAdditionalAnnotations() {
60     List<String> annos = ADDITIONAL_ANNOS;
61     if (annos == null) {
62       annos = new ArrayList<String>();
63       Collections.addAll(annos, STANDARD_ANNOS);
64       final EntryPoint[] extensions = Extensions.getExtensions(ToolExtensionPoints.DEAD_CODE_TOOL, null);
65       for (EntryPoint extension : extensions) {
66         final String[] ignoredAnnotations = extension.getIgnoreAnnotations();
67         if (ignoredAnnotations != null) {
68           ContainerUtil.addAll(annos, ignoredAnnotations);
69         }
70       }
71       ADDITIONAL_ANNOS = annos = Collections.unmodifiableList(annos);
72     }
73     return annos;
74   }
75   public JDOMExternalizableStringList ADDITIONAL_ANNOTATIONS = new JDOMExternalizableStringList();
76   private final Map<String, SmartRefElementPointer> myPersistentEntryPoints;
77   private final List<ClassPattern> myPatterns = new ArrayList<>();
78   private final Set<RefElement> myTemporaryEntryPoints;
79   private static final String VERSION = "2.0";
80   @NonNls private static final String VERSION_ATTR = "version";
81   @NonNls private static final String ENTRY_POINT_ATTR = "entry_point";
82   private boolean myAddNonJavaEntries = true;
83   private boolean myResolved;
84   protected final Project myProject;
85   private long myLastModificationCount = -1;
86
87   public EntryPointsManagerBase(@NotNull Project project) {
88     myProject = project;
89     myTemporaryEntryPoints = new HashSet<RefElement>();
90     myPersistentEntryPoints = new LinkedHashMap<String, SmartRefElementPointer>(); // To keep the order between readExternal to writeExternal
91     final ExtensionPoint<EntryPoint> point = Extensions.getRootArea().getExtensionPoint(ToolExtensionPoints.DEAD_CODE_TOOL);
92     ((ExtensionPointImpl)point).addExtensionPointListener(new ExtensionPointListener<EntryPoint>() {
93       @Override
94       public void extensionAdded(@NotNull EntryPoint extension, @Nullable PluginDescriptor pluginDescriptor) {
95         extensionRemoved(extension, pluginDescriptor);
96       }
97
98       @Override
99       public void extensionRemoved(@NotNull EntryPoint extension, @Nullable PluginDescriptor pluginDescriptor) {
100         if (ADDITIONAL_ANNOS != null) {
101           ADDITIONAL_ANNOS = null;
102           UIUtil.invokeLaterIfNeeded(() -> {
103             if (!ApplicationManager.getApplication().isDisposed()) {
104               InspectionProfileManager.getInstance().fireProfileChanged(null);
105             }
106           });
107         }
108         // annotations changed
109         DaemonCodeAnalyzer.getInstance(myProject).restart();
110       }
111     }, false, this);
112   }
113
114   public static EntryPointsManagerBase getInstance(Project project) {
115     return (EntryPointsManagerBase)ServiceManager.getService(project, EntryPointsManager.class);
116   }
117
118   @Override
119   @SuppressWarnings({"HardCodedStringLiteral"})
120   public void loadState(Element element) {
121     Element entryPointsElement = element.getChild("entry_points");
122     if (entryPointsElement != null) {
123       final String version = entryPointsElement.getAttributeValue(VERSION_ATTR);
124       if (!Comparing.strEqual(version, VERSION)) {
125         convert(entryPointsElement, myPersistentEntryPoints);
126       }
127       else {
128         List content = entryPointsElement.getChildren();
129         for (final Object aContent : content) {
130           Element entryElement = (Element)aContent;
131           if (ENTRY_POINT_ATTR.equals(entryElement.getName())) {
132             SmartRefElementPointerImpl entryPoint = new SmartRefElementPointerImpl(entryElement);
133             myPersistentEntryPoints.put(entryPoint.getFQName(), entryPoint);
134           }
135         }
136       }
137     }
138     try {
139       ADDITIONAL_ANNOTATIONS.readExternal(element);
140     }
141     catch (Throwable ignored) {
142     }
143
144     getPatterns().clear();
145     for (Element pattern : element.getChildren("pattern")) {
146       final ClassPattern classPattern = new ClassPattern();
147       XmlSerializer.deserializeInto(classPattern, pattern);
148       getPatterns().add(classPattern);
149     }
150   }
151
152   @Override
153   @SuppressWarnings({"HardCodedStringLiteral"})
154   public Element getState()  {
155     Element element = new Element("state");
156     writeExternal(element, myPersistentEntryPoints, ADDITIONAL_ANNOTATIONS);
157     if (!getPatterns().isEmpty()) {
158       for (ClassPattern pattern : getPatterns()) {
159         element.addContent(XmlSerializer.serialize(pattern, new SkipDefaultsSerializationFilter()));
160       }
161     }
162     return element;
163   }
164
165   @SuppressWarnings({"HardCodedStringLiteral"})
166   public static void writeExternal(final Element element,
167                             final Map<String, SmartRefElementPointer> persistentEntryPoints,
168                             final JDOMExternalizableStringList additional_annotations) {
169     Element entryPointsElement = new Element("entry_points");
170     entryPointsElement.setAttribute(VERSION_ATTR, VERSION);
171     for (SmartRefElementPointer entryPoint : persistentEntryPoints.values()) {
172       assert entryPoint.isPersistent();
173       entryPoint.writeExternal(entryPointsElement);
174     }
175
176     element.addContent(entryPointsElement);
177     if (!additional_annotations.isEmpty()) {
178       additional_annotations.writeExternal(element);
179     }
180   }
181
182   @Override
183   public void resolveEntryPoints(@NotNull final RefManager manager) {
184     if (!myResolved) {
185       myResolved = true;
186       cleanup();
187       validateEntryPoints();
188
189       ApplicationManager.getApplication().runReadAction(() -> {
190         for (SmartRefElementPointer entryPoint : myPersistentEntryPoints.values()) {
191           if (entryPoint.resolve(manager)) {
192             RefEntity refElement = entryPoint.getRefElement();
193             ((RefElementImpl)refElement).setEntry(true);
194             ((RefElementImpl)refElement).setPermanentEntry(entryPoint.isPersistent());
195           }
196         }
197
198         for (ClassPattern pattern : myPatterns) {
199           final RefEntity refClass = manager.getReference(RefJavaManager.CLASS, pattern.pattern);
200           if (refClass != null) {
201             for (RefMethod constructor : ((RefClass)refClass).getConstructors()) {
202               ((RefMethodImpl)constructor).setEntry(true);
203               ((RefMethodImpl)constructor).setPermanentEntry(true);
204             }
205           }
206         }
207       });
208     }
209   }
210
211   private void purgeTemporaryEntryPoints() {
212     for (RefElement entryPoint : myTemporaryEntryPoints) {
213       ((RefElementImpl)entryPoint).setEntry(false);
214     }
215
216     myTemporaryEntryPoints.clear();
217   }
218
219   @Override
220   public void addEntryPoint(@NotNull RefElement newEntryPoint, boolean isPersistent) {
221     if (!newEntryPoint.isValid()) return;
222     if (isPersistent) {
223       if (newEntryPoint instanceof RefMethod && ((RefMethod)newEntryPoint).isConstructor() || newEntryPoint instanceof RefClass) {
224         final ClassPattern classPattern = new ClassPattern();
225         classPattern.pattern = new SmartRefElementPointerImpl(newEntryPoint, true).getFQName();
226         getPatterns().add(classPattern);
227
228         final EntryPointsManager entryPointsManager = getInstance(newEntryPoint.getElement().getProject());
229         if (this != entryPointsManager) {
230           entryPointsManager.addEntryPoint(newEntryPoint, true);
231         }
232
233         return;
234       }
235     }
236
237     if (newEntryPoint instanceof RefClass) {
238       RefClass refClass = (RefClass)newEntryPoint;
239
240       if (refClass.isAnonymous()) {
241         // Anonymous class cannot be an entry point.
242         return;
243       }
244
245       List<RefMethod> refConstructors = refClass.getConstructors();
246       if (refConstructors.size() == 1) {
247         addEntryPoint(refConstructors.get(0), isPersistent);
248       }
249       else if (refConstructors.size() > 1) {
250         // Many constructors here. Need to ask user which ones are used
251         for (RefMethod refConstructor : refConstructors) {
252           addEntryPoint(refConstructor, isPersistent);
253         }
254       }
255     }
256
257     if (!isPersistent) {
258       myTemporaryEntryPoints.add(newEntryPoint);
259       ((RefElementImpl)newEntryPoint).setEntry(true);
260     }
261     else {
262       if (myPersistentEntryPoints.get(newEntryPoint.getExternalName()) == null) {
263         final SmartRefElementPointerImpl entry = new SmartRefElementPointerImpl(newEntryPoint, true);
264         myPersistentEntryPoints.put(entry.getFQName(), entry);
265         ((RefElementImpl)newEntryPoint).setEntry(true);
266         ((RefElementImpl)newEntryPoint).setPermanentEntry(true);
267         if (entry.isPersistent()) { //do save entry points
268           final EntryPointsManager entryPointsManager = getInstance(newEntryPoint.getElement().getProject());
269           if (this != entryPointsManager) {
270             entryPointsManager.addEntryPoint(newEntryPoint, true);
271           }
272         }
273       }
274     }
275   }
276
277   @Override
278   public void removeEntryPoint(@NotNull RefElement anEntryPoint) {
279     myTemporaryEntryPoints.remove(anEntryPoint);
280
281     Set<Map.Entry<String, SmartRefElementPointer>> set = myPersistentEntryPoints.entrySet();
282     String key = null;
283     for (Map.Entry<String, SmartRefElementPointer> entry : set) {
284       SmartRefElementPointer value = entry.getValue();
285       if (value.getRefElement() == anEntryPoint) {
286         key = entry.getKey();
287         break;
288       }
289     }
290
291     if (key != null) {
292       myPersistentEntryPoints.remove(key);
293     }
294     ((RefElementImpl)anEntryPoint).setEntry(false);
295
296     if (anEntryPoint.isPermanentEntry() && anEntryPoint.isValid()) {
297       final Project project = anEntryPoint.getElement().getProject();
298       final EntryPointsManager entryPointsManager = getInstance(project);
299       if (this != entryPointsManager) {
300         entryPointsManager.removeEntryPoint(anEntryPoint);
301       }
302     }
303
304     if (anEntryPoint instanceof RefMethod && ((RefMethod)anEntryPoint).isConstructor() || anEntryPoint instanceof RefClass) {
305       final RefClass aClass = anEntryPoint instanceof RefClass ? (RefClass)anEntryPoint : ((RefMethod)anEntryPoint).getOwnerClass();
306       final String qualifiedName = aClass.getQualifiedName();
307       for (Iterator<ClassPattern> iterator = getPatterns().iterator(); iterator.hasNext(); ) {
308         if (Comparing.equal(iterator.next().pattern, qualifiedName)) {
309           //todo if inheritance or pattern?
310           iterator.remove();
311         }
312       }
313     }
314   }
315
316   @NotNull
317   @Override
318   public RefElement[] getEntryPoints() {
319     validateEntryPoints();
320     List<RefElement> entries = new ArrayList<RefElement>();
321     Collection<SmartRefElementPointer> collection = myPersistentEntryPoints.values();
322     for (SmartRefElementPointer refElementPointer : collection) {
323       final RefEntity elt = refElementPointer.getRefElement();
324       if (elt instanceof RefElement) {
325         entries.add((RefElement)elt);
326       }
327     }
328     entries.addAll(myTemporaryEntryPoints);
329
330     return entries.toArray(new RefElement[entries.size()]);
331   }
332
333   @Override
334   public void dispose() {
335     cleanup();
336   }
337
338   private void validateEntryPoints() {
339     long count = PsiManager.getInstance(myProject).getModificationTracker().getModificationCount();
340     if (count != myLastModificationCount) {
341       myLastModificationCount = count;
342       Collection<SmartRefElementPointer> collection = myPersistentEntryPoints.values();
343       SmartRefElementPointer[] entries = collection.toArray(new SmartRefElementPointer[collection.size()]);
344       for (SmartRefElementPointer entry : entries) {
345         RefElement refElement = (RefElement)entry.getRefElement();
346         if (refElement != null && !refElement.isValid()) {
347           myPersistentEntryPoints.remove(entry.getFQName());
348         }
349       }
350
351       final Iterator<RefElement> it = myTemporaryEntryPoints.iterator();
352       while (it.hasNext()) {
353         RefElement refElement = it.next();
354         if (!refElement.isValid()) {
355           it.remove();
356         }
357       }
358     }
359   }
360
361   @Override
362   public void cleanup() {
363     purgeTemporaryEntryPoints();
364     Collection<SmartRefElementPointer> entries = myPersistentEntryPoints.values();
365     for (SmartRefElementPointer entry : entries) {
366       entry.freeReference();
367     }
368   }
369
370   @Override
371   public boolean isAddNonJavaEntries() {
372     return myAddNonJavaEntries;
373   }
374
375   public void addAllPersistentEntries(EntryPointsManagerBase manager) {
376     myPersistentEntryPoints.putAll(manager.myPersistentEntryPoints);
377     myPatterns.addAll(manager.getPatterns());
378   }
379
380   public static void convert(Element element, final Map<String, SmartRefElementPointer> persistentEntryPoints) {
381     List content = element.getChildren();
382     for (final Object aContent : content) {
383       Element entryElement = (Element)aContent;
384       if (ENTRY_POINT_ATTR.equals(entryElement.getName())) {
385         String fqName = entryElement.getAttributeValue(SmartRefElementPointerImpl.FQNAME_ATTR);
386         final String type = entryElement.getAttributeValue(SmartRefElementPointerImpl.TYPE_ATTR);
387         if (Comparing.strEqual(type, RefJavaManager.METHOD)) {
388
389           int spaceIdx = fqName.indexOf(' ');
390           int lastDotIdx = fqName.lastIndexOf('.');
391
392           int parenIndex = fqName.indexOf('(');
393
394           while (lastDotIdx > parenIndex) lastDotIdx = fqName.lastIndexOf('.', lastDotIdx - 1);
395
396           boolean notype = false;
397           if (spaceIdx < 0 || spaceIdx + 1 > lastDotIdx || spaceIdx > parenIndex) {
398             notype = true;
399           }
400
401           final String className = fqName.substring(notype ? 0 : spaceIdx + 1, lastDotIdx);
402           final String methodSignature =
403               notype ? fqName.substring(lastDotIdx + 1) : fqName.substring(0, spaceIdx) + ' ' + fqName.substring(lastDotIdx + 1);
404
405           fqName = className + " " + methodSignature;
406         }
407         else if (Comparing.strEqual(type, RefJavaManager.FIELD)) {
408           final int lastDotIdx = fqName.lastIndexOf('.');
409           if (lastDotIdx > 0 && lastDotIdx < fqName.length() - 2) {
410             String className = fqName.substring(0, lastDotIdx);
411             String fieldName = fqName.substring(lastDotIdx + 1);
412             fqName = className + " " + fieldName;
413           }
414           else {
415             continue;
416           }
417         }
418         SmartRefElementPointerImpl entryPoint = new SmartRefElementPointerImpl(type, fqName);
419         persistentEntryPoints.put(entryPoint.getFQName(), entryPoint);
420       }
421     }
422   }
423
424   public void setAddNonJavaEntries(final boolean addNonJavaEntries) {
425     myAddNonJavaEntries = addNonJavaEntries;
426   }
427
428   @Override
429   public boolean isEntryPoint(@NotNull PsiElement element) {
430     if (!(element instanceof PsiModifierListOwner)) return false;
431     PsiModifierListOwner owner = (PsiModifierListOwner)element;
432     if (!ADDITIONAL_ANNOTATIONS.isEmpty() && ADDITIONAL_ANNOTATIONS.contains(Deprecated.class.getName()) &&
433         element instanceof PsiDocCommentOwner && ((PsiDocCommentOwner)element).isDeprecated()) {
434       return true;
435     }
436
437     if (element instanceof PsiClass) {
438       final String qualifiedName = ((PsiClass)element).getQualifiedName();
439       if (qualifiedName != null) {
440         for (ClassPattern pattern : getPatterns()) {
441           if (isAcceptedByPattern((PsiClass)element, qualifiedName, pattern, new HashSet<>())) {
442             return true;
443           }
444         }
445       }
446     }
447
448     return AnnotationUtil.checkAnnotatedUsingPatterns(owner, ADDITIONAL_ANNOTATIONS) ||
449            AnnotationUtil.checkAnnotatedUsingPatterns(owner, getAdditionalAnnotations());
450   }
451
452   private static boolean isAcceptedByPattern(@NotNull PsiClass element, String qualifiedName, ClassPattern pattern, Set<PsiClass> visited) {
453     if (qualifiedName == null) {
454       return false;
455     }
456
457     if (qualifiedName.equals(pattern.pattern)) {
458       return true;
459     }
460
461     if (pattern.pattern.endsWith(PATTERN_SUFFIX) && qualifiedName.startsWith(StringUtil.trimEnd(pattern.pattern, PATTERN_SUFFIX))) {
462       return true;
463     }
464
465     if (pattern.hierarchically) {
466       for (PsiClass superClass : element.getSupers()) {
467         final String superClassQualifiedName = superClass.getQualifiedName();
468         if (visited.add(superClass) && isAcceptedByPattern(superClass, superClassQualifiedName, pattern, visited)) {
469           return true;
470         }
471       }
472     }
473     return false;
474   }
475
476   public List<ClassPattern> getPatterns() {
477     return myPatterns;
478   }
479
480   @Tag("pattern")
481   public static class ClassPattern {
482     @Attribute("value")
483     public String pattern;
484     @Attribute("hierarchically")
485     public boolean hierarchically = false;
486
487     @Override
488     public boolean equals(Object o) {
489       if (this == o) return true;
490       if (o == null || getClass() != o.getClass()) return false;
491
492       ClassPattern otherPattern = (ClassPattern)o;
493
494       if (hierarchically != otherPattern.hierarchically) return false;
495       if (!pattern.equals(otherPattern.pattern)) return false;
496
497       return true;
498     }
499
500     @Override
501     public int hashCode() {
502       int result = pattern.hashCode();
503       result = 31 * result + (hierarchically ? 1 : 0);
504       return result;
505     }
506   }
507 }