Skip to content

Commit

Permalink
added some tests for class constructor
Browse files Browse the repository at this point in the history
  • Loading branch information
i582 committed Mar 27, 2022
1 parent 26102d0 commit b9d8923
Show file tree
Hide file tree
Showing 18 changed files with 453 additions and 59 deletions.
23 changes: 17 additions & 6 deletions src/main/kotlin/com/vk/kphpstorm/exphptype/PsiToExPhpType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,31 @@ object PsiToExPhpType {
else -> null
}

fun dropGenerics(type: ExPhpType): ExPhpType {
fun dropGenerics(type: ExPhpType): ExPhpType? {
// TODO: добавить все типы
if (type is ExPhpTypePipe) {
return ExPhpTypePipe(type.items.filter {
it !is ExPhpTypeGenericsT && (it !is ExPhpTypeArray || it.inner !is ExPhpTypeGenericsT)
}.map { dropGenerics(it) })
val items = type.items.mapNotNull { dropGenerics(it) }
if (items.isEmpty()) return null

return ExPhpTypePipe(items)
}

if (type is ExPhpTypeTplInstantiation) {
val list = type.specializationList.mapNotNull { dropGenerics(it) }
if (list.isEmpty()) return null
return ExPhpTypeTplInstantiation(type.classFqn, list)
}

if (type is ExPhpTypeNullable) {
return ExPhpTypeNullable(dropGenerics(type.inner))
return dropGenerics(type.inner)?.let { ExPhpTypeNullable(it) }
}

if (type is ExPhpTypeArray) {
return ExPhpTypeArray(dropGenerics(type.inner))
return dropGenerics(type.inner)?.let { ExPhpTypeArray(it) }
}

if (type is ExPhpTypeGenericsT) {
return null
}

return type
Expand Down
148 changes: 120 additions & 28 deletions src/main/kotlin/com/vk/kphpstorm/generics/GenericFunctionCall.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.vk.kphpstorm.generics
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.jetbrains.php.PhpIndex
import com.jetbrains.php.lang.psi.PhpPsiElementFactory
import com.jetbrains.php.lang.psi.elements.*
import com.jetbrains.php.lang.psi.elements.Function
import com.jetbrains.php.lang.psi.resolve.types.PhpType
Expand Down Expand Up @@ -76,6 +77,25 @@ class GenericsReifier(val project: Project) {
}

if (paramExType is ExPhpTypePipe) {
// если случай paramExType это Vector|Vector<%T> и argExType это Vector|Vector<A>
val instantiationParamType =
paramExType.items.find { it is ExPhpTypeTplInstantiation } as ExPhpTypeTplInstantiation?
if (instantiationParamType != null && argExType is ExPhpTypePipe) {
val instantiationArgType =
argExType.items.find { it is ExPhpTypeTplInstantiation } as ExPhpTypeTplInstantiation?
if (instantiationArgType != null) {
for (i in 0 until min(
instantiationArgType.specializationList.size,
instantiationParamType.specializationList.size
)) {
reifyArgumentGenericsT(
instantiationArgType.specializationList[i],
instantiationParamType.specializationList[i]
)
}
}
}

// если случай T|false
if (paramExType.items.size == 2 && paramExType.items.any { it == ExPhpType.FALSE }) {
if (argExType is ExPhpTypePipe) {
Expand Down Expand Up @@ -377,6 +397,85 @@ class ResolvingGenericMethodCall(project: Project) : ResolvingGenericBase(projec
}
}

class GenericConstructorCall(call: NewExpression) : GenericCall(call.project) {
override val callArgs: Array<PsiElement> = call.parameters
override val argumentsTypes: List<ExPhpType?> = callArgs
.filterIsInstance<PhpTypedElement>().map { it.type.global(project).toExPhpType() }
override val explicitSpecsPsi = findInstantiationComment(call)

private val klass: PhpClass?
private val method: Method?

init {
val className = call.classReference?.fqn
klass = PhpIndex.getInstance(project).getClassesByFQN(className).firstOrNull()
val constructor = klass?.constructor

// Если у класса нет конструктора, то создаем его псевдо версию
method = constructor ?: createMethod(project, klass?.name ?: "Foo", "__construct")

init()
}

override fun function() = method

override fun isResolved() = method != null && klass != null

override fun genericNames(): List<String> {
val methodsNames = method?.genericNames() ?: emptyList()
val classesNames = klass?.genericNames() ?: emptyList()

return mutableListOf<String>()
.apply { addAll(methodsNames) }
.apply { addAll(classesNames) }
.toList()
}

override fun isGeneric() = genericNames().isNotEmpty()

override fun toString(): String {
val function = function()
val explicit = explicitSpecs.joinToString(",")
val implicit = implicitSpecs.joinToString(",")
return "${klass?.fqn ?: "UnknownClass"}->${function?.name ?: "UnknownMethod"}<$explicit>($implicit)"
}

private fun createMethod(project: Project, className: String, name: String): Method {
return PhpPsiElementFactory.createPhpPsiFromText(
project,
Method::class.java, "class $className{ public function $name() {} }"
)
}
}

class GenericFunctionCall(call: FunctionReference) : GenericCall(call.project) {
override val callArgs: Array<PsiElement> = call.parameters
override val argumentsTypes: List<ExPhpType?> = callArgs
.filterIsInstance<PhpTypedElement>().map { it.type.global(project).toExPhpType() }
override val explicitSpecsPsi = findInstantiationComment(call)

private val function: Function? = call.resolve() as? Function

init {
init()
}

override fun function() = function

override fun isResolved() = function != null

override fun genericNames() = function?.genericNames() ?: emptyList()

override fun isGeneric() = function()?.isGeneric() == true

override fun toString(): String {
val function = function()
val explicit = explicitSpecs.joinToString(",")
val implicit = implicitSpecs.joinToString(",")
return "${function?.fqn ?: "UnknownFunction"}<$explicit>($implicit)"
}
}

/**
* Ввиду причин описанных в [IndexingGenericFunctionCall], мы не можем использовать
* объединенный класс для обработки вызова во время вывода типов. Однако в других
Expand Down Expand Up @@ -413,41 +512,42 @@ class ResolvingGenericMethodCall(project: Project) : ResolvingGenericBase(projec
* f/*<C, D>*/(new A, new B); // => T1 = C, T2 = D
* ```
*/
class GenericFunctionCall(call: FunctionReference) {
val project = call.project
val function = call.resolve() as? Function
abstract class GenericCall(val project: Project) {
abstract val callArgs: Array<PsiElement>
abstract val explicitSpecsPsi: GenericInstantiationPsiCommentImpl?
abstract val argumentsTypes: List<ExPhpType?>

abstract fun function(): Function?
abstract fun isResolved(): Boolean
abstract fun genericNames(): List<String>
abstract fun isGeneric(): Boolean

val genericTs = mutableListOf<String>()
private val parameters = mutableListOf<Parameter>()
private val callArgs = call.parameters
private val argumentsTypes =
callArgs.filterIsInstance<PhpTypedElement>().map { it.type.global(project).toExPhpType() }

val explicitSpecsPsi = findInstantiationComment(call)

private val extractor = GenericInstantiationExtractor()
private val reifier = GenericsReifier(call.project)
protected val extractor = GenericInstantiationExtractor()
protected val reifier = GenericsReifier(project)

val explicitSpecs get() = extractor.explicitSpecs
val implicitSpecs get() = reifier.implicitSpecs
val implicitSpecializationNameMap get() = reifier.implicitSpecializationNameMap
val implicitSpecializationErrors get() = reifier.implicitSpecializationErrors

init {
init()
}
protected fun init() {
val function = function() ?: return
if (!isGeneric()) return

private fun init() {
if (function == null || !isGeneric()) return
val genericNames = genericNames()

parameters.addAll(function.parameters)
genericTs.addAll(function.genericNames())
genericTs.addAll(genericNames)

// Несмотря на то, что явный список является превалирующим над
// типами выведенными из аргументов функций, нам все равно
// необходимы обв списка для дальнейших инспекций

// В первую очередь, выводим все типы шаблонов из аргументов функции (при наличии)
reifier.reifyAllGenericsT(function.parameters, function.genericNames(), argumentsTypes)
reifier.reifyAllGenericsT(function.parameters, genericNames, argumentsTypes)
// Далее, выводим все типы шаблонов из явного списка типов (при наличии)
extractor.extractExplicitGenericsT(function, explicitSpecsPsi)
}
Expand All @@ -456,10 +556,6 @@ class GenericFunctionCall(call: FunctionReference) {
return explicitSpecsPsi != null
}

fun isGeneric(): Boolean {
return function?.isGeneric() == true
}

/**
* Функция проверяющая, что явно указанные шаблонные типы
* соответствуют автоматически выведенным типам и их можно
Expand All @@ -478,7 +574,7 @@ class GenericFunctionCall(call: FunctionReference) {
* ```
*/
fun isNoNeedExplicitSpec(): Boolean {
if (function == null) return false
val function = function() ?: return false
if (explicitSpecsPsi == null) return false

val countGenericNames = function.genericNames().size
Expand Down Expand Up @@ -523,7 +619,7 @@ class GenericFunctionCall(call: FunctionReference) {
* В примере выше в результате будет возвращен тип `Foo`.
*/
fun typeOfParam(index: Int): ExPhpType? {
if (function == null) return null
val function = function() ?: return null

val param = function.getParameter(index) ?: return null
val paramType = param.type
Expand All @@ -537,9 +633,5 @@ class GenericFunctionCall(call: FunctionReference) {
return null
}

override fun toString(): String {
val explicit = explicitSpecs.joinToString(",")
val implicit = implicitSpecs.joinToString(",")
return "${function?.fqn ?: "UnknownFunction"}<$explicit>($implicit)"
}
abstract override fun toString(): String
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ class KphpStormTypeInfoProvider : ExpressionTypeProvider<PhpTypedElement>() {

override fun getInformationHint(element: PhpTypedElement): String {
val phpType = element.type.global(element.project)
return phpType.toExPhpType()?.let { PsiToExPhpType.dropGenerics(it).toHumanReadable(element) } ?: phpType.toString()
val exPhpType = phpType.toExPhpType() ?: return phpType.toString()
return PsiToExPhpType.dropGenerics(exPhpType)?.toHumanReadable(element) ?: phpType.toString()
}

override fun getExpressionsAt(elementAt: PsiElement): List<PhpTypedElement> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.refactoring.suggested.endOffset
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.vk.kphpstorm.generics.GenericFunctionCall
import com.vk.kphpstorm.generics.GenericUtil.genericNames

@Suppress("UnstableApiUsage")
class InlayHintsCollector(
Expand Down Expand Up @@ -41,11 +41,17 @@ class InlayHintsCollector(
return
}

val genericNames = call.function!!.genericNames().joinToString(", ")
val genericNames = call.genericNames().joinToString(", ")
val simplePresentation = myHintsFactory.inlayHint("<$genericNames>")

val namePsi = element.firstChild
val offset = if (element is MethodReference) {
// offset after "->method_name"
namePsi.nextSibling.nextSibling.endOffset
} else {
namePsi.endOffset
}

sink.addInlineElement(namePsi.endOffset, false, simplePresentation, false)
sink.addInlineElement(offset, false, simplePresentation, false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,41 @@ import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElementVisitor
import com.jetbrains.php.lang.inspections.PhpInspection
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.lang.psi.elements.NewExpression
import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor
import com.vk.kphpstorm.generics.GenericConstructorCall
import com.vk.kphpstorm.generics.GenericFunctionCall
import com.vk.kphpstorm.generics.GenericUtil.genericNames

class FunctionGenericInstantiationArgsCountMismatchInspection : PhpInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return object : PhpElementVisitor() {
override fun visitPhpNewExpression(expression: NewExpression) {
val call = GenericConstructorCall(expression)
if (!call.isResolved()) return

val countGenericNames = call.genericNames().size
val countExplicitSpecs = call.explicitSpecs.size

if (countGenericNames != countExplicitSpecs && call.explicitSpecsPsi != null) {
holder.registerProblem(
call.explicitSpecsPsi,
"$countGenericNames type arguments expected for ${call.function()!!.fqn}",
ProblemHighlightType.GENERIC_ERROR
)
}
}

override fun visitPhpFunctionCall(reference: FunctionReference) {
val call = GenericFunctionCall(reference)
if (call.function == null) return
if (!call.isResolved()) return

val countGenericNames = call.function.genericNames().size
val countGenericNames = call.genericNames().size
val countExplicitSpecs = call.explicitSpecs.size

if (countGenericNames != countExplicitSpecs && call.explicitSpecsPsi != null) {
holder.registerProblem(
call.explicitSpecsPsi,
"$countGenericNames type arguments expected for ${call.function.fqn}",
"$countGenericNames type arguments expected for ${call.function()!!.fqn}",
ProblemHighlightType.GENERIC_ERROR
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,44 @@ import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElementVisitor
import com.jetbrains.php.lang.inspections.PhpInspection
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.lang.psi.elements.NewExpression
import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor
import com.vk.kphpstorm.generics.GenericConstructorCall
import com.vk.kphpstorm.generics.GenericFunctionCall
import com.vk.kphpstorm.generics.GenericUtil.genericNames

class FunctionGenericNoEnoughInformationInspection : PhpInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return object : PhpElementVisitor() {
override fun visitPhpNewExpression(expression: NewExpression) {
val call = GenericConstructorCall(expression)
if (!call.isResolved()) return

val genericNames = call.genericNames()

if (call.explicitSpecsPsi == null) {
genericNames.any {
val resolved = call.implicitSpecializationNameMap.contains(it)

if (!resolved) {
holder.registerProblem(
expression,
"Not enough information to infer generic $it",
ProblemHighlightType.GENERIC_ERROR
)

return@any true
}

false
}
}
}

override fun visitPhpFunctionCall(reference: FunctionReference) {
val call = GenericFunctionCall(reference)
if (call.function == null) return
if (!call.isResolved()) return

val genericNames = call.function.genericNames()
val genericNames = call.genericNames()

if (call.explicitSpecsPsi == null) {
genericNames.any {
Expand Down
Loading

0 comments on commit b9d8923

Please sign in to comment.