diff --git a/plugins/devkit/resources/META-INF/plugin.xml b/plugins/devkit/resources/META-INF/plugin.xml index 49b5568116e5..9108d97e3e45 100644 --- a/plugins/devkit/resources/META-INF/plugin.xml +++ b/plugins/devkit/resources/META-INF/plugin.xml @@ -129,6 +129,10 @@ + diff --git a/plugins/devkit/resources/inspectionDescriptions/StatefulEp.html b/plugins/devkit/resources/inspectionDescriptions/StatefulEp.html new file mode 100644 index 000000000000..e54b56826000 --- /dev/null +++ b/plugins/devkit/resources/inspectionDescriptions/StatefulEp.html @@ -0,0 +1,5 @@ + + +Are you sure about holding heavy objects in Extension Point implementation? + + \ No newline at end of file diff --git a/plugins/devkit/src/inspections/StatefulEpInspection.java b/plugins/devkit/src/inspections/StatefulEpInspection.java new file mode 100644 index 000000000000..46f266422792 --- /dev/null +++ b/plugins/devkit/src/inspections/StatefulEpInspection.java @@ -0,0 +1,98 @@ +/* + * Copyright 2000-2015 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 org.jetbrains.idea.devkit.inspections; + +import com.intellij.codeInspection.InspectionManager; +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.codeInspection.ProblemHighlightType; +import com.intellij.openapi.project.Project; +import com.intellij.psi.*; +import com.intellij.psi.util.InheritanceUtil; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.idea.devkit.util.ExtensionPointCandidate; +import org.jetbrains.idea.devkit.util.ExtensionPointLocator; + +import java.util.List; + +public class StatefulEpInspection extends DevKitInspectionBase { + @Nullable + @Override + public ProblemDescriptor[] checkClass(@NotNull PsiClass psiClass, @NotNull InspectionManager manager, boolean isOnTheFly) { + PsiField[] fields = psiClass.getFields(); + if (fields.length == 0) return super.checkClass(psiClass, manager, isOnTheFly); + final boolean isQuickFix = InheritanceUtil.isInheritor(psiClass, LocalQuickFix.class.getCanonicalName()); + if (isQuickFix || shouldCheck(psiClass)) { + List result = ContainerUtil.newArrayList(); + for (final PsiField field : fields) { + Checker projectChecker = new Checker(Project.class) { + @Override + boolean predicate() { + return !field.hasModifierProperty(PsiModifier.FINAL); + } + }; + Checker psiChecker = new Checker(PsiElement.class) { + @NotNull + @Override + String getMessage() { + return "Potential memory leak: don't hold PsiElement, use SmartPsiElementPointer instead of" + + (isQuickFix ? "; also see LocalQuickFixOnPsiElement" : ""); + } + }; + Checker refChecker = new Checker(PsiReference.class); + for (Checker checker : new Checker[]{projectChecker, psiChecker, refChecker}) { + checker.check(field, manager, isOnTheFly, result); + } + } + return result.toArray(new ProblemDescriptor[result.size()]); + } + return super.checkClass(psiClass, manager, isOnTheFly); + } + + boolean shouldCheck(@NotNull PsiClass psiClass) { + for (ExtensionPointCandidate candidate : new ExtensionPointLocator(psiClass).findSuperCandidates()) { + if (ExtensionPointLocator.isImplementedEp(psiClass, candidate)) return true; + } + return false; + } + + private static class Checker { + @NotNull Class myClass; + + private Checker(@NotNull Class psiClass) { + myClass = psiClass; + } + + private void check(@NotNull PsiField field, @NotNull InspectionManager manager, boolean isOnTheFly, @NotNull List result) { + if (predicate() && InheritanceUtil.isInheritor(field.getType(), myClass.getCanonicalName())) { + String message = getMessage(); + String actual = message.isEmpty() ? "Don't use " + myClass.getSimpleName() + " as a field in extension points" : message; + result.add(manager.createProblemDescriptor(field, actual, true, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, isOnTheFly)); + } + } + + @NotNull + String getMessage() { + return ""; + } + + boolean predicate() { + return true; + } + } +} \ No newline at end of file diff --git a/plugins/devkit/src/util/ExtensionPointLocator.java b/plugins/devkit/src/util/ExtensionPointLocator.java index 5aa1858a2d79..c65d8e1113a7 100644 --- a/plugins/devkit/src/util/ExtensionPointLocator.java +++ b/plugins/devkit/src/util/ExtensionPointLocator.java @@ -27,6 +27,7 @@ import com.intellij.util.SmartList; import com.intellij.util.xml.DomElement; import com.intellij.util.xml.DomService; import com.intellij.util.xml.DomUtil; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.idea.devkit.dom.ExtensionPoint; import org.jetbrains.idea.devkit.dom.IdeaPlugin; @@ -70,13 +71,10 @@ public class ExtensionPointLocator { private static void findExtensionPointCandidates(PsiClass psiClass, final List list) { String name = psiClass.getQualifiedName(); - if (name == null) { - return; - } + if (name == null) return; - final Project project = psiClass.getProject(); - final Collection candidates = DomService.getInstance().getDomFileCandidates(IdeaPlugin.class, project, GlobalSearchScope.allScope(project)); - GlobalSearchScope scope = GlobalSearchScope.filesScope(project, candidates); + Project project = psiClass.getProject(); + GlobalSearchScope scope = getCandidatesScope(project); PsiSearchHelper.SERVICE.getInstance(project).processUsagesInNonJavaFiles(name, new PsiNonJavaFileReferenceProcessor() { @Override public boolean process(PsiFile file, int startOffset, int endOffset) { @@ -87,6 +85,27 @@ public class ExtensionPointLocator { }, scope); } + @NotNull + private static GlobalSearchScope getCandidatesScope(@NotNull Project project) { + Collection candidates = DomService.getInstance().getDomFileCandidates(IdeaPlugin.class, project, GlobalSearchScope.allScope(project)); + return GlobalSearchScope.filesScope(project, candidates); + } + + public static boolean isImplementedEp(@NotNull PsiClass psiClass, @NotNull final ExtensionPointCandidate candidate) { + String name = psiClass.getQualifiedName(); + if (name == null) return false; + + Project project = psiClass.getProject(); + GlobalSearchScope scope = getCandidatesScope(project); + return !PsiSearchHelper.SERVICE.getInstance(project).processUsagesInNonJavaFiles(name, new PsiNonJavaFileReferenceProcessor() { + @Override + public boolean process(PsiFile file, int startOffset, int endOffset) { + XmlTag tag = PsiTreeUtil.getParentOfType(file.findElementAt(startOffset), XmlTag.class); + return tag == null || !candidate.epName.endsWith(tag.getName()); + } + }, scope); + } + private static void processExtensionPointCandidate(PsiElement element, List list) { XmlTag tag = PsiTreeUtil.getParentOfType(element, XmlTag.class); if (tag == null) return;