From ec8c5155569cf5697b8c76b5ce4652e99fa96dd7 Mon Sep 17 00:00:00 2001 From: Ilia Smirnov Date: Tue, 16 Jan 2024 18:14:21 +0100 Subject: [PATCH] WI-75699 Add Go To "method to test" and "test to method" navigations GitOrigin-RevId: 2e0dc1e8cedf3cb2dfe8505cac1201211fe76d7a --- .../com/pestphp/pest/PestTestDescriptor.kt | 23 ++++++ .../PestGotoTargetPresentationProvider.kt | 22 ++++++ .../com/pestphp/pest/goto/PestTestFinder.kt | 74 ++++++++++++++++--- src/main/resources/META-INF/plugin.xml | 1 + .../pestphp/pest/goto/PestTestFinderTest.kt | 50 +++++++++++++ .../pest/goto/PestTestFinder/App/User.php | 15 ++++ .../goto/PestTestFinder/test/App/UserTest.php | 20 ++++- 7 files changed, 192 insertions(+), 13 deletions(-) create mode 100644 src/main/kotlin/com/pestphp/pest/goto/PestGotoTargetPresentationProvider.kt diff --git a/src/main/kotlin/com/pestphp/pest/PestTestDescriptor.kt b/src/main/kotlin/com/pestphp/pest/PestTestDescriptor.kt index f5767a4e..d5d29238 100644 --- a/src/main/kotlin/com/pestphp/pest/PestTestDescriptor.kt +++ b/src/main/kotlin/com/pestphp/pest/PestTestDescriptor.kt @@ -1,10 +1,33 @@ package com.pestphp.pest import com.intellij.util.SmartList +import com.jetbrains.php.lang.psi.elements.Method +import com.jetbrains.php.lang.psi.elements.PhpClass import com.jetbrains.php.phpunit.PhpUnitTestDescriptor import com.jetbrains.php.testFramework.PhpTestCreateInfo +import java.util.* +/** + * findTests, findClasses, and findMethods return empty collections, + * since Pest tests are function calls, not methods, and therefore are not located in PHP classes + */ object PestTestDescriptor : PhpUnitTestDescriptor() { + override fun findTests(clazz: PhpClass): MutableCollection { + return Collections.emptySet() + } + + override fun findTests(method: Method): MutableCollection { + return Collections.emptySet() + } + + override fun findClasses(test: PhpClass, testName: String): MutableCollection { + return Collections.emptySet() + } + + override fun findMethods(testMethod: Method): MutableCollection { + return Collections.emptySet() + } + override fun getTestCreateInfos(): MutableCollection { return SmartList(PestTestCreateInfo) } diff --git a/src/main/kotlin/com/pestphp/pest/goto/PestGotoTargetPresentationProvider.kt b/src/main/kotlin/com/pestphp/pest/goto/PestGotoTargetPresentationProvider.kt new file mode 100644 index 00000000..32195c88 --- /dev/null +++ b/src/main/kotlin/com/pestphp/pest/goto/PestGotoTargetPresentationProvider.kt @@ -0,0 +1,22 @@ +package com.pestphp.pest.goto + +import com.intellij.codeInsight.navigation.GotoTargetPresentationProvider +import com.intellij.openapi.util.NlsSafe +import com.intellij.platform.backend.presentation.TargetPresentation +import com.intellij.psi.PsiElement +import com.pestphp.pest.PestIcons +import com.pestphp.pest.getPestTestName +import com.pestphp.pest.isPestTestReference + +class PestGotoTargetPresentationProvider: GotoTargetPresentationProvider { + override fun getTargetPresentation(element: PsiElement, differentNames: Boolean): TargetPresentation? { + if (element.isPestTestReference()) { + @NlsSafe val pestTestName = element.getPestTestName() + return TargetPresentation.builder(pestTestName ?: element.containingFile.name) + .containerText(element.containingFile?.presentation?.locationString) + .icon(PestIcons.Logo) + .presentation() + } + return null + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/pestphp/pest/goto/PestTestFinder.kt b/src/main/kotlin/com/pestphp/pest/goto/PestTestFinder.kt index 8f2f1706..67bd387d 100644 --- a/src/main/kotlin/com/pestphp/pest/goto/PestTestFinder.kt +++ b/src/main/kotlin/com/pestphp/pest/goto/PestTestFinder.kt @@ -1,23 +1,47 @@ package com.pestphp.pest.goto +import com.intellij.openapi.util.Pair import com.intellij.psi.PsiElement -import com.intellij.psi.PsiManager +import com.intellij.psi.PsiFile import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.util.PsiTreeUtil import com.intellij.testIntegration.TestFinder +import com.intellij.testIntegration.TestFinderHelper import com.intellij.util.indexing.FileBasedIndex import com.jetbrains.php.PhpIndex +import com.jetbrains.php.lang.psi.elements.FunctionReference +import com.jetbrains.php.lang.psi.elements.Method import com.jetbrains.php.lang.psi.elements.PhpClass +import com.pestphp.pest.getPestTestName +import com.pestphp.pest.getPestTests import com.pestphp.pest.indexers.PestTestIndex +import com.pestphp.pest.inspections.convertTestNameToSentenceCase import com.pestphp.pest.isPestTestFile class PestTestFinder : TestFinder { - override fun findClassesForTest(element: PsiElement): MutableCollection { - return PhpIndex.getInstance(element.project) + /** + * @return methods if the given element is a psi child of Pest function call, + * classes otherwise + */ + override fun findClassesForTest(element: PsiElement): Collection { + val classes = PhpIndex.getInstance(element.project) .getClassesByNameInScope( element.containingFile.name.removeSuffix("Test.php"), GlobalSearchScope.projectScope(element.project) ) + + val testName = PsiTreeUtil.getNonStrictParentOfType(element, FunctionReference::class.java) + ?.getPestTestName() + ?.split(" ") + ?.joinToString("") + ?: return classes + val methodsAndProximityScores = classes.flatMap { phpClass -> phpClass.ownMethods.toList() } + .filter { method -> testName.contains(method.name, ignoreCase = true) } + .map { method -> Pair(method, TestFinderHelper.calcTestNameProximity(method.name, testName)) } + return if (!methodsAndProximityScores.isEmpty()) + TestFinderHelper.getSortedElements(methodsAndProximityScores, true) + else + classes } override fun findSourceElement(from: PsiElement): PsiElement? { @@ -28,21 +52,47 @@ class PestTestFinder : TestFinder { return element.containingFile.isPestTestFile() } - override fun findTestsForClass(element: PsiElement): MutableCollection { - val phpClass = PsiTreeUtil.getNonStrictParentOfType(element, PhpClass::class.java) ?: return arrayListOf() + override fun findTestsForClass(element: PsiElement): Collection { + val parent = PsiTreeUtil.getNonStrictParentOfType(element, PhpClass::class.java, Method::class.java) ?: return arrayListOf() + + return when (parent) { + is PhpClass -> findTestFilesForClass(parent) + is Method -> findTestsForMethod(parent) + else -> arrayListOf() + } + } + + private fun findTestsForMethod(method: Method): List { + val phpClass = method.containingClass ?: return emptyList() + val sentenceCaseMethodName = convertTestNameToSentenceCase(method.name) + + val testsAndProximityScores = findTestFilesForClass(phpClass) + .flatMap { psiFile -> + psiFile.getPestTests().mapNotNull { test -> + val testName = test.getPestTestName() ?: return@mapNotNull null + val sentenceCaseTestName = if (testName.contains(' ')) testName else convertTestNameToSentenceCase(testName) + if (sentenceCaseTestName.contains(sentenceCaseMethodName, ignoreCase = true)) { + Pair(test, TestFinderHelper.calcTestNameProximity(sentenceCaseMethodName, sentenceCaseTestName)) + } else { + null + } + } + } + return testsAndProximityScores.sortedBy { it.second }.map { it.first } + } + private fun findTestFilesForClass(phpClass: PhpClass): List { return FileBasedIndex.getInstance().getAllKeys( PestTestIndex.key, - element.project - ).filter { it.contains(phpClass.name) } - .flatMap { + phpClass.project + ).filter { testClassName -> testClassName.contains(phpClass.name) } + .flatMap { testClassName -> FileBasedIndex.getInstance().getContainingFiles( PestTestIndex.key, - it, - GlobalSearchScope.projectScope(element.project) + testClassName, + GlobalSearchScope.projectScope(phpClass.project) ) } - .mapNotNull { PsiManager.getInstance(element.project).findFile(it) } - .toCollection(ArrayList()) + .mapNotNull { testFile -> phpClass.manager.findFile(testFile) } } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index e51f11b6..b0b003ee 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -40,6 +40,7 @@ + diff --git a/src/test/kotlin/com/pestphp/pest/goto/PestTestFinderTest.kt b/src/test/kotlin/com/pestphp/pest/goto/PestTestFinderTest.kt index 81be350b..2305e35f 100644 --- a/src/test/kotlin/com/pestphp/pest/goto/PestTestFinderTest.kt +++ b/src/test/kotlin/com/pestphp/pest/goto/PestTestFinderTest.kt @@ -1,8 +1,10 @@ package com.pestphp.pest.goto +import com.intellij.testFramework.UsefulTestCase import com.jetbrains.php.lang.psi.PhpFile import com.jetbrains.php.lang.psi.PhpPsiUtil import com.pestphp.pest.PestLightCodeFixture +import com.pestphp.pest.getPestTestName import junit.framework.TestCase class PestTestFinderTest : PestLightCodeFixture() { @@ -40,4 +42,52 @@ class PestTestFinderTest : PestLightCodeFixture() { ) ) } + + fun testFindTestFileForClass() { + val file = myFixture.configureByFile("App/User.php") + val testFile = myFixture.configureByFile("test/App/UserTest.php") + + TestCase.assertSame( + testFile, + PestTestFinder().findTestsForClass(PhpPsiUtil.findAllClasses(file as PhpFile).first()).first(), + ) + } + + fun testFindClassForTestFile() { + val file = myFixture.configureByFile("App/User.php") + val testFile = myFixture.configureByFile("test/App/UserTest.php") + + TestCase.assertSame( + PhpPsiUtil.findAllClasses(file as PhpFile).first(), + PestTestFinder().findClassesForTest(testFile.firstChild).first() + ) + } + + fun testFindTestsForMethod() { + val file = myFixture.configureByFile("App/User.php") + val testFile = myFixture.configureByFile("test/App/UserTest.php") + val method = PhpPsiUtil.findAllClasses(file as PhpFile).first().findMethodByName("isPestDeveloper") + val tests = testFile.firstChild.children.map { it.firstChild }.filter { + it.getPestTestName()?.contains("is pest developer") == true + } + + UsefulTestCase.assertSameElements( + PestTestFinder().findTestsForClass(method!!), + tests + ) + } + + fun testFindMethodsForTest() { + val file = myFixture.configureByFile("App/User.php") + val testFile = myFixture.configureByFile("test/App/UserTest.php") + val test = testFile.firstChild.children.map { it.firstChild }.first { + it.getPestTestName() == "is pest developer" + } + val methods = PhpPsiUtil.findAllClasses(file as PhpFile).first().methods.filter { it.name.contains("is") } + + UsefulTestCase.assertSameElements( + PestTestFinder().findClassesForTest(test), + methods + ) + } } diff --git a/src/test/resources/com/pestphp/pest/goto/PestTestFinder/App/User.php b/src/test/resources/com/pestphp/pest/goto/PestTestFinder/App/User.php index c1d85034..8ee8c402 100644 --- a/src/test/resources/com/pestphp/pest/goto/PestTestFinder/App/User.php +++ b/src/test/resources/com/pestphp/pest/goto/PestTestFinder/App/User.php @@ -7,4 +7,19 @@ public function getName(): String { return "Oliver Nybroe"; } + + public function isPestDeveloper(): bool + { + return true; + } + + public function isPest(): bool + { + return true; + } + + public function is(): bool + { + return true; + } } \ No newline at end of file diff --git a/src/test/resources/com/pestphp/pest/goto/PestTestFinder/test/App/UserTest.php b/src/test/resources/com/pestphp/pest/goto/PestTestFinder/test/App/UserTest.php index e0baf2cf..3903e2b9 100644 --- a/src/test/resources/com/pestphp/pest/goto/PestTestFinder/test/App/UserTest.php +++ b/src/test/resources/com/pestphp/pest/goto/PestTestFinder/test/App/UserTest.php @@ -5,5 +5,23 @@ test("Can get user's name", function () { $user = new User(); - $this->asserEquals("Oliver Nybroe", $user->getName()); + $this->assertEquals("Oliver Nybroe", $user->getName()); +}); + +test("is pest developer", function () { + $user = new User(); + + $this->assertTrue($user->isPestDeveloper()); +}); + +test("is pest developer check if not false", function () { + $user = new User(); + + $this->assertNotEquals(false, $user->isPestDeveloper()); +}); + +test("incorrect is pest developer", function () { + $user = new User(); + + $this->assertNotEquals(false, $user->isPestDeveloper()); }); \ No newline at end of file