diff --git a/src/main/kotlin/com/pestphp/pest/PestFunctionsUtil.kt b/src/main/kotlin/com/pestphp/pest/PestFunctionsUtil.kt index 532b8021..846abab3 100644 --- a/src/main/kotlin/com/pestphp/pest/PestFunctionsUtil.kt +++ b/src/main/kotlin/com/pestphp/pest/PestFunctionsUtil.kt @@ -22,17 +22,15 @@ fun FunctionReferenceImpl.isPestTestFunction(): Boolean { return this.canonicalText in testNames } -private val beforeNames = setOf("beforeEach", "beforeAll") fun FunctionReferenceImpl.isPestBeforeFunction(): Boolean { - return this.canonicalText in beforeNames + return this.canonicalText == "beforeEach" } -private val afterNames = setOf("afterEach", "afterAll") fun FunctionReferenceImpl.isPestAfterFunction(): Boolean { - return this.canonicalText in afterNames + return this.canonicalText == "afterEach" } -private val allPestNames = setOf("it", "test", "beforeEach", "beforeAll", "afterAll", "afterEach") +private val allPestNames = setOf("it", "test", "beforeEach", "afterEach") fun FunctionReferenceImpl.isAnyPestFunction(): Boolean { return this.canonicalText in allPestNames } diff --git a/src/main/kotlin/com/pestphp/pest/PestSettings.kt b/src/main/kotlin/com/pestphp/pest/PestSettings.kt new file mode 100644 index 00000000..f84410ba --- /dev/null +++ b/src/main/kotlin/com/pestphp/pest/PestSettings.kt @@ -0,0 +1,35 @@ +package com.pestphp.pest + +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.ServiceManager +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.project.Project +import com.intellij.util.xmlb.XmlSerializerUtil +import com.pestphp.pest.parser.PestConfigurationFile +import com.pestphp.pest.parser.PestConfigurationFileParser + +@State(name = "PestSettings", storages = [Storage("pest.xml")]) +class PestSettings : PersistentStateComponent { + var pestFilePath = "tests/Pest.php" + + override fun getState(): PestSettings? { + return this + } + + override fun loadState(state: PestSettings) { + XmlSerializerUtil.copyBean(state, this) + } + + private val parser = PestConfigurationFileParser(this) + + fun getPestConfiguration(project: Project): PestConfigurationFile { + return parser.parse(project) + } + + companion object { + fun getInstance(project: Project): PestSettings { + return ServiceManager.getService(project, PestSettings::class.java) + } + } +} diff --git a/src/main/kotlin/com/pestphp/pest/PestTestFileUtil.kt b/src/main/kotlin/com/pestphp/pest/PestTestFileUtil.kt new file mode 100644 index 00000000..4dfe12b6 --- /dev/null +++ b/src/main/kotlin/com/pestphp/pest/PestTestFileUtil.kt @@ -0,0 +1,76 @@ +package com.pestphp.pest + +import com.intellij.openapi.util.Key +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.util.CachedValue +import com.intellij.psi.util.CachedValueProvider +import com.intellij.psi.util.CachedValuesManager +import com.intellij.psi.util.PsiTreeUtil +import com.jetbrains.php.lang.psi.elements.AssignmentExpression +import com.jetbrains.php.lang.psi.elements.ClassConstantReference +import com.jetbrains.php.lang.psi.elements.ClassReference +import com.jetbrains.php.lang.psi.elements.FieldReference +import com.jetbrains.php.lang.psi.elements.Function +import com.jetbrains.php.lang.psi.elements.FunctionReference +import com.jetbrains.php.lang.psi.elements.ParameterList +import com.jetbrains.php.lang.psi.elements.Statement +import com.jetbrains.php.lang.psi.elements.Variable +import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl +import com.jetbrains.php.lang.psi.resolve.types.PhpType + +inline fun PsiElement?.isThisVariableInPest(condition: (FunctionReferenceImpl) -> Boolean): Boolean { + if ((this as? Variable)?.name != "this") return false + + val closure = PsiTreeUtil.getParentOfType(this, Function::class.java) + + if (closure == null || !closure.isClosure) return false + + val parameterList = closure.parent?.parent as? ParameterList ?: return false + + if (parameterList.parent !is FunctionReferenceImpl) return false + + return condition(parameterList.parent as FunctionReferenceImpl) +} + +fun PsiFile.getAllBeforeThisAssignments(): List { + return this.firstChild.children + .filterIsInstance() + .mapNotNull { it.firstChild } + .filterIsInstance() + .filter { it.isPestBeforeFunction() } + .flatMap { it.getThisStatements() } +} + +private val cacheKey = Key>>("com.pestphp.pest_assignments") +private fun FunctionReferenceImpl.getThisStatements(): List { + return CachedValuesManager.getCachedValue(this, cacheKey) { + val result = PsiTreeUtil.findChildrenOfType( + this.parameterList?.getParameter(0), + AssignmentExpression::class.java + ) + .filter { ((it.variable as? FieldReference)?.classReference as? Variable)?.name == "this" } + + CachedValueProvider.Result.create(result, this) + } +} + +fun FunctionReference.getUsesPhpType(): PhpType? { + parameters.mapNotNull { + val classRef = it as? ClassConstantReference ?: return@mapNotNull null + + if (classRef.fqn != "\\class") return@mapNotNull null + + (classRef.classReference as? ClassReference)?.fqn + }.apply { + if (this.isEmpty()) return null + + val res = PhpType() + + this.forEach { + res.add(it) + } + + return res + } +} diff --git a/src/main/kotlin/com/pestphp/pest/parser/PestConfigurationFile.kt b/src/main/kotlin/com/pestphp/pest/parser/PestConfigurationFile.kt new file mode 100644 index 00000000..5b0f71ef --- /dev/null +++ b/src/main/kotlin/com/pestphp/pest/parser/PestConfigurationFile.kt @@ -0,0 +1,8 @@ +package com.pestphp.pest.parser + +import com.jetbrains.php.lang.psi.resolve.types.PhpType + +data class PestConfigurationFile( + val baseTestType: PhpType, + val pathsClasses: List> +) diff --git a/src/main/kotlin/com/pestphp/pest/parser/PestConfigurationFileParser.kt b/src/main/kotlin/com/pestphp/pest/parser/PestConfigurationFileParser.kt new file mode 100644 index 00000000..8e3b133f --- /dev/null +++ b/src/main/kotlin/com/pestphp/pest/parser/PestConfigurationFileParser.kt @@ -0,0 +1,99 @@ +package com.pestphp.pest.parser + +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.guessProjectDir +import com.intellij.openapi.util.Key +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiManager +import com.intellij.psi.PsiRecursiveElementWalkingVisitor +import com.intellij.psi.util.CachedValue +import com.intellij.psi.util.CachedValueProvider +import com.intellij.psi.util.CachedValuesManager +import com.jetbrains.php.lang.psi.PhpFile +import com.jetbrains.php.lang.psi.elements.MethodReference +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression +import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.pestphp.pest.PestSettings +import com.pestphp.pest.getUsesPhpType + +class PestConfigurationFileParser(private val settings: PestSettings) { + fun parse(project: Project): PestConfigurationFile { + val baseDir = project.guessProjectDir() ?: return defaultConfig + + val pestFile = VirtualFileManager.getInstance().findFileByUrl(baseDir.url + "/" + settings.pestFilePath) + ?: return defaultConfig + + val psiFile = PsiManager.getInstance(project).findFile(pestFile) as? PhpFile ?: return defaultConfig + + return CachedValuesManager.getCachedValue(psiFile, cacheKey) { + var baseType = PhpType().add("\\PHPUnit\\Framework\\TestCase") + val inPaths = mutableListOf>() + val testsPath = settings.pestFilePath.replaceAfterLast('/', "") + + psiFile.acceptChildren( + Visitor { type, inPath -> + if (inPath != null) { + inPaths.add(Pair(testsPath + inPath, type)) + } else { + baseType = type + } + } + ) + + CachedValueProvider.Result.create(PestConfigurationFile(baseType, inPaths), psiFile) + } ?: defaultConfig + } + + private class Visitor(private val collect: (PhpType, String?) -> Unit) : PsiRecursiveElementWalkingVisitor() { + override fun visitElement(element: PsiElement) { + if (element is MethodReference) { + visitInReference(element) + return + } else if (element is FunctionReferenceImpl) { + if (element.name == "uses") { + collect(element.getUsesPhpType() ?: return, null) + } + return + } + + super.visitElement(element) + } + + private fun visitInReference(inReference: MethodReference) { + var reference = inReference + var usesType: PhpType? = null + while (true) { + val ref = reference.classReference ?: return + + if (ref is MethodReference) { + reference = ref + } else if (ref is FunctionReferenceImpl) { + if (ref.name == "uses") { + usesType = ref.getUsesPhpType() + } + + break + } else { + return + } + } + + if (usesType == null) return + + inReference.parameters.filterIsInstance().forEach { + collect(usesType, it.contents) + } + } + } + + companion object { + private val defaultConfig = PestConfigurationFile( + PhpType().add("\\PHPUnit\\Framework\\TestCase"), + emptyList() + ) + + private val cacheKey = Key>("com.pestphp.pest_configuration") + } +} diff --git a/src/main/kotlin/com/pestphp/pest/types/BaseTypeProvider.kt b/src/main/kotlin/com/pestphp/pest/types/BaseTypeProvider.kt deleted file mode 100644 index e592497b..00000000 --- a/src/main/kotlin/com/pestphp/pest/types/BaseTypeProvider.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.pestphp.pest.types - -import com.intellij.psi.PsiElement -import com.intellij.psi.util.PsiTreeUtil -import com.jetbrains.php.lang.psi.elements.Function -import com.jetbrains.php.lang.psi.elements.ParameterList -import com.jetbrains.php.lang.psi.elements.Variable -import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl - -@Suppress("UnnecessaryAbstractClass") -abstract class BaseTypeProvider { - protected inline fun PsiElement?.isThisVariableInPest(condition: (FunctionReferenceImpl) -> Boolean): Boolean { - if ((this as? Variable)?.name != "this") return false - - val closure = PsiTreeUtil.getParentOfType(this, Function::class.java) - - if (closure == null || !closure.isClosure) return false - - val parameterList = closure.parent?.parent as? ParameterList ?: return false - - if (parameterList.parent !is FunctionReferenceImpl) return false - - return condition(parameterList.parent as FunctionReferenceImpl) - } -} diff --git a/src/main/kotlin/com/pestphp/pest/types/ThisFieldTypeProvider.kt b/src/main/kotlin/com/pestphp/pest/types/ThisFieldTypeProvider.kt index 6b6de135..e835c61e 100644 --- a/src/main/kotlin/com/pestphp/pest/types/ThisFieldTypeProvider.kt +++ b/src/main/kotlin/com/pestphp/pest/types/ThisFieldTypeProvider.kt @@ -1,41 +1,35 @@ package com.pestphp.pest.types +import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement -import com.intellij.psi.util.PsiTreeUtil -import com.jetbrains.php.lang.psi.elements.AssignmentExpression import com.jetbrains.php.lang.psi.elements.FieldReference import com.jetbrains.php.lang.psi.elements.PhpNamedElement import com.jetbrains.php.lang.psi.elements.PhpTypedElement -import com.jetbrains.php.lang.psi.elements.Statement -import com.jetbrains.php.lang.psi.elements.Variable import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4 +import com.pestphp.pest.getAllBeforeThisAssignments import com.pestphp.pest.isPestAfterFunction -import com.pestphp.pest.isPestBeforeFunction import com.pestphp.pest.isPestTestFunction +import com.pestphp.pest.isThisVariableInPest -class ThisFieldTypeProvider : BaseTypeProvider(), PhpTypeProvider4 { +class ThisFieldTypeProvider : PhpTypeProvider4 { override fun getKey(): Char { return '\u0222' } override fun getType(psiElement: PsiElement): PhpType? { + if (DumbService.isDumb(psiElement.project)) return null + val fieldReference = psiElement as? FieldReference ?: return null if (!fieldReference.classReference.isThisVariableInPest { check(it) }) return null val fieldName = fieldReference.name ?: return null - return psiElement.containingFile.firstChild.children - .filterIsInstance() - .mapNotNull { it.firstChild } - .filterIsInstance() - .filter { it.isPestBeforeFunction() } - .mapNotNull { it.parameterList?.getParameter(0) } - .flatMap { PsiTreeUtil.findChildrenOfType(it, AssignmentExpression::class.java) } - .filter { isNeededFieldReference(it.variable, fieldName) } + return (psiElement.containingFile ?: return null).getAllBeforeThisAssignments() + .filter { (it.variable as? FieldReference)?.name == fieldName } .mapNotNull { it.value } .filterIsInstance() .firstOrNull()?.type @@ -43,16 +37,6 @@ class ThisFieldTypeProvider : BaseTypeProvider(), PhpTypeProvider4 { private fun check(it: FunctionReferenceImpl) = it.isPestTestFunction() || it.isPestAfterFunction() - private fun isNeededFieldReference(psiElement: PsiElement?, fieldName: String): Boolean { - if (psiElement !is FieldReference) return false - - if (psiElement.name != fieldName) return false - - if ((psiElement.classReference as? Variable)?.name != "this") return false - - return true - } - override fun complete(s: String, project: Project): PhpType? { return null } diff --git a/src/main/kotlin/com/pestphp/pest/types/ThisTypeProvider.kt b/src/main/kotlin/com/pestphp/pest/types/ThisTypeProvider.kt index da912d53..d85398d2 100644 --- a/src/main/kotlin/com/pestphp/pest/types/ThisTypeProvider.kt +++ b/src/main/kotlin/com/pestphp/pest/types/ThisTypeProvider.kt @@ -1,21 +1,51 @@ package com.pestphp.pest.types +import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project +import com.intellij.openapi.project.guessProjectDir +import com.intellij.openapi.vfs.VfsUtil import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiTreeUtil import com.jetbrains.php.lang.psi.elements.PhpNamedElement +import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4 +import com.pestphp.pest.PestSettings +import com.pestphp.pest.getUsesPhpType import com.pestphp.pest.isAnyPestFunction +import com.pestphp.pest.isThisVariableInPest -class ThisTypeProvider : BaseTypeProvider(), PhpTypeProvider4 { +class ThisTypeProvider : PhpTypeProvider4 { override fun getKey(): Char { return '\u0221' } override fun getType(psiElement: PsiElement): PhpType? { - if (psiElement.isThisVariableInPest { it.isAnyPestFunction() }) return TEST_CASE_TYPE + if (DumbService.isDumb(psiElement.project)) return null - return null + if (!psiElement.isThisVariableInPest { it.isAnyPestFunction() }) return null + + val virtualFile = psiElement.containingFile?.originalFile?.virtualFile ?: return null + + val config = PestSettings.getInstance(psiElement.project).getPestConfiguration(psiElement.project) + + val baseDir = (psiElement.project.guessProjectDir() ?: return config.baseTestType) + val relativePath = VfsUtil.getRelativePath(virtualFile, baseDir) ?: return config.baseTestType + + val result = PhpType().add(config.baseTestType) + + config.pathsClasses.forEach { (path, type) -> + if (relativePath.startsWith(path)) { + result.add(type) + } + } + + PsiTreeUtil.findChildrenOfType(psiElement.containingFile, FunctionReferenceImpl::class.java) + .filter { it.name == "uses" } + .mapNotNull { it.getUsesPhpType() } + .forEach { result.add(it) } + + return result } override fun complete(s: String, project: Project): PhpType? { @@ -25,8 +55,4 @@ class ThisTypeProvider : BaseTypeProvider(), PhpTypeProvider4 { override fun getBySignature(s: String, set: Set, i: Int, project: Project): Collection { return emptyList() } - - companion object { - private val TEST_CASE_TYPE = PhpType().add("\\PHPUnit\\Framework\\TestCase") - } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index af24fc86..805f3bba 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -19,6 +19,8 @@ + + diff --git a/src/test/kotlin/com/pestphp/pest/tests/configuration/PestConfigurationFileTest.kt b/src/test/kotlin/com/pestphp/pest/tests/configuration/PestConfigurationFileTest.kt new file mode 100644 index 00000000..45c66231 --- /dev/null +++ b/src/test/kotlin/com/pestphp/pest/tests/configuration/PestConfigurationFileTest.kt @@ -0,0 +1,39 @@ +package com.pestphp.pest.tests.configuration + +import com.pestphp.pest.tests.PestLightCodeFixture + +class PestConfigurationFileTest: PestLightCodeFixture() { + override fun setUp() { + super.setUp() + + myFixture.copyDirectoryToProject("tests", "tests") + } + + override fun getTestDataPath(): String? { + return basePath + "configuration/fixtures" + } + + fun testUnit() { + myFixture.configureByFile("tests/Unit/UnitTest.php") + + assertCompletion("baseTestFunc") + } + + fun testUsesUnit() { + myFixture.configureByFile("tests/Unit/UsesUnitTest.php") + + assertCompletion("baseTestFunc", "traitFunc") + } + + fun testFeature() { + myFixture.configureByFile("tests/Feature/FeatureTest.php") + + assertCompletion("baseTestFunc", "featureTestFunc") + } + + fun testGroupedFeature() { + myFixture.configureByFile("tests/GroupedFeature/GroupedFeatureTest.php") + + assertCompletion("baseTestFunc", "featureTestFunc", "someBaseTraitFunc") + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/pestphp/pest/tests/configuration/fixtures/tests/Feature/FeatureTest.php b/src/test/kotlin/com/pestphp/pest/tests/configuration/fixtures/tests/Feature/FeatureTest.php new file mode 100644 index 00000000..5944cb2d --- /dev/null +++ b/src/test/kotlin/com/pestphp/pest/tests/configuration/fixtures/tests/Feature/FeatureTest.php @@ -0,0 +1,5 @@ + +}); \ No newline at end of file diff --git a/src/test/kotlin/com/pestphp/pest/tests/configuration/fixtures/tests/GroupedFeature/GroupedFeatureTest.php b/src/test/kotlin/com/pestphp/pest/tests/configuration/fixtures/tests/GroupedFeature/GroupedFeatureTest.php new file mode 100644 index 00000000..5944cb2d --- /dev/null +++ b/src/test/kotlin/com/pestphp/pest/tests/configuration/fixtures/tests/GroupedFeature/GroupedFeatureTest.php @@ -0,0 +1,5 @@ + +}); \ No newline at end of file diff --git a/src/test/kotlin/com/pestphp/pest/tests/configuration/fixtures/tests/Pest.php b/src/test/kotlin/com/pestphp/pest/tests/configuration/fixtures/tests/Pest.php new file mode 100644 index 00000000..fbe262e4 --- /dev/null +++ b/src/test/kotlin/com/pestphp/pest/tests/configuration/fixtures/tests/Pest.php @@ -0,0 +1,22 @@ +in("Feature"); + +uses(FeatureTestCase::class, SomeBaseTrait::class)->group("some group")->in("GroupedFeature"); \ No newline at end of file diff --git a/src/test/kotlin/com/pestphp/pest/tests/configuration/fixtures/tests/Unit/UnitTest.php b/src/test/kotlin/com/pestphp/pest/tests/configuration/fixtures/tests/Unit/UnitTest.php new file mode 100644 index 00000000..5944cb2d --- /dev/null +++ b/src/test/kotlin/com/pestphp/pest/tests/configuration/fixtures/tests/Unit/UnitTest.php @@ -0,0 +1,5 @@ + +}); \ No newline at end of file diff --git a/src/test/kotlin/com/pestphp/pest/tests/configuration/fixtures/tests/Unit/UsesUnitTest.php b/src/test/kotlin/com/pestphp/pest/tests/configuration/fixtures/tests/Unit/UsesUnitTest.php new file mode 100644 index 00000000..7033086c --- /dev/null +++ b/src/test/kotlin/com/pestphp/pest/tests/configuration/fixtures/tests/Unit/UsesUnitTest.php @@ -0,0 +1,11 @@ + +}); \ No newline at end of file diff --git a/src/test/kotlin/com/pestphp/pest/tests/types/ThisFieldTypeTest.kt b/src/test/kotlin/com/pestphp/pest/tests/types/ThisFieldTypeTest.kt index 8c72b237..a1f6f05c 100644 --- a/src/test/kotlin/com/pestphp/pest/tests/types/ThisFieldTypeTest.kt +++ b/src/test/kotlin/com/pestphp/pest/tests/types/ThisFieldTypeTest.kt @@ -4,23 +4,17 @@ class ThisFieldTypeTest: BaseTypeTest() { override fun setUp() { super.setUp() - myFixture.copyDirectoryToProject("thisField", "/") + myFixture.copyDirectoryToProject("thisField", "tests") } fun testBeforeEach() { - myFixture.configureByFile("beforeEach.php") - - assertCompletion("a", "b") - } - - fun testBeforeAll() { - myFixture.configureByFile("beforeAll.php") + myFixture.configureByFile("tests/beforeEach.php") assertCompletion("a", "b") } fun testAfterEach() { - myFixture.configureByFile("afterEach.php") + myFixture.configureByFile("tests/afterEach.php") assertCompletion("a", "b") } diff --git a/src/test/kotlin/com/pestphp/pest/tests/types/fixtures/thisField/beforeAll.php b/src/test/kotlin/com/pestphp/pest/tests/types/fixtures/thisField/beforeAll.php deleted file mode 100644 index 92c6aab5..00000000 --- a/src/test/kotlin/com/pestphp/pest/tests/types/fixtures/thisField/beforeAll.php +++ /dev/null @@ -1,13 +0,0 @@ - $this->foo = new Foo()); - -it('has home', function () { - $this->foo-> -});