DevKit: ExtensionLocator refactoring, fixes and performance improvements

This commit is contained in:
Yaroslav Pankratyev
2018-04-24 22:26:53 +07:00
parent 8ef96ef069
commit 3cab47cfa1
5 changed files with 131 additions and 75 deletions
@@ -7,18 +7,14 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.SmartPointerManager;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.PsiNonJavaFileReferenceProcessor;
import com.intellij.psi.search.PsiSearchHelper;
import com.intellij.psi.search.UsageSearchContext;
import com.intellij.psi.util.ClassUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlTag;
import com.intellij.psi.xml.XmlToken;
import com.intellij.util.SmartList;
import com.intellij.util.xml.DomElement;
import com.intellij.util.xml.DomManager;
import com.intellij.util.xml.DomUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -27,6 +23,7 @@ import org.jetbrains.idea.devkit.dom.ExtensionPoint;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;
public abstract class ExtensionLocator {
@NotNull
@@ -37,7 +34,7 @@ public abstract class ExtensionLocator {
return new ExtensionByClassLocator(project, clazz);
}
public static ExtensionLocator byPsiClass(PsiClass psiClass) {
public static ExtensionLocator byPsiClass(@NotNull PsiClass psiClass) {
return new ExtensionByPsiClassLocator(psiClass);
}
@@ -49,8 +46,8 @@ public abstract class ExtensionLocator {
return new ExtensionByExtensionPointLocator(extensionPoint, extensionId);
}
private static class ExtensionByClassLocator extends ExtensionLocator {
private static class ExtensionByClassLocator extends ExtensionLocator {
private final Project myProject;
private final JvmClass myClazz;
@@ -62,22 +59,7 @@ public abstract class ExtensionLocator {
@NotNull
@Override
public List<ExtensionCandidate> findCandidates() {
String jvmName = JvmClassUtil.getJvmClassName(myClazz);
if (jvmName == null) {
return Collections.emptyList();
}
List<ExtensionCandidate> result = new SmartList<>();
processExtensionDeclarations(myClazz.getQualifiedName(), myProject, (file, startOffset, endOffset) -> {
XmlTag tag = getXmlTagOfTokenElement(file, startOffset, jvmName, true);
DomElement dom = DomUtil.getDomElement(tag);
if (dom instanceof Extension && ((Extension)dom).getExtensionPoint() != null) {
result.add(new ExtensionCandidate(SmartPointerManager.getInstance(tag.getProject()).createSmartPsiElementPointer(tag)));
}
return true; // continue processing
});
return result;
return findCandidatesByClassName(JvmClassUtil.getJvmClassName(myClazz), myProject);
}
}
@@ -90,22 +72,7 @@ public abstract class ExtensionLocator {
@NotNull
public List<ExtensionCandidate> findCandidates() {
String name = ClassUtil.getJVMClassName(myPsiClass);
if (name == null) {
return Collections.emptyList();
}
List<ExtensionCandidate> result = new SmartList<>();
processExtensionDeclarations(myPsiClass.getQualifiedName(), myPsiClass.getProject(), (file, startOffset, endOffset) -> {
XmlTag tag = getXmlTagOfTokenElement(file, startOffset, name, true);
DomElement dom = DomUtil.getDomElement(tag);
if (dom instanceof Extension && ((Extension)dom).getExtensionPoint() != null) {
result.add(new ExtensionCandidate(SmartPointerManager.getInstance(tag.getProject()).createSmartPsiElementPointer(tag)));
}
return true; // continue processing
});
return result;
return findCandidatesByClassName(ClassUtil.getJVMClassName(myPsiClass), myPsiClass.getProject());
}
}
@@ -126,62 +93,69 @@ public abstract class ExtensionLocator {
return Collections.emptyList();
}
Project project = epTag.getProject();
DomManager domManager = DomManager.getDomManager(project);
// We must search for the last part of EP name, because for instance 'com.intellij.console.folding' extension
// may be declared as <extensions defaultExtensionNs="com"><intellij.console.folding ...
String epNameToSearch = StringUtil.substringAfterLast(myExtensionPoint.getEffectiveQualifiedName(), ".");
List<ExtensionCandidate> result = new SmartList<>();
processExtensionDeclarations(epNameToSearch, project, (file, startOffset, endOffset) -> {
XmlTag tag = getXmlTagOfTokenElement(file, startOffset, epNameToSearch, false);
if (tag == null) {
return true;
}
DomElement domElement = domManager.getDomElement(tag);
if (!(domElement instanceof Extension)) {
return true;
}
Extension extension = (Extension)domElement;
processExtensionDeclarations(epNameToSearch, epTag.getProject(), false, (extension, tag) -> {
ExtensionPoint ep = extension.getExtensionPoint();
if (ep == null) {
return true;
}
if (ep == null) return true;
if (StringUtil.equals(ep.getEffectiveQualifiedName(), myExtensionPoint.getEffectiveQualifiedName())
&& (myExtensionId == null || myExtensionId.equals(extension.getId().getStringValue()))) {
result.add(new ExtensionCandidate(SmartPointerManager.getInstance(tag.getProject()).createSmartPsiElementPointer(tag)));
return myExtensionId == null; // stop after the first found candidate if ID is specified
}
return true;
});
return result;
}
}
private static void processExtensionDeclarations(String name, Project project, PsiNonJavaFileReferenceProcessor referenceProcessor) {
private static void processExtensionDeclarations(@Nullable String name,
@NotNull Project project,
boolean strictMatch,
@NotNull BiFunction<Extension, XmlTag, Boolean> callback) {
if (name == null) return;
GlobalSearchScope scope = PluginRelatedLocatorsUtils.getCandidatesScope(project);
PsiSearchHelper.getInstance(project).processUsagesInNonJavaFiles(name, referenceProcessor, scope);
PsiSearchHelper.getInstance(project).processElementsWithWord((element, offsetInElement) -> {
if (!(element instanceof XmlTag)) {
return true;
}
PsiElement elementAtOffset = element.findElementAt(offsetInElement);
if (elementAtOffset == null) {
return true;
}
String foundText = elementAtOffset.getText();
if (!strictMatch && !StringUtil.contains(foundText, name)) {
return true;
}
if (strictMatch && !StringUtil.equals(foundText, name)) {
return true;
}
XmlTag tag = (XmlTag)element;
DomElement dom = DomUtil.getDomElement(tag);
if (!(dom instanceof Extension)) {
return true;
}
return callback.apply((Extension)dom, tag);
}, scope, name, UsageSearchContext.IN_FOREIGN_LANGUAGES, true);
}
@Nullable
private static XmlTag getXmlTagOfTokenElement(PsiFile file, int startOffset, String matchStr, boolean strictMatch) {
PsiElement element = file.findElementAt(startOffset);
String tokenText = element instanceof XmlToken ? element.getText() : null;
if (tokenText == null) {
return null;
}
if (!strictMatch && !StringUtil.contains(tokenText, matchStr)) {
return null;
}
if (strictMatch && !StringUtil.equals(tokenText, matchStr)) {
return null;
}
return PsiTreeUtil.getParentOfType(element, XmlTag.class);
private static List<ExtensionCandidate> findCandidatesByClassName(@Nullable String jvmClassName, @NotNull Project project) {
List<ExtensionCandidate> result = new SmartList<>();
processExtensionDeclarations(jvmClassName, project, true, (extension, tag) -> {
if (extension.getExtensionPoint() != null) {
result.add(new ExtensionCandidate(SmartPointerManager.getInstance(tag.getProject()).createSmartPsiElementPointer(tag)));
}
return true; // continue processing
});
return result;
}
}
@@ -0,0 +1,4 @@
public class SomeClass {
public static class MyList1 extends java.util.ArrayList {}
public static class MyList2 extends java.util.ArrayList {}
}
@@ -5,10 +5,11 @@
<extensionPoints>
<extensionPoint name="myEp" interface="java.util.List" />
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<myEp implementation="java.util.ArrayList" id="arrayListExtension1"/>
<myEp implementation="java.util.ArrayList" id="arrayListExtension2"/>
<myEp implementation="java.util.LinkedList" id="linkedListExtension"/>
<myEp implementation="SomeClass$MyList1" id="myList1Extension"/>
<myEp implementation="SomeClass.MyList2" id="myList2Extension"/> <!-- invalid nested class qualified name -->
</extensions>
</idea-plugin>
@@ -0,0 +1,71 @@
// Copyright 2000-2018 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.
package org.jetbrains.idea.devkit.util;
import com.intellij.execution.console.CustomizableConsoleFoldingBean;
import com.intellij.psi.PsiClass;
import com.intellij.testFramework.PlatformTestUtil;
import com.intellij.testFramework.builders.JavaModuleFixtureBuilder;
import com.intellij.testFramework.fixtures.JavaCodeInsightFixtureTestCase;
import com.intellij.ui.components.JBList;
import com.intellij.util.PathUtil;
import com.intellij.util.xmlb.annotations.Attribute;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class ExtensionLocatorPerformanceTest extends JavaCodeInsightFixtureTestCase {
@Override
protected void tuneFixture(JavaModuleFixtureBuilder moduleBuilder) {
moduleBuilder.addLibrary("util", PathUtil.getJarPathForClass(Attribute.class));
moduleBuilder.addLibrary("jblist", PathUtil.getJarPathForClass(JBList.class));
moduleBuilder.addLibrary("javaUtil", PathUtil.getJarPathForClass(ArrayList.class));
moduleBuilder.addLibrary("console", PathUtil.getJarPathForClass(CustomizableConsoleFoldingBean.class));
}
public void testLocatingByPsiClass() {
List<String> randomMethodNames = generateRandomMethodNames();
myFixture.configureByText("plugin.xml", generatePluginXmlText(randomMethodNames));
PsiClass psiClass = myFixture.addClass(generateJavaClassText(randomMethodNames));
PlatformTestUtil.startPerformanceTest("Locating extension tag by PsiClass", 2000, () -> {
List<ExtensionCandidate> result = ExtensionLocator.byPsiClass(psiClass).findCandidates();
assertSize(1, result);
}).attempts(1).assertTiming();
}
private static List<String> generateRandomMethodNames() {
return IntStream.range(0, 1000)
.mapToObj(i -> UUID.randomUUID().toString().replace("-", "").toLowerCase())
.collect(Collectors.toList());
}
private static String generateJavaClassText(List<String> methodNames) {
StringBuilder sb = new StringBuilder("package myPkg;\n\npublic class MyClass {\n");
methodNames.forEach(s -> sb.append(" public void ").append(s).append("() {}\n"));
sb.append("}");
return sb.toString();
}
private static String generatePluginXmlText(List<String> methodNames) {
StringBuilder sb = new StringBuilder().append("<idea-plugin>\n")
.append(" <id>com.intellij</id>\n")
.append(" <name>myPlugin</name>\n");
sb.append(" <extensionPoints>");
sb.append(" <extensionPoint name=\"stacktrace.fold\" beanClass=\"com.intellij.execution.console.CustomizableConsoleFoldingBean\"/>\n");
sb.append(" <extensionPoint name=\"myEp\" interface=\"java.util.ArrayList\" />\n");
sb.append(" </extensionPoints>");
sb.append("<extensions defaultExtensionNs=\"com.intellij\">");
methodNames.forEach(s -> sb.append("<stacktrace.fold substring=\"at myPkg.MyClass.").append(s).append("(\"/>\n"));
sb.append("<myEp implementation=\"myPkg.MyClass\"/>"); // the only valid target for locating
sb.append("</extensions>");
sb.append(" </extensions>\n</idea-plugin>");
return sb.toString();
}
}
@@ -66,12 +66,18 @@ public class ExtensionLocatorTest extends JavaCodeInsightFixtureTestCase {
public void testByPsiClass() {
myFixture.copyFileToProject("pluginXml_locateByPsiClass.xml");
myFixture.copyFileToProject("SomeClass.java");
JavaPsiFacade javaPsiFacade = myFixture.getJavaFacade();
PsiClass arrayListPsiClass = javaPsiFacade.findClass("java.util.ArrayList", GlobalSearchScope.allScope(getProject()));
PsiClass linkedListPsiClass = javaPsiFacade.findClass("java.util.LinkedList", GlobalSearchScope.allScope(getProject()));
PsiClass myList1PsiClass = javaPsiFacade.findClass("SomeClass.MyList1", GlobalSearchScope.allScope(getProject()));
PsiClass myList2PsiClass = javaPsiFacade.findClass("SomeClass.MyList2", GlobalSearchScope.allScope(getProject()));
verifyLocator(ExtensionLocator.byPsiClass(arrayListPsiClass), 2);
verifyLocator(ExtensionLocator.byPsiClass(linkedListPsiClass), 1);
verifyLocator(ExtensionLocator.byPsiClass(myList1PsiClass), 1);
verifyLocator(ExtensionLocator.byPsiClass(myList2PsiClass), 0);
}