Skip to content

Commit

Permalink
WI-75699 Add Go To "method to test" and "test to method" navigations
Browse files Browse the repository at this point in the history
GitOrigin-RevId: 2e0dc1e8cedf3cb2dfe8505cac1201211fe76d7a
  • Loading branch information
smirok authored and intellij-monorepo-bot committed Feb 29, 2024
1 parent 5ab16a9 commit ec8c515
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 13 deletions.
23 changes: 23 additions & 0 deletions src/main/kotlin/com/pestphp/pest/PestTestDescriptor.kt
Original file line number Diff line number Diff line change
@@ -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<PhpClass> {
return Collections.emptySet()
}

override fun findTests(method: Method): MutableCollection<Method> {
return Collections.emptySet()
}

override fun findClasses(test: PhpClass, testName: String): MutableCollection<PhpClass> {
return Collections.emptySet()
}

override fun findMethods(testMethod: Method): MutableCollection<Method> {
return Collections.emptySet()
}

override fun getTestCreateInfos(): MutableCollection<PhpTestCreateInfo> {
return SmartList(PestTestCreateInfo)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
74 changes: 62 additions & 12 deletions src/main/kotlin/com/pestphp/pest/goto/PestTestFinder.kt
Original file line number Diff line number Diff line change
@@ -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<PhpClass> {
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<PsiElement> {
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? {
Expand All @@ -28,21 +52,47 @@ class PestTestFinder : TestFinder {
return element.containingFile.isPestTestFile()
}

override fun findTestsForClass(element: PsiElement): MutableCollection<PsiElement> {
val phpClass = PsiTreeUtil.getNonStrictParentOfType(element, PhpClass::class.java) ?: return arrayListOf()
override fun findTestsForClass(element: PsiElement): Collection<PsiElement> {
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<FunctionReference> {
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<PsiFile> {
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) }
}
}
1 change: 1 addition & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<fileIconProvider implementation="com.pestphp.pest.PestIconProvider" order="before PhpFileIconProvider"/>
<postStartupActivity implementation="com.pestphp.pest.features.customExpectations.CustomExpectationRemoveGeneratedFileStartupActivity"/>
<testFinder implementation="com.pestphp.pest.goto.PestTestFinder"/>
<gotoTargetPresentationProvider implementation="com.pestphp.pest.goto.PestGotoTargetPresentationProvider"/>
<fileBasedIndex implementation="com.pestphp.pest.indexers.PestTestIndex"/>
<fileBasedIndex implementation="com.pestphp.pest.features.customExpectations.CustomExpectationIndex"/>
<annotator language="PHP" implementationClass="com.pestphp.pest.annotator.PestAnnotator"/>
Expand Down
50 changes: 50 additions & 0 deletions src/test/kotlin/com/pestphp/pest/goto/PestTestFinderTest.kt
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -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
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
});

0 comments on commit ec8c515

Please sign in to comment.