Skip to content

Commit

Permalink
Merge branch 'master' of github.com:pestphp/pest-intellij into feat-c…
Browse files Browse the repository at this point in the history
…overage
  • Loading branch information
olivernybroe committed Aug 5, 2020
2 parents 11af095 + 87908f6 commit 11dbc08
Show file tree
Hide file tree
Showing 17 changed files with 354 additions and 83 deletions.
8 changes: 3 additions & 5 deletions src/main/kotlin/com/pestphp/pest/PestFunctionsUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
35 changes: 35 additions & 0 deletions src/main/kotlin/com/pestphp/pest/PestSettings.kt
Original file line number Diff line number Diff line change
@@ -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<PestSettings> {
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)
}
}
}
76 changes: 76 additions & 0 deletions src/main/kotlin/com/pestphp/pest/PestTestFileUtil.kt
Original file line number Diff line number Diff line change
@@ -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<AssignmentExpression> {
return this.firstChild.children
.filterIsInstance<Statement>()
.mapNotNull { it.firstChild }
.filterIsInstance<FunctionReferenceImpl>()
.filter { it.isPestBeforeFunction() }
.flatMap { it.getThisStatements() }
}

private val cacheKey = Key<CachedValue<List<AssignmentExpression>>>("com.pestphp.pest_assignments")
private fun FunctionReferenceImpl.getThisStatements(): List<AssignmentExpression> {
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
}
}
Original file line number Diff line number Diff line change
@@ -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<Pair<String, PhpType>>
)
Original file line number Diff line number Diff line change
@@ -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<Pair<String, PhpType>>()
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<StringLiteralExpression>().forEach {
collect(usesType, it.contents)
}
}
}

companion object {
private val defaultConfig = PestConfigurationFile(
PhpType().add("\\PHPUnit\\Framework\\TestCase"),
emptyList()
)

private val cacheKey = Key<CachedValue<PestConfigurationFile>>("com.pestphp.pest_configuration")
}
}
25 changes: 0 additions & 25 deletions src/main/kotlin/com/pestphp/pest/types/BaseTypeProvider.kt

This file was deleted.

32 changes: 8 additions & 24 deletions src/main/kotlin/com/pestphp/pest/types/ThisFieldTypeProvider.kt
Original file line number Diff line number Diff line change
@@ -1,58 +1,42 @@
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<Statement>()
.mapNotNull { it.firstChild }
.filterIsInstance<FunctionReferenceImpl>()
.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<PhpTypedElement>()
.firstOrNull()?.type
}

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
}
Expand Down
40 changes: 33 additions & 7 deletions src/main/kotlin/com/pestphp/pest/types/ThisTypeProvider.kt
Original file line number Diff line number Diff line change
@@ -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? {
Expand All @@ -25,8 +55,4 @@ class ThisTypeProvider : BaseTypeProvider(), PhpTypeProvider4 {
override fun getBySignature(s: String, set: Set<String>, i: Int, project: Project): Collection<PhpNamedElement?> {
return emptyList()
}

companion object {
private val TEST_CASE_TYPE = PhpType().add("\\PHPUnit\\Framework\\TestCase")
}
}
Loading

0 comments on commit 11dbc08

Please sign in to comment.