diff --git a/build.gradle b/build.gradle index 1ff98e1..07802b5 100644 --- a/build.gradle +++ b/build.gradle @@ -47,8 +47,10 @@ intellij { // for release versions: https://www.jetbrains.com/intellij-repository/releases (com.jetbrains.intellij.idea) // for EAPs: https://www.jetbrains.com/intellij-repository/snapshots version = '2022.1.1' +// version = '2021.3.1' plugins = [ 'com.jetbrains.php:221.5591.58', // https://plugins.jetbrains.com/plugin/6610-php/versions +// 'com.jetbrains.php:213.5744.223', ] } runIde { diff --git a/src/main/kotlin/com/vk/kphpstorm/KphpStormASTFactory.kt b/src/main/kotlin/com/vk/kphpstorm/KphpStormASTFactory.kt index cf38dfa..af748c9 100644 --- a/src/main/kotlin/com/vk/kphpstorm/KphpStormASTFactory.kt +++ b/src/main/kotlin/com/vk/kphpstorm/KphpStormASTFactory.kt @@ -1,11 +1,16 @@ package com.vk.kphpstorm import com.intellij.lang.DefaultASTFactoryImpl +import com.intellij.psi.impl.source.tree.CompositeElement import com.intellij.psi.impl.source.tree.LeafElement import com.intellij.psi.tree.IElementType import com.vk.kphpstorm.generics.psi.GenericInstantiationPsiCommentImpl class KphpStormASTFactory : DefaultASTFactoryImpl() { + override fun createComposite(type: IElementType): CompositeElement { + return super.createComposite(type) + } + override fun createComment(type: IElementType, text: CharSequence): LeafElement { if (text.startsWith("/*<") && text.endsWith(">*/")) { return GenericInstantiationPsiCommentImpl(type, text) diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeClassString.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeClassString.kt index d5ddb23..5468beb 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeClassString.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeClassString.kt @@ -5,7 +5,7 @@ import com.jetbrains.php.lang.psi.elements.PhpPsiElement import com.jetbrains.php.lang.psi.resolve.types.PhpType /** - * Type of special class constant (`Foo::class`). + * Type of special class constant (e.g. `Foo::class`). */ class ExPhpTypeClassString(val inner: ExPhpType) : ExPhpType { override fun toString() = "class-string($inner)" @@ -16,13 +16,9 @@ class ExPhpTypeClassString(val inner: ExPhpType) : ExPhpType { override fun hashCode() = 35 - override fun toPhpType(): PhpType { - return PhpType().add("class-string($inner)") - } + override fun toPhpType() = PhpType().add("class-string(${inner.toPhpType()})") - override fun getSubkeyByIndex(indexKey: String): ExPhpType? { - return this - } + override fun getSubkeyByIndex(indexKey: String) = this override fun instantiateGeneric(nameMap: Map): ExPhpType { // TODO: подумать тут @@ -36,34 +32,9 @@ class ExPhpTypeClassString(val inner: ExPhpType) : ExPhpType { } override fun isAssignableFrom(rhs: ExPhpType, project: Project): Boolean = when (rhs) { - // нативный вывод типов дает тип string|class-string для T::class, поэтому - // необходимо обработать этот случай отдельно - is ExPhpTypePipe -> { - val containsString = rhs.items.any { it == ExPhpType.STRING } - if (rhs.items.size == 2 && containsString) { - val otherType = rhs.items.find { it != ExPhpType.STRING } - if (otherType == null) false - else isAssignableFrom(otherType, project) - } else false - } - // class-string совместим только с class-string при условии - // что класс E является допустимым для класса T. + // class-string is only compatible with class-string + // if class E is compatible with class T. is ExPhpTypeClassString -> inner.isAssignableFrom(rhs.inner, project) else -> false } - - companion object { - // нативный вывод типов дает тип string|class-string для T::class, - // из-за этого в некоторых местах нужна дополнительная логика. - fun isNativePipeWithString(pipe: ExPhpTypePipe): Boolean { - if (pipe.items.size != 2) return false - val otherType = pipe.items.find { it != ExPhpType.STRING } - - return otherType is ExPhpTypeClassString - } - - fun getClassFromNativePipeWithString(pipe: ExPhpTypePipe): ExPhpType { - return pipe.items.find { it != ExPhpType.STRING }!! - } - } } diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeGenericT.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeGenericT.kt index 285c5fb..e13b4ca 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeGenericT.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeGenericT.kt @@ -14,7 +14,7 @@ class ExPhpTypeGenericsT(val nameT: String) : ExPhpType { override fun toHumanReadable(expr: PhpPsiElement) = "%$nameT" override fun toPhpType(): PhpType { - return PhpType().add("%$nameT") + return PhpType.EMPTY } override fun getSubkeyByIndex(indexKey: String): ExPhpType? { diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePipe.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePipe.kt index e2ee690..15cdd54 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePipe.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePipe.kt @@ -134,10 +134,6 @@ class ExPhpTypePipe(val items: List) : ExPhpType { }) ok = true } - - if (ExPhpTypeClassString.isNativePipeWithString(this)) { - return lhs.isAssignableFrom(ExPhpTypeClassString.getClassFromNativePipeWithString(this), project) - } } return ok diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTplInstantiation.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTplInstantiation.kt index 482437f..37c2afd 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTplInstantiation.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTplInstantiation.kt @@ -1,6 +1,8 @@ package com.vk.kphpstorm.exphptype import com.intellij.openapi.project.Project +import com.jetbrains.php.PhpClassHierarchyUtils +import com.jetbrains.php.PhpIndex import com.jetbrains.php.codeInsight.PhpCodeInsightUtil import com.jetbrains.php.lang.psi.elements.PhpPsiElement import com.jetbrains.php.lang.psi.resolve.types.PhpType @@ -31,6 +33,20 @@ class ExPhpTypeTplInstantiation(val classFqn: String, val specializationList: Li // not finished is ExPhpTypePipe -> rhs.items.any { it.isAssignableFrom(this, project) } is ExPhpTypeTplInstantiation -> classFqn == rhs.classFqn && specializationList.size == rhs.specializationList.size + + is ExPhpTypeInstance -> rhs.fqn == classFqn || run { + val phpIndex = PhpIndex.getInstance(project) + val lhsClass = phpIndex.getAnyByFQN(classFqn).firstOrNull() ?: return false + var rhsIsChild = false + phpIndex.getAnyByFQN(rhs.fqn).forEach { rhsClass -> + PhpClassHierarchyUtils.processSuperWithoutMixins(rhsClass, true, true) { clazz -> + if (PhpClassHierarchyUtils.classesEqual(lhsClass, clazz)) + rhsIsChild = true + !rhsIsChild + } + } + rhsIsChild + } else -> false } } diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/PhpTypeToExPhpTypeParsing.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/PhpTypeToExPhpTypeParsing.kt index 92e117e..2241eb7 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/PhpTypeToExPhpTypeParsing.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/PhpTypeToExPhpTypeParsing.kt @@ -302,9 +302,11 @@ object PhpTypeToExPhpTypeParsing { private fun parseTypeExpression(builder: ExPhpTypeBuilder): ExPhpType? { val lhs = parseTypeArray(builder) ?: return null // wrap with ExPhpTypePipe only 'T1|T2', leaving 'T' being as is - if (!builder.compare('|') && !builder.compare('/')) - return if (lhs is ExPhpTypeForcing) lhs.inner else lhs - + if (!builder.compare('|') && !builder.compare('/')) { + // TODO: здесь была строчка, точно ли она не нужна? + // return if (lhs is ExPhpTypeForcing) lhs.inner else lhs + return lhs + } val pipeItems = mutableListOf(lhs) while (builder.compareAndEat('|') || builder.compareAndEat('/')) { val rhs = parseTypeArray(builder) ?: break diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/PsiToExPhpType.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/PsiToExPhpType.kt index 94e2cf3..5f0d40e 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/PsiToExPhpType.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/PsiToExPhpType.kt @@ -20,6 +20,7 @@ object PsiToExPhpType { if (type is ExPhpTypePipe) { val items = type.items.mapNotNull { dropGenerics(it) } if (items.isEmpty()) return null + if (items.size == 1) return items.first() return ExPhpTypePipe(items) } diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeClassStringPsiImpl.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeClassStringPsiImpl.kt index b303a90..8f87cc3 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeClassStringPsiImpl.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeClassStringPsiImpl.kt @@ -6,6 +6,7 @@ import com.jetbrains.php.lang.documentation.phpdoc.psi.impl.PhpDocTypeImpl import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.vk.kphpstorm.exphptype.KphpPrimitiveTypes import com.vk.kphpstorm.generics.GenericUtil +import com.vk.kphpstorm.helpers.toExPhpType /** * class-string — psi is class-string(Foo) corresponding type of Foo::class @@ -38,6 +39,12 @@ class ExPhpTypeClassStringPsiImpl(node: ASTNode) : PhpDocTypeImpl(node) { override fun getType(): PhpType { if (className.isEmpty()) return KphpPrimitiveTypes.PHP_TYPE_ANY - return PhpType().add("class-string($className)") + if (className.startsWith("%")) return PhpType().add("class-string($className)") + + val classPsi = firstChild?.nextSibling?.nextSibling as? ExPhpTypeInstancePsiImpl + ?: return KphpPrimitiveTypes.PHP_TYPE_ANY + + val innerType = getType(classPsi, text).toExPhpType() + return PhpType().add("class-string($innerType)") } } diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/GenericCall.kt b/src/main/kotlin/com/vk/kphpstorm/generics/GenericCall.kt new file mode 100644 index 0000000..fe07522 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/GenericCall.kt @@ -0,0 +1,182 @@ +package com.vk.kphpstorm.generics + +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.util.parentOfType +import com.jetbrains.php.lang.psi.elements.* +import com.jetbrains.php.lang.psi.elements.Function +import com.vk.kphpstorm.exphptype.ExPhpType +import com.vk.kphpstorm.generics.GenericUtil.isGeneric +import com.vk.kphpstorm.generics.psi.GenericInstantiationPsiCommentImpl +import com.vk.kphpstorm.helpers.toExPhpType +import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl + +/** + * Ввиду причин описанных в [IndexingGenericFunctionCall], мы не можем использовать + * объединенный класс для обработки вызова во время вывода типов. Однако в других + * местах мы можем использовать индекс и поэтому нам не нужно паковать данные и + * потом их распаковывать, мы можем делать все за раз. + * + * Данный класс является объединением [IndexingGenericFunctionCall] и + * [ResolvingGenericFunctionCall] и может быть использован для обработки шаблонных + * вызовов в других местах; + * + * Класс инкапсулирующий в себе всю логику обработки шаблонных вызовов. + * + * Он выводит неявные типы, когда нет явного определения списка типов при инстанциации. + * + * Например: + * + * ```php + * /** + * * @kphp-generic T1, T2 + * * @param T1 $a + * * @param T2 $b + * */ + * function f($a, $b) {} + * + * f(new A, new B); // => T1 = A, T2 = B + * ``` + * + * В случае когда есть явный список типов при инстанциации, он собирает типы + * из него. + * + * Например: + * + * ```php + * f/**/(new A, new B); // => T1 = C, T2 = D + * ``` + */ +abstract class GenericCall(val project: Project) { + abstract val callArgs: Array + abstract val explicitSpecsPsi: GenericInstantiationPsiCommentImpl? + abstract val argumentsTypes: List + protected var contextType: ExPhpType? = null + + abstract fun element(): PsiElement? + abstract fun function(): Function? + abstract fun isResolved(): Boolean + abstract fun genericNames(): List + abstract fun isGeneric(): Boolean + + val genericTs = mutableListOf() + private val parameters = mutableListOf() + + protected val extractor = GenericInstantiationExtractor() + protected val reifier = GenericsReifier(project) + + val explicitSpecs get() = extractor.explicitSpecs + val specializationNameMap get() = extractor.specializationNameMap + val implicitSpecs get() = reifier.implicitSpecs + val implicitSpecializationNameMap get() = reifier.implicitSpecializationNameMap + val implicitClassSpecializationNameMap get() = reifier.implicitClassSpecializationNameMap + val implicitSpecializationErrors get() = reifier.implicitSpecializationErrors + + protected fun init(element: PsiElement) { + val function = function() ?: return + if (!isGeneric()) return + + val genericNames = genericNames() + + parameters.addAll(function.parameters) + genericTs.addAll(genericNames) + + // Если текущий вызов находится в return или является аргументом + // функции, то мы можем извлечь дополнительные подсказки по типам. + calcContextType(element) + + // Несмотря на то, что явный список является превалирующим над + // типами выведенными из аргументов функций, нам все равно + // необходимы обв списка для дальнейших инспекций + + // В первую очередь, выводим все типы шаблонов из аргументов функции (при наличии) + reifier.reifyAllGenericsT(null, function.parameters, genericNames, argumentsTypes, contextType) + // Далее, выводим все типы шаблонов из явного списка типов (при наличии) + extractor.extractExplicitGenericsT(genericNames(), explicitSpecsPsi) + } + + private fun calcContextType(element: PsiElement) { + val parent = element.parent + if (parent is PhpReturn) { + val parentFunction = parent.parentOfType() + if (parentFunction != null) { + val returnType = parentFunction.docComment?.returnTag?.type + contextType = returnType?.toExPhpType() + } + + return + } + + if (parent is ParameterList) { + val calledInFunctionCall = parent.parentOfType() + if (calledInFunctionCall != null) { + val calledFunction = calledInFunctionCall.resolve() as? Function + if (calledFunction != null) { + val index = parent.parameters.indexOf(element) + calledFunction.getParameter(index)?.let { + contextType = it.type.toExPhpType() + } + } + } + } + } + + fun withExplicitSpecs(): Boolean { + return explicitSpecsPsi != null + } + + /** + * Имея следующую функцию: + * + * ```php + * /** + * * @kphp-generic T + * * @param T $arg + * */ + * function f($arg) {} + * ``` + * + * И следующий вызов: + * + * ```php + * f/**/(new Foo); + * ``` + * + * Нам необходимо вывести тип `$arg`, для того чтобы проверить, что + * переданное выражение `new Foo` имеет правильный тип. + * + * Так как функция может вызываться с разными шаблонными типа, нам + * необходимо найти тип `$arg` для каждого конкретного вызова. + * В примере выше в результате будет возвращен тип `Foo`. + */ + fun typeOfParam(index: Int): ExPhpType? { + val function = function() ?: return null + + val param = function.getParameter(index) ?: return null + val paramType = param.type + if (paramType.isGeneric(genericNames())) { + val usedNameMap = extractor.specializationNameMap.ifEmpty { + reifier.implicitSpecializationNameMap + } + return paramType.toExPhpType()?.instantiateGeneric(usedNameMap) + } + + return null + } + + fun isNotEnoughInformation(): KphpDocGenericParameterDecl? { + if (explicitSpecsPsi != null) return null + + genericNames().forEach { decl -> + val resolved = implicitSpecializationNameMap.contains(decl.name) + + if (!resolved) { + return decl + } + } + + return null + } + + abstract override fun toString(): String +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/GenericConstructorCall.kt b/src/main/kotlin/com/vk/kphpstorm/generics/GenericConstructorCall.kt new file mode 100644 index 0000000..2f8fd30 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/GenericConstructorCall.kt @@ -0,0 +1,65 @@ +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.Method +import com.jetbrains.php.lang.psi.elements.NewExpression +import com.jetbrains.php.lang.psi.elements.PhpClass +import com.jetbrains.php.lang.psi.elements.PhpTypedElement +import com.vk.kphpstorm.exphptype.ExPhpType +import com.vk.kphpstorm.generics.GenericUtil.genericNames +import com.vk.kphpstorm.helpers.toExPhpType +import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl + +class GenericConstructorCall(private val call: NewExpression) : GenericCall(call.project) { + override val callArgs: Array = call.parameters + override val argumentsTypes: List = callArgs + .filterIsInstance().map { it.type.global(project).toExPhpType() } + override val explicitSpecsPsi = GenericUtil.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 ?: createPseudoConstructor(project, klass?.name ?: "Foo") + + init(call) + } + + override fun element() = call + override fun function() = method + + override fun isResolved() = method != null && klass != null + + override fun genericNames(): List { + val methodsNames = method?.genericNames() ?: emptyList() + val classesNames = klass?.genericNames() ?: emptyList() + + return mutableListOf() + .apply { addAll(methodsNames) } + .apply { addAll(classesNames) } + .toList() + } + + override fun isGeneric() = genericNames().isNotEmpty() + + override fun toString(): String { + val explicit = explicitSpecs.joinToString(",") + val implicit = implicitSpecs.joinToString(",") + return "${klass?.fqn ?: "UnknownClass"}->__construct<$explicit>($implicit)" + } + + private fun createPseudoConstructor(project: Project, className: String): Method { + return PhpPsiElementFactory.createPhpPsiFromText( + project, + Method::class.java, "class $className{ public function __construct() {} }" + ) + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/GenericFunctionCall.kt b/src/main/kotlin/com/vk/kphpstorm/generics/GenericFunctionCall.kt index 4f9ce87..d79bf42 100644 --- a/src/main/kotlin/com/vk/kphpstorm/generics/GenericFunctionCall.kt +++ b/src/main/kotlin/com/vk/kphpstorm/generics/GenericFunctionCall.kt @@ -1,550 +1,17 @@ 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 -import com.vk.kphpstorm.exphptype.* +import com.jetbrains.php.lang.psi.elements.FunctionReference +import com.jetbrains.php.lang.psi.elements.PhpTypedElement +import com.vk.kphpstorm.exphptype.ExPhpType import com.vk.kphpstorm.generics.GenericUtil.findInstantiationComment import com.vk.kphpstorm.generics.GenericUtil.genericNames import com.vk.kphpstorm.generics.GenericUtil.isGeneric -import com.vk.kphpstorm.generics.psi.GenericInstantiationPsiCommentImpl import com.vk.kphpstorm.helpers.toExPhpType -import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl -import kotlin.math.min -/** - * Класс инкапсулирующий логику вывода шаблонных типов из параметров функций. - */ -class GenericsReifier(val project: Project) { - val implicitSpecs = mutableListOf() - val implicitSpecializationNameMap = mutableMapOf() - val implicitClassSpecializationNameMap = mutableMapOf() - val implicitSpecializationErrors = mutableMapOf>() - /** - * Having a call `f(...)` of a template function `f(...)`, deduce T1 and T2 - * "auto deducing" for generics arguments is typically called "reification". - */ - fun reifyAllGenericsT( - parameters: Array, - genericTs: List, - argumentsTypes: List - ) { - for (i in 0 until min(argumentsTypes.size, parameters.size)) { - val param = parameters[i] as? PhpTypedElement ?: continue - val paramType = param.type.global(project) - val paramExType = paramType.toExPhpType() ?: continue - - // Если параметр не является шаблонным, то мы его пропускаем - if (!paramType.isGeneric(genericTs)) { - continue - } - - val argExType = argumentsTypes[i] ?: continue - reifyArgumentGenericsT(argExType, paramExType) - } - - implicitSpecializationNameMap.putAll(implicitClassSpecializationNameMap) - } - - /** - * Having a call `f($arg)`, where `f` is `f`, and `$arg` is `@param T[] $array`, deduce T. - * - * For example: - * 1. if `@param T[]` and `$arg` is `A[]`, then T is A - * 2. if `@param class-string` and `$arg` is `class-string`, then T is A - * 3. if `@param shape(key: T)` and `$arg` is `shape(key: A)`, then T is A - * - * This function is called for every template argument of `f()` invocation. - * - * TODO: add callable support - */ - private fun reifyArgumentGenericsT(argExType: ExPhpType, paramExType: ExPhpType) { - if (paramExType is ExPhpTypeGenericsT) { - val prevReifiedType = implicitSpecializationNameMap[paramExType.nameT] - if (prevReifiedType != null) { - // В таком случае мы получаем ситуацию когда один шаблонный тип - // имеет несколько возможных вариантов типа, что является ошибкой. - implicitSpecializationErrors[paramExType.nameT] = Pair(argExType, prevReifiedType) - } - - implicitSpecializationNameMap[paramExType.nameT] = argExType - implicitSpecs.add(argExType) - } - - if (paramExType is ExPhpTypeNullable) { - if (argExType is ExPhpTypeNullable) { - reifyArgumentGenericsT(argExType.inner, paramExType.inner) - } else { - reifyArgumentGenericsT(argExType, paramExType.inner) - } - } - - if (paramExType is ExPhpTypePipe) { - // если случай paramExType это Vector|Vector<%T> и argExType это Vector|Vector - 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) { - val argTypeWithoutFalse = ExPhpTypePipe(argExType.items.filter { it != ExPhpType.FALSE }) - val paramTypeWithoutFalse = paramExType.items.first { it != ExPhpType.FALSE } - reifyArgumentGenericsT(argTypeWithoutFalse, paramTypeWithoutFalse) - } - // TODO: подумать над пайпами, так как не все так очевидно - } - } - - if (paramExType is ExPhpTypeCallable) { - if (argExType is ExPhpTypeCallable) { - if (argExType.returnType != null && paramExType.returnType != null) { - reifyArgumentGenericsT(argExType.returnType, paramExType.returnType) - } - for (i in 0 until min(argExType.argTypes.size, paramExType.argTypes.size)) { - reifyArgumentGenericsT(argExType.argTypes[i], paramExType.argTypes[i]) - } - } - } - - if (paramExType is ExPhpTypeClassString) { - val isPipeWithClassString = argExType is ExPhpTypePipe && - argExType.items.any { it is ExPhpTypeClassString } - - // Для случаев когда нативный вывод типов дает в результате string|class-string - // В таком случае нам необходимо найти более точный тип. - val classStringType = if (isPipeWithClassString) { - (argExType as ExPhpTypePipe).items.find { it is ExPhpTypeClassString } - } else if (argExType is ExPhpTypeClassString) { - argExType - } else { - null - } - - if (classStringType is ExPhpTypeClassString) { - reifyArgumentGenericsT(classStringType.inner, paramExType.inner) - } - } - - if (paramExType is ExPhpTypeArray) { - if (argExType is ExPhpTypeArray) { - reifyArgumentGenericsT(argExType.inner, paramExType.inner) - } - } - - if (paramExType is ExPhpTypeTuple) { - if (argExType is ExPhpTypeTuple) { - for (i in 0 until min(argExType.items.size, paramExType.items.size)) { - reifyArgumentGenericsT(argExType.items[i], paramExType.items[i]) - } - } - } - - if (paramExType is ExPhpTypeShape) { - val isPipeWithShapes = argExType is ExPhpTypePipe && - argExType.items.any { it is ExPhpTypeShape && it.items.isNotEmpty() } - - // Для случаев когда нативный вывод типов дает в результате shape()|shape(key1:Foo...) - // В таком случае нам необходимо вычленить более точный шейп. - val shapeWithKeys = if (isPipeWithShapes) { - (argExType as ExPhpTypePipe).items.find { it is ExPhpTypeShape && it.items.isNotEmpty() } - } else if (argExType is ExPhpTypeShape) { - argExType - } else { - null - } - - if (shapeWithKeys is ExPhpTypeShape) { - shapeWithKeys.items.forEach { argShapeItem -> - val correspondingParamShapeItem = paramExType.items.find { paramShapeItem -> - argShapeItem.keyName == paramShapeItem.keyName - } ?: return@forEach - - reifyArgumentGenericsT(argShapeItem.type, correspondingParamShapeItem.type) - } - } - } - } -} - -/** - * Класс инкапсулирующий логику выделения шаблонных типов из списка инстанциации. - */ -class GenericInstantiationExtractor { - val explicitSpecs = mutableListOf() - val specializationNameMap = mutableMapOf() - - /** - * Having a call `f/**/(...)`, where `f` is `f`, deduce T1 and T2 from - * comment `/**/`. - */ - fun extractExplicitGenericsT( - genericsNames: List, - explicitSpecsPsi: GenericInstantiationPsiCommentImpl? - ) { - if (explicitSpecsPsi == null) return - - val explicitSpecsTypes = explicitSpecsPsi.instantiationPartsTypes() - - explicitSpecs.addAll(explicitSpecsTypes) - - for (i in 0 until min(genericsNames.size, explicitSpecsTypes.size)) { - specializationNameMap[genericsNames[i].name] = explicitSpecsTypes[i] - } - } -} - -/** - * Ввиду того, что мы не можем резолвить функции во время вывода типов, - * нам необходимо выделить всю необходимую информацию для дальнейшего - * вывода. - * - * Таким образом данный класс выделяет типы из явного списка инстанциации - * и выводит типы аргументов для вызова. Полученные данные пакуются в - * строку. - * - * Полученная строка может быть передана далее в [ResolvingGenericFunctionCall.unpack], - * для дальнейшей обработки. - */ -class IndexingGenericFunctionCall( - private val fqn: String, - private val callArgs: Array, - reference: PsiElement, - private val separator: String = "@@", -) { - private val explicitSpecsPsi = findInstantiationComment(reference) - - fun pack(): String? { - val explicitSpecsString = extractExplicitGenericsT().joinToString("$$") - val callArgsString = argumentsTypes().joinToString("$$") { - // Это необходимо здесь так как например для выражения [new Boo] тип будет #_\int и \Boo - // и если мы сохраним его как #_\int|\Boo, то в дальнейшем тип будет "#_\int|\Boo", и - // этот тип не разрешится верно, поэтому сохраняем типы через стрелочку, таким образом - // внутри PhpType типы будут также разделены, как были на момент сохранения здесь - if (it.types.size == 1) { - it.toString() - } else { - it.types.joinToString("→") - } - } - // В случае когда нет информации, то мы не сможем вывести более точный тип - if (explicitSpecsString.isEmpty() && callArgsString.isEmpty()) { - return null - } - return "${fqn}$separator$explicitSpecsString$separator$callArgsString" - } - - private fun argumentsTypes(): List { - return callArgs.filterIsInstance().map { it.type } - } - - private fun extractExplicitGenericsT(): List { - if (explicitSpecsPsi == null) return emptyList() - return explicitSpecsPsi.instantiationPartsTypes() - } -} - -/** - * Данный класс инкапсулирует логику обработки данных полученных на этапе - * индексации и вывода типов ([IndexingGenericFunctionCall]). - * - * Результатом для данного класса являются данные возвращаемые методом - * [specialization], данный метод возвращает список шаблонных типов - * для данного вызова. - */ -abstract class ResolvingGenericBase(val project: Project) { - abstract var parameters: Array - abstract var genericTs: List - - protected lateinit var argumentsTypes: List - protected lateinit var explicitGenericsT: List - - private val reifier = GenericsReifier(project) - - fun specialization(): List { - return explicitGenericsT.ifEmpty { reifier.implicitSpecs } - } - - fun unpack(packedData: String): Boolean { - if (unpackImpl(packedData)) { - reifier.reifyAllGenericsT(parameters, genericTs, argumentsTypes) - return true - } - - return false - } - - protected abstract fun unpackImpl(packedData: String): Boolean - - protected fun unpackTypeArray(text: String) = if (text.isNotEmpty()) - text.split("$$").mapNotNull { - val types = it.split("→") - val type = PhpType() - types.forEach { singleType -> - type.add(singleType) - } - type.global(project).toExPhpType() - } - else - emptyList() -} - - -class ResolvingGenericFunctionCall(project: Project) : ResolvingGenericBase(project) { - lateinit var function: Function - override lateinit var parameters: Array - override lateinit var genericTs: List - - override fun unpackImpl(packedData: String): Boolean { - val firstSeparatorIndex = packedData.indexOf("@@") - if (firstSeparatorIndex == -1) { - return false - } - val functionName = packedData.substring(0, firstSeparatorIndex) - - val remainingPackedData = packedData.substring(firstSeparatorIndex + "@@".length) - val secondSeparatorIndex = remainingPackedData.indexOf("@@") - if (secondSeparatorIndex == -1) { - return false - } - val explicitGenericsString = remainingPackedData.substring(0, secondSeparatorIndex) - val argumentsTypesString = remainingPackedData.substring(secondSeparatorIndex + "@@".length) - - function = PhpIndex.getInstance(project).getFunctionsByFQN(functionName).firstOrNull() ?: return false - - genericTs = function.genericNames() - parameters = function.parameters - - explicitGenericsT = unpackTypeArray(explicitGenericsString) - argumentsTypes = unpackTypeArray(argumentsTypesString) - - return true - } -} - -class ResolvingGenericConstructorCall(project: Project) : ResolvingGenericBase(project) { - var klass: PhpClass? = null - var method: Method? = null - override lateinit var parameters: Array - override lateinit var genericTs: List - - override fun unpackImpl(packedData: String): Boolean { - val firstSeparatorIndex = packedData.indexOf("@CO@") - if (firstSeparatorIndex == -1) { - return false - } - val fqn = packedData.substring(0, firstSeparatorIndex) - - val remainingPackedData = packedData.substring(firstSeparatorIndex + "@CO@".length) - val secondSeparatorIndex = remainingPackedData.indexOf("@CO@") - val secondSepIndex = if (secondSeparatorIndex == -1) - 0 - else - secondSeparatorIndex - val explicitGenericsString = remainingPackedData.substring(0, secondSepIndex) - val argumentsTypesString = remainingPackedData.substring(if (secondSepIndex == 0) 0 else secondSepIndex + "@CO@".length) - - val className = fqn.substring(0, fqn.indexOf("__construct")) - - klass = PhpIndex.getInstance(project).getClassesByFQN(className).firstOrNull() ?: return false - method = klass!!.constructor - - parameters = if (klass!!.constructor != null) klass!!.constructor!!.parameters else emptyArray() - genericTs = klass!!.genericNames() - - explicitGenericsT = unpackTypeArray(explicitGenericsString) - argumentsTypes = unpackTypeArray(argumentsTypesString) - - return true - } -} - -class ResolvingGenericMethodCall(project: Project) : ResolvingGenericBase(project) { - var klass: PhpClass? = null - var method: Method? = null - var classGenericType: ExPhpTypeTplInstantiation? = null - override lateinit var parameters: Array - override lateinit var genericTs: List - lateinit var classGenericTs: List - - override fun unpackImpl(packedData: String): Boolean { - val parts = packedData.split("@MC@") - if (parts.size != 3) { - return false - } - - val fqn = parts[0] - - val classNameTypeString = fqn.substring(1, fqn.indexOf('.')) - - val classType = PhpType().add(classNameTypeString).global(project) - val parsed = classType.toExPhpType() - - // for IDE we return PhpType "A"|"A", that's why - // A> is resolved as "A"|"A>", so if pipe — search for instantiation - val instantiation = when (parsed) { - is ExPhpTypePipe -> parsed.items.firstOrNull { it is ExPhpTypeTplInstantiation } - is ExPhpTypeNullable -> parsed.inner - else -> parsed - } as? ExPhpTypeTplInstantiation ?: return false - - classGenericType = instantiation - val methodName = fqn.substring(fqn.indexOf('.') + 1, fqn.length) - - klass = PhpIndex.getInstance(project).getClassesByFQN(instantiation.classFqn).firstOrNull() ?: return false - method = klass!!.findMethodByName(methodName) - if (method == null) { - return false - } - - parameters = method!!.parameters - genericTs = method!!.genericNames() - classGenericTs = klass!!.genericNames() - - explicitGenericsT = unpackTypeArray(parts[1]) - argumentsTypes = unpackTypeArray(parts[2]) - - return true - } -} - -class GenericConstructorCall(call: NewExpression) : GenericCall(call.project) { - override val callArgs: Array = call.parameters - override val argumentsTypes: List = callArgs - .filterIsInstance().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 ?: createPseudoConstructor(project, klass?.name ?: "Foo") - - init() - } - - override fun function() = method - - override fun isResolved() = method != null && klass != null - - override fun genericNames(): List { - val methodsNames = method?.genericNames() ?: emptyList() - val classesNames = klass?.genericNames() ?: emptyList() - - return mutableListOf() - .apply { addAll(methodsNames) } - .apply { addAll(classesNames) } - .toList() - } - - override fun isGeneric() = genericNames().isNotEmpty() - - override fun toString(): String { - val explicit = explicitSpecs.joinToString(",") - val implicit = implicitSpecs.joinToString(",") - return "${klass?.fqn ?: "UnknownClass"}->__construct<$explicit>($implicit)" - } - - private fun createPseudoConstructor(project: Project, className: String): Method { - return PhpPsiElementFactory.createPhpPsiFromText( - project, - Method::class.java, "class $className{ public function __construct() {} }" - ) - } -} - -class GenericMethodCall(call: MethodReference) : GenericCall(call.project) { - override val callArgs: Array = call.parameters - override val argumentsTypes: List = callArgs - .filterIsInstance().map { it.type.global(project).toExPhpType() } - override val explicitSpecsPsi = findInstantiationComment(call) - - private val klass: PhpClass? - private val method: Method? - - init { - method = call.resolve() as? Method - klass = method?.containingClass - - val callType = call.classReference?.type?.global(project) - - val classType = PhpType().add(callType).global(project) - val parsed = classType.toExPhpType() - - // for IDE we return PhpType "A"|"A", that's why - // A> is resolved as "A"|"A>", so if pipe — search for instantiation - val instantiation = when (parsed) { - is ExPhpTypePipe -> parsed.items.firstOrNull { it is ExPhpTypeTplInstantiation } - is ExPhpTypeNullable -> parsed.inner - else -> parsed - } as? ExPhpTypeTplInstantiation - - if (instantiation != null) { - val specialization = instantiation.specializationList - val classSpecializationNameMap = mutableMapOf() - val genericNames = klass?.genericNames() ?: emptyList() - - for (i in 0 until min(genericNames.size, specialization.size)) { - classSpecializationNameMap[genericNames[i].name] = specialization[i] - } - - classSpecializationNameMap.forEach { (name, type) -> - reifier.implicitClassSpecializationNameMap[name] = type - } - } - - init() - } - - override fun function() = method - - override fun isResolved() = method != null && klass != null - - override fun genericNames(): List { - val methodsNames = method?.genericNames() ?: emptyList() - val classesNames = klass?.genericNames() ?: emptyList() - - return mutableListOf() - .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)" - } -} - -class GenericFunctionCall(call: FunctionReference) : GenericCall(call.project) { +class GenericFunctionCall(private val call: FunctionReference) : GenericCall(call.project) { override val callArgs: Array = call.parameters override val argumentsTypes: List = callArgs .filterIsInstance().map { it.type.global(project).toExPhpType() } @@ -553,9 +20,11 @@ class GenericFunctionCall(call: FunctionReference) : GenericCall(call.project) { private val function: Function? = call.resolve() as? Function init { - init() + init(call) } + override fun element() = call + override fun function() = function override fun isResolved() = function != null @@ -572,165 +41,3 @@ class GenericFunctionCall(call: FunctionReference) : GenericCall(call.project) { } } -/** - * Ввиду причин описанных в [IndexingGenericFunctionCall], мы не можем использовать - * объединенный класс для обработки вызова во время вывода типов. Однако в других - * местах мы можем использовать индекс и поэтому нам не нужно паковать данные и - * потом их распаковывать, мы можем делать все за раз. - * - * Данный класс является объединением [IndexingGenericFunctionCall] и - * [ResolvingGenericFunctionCall] и может быть использован для обработки шаблонных - * вызовов в других местах; - * - * Класс инкапсулирующий в себе всю логику обработки шаблонных вызовов. - * - * Он выводит неявные типы, когда нет явного определения списка типов при инстанциации. - * - * Например: - * - * ```php - * /** - * * @kphp-generic T1, T2 - * * @param T1 $a - * * @param T2 $b - * */ - * function f($a, $b) {} - * - * f(new A, new B); // => T1 = A, T2 = B - * ``` - * - * В случае когда есть явный список типов при инстанциации, он собирает типы - * из него. - * - * Например: - * - * ```php - * f/**/(new A, new B); // => T1 = C, T2 = D - * ``` - */ -abstract class GenericCall(val project: Project) { - abstract val callArgs: Array - abstract val explicitSpecsPsi: GenericInstantiationPsiCommentImpl? - abstract val argumentsTypes: List - - abstract fun function(): Function? - abstract fun isResolved(): Boolean - abstract fun genericNames(): List - abstract fun isGeneric(): Boolean - - val genericTs = mutableListOf() - private val parameters = mutableListOf() - - protected val extractor = GenericInstantiationExtractor() - protected val reifier = GenericsReifier(project) - - val explicitSpecs get() = extractor.explicitSpecs - val specializationNameMap get() = extractor.specializationNameMap - val implicitSpecs get() = reifier.implicitSpecs - val implicitSpecializationNameMap get() = reifier.implicitSpecializationNameMap - val implicitClassSpecializationNameMap get() = reifier.implicitClassSpecializationNameMap - val implicitSpecializationErrors get() = reifier.implicitSpecializationErrors - - protected fun init() { - val function = function() ?: return - if (!isGeneric()) return - - val genericNames = genericNames() - - parameters.addAll(function.parameters) - genericTs.addAll(genericNames) - - // Несмотря на то, что явный список является превалирующим над - // типами выведенными из аргументов функций, нам все равно - // необходимы обв списка для дальнейших инспекций - - // В первую очередь, выводим все типы шаблонов из аргументов функции (при наличии) - reifier.reifyAllGenericsT(function.parameters, genericNames, argumentsTypes) - // Далее, выводим все типы шаблонов из явного списка типов (при наличии) - extractor.extractExplicitGenericsT(genericNames(), explicitSpecsPsi) - } - - fun withExplicitSpecs(): Boolean { - return explicitSpecsPsi != null - } - - /** - * Функция проверяющая, что явно указанные шаблонные типы - * соответствуют автоматически выведенным типам и их можно - * безопасно удалить. - * - * Например: - * - * ```php - * /** - * * @kphp-generic T - * * @param T $arg - * */ - * function f($arg) {} - * - * f/**/(new Foo); // === f(new Foo); - * ``` - */ - fun isNoNeedExplicitSpec(): Boolean { - if (explicitSpecsPsi == null) return false - - val countGenericNames = genericNames().size - val countExplicitSpecs = explicitSpecs.size - val countImplicitSpecs = implicitSpecs.size - - if (countGenericNames == countExplicitSpecs && countExplicitSpecs == countImplicitSpecs) { - var isEqual = true - explicitSpecs.forEachIndexed { index, explicitSpec -> - // TODO: Здесь должна быть проверка что они не равны - // Такой подход не работает для nullable - if (!implicitSpecs[index].isAssignableFrom(explicitSpec, project)) { - isEqual = false - } - } - return isEqual - } - - return false - } - - /** - * Имея следующую функцию: - * - * ```php - * /** - * * @kphp-generic T - * * @param T $arg - * */ - * function f($arg) {} - * ``` - * - * И следующий вызов: - * - * ```php - * f/**/(new Foo); - * ``` - * - * Нам необходимо вывести тип `$arg`, для того чтобы проверить, что - * переданное выражение `new Foo` имеет правильный тип. - * - * Так как функция может вызываться с разными шаблонными типа, нам - * необходимо найти тип `$arg` для каждого конкретного вызова. - * В примере выше в результате будет возвращен тип `Foo`. - */ - fun typeOfParam(index: Int): ExPhpType? { - val function = function() ?: return null - - val param = function.getParameter(index) ?: return null - val paramType = param.type - if (paramType.isGeneric(genericNames())) { - val usedNameMap = extractor.specializationNameMap.ifEmpty { - reifier.implicitSpecializationNameMap - } - return paramType.toExPhpType()?.instantiateGeneric(usedNameMap) - } - - return null - } - - abstract override fun toString(): String -} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/GenericInstantiationExtractor.kt b/src/main/kotlin/com/vk/kphpstorm/generics/GenericInstantiationExtractor.kt new file mode 100644 index 0000000..a83786a --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/GenericInstantiationExtractor.kt @@ -0,0 +1,33 @@ +package com.vk.kphpstorm.generics + +import com.vk.kphpstorm.exphptype.ExPhpType +import com.vk.kphpstorm.generics.psi.GenericInstantiationPsiCommentImpl +import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl +import kotlin.math.min + +/** + * Класс инкапсулирующий логику выделения шаблонных типов из списка инстанциации. + */ +class GenericInstantiationExtractor { + val explicitSpecs = mutableListOf() + val specializationNameMap = mutableMapOf() + + /** + * Having a call `f/**/(...)`, where `f` is `f`, deduce T1 and T2 from + * comment `/**/`. + */ + fun extractExplicitGenericsT( + genericsNames: List, + explicitSpecsPsi: GenericInstantiationPsiCommentImpl? + ) { + if (explicitSpecsPsi == null) return + + val explicitSpecsTypes = explicitSpecsPsi.instantiationTypes() + + explicitSpecs.addAll(explicitSpecsTypes) + + for (i in 0 until min(genericsNames.size, explicitSpecsTypes.size)) { + specializationNameMap[genericsNames[i].name] = explicitSpecsTypes[i] + } + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/GenericMethodCall.kt b/src/main/kotlin/com/vk/kphpstorm/generics/GenericMethodCall.kt new file mode 100644 index 0000000..7a9bdec --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/GenericMethodCall.kt @@ -0,0 +1,73 @@ +package com.vk.kphpstorm.generics + +import com.intellij.psi.PsiElement +import com.jetbrains.php.lang.psi.elements.Method +import com.jetbrains.php.lang.psi.elements.MethodReference +import com.jetbrains.php.lang.psi.elements.PhpTypedElement +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.vk.kphpstorm.exphptype.ExPhpType +import com.vk.kphpstorm.generics.GenericUtil.genericNames +import com.vk.kphpstorm.generics.GenericUtil.getInstantiation +import com.vk.kphpstorm.helpers.toExPhpType +import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl +import kotlin.math.min + +class GenericMethodCall(private val call: MethodReference) : GenericCall(call.project) { + override val callArgs: Array = call.parameters + override val argumentsTypes: List = callArgs + .filterIsInstance().map { it.type.global(project).toExPhpType() } + override val explicitSpecsPsi = GenericUtil.findInstantiationComment(call) + + private val method = call.resolve() as? Method + private val klass = method?.containingClass + + init { + val callType = call.classReference?.type?.global(project) + + val classType = PhpType().add(callType).global(project) + val parsed = classType.toExPhpType() + + val instantiation = parsed?.getInstantiation() + + if (instantiation != null) { + val specialization = instantiation.specializationList + val classSpecializationNameMap = mutableMapOf() + val genericNames = klass?.genericNames() ?: emptyList() + + for (i in 0 until min(genericNames.size, specialization.size)) { + classSpecializationNameMap[genericNames[i].name] = specialization[i] + } + + classSpecializationNameMap.forEach { (name, type) -> + reifier.implicitClassSpecializationNameMap[name] = type + } + } + + init(call) + } + + override fun element() = call + + override fun function() = method + + override fun isResolved() = method != null && klass != null + + override fun genericNames(): List { + val methodsNames = method?.genericNames() ?: emptyList() + val classesNames = klass?.genericNames() ?: emptyList() + + return mutableListOf() + .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)" + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/GenericUtil.kt b/src/main/kotlin/com/vk/kphpstorm/generics/GenericUtil.kt index b34d042..f7094ca 100644 --- a/src/main/kotlin/com/vk/kphpstorm/generics/GenericUtil.kt +++ b/src/main/kotlin/com/vk/kphpstorm/generics/GenericUtil.kt @@ -9,33 +9,26 @@ import com.jetbrains.php.lang.psi.elements.PhpClass import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl import com.jetbrains.php.lang.psi.elements.impl.NewExpressionImpl import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.vk.kphpstorm.exphptype.* import com.vk.kphpstorm.generics.psi.GenericInstantiationPsiCommentImpl import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl import com.vk.kphpstorm.kphptags.psi.KphpDocTagGenericPsiImpl object GenericUtil { - fun Function.isGeneric(): Boolean { - return docComment?.getTagElementsByName("@kphp-generic")?.firstOrNull() != null - } + fun Function.isGeneric() = docComment?.getTagElementsByName("@kphp-generic")?.firstOrNull() != null - fun PhpClass.isGeneric(): Boolean { - return docComment?.getTagElementsByName("@kphp-generic")?.firstOrNull() != null - } + fun PhpClass.isGeneric() = docComment?.getTagElementsByName("@kphp-generic")?.firstOrNull() != null - fun PhpType.isGeneric(f: Function): Boolean { - val genericNames = f.genericNames() - return isGeneric(genericNames) - } + fun ExPhpType.isGeneric() = toString().contains("%") - fun PhpType.isGeneric(c: PhpClass): Boolean { - val genericNames = c.genericNames() - return isGeneric(genericNames) - } + fun PhpType.isGeneric(f: Function) = isGeneric(f.genericNames()) + + fun PhpType.isGeneric(c: PhpClass) = isGeneric(c.genericNames()) fun PhpType.isGeneric(genericNames: List): Boolean { // TODO: Подумать как сделать улучшить return genericNames.any { decl -> types.contains("%${decl.name}") } || - genericNames.any { decl -> types.any { type -> type.contains("%${decl.name}") } } + genericNames.any { decl -> types.any { type -> type.contains("%${decl.name}") } } } fun Function.isReturnGeneric(): Boolean { @@ -56,6 +49,41 @@ object GenericUtil { return docT.getGenericArgumentsWithExtends() } + fun ExPhpType.isGenericPipe(): Boolean { + if (this is ExPhpTypePipe) { + if (this.items.size != 2) return false + return this.items.any { + it is ExPhpTypeTplInstantiation || (it is ExPhpTypeArray && it.inner is ExPhpTypeTplInstantiation) + } + } + + return false + } + + fun ExPhpType.getGenericPipeType(): ExPhpType? { + if (!isGenericPipe()) { + return null + } + + return (this as ExPhpTypePipe).items.firstOrNull { + // TODO: + it is ExPhpTypeTplInstantiation || (it is ExPhpTypeArray && it.inner is ExPhpTypeTplInstantiation) + } + } + + /** + * for IDE, we return PhpType "A"|"A", that's why + * A> is resolved as "A"|"A>", so if pipe — search for instantiation + */ + fun ExPhpType.getInstantiation(): ExPhpTypeTplInstantiation? { + return when (this) { + is ExPhpTypePipe -> items.firstOrNull { it is ExPhpTypeTplInstantiation } + is ExPhpTypeNullable -> inner + is ExPhpTypeForcing -> inner + else -> this + } as? ExPhpTypeTplInstantiation + } + fun findInstantiationComment(el: PsiElement): GenericInstantiationPsiCommentImpl? { val candidate = when (el) { is NewExpressionImpl -> { @@ -67,9 +95,8 @@ object GenericUtil { else -> { el.firstChild?.nextSibling } - } + } ?: return null - if (candidate == null) return null if (candidate !is GenericInstantiationPsiCommentImpl) return null return candidate } diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/GenericsReifier.kt b/src/main/kotlin/com/vk/kphpstorm/generics/GenericsReifier.kt new file mode 100644 index 0000000..e4cd420 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/GenericsReifier.kt @@ -0,0 +1,202 @@ +package com.vk.kphpstorm.generics + +import com.intellij.openapi.project.Project +import com.jetbrains.php.lang.psi.elements.Parameter +import com.jetbrains.php.lang.psi.elements.PhpClass +import com.jetbrains.php.lang.psi.elements.PhpTypedElement +import com.vk.kphpstorm.exphptype.* +import com.vk.kphpstorm.generics.GenericUtil.getInstantiation +import com.vk.kphpstorm.generics.GenericUtil.isGeneric +import com.vk.kphpstorm.helpers.toExPhpType +import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl +import kotlin.math.min + +/** + * Класс инкапсулирующий логику вывода шаблонных типов из параметров функций. + */ +class GenericsReifier(val project: Project) { + val implicitSpecs = mutableListOf() + val implicitSpecializationNameMap = mutableMapOf() + val implicitClassSpecializationNameMap = mutableMapOf() + val implicitSpecializationErrors = mutableMapOf>() + + /** + * Having a call `f(...)` of a template function `f(...)`, deduce T1 and T2 + * "auto deducing" for generics arguments is typically called "reification". + */ + fun reifyAllGenericsT( + klass: PhpClass?, + parameters: Array, + genericTs: List, + argumentsTypes: List, + contextType: ExPhpType?, + ) { + for (i in 0 until min(argumentsTypes.size, parameters.size)) { + val param = parameters[i] as? PhpTypedElement ?: continue + val paramType = param.type.global(project) + val paramExType = paramType.toExPhpType() ?: continue + + // Если параметр не является шаблонным, то мы его пропускаем + if (!paramType.isGeneric(genericTs)) { + continue + } + + val argExType = argumentsTypes[i] ?: continue + reifyArgumentGenericsT(argExType, paramExType) + } + + var instantiation = contextType?.getInstantiation() + if (instantiation != null) { + // TODO: сделать верный вывод типов тут + if (instantiation.classFqn == klass?.fqn) { + instantiation = instantiation.specializationList.firstOrNull() as? ExPhpTypeTplInstantiation + } + + if (instantiation != null) { + val specList = instantiation.specializationList + for (i in 0 until min(specList.size, genericTs.size)) { + val type = specList[i] + val genericT = genericTs[i] + + implicitSpecializationNameMap[genericT.name] = type + } + } + } + + implicitSpecializationNameMap.putAll(implicitClassSpecializationNameMap) + } + + /** + * Having a call `f($arg)`, where `f` is `f`, and `$arg` is `@param T[] $array`, deduce T. + * + * For example: + * 1. if `@param T[]` and `$arg` is `A[]`, then T is A + * 2. if `@param class-string` and `$arg` is `class-string`, then T is A + * 3. if `@param shape(key: T)` and `$arg` is `shape(key: A)`, then T is A + * + * This function is called for every template argument of `f()` invocation. + * + * TODO: add callable support + */ + private fun reifyArgumentGenericsT(argExType: ExPhpType, paramExType: ExPhpType) { + if (paramExType is ExPhpTypeGenericsT) { + val prevReifiedType = implicitSpecializationNameMap[paramExType.nameT] + if (prevReifiedType != null) { + // В таком случае мы получаем ситуацию когда один шаблонный тип + // имеет несколько возможных вариантов типа, что является ошибкой. + implicitSpecializationErrors[paramExType.nameT] = Pair(argExType, prevReifiedType) + } + + implicitSpecializationNameMap[paramExType.nameT] = argExType + implicitSpecs.add(argExType) + } + + if (paramExType is ExPhpTypeNullable) { + if (argExType is ExPhpTypeNullable) { + reifyArgumentGenericsT(argExType.inner, paramExType.inner) + } else { + reifyArgumentGenericsT(argExType, paramExType.inner) + } + } + + if (paramExType is ExPhpTypePipe) { + // если случай paramExType это Vector|Vector<%T> и argExType это Vector|Vector + 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) { + val argTypeWithoutFalse = ExPhpTypePipe(argExType.items.filter { it != ExPhpType.FALSE }) + val paramTypeWithoutFalse = paramExType.items.first { it != ExPhpType.FALSE } + reifyArgumentGenericsT(argTypeWithoutFalse, paramTypeWithoutFalse) + } + // TODO: подумать над пайпами, так как не все так очевидно + } + } + + if (paramExType is ExPhpTypeCallable) { + if (argExType is ExPhpTypeCallable) { + if (argExType.returnType != null && paramExType.returnType != null) { + reifyArgumentGenericsT(argExType.returnType, paramExType.returnType) + } + for (i in 0 until min(argExType.argTypes.size, paramExType.argTypes.size)) { + reifyArgumentGenericsT(argExType.argTypes[i], paramExType.argTypes[i]) + } + } + } + + if (paramExType is ExPhpTypeClassString) { + val isPipeWithClassString = argExType is ExPhpTypePipe && + argExType.items.any { it is ExPhpTypeClassString } + + // Для случаев когда нативный вывод типов дает в результате string|class-string + // В таком случае нам необходимо найти более точный тип. + val classStringType = if (isPipeWithClassString) { + (argExType as ExPhpTypePipe).items.find { it is ExPhpTypeClassString } + } else if (argExType is ExPhpTypeClassString) { + argExType + } else { + null + } + + if (classStringType is ExPhpTypeClassString) { + reifyArgumentGenericsT(classStringType.inner, paramExType.inner) + } + } + + if (paramExType is ExPhpTypeArray) { + if (argExType is ExPhpTypeArray) { + reifyArgumentGenericsT(argExType.inner, paramExType.inner) + } + } + + if (paramExType is ExPhpTypeTuple) { + if (argExType is ExPhpTypeTuple) { + for (i in 0 until min(argExType.items.size, paramExType.items.size)) { + reifyArgumentGenericsT(argExType.items[i], paramExType.items[i]) + } + } + } + + if (paramExType is ExPhpTypeShape) { + val isPipeWithShapes = argExType is ExPhpTypePipe && + argExType.items.any { it is ExPhpTypeShape && it.items.isNotEmpty() } + + // Для случаев когда нативный вывод типов дает в результате shape()|shape(key1:Foo...) + // В таком случае нам необходимо вычленить более точный шейп. + val shapeWithKeys = if (isPipeWithShapes) { + (argExType as ExPhpTypePipe).items.find { it is ExPhpTypeShape && it.items.isNotEmpty() } + } else if (argExType is ExPhpTypeShape) { + argExType + } else { + null + } + + if (shapeWithKeys is ExPhpTypeShape) { + shapeWithKeys.items.forEach { argShapeItem -> + val correspondingParamShapeItem = paramExType.items.find { paramShapeItem -> + argShapeItem.keyName == paramShapeItem.keyName + } ?: return@forEach + + reifyArgumentGenericsT(argShapeItem.type, correspondingParamShapeItem.type) + } + } + } + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/IndexingGenericFunctionCall.kt b/src/main/kotlin/com/vk/kphpstorm/generics/IndexingGenericFunctionCall.kt new file mode 100644 index 0000000..89b640a --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/IndexingGenericFunctionCall.kt @@ -0,0 +1,81 @@ +package com.vk.kphpstorm.generics + +import com.intellij.psi.PsiElement +import com.jetbrains.php.lang.psi.PhpFile +import com.jetbrains.php.lang.psi.elements.PhpTypedElement +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.vk.kphpstorm.exphptype.ExPhpType + +/** + * Ввиду того, что мы не можем резолвить функции во время вывода типов, + * нам необходимо выделить всю необходимую информацию для дальнейшего + * вывода. + * + * Таким образом данный класс выделяет типы из явного списка инстанциации + * и выводит типы аргументов для вызова. Полученные данные пакуются в + * строку. + * + * Полученная строка может быть передана далее в [ResolvingGenericFunctionCall.unpack], + * для дальнейшей обработки. + */ +class IndexingGenericFunctionCall( + private val fqn: String, + private val callArgs: Array, + private val reference: PsiElement, + private val separator: String = "@@", +) { + private val explicitSpecsPsi = GenericUtil.findInstantiationComment(reference) + + fun pack(): String? { + val file = reference.containingFile as PhpFile +// if (fqn.contains(".")) { +// val (className, functionName) = fqn.split(".") +// val klass = file.topLevelDefs[className].firstOrNull() as? PhpClass +// if (klass != null) { +// val method = klass.findOwnMethodByName(functionName) +// if (method != null && !method.isGeneric()) { +// return null +// } +// } +// } +// val function = (reference.containingFile as PhpFile).topLevelDefs[fqn].firstOrNull() as? Function +// if (function != null && !function.isGeneric()) { +// return null +// } + + val explicitSpecsString = extractExplicitGenericsT().joinToString("$$") + val callArgsString = argumentsTypes().joinToString("$$") { +// if (it.types.size == 2) { +// val containsUnresolved = it.types.find { type -> type.startsWith("#") } != null +// val containsResolved = it.types.find { type -> !type.startsWith("#") } != null +// if (containsUnresolved && containsResolved) { +// return@joinToString it.types.find { type -> !type.startsWith("#") }.toString() +// } +// } + + // Это необходимо здесь так как например для выражения [new Boo] тип будет #_\int и \Boo + // и если мы сохраним его как #_\int|\Boo, то в дальнейшем тип будет "#_\int|\Boo", и + // этот тип не разрешится верно, поэтому сохраняем типы через стрелочку, таким образом + // внутри PhpType типы будут также разделены, как были на момент сохранения здесь + if (it.types.size == 1) { + it.toString() + } else { + it.types.joinToString("→") + } + } + // В случае когда нет информации, то мы не сможем вывести более точный тип + if (explicitSpecsString.isEmpty() && callArgsString.isEmpty() && !fqn.contains(".")) { + return null + } + return "${fqn}$separator$explicitSpecsString$separator$callArgsString$separator" + } + + private fun argumentsTypes(): List { + return callArgs.filterIsInstance().map { it.type } + } + + private fun extractExplicitGenericsT(): List { + if (explicitSpecsPsi == null) return emptyList() + return explicitSpecsPsi.instantiationTypes() + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericBase.kt b/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericBase.kt new file mode 100644 index 0000000..4fd07ae --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericBase.kt @@ -0,0 +1,86 @@ +package com.vk.kphpstorm.generics + +import com.intellij.openapi.project.Project +import com.jetbrains.php.lang.psi.elements.Parameter +import com.jetbrains.php.lang.psi.elements.PhpClass +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.vk.kphpstorm.exphptype.ExPhpType +import com.vk.kphpstorm.helpers.toExPhpType +import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl + +/** + * Данный класс инкапсулирует логику обработки данных полученных на этапе + * индексации и вывода типов ([IndexingGenericFunctionCall]). + * + * Результатом для данного класса являются данные возвращаемые методом + * [specialization], данный метод возвращает список шаблонных типов + * для данного вызова. + */ +abstract class ResolvingGenericBase(val project: Project) { + abstract var parameters: Array + abstract var genericTs: List + + protected lateinit var argumentsTypes: List + protected lateinit var explicitGenericsT: List + + private val reifier = GenericsReifier(project) + + fun specialization(): List { + return explicitGenericsT.ifEmpty { reifier.implicitSpecs } + } + + fun unpack(packedData: String): Boolean { + if (unpackImpl(packedData)) { + reifier.reifyAllGenericsT(klass(), parameters, genericTs, argumentsTypes, null) + return true + } + + return false + } + + abstract fun klass(): PhpClass? + + protected fun getAtLeast(data: String, count: Int, separator: String): List? { + var remainingData = data + var countTaken = 0 + val parts = mutableListOf() + + while (true) { + val sepIndex = remainingData.indexOf(separator) + if (sepIndex == -1) { + parts.add(remainingData) + countTaken++ + if (countTaken == count) { + return parts + } + return null + } + + val part = remainingData.substring(0, sepIndex) + parts.add(part) + + remainingData = remainingData.substring(sepIndex + separator.length) + countTaken++ + if (countTaken == count) { + parts.add(remainingData) + break + } + } + + return if (parts.count() >= count) parts else null + } + + protected abstract fun unpackImpl(packedData: String): Boolean + + protected fun unpackTypeArray(text: String) = if (text.isNotEmpty()) + text.split("$$").mapNotNull { + val types = it.split("→") + val type = PhpType() + types.forEach { singleType -> + type.add(singleType) + } + type.global(project).toExPhpType() + } + else + emptyList() +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericConstructorCall.kt b/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericConstructorCall.kt new file mode 100644 index 0000000..bcfee8b --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericConstructorCall.kt @@ -0,0 +1,39 @@ +package com.vk.kphpstorm.generics + +import com.intellij.openapi.project.Project +import com.jetbrains.php.PhpIndex +import com.jetbrains.php.lang.psi.elements.Method +import com.jetbrains.php.lang.psi.elements.Parameter +import com.jetbrains.php.lang.psi.elements.PhpClass +import com.vk.kphpstorm.generics.GenericUtil.genericNames +import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl +import com.vk.kphpstorm.typeProviders.GenericClassesTypeProvider + +class ResolvingGenericConstructorCall(project: Project) : ResolvingGenericBase(project) { + var klass: PhpClass? = null + var method: Method? = null + override lateinit var parameters: Array + override lateinit var genericTs: List + + override fun klass(): PhpClass? = klass + + override fun unpackImpl(packedData: String): Boolean { + val parts = getAtLeast(packedData, 3, GenericClassesTypeProvider.SEP) ?: return false + val fqn = parts[0] + val explicitGenericsString = parts[1] + val argumentsTypesString = parts[2] + + val className = fqn.split(".").first() + + klass = PhpIndex.getInstance(project).getClassesByFQN(className).firstOrNull() ?: return false + method = klass!!.constructor + + parameters = if (klass!!.constructor != null) klass!!.constructor!!.parameters else emptyArray() + genericTs = klass!!.genericNames() + + explicitGenericsT = unpackTypeArray(explicitGenericsString) + argumentsTypes = unpackTypeArray(argumentsTypesString) + + return true + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericFunctionCall.kt b/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericFunctionCall.kt new file mode 100644 index 0000000..4643de9 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericFunctionCall.kt @@ -0,0 +1,37 @@ +package com.vk.kphpstorm.generics + +import com.intellij.openapi.project.Project +import com.jetbrains.php.PhpIndex +import com.jetbrains.php.lang.psi.elements.Function +import com.jetbrains.php.lang.psi.elements.Parameter +import com.jetbrains.php.lang.psi.elements.PhpClass +import com.vk.kphpstorm.generics.GenericUtil.genericNames +import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl + +class ResolvingGenericFunctionCall(project: Project) : ResolvingGenericBase(project) { + lateinit var function: Function + override lateinit var parameters: Array + override lateinit var genericTs: List + + override fun klass(): PhpClass? = null + + override fun unpackImpl(packedData: String): Boolean { + val parts = getAtLeast(packedData, 3, "@@") + if (parts == null) { + return false + } + val functionName = parts[0] + val explicitGenericsString = parts[1] + val argumentsTypesString = parts[2] + + function = PhpIndex.getInstance(project).getFunctionsByFQN(functionName).firstOrNull() ?: return false + + genericTs = function.genericNames() + parameters = function.parameters + + explicitGenericsT = unpackTypeArray(explicitGenericsString) + argumentsTypes = unpackTypeArray(argumentsTypesString) + + return true + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericMethodCall.kt b/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericMethodCall.kt new file mode 100644 index 0000000..b9bd8ea --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericMethodCall.kt @@ -0,0 +1,104 @@ +package com.vk.kphpstorm.generics + +import com.intellij.openapi.project.Project +import com.jetbrains.php.PhpIndex +import com.jetbrains.php.lang.psi.elements.Method +import com.jetbrains.php.lang.psi.elements.Parameter +import com.jetbrains.php.lang.psi.elements.PhpClass +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.vk.kphpstorm.exphptype.ExPhpTypeGenericsT +import com.vk.kphpstorm.exphptype.ExPhpTypeTplInstantiation +import com.vk.kphpstorm.generics.GenericUtil.genericNames +import com.vk.kphpstorm.generics.GenericUtil.getInstantiation +import com.vk.kphpstorm.helpers.toExPhpType +import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl +import com.vk.kphpstorm.typeProviders.GenericMethodsTypeProvider + +class ResolvingGenericMethodCall(project: Project) : ResolvingGenericBase(project) { + var klass: PhpClass? = null + var method: Method? = null + var classGenericType: ExPhpTypeTplInstantiation? = null + override lateinit var parameters: Array + override lateinit var genericTs: List + lateinit var classGenericTs: List + + override fun klass(): PhpClass? = klass + + private fun unpackRecursive(data: String): Boolean { + val sep = GenericMethodsTypeProvider.SEP + + if (GenericMethodsTypeProvider.KEY.signed(data)) { + val parts = getAtLeast(data.substring(2), 3, sep) ?: return false + + val joinedType = (parts.take(3).joinToString(sep)) + .removePrefix("#" + GenericMethodsTypeProvider.KEY) + .removePrefix("#" + GenericMethodsTypeProvider.KEY) + val count = joinedType.count { it == sep[0] } / 2 + var forResolve = joinedType + for (i in 0 until count) { + forResolve = GenericMethodsTypeProvider.KEY.sign(forResolve) + } + + val fqnResolved = + PhpType().add(forResolve).global(project).toExPhpType() + if (fqnResolved == null) { + return false + } + + val remainingPart = parts.getOrElse(3) { "" }.removePrefix(sep) + val newPackedData = "$fqnResolved$remainingPart" + + return unpackRecursive(newPackedData) + } else { + if (data.contains("#M") || data.contains("#C")) { + return false + } + val parts = getAtLeast(data, 3, sep) + if (parts == null) { + return false + } +// if (parts.size != 3) { +// if (parts.size > 4 || parts.getOrNull(3) != "") { +// return false +// } +// } + + val fqn = parts[0] + + val dotIndex = fqn.lastIndexOf('.') + val className = fqn.substring(0, dotIndex) + val methodName = fqn.substring(dotIndex + 1) + + val classType = PhpType().add(className).global(project) + val parsed = classType.toExPhpType() + val instantiation = parsed?.getInstantiation() ?: return false + + if (instantiation.specializationList.first() is ExPhpTypeGenericsT) { + return false + } + + classGenericType = instantiation + + klass = PhpIndex.getInstance(project).getClassesByFQN(instantiation.classFqn).firstOrNull() ?: return false + method = klass!!.findMethodByName(methodName) + if (method == null) { + return false + } + + parameters = method!!.parameters + genericTs = method!!.genericNames() + classGenericTs = klass!!.genericNames() + + explicitGenericsT = unpackTypeArray(parts[1]) + argumentsTypes = unpackTypeArray(parts[2]) + + return true + } + + return false + } + + override fun unpackImpl(packedData: String): Boolean { + return unpackRecursive(packedData.removePrefix(":")) + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/psi/GenericInstantiationPsiCommentImpl.kt b/src/main/kotlin/com/vk/kphpstorm/generics/psi/GenericInstantiationPsiCommentImpl.kt index e8810a2..6302614 100644 --- a/src/main/kotlin/com/vk/kphpstorm/generics/psi/GenericInstantiationPsiCommentImpl.kt +++ b/src/main/kotlin/com/vk/kphpstorm/generics/psi/GenericInstantiationPsiCommentImpl.kt @@ -1,199 +1,46 @@ package com.vk.kphpstorm.generics.psi +import com.intellij.lang.injection.InjectedLanguageManager import com.intellij.psi.PsiComment -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiWhiteSpace -import com.intellij.psi.impl.DebugUtil -import com.intellij.psi.impl.source.tree.LeafPsiElement import com.intellij.psi.impl.source.tree.PsiCommentImpl import com.intellij.psi.tree.IElementType -import com.intellij.psi.util.PsiTreeUtil -import com.jetbrains.php.lang.PhpLangUtil -import com.jetbrains.php.lang.documentation.phpdoc.psi.impl.PhpDocTypeImpl -import com.jetbrains.php.lang.documentation.phpdoc.psi.impl.tags.PhpDocReturnTagImpl -import com.jetbrains.php.lang.psi.PhpPsiElementFactory -import com.jetbrains.php.lang.psi.elements.FunctionReference -import com.jetbrains.php.lang.psi.elements.PhpClass -import com.jetbrains.php.lang.psi.elements.PhpNamedElement -import com.jetbrains.php.lang.psi.elements.PhpReference -import com.jetbrains.php.lang.psi.elements.impl.ClassReferenceImpl -import com.jetbrains.php.lang.psi.elements.impl.NewExpressionImpl -import com.jetbrains.php.lang.psi.elements.impl.PhpReferenceImpl -import com.jetbrains.php.lang.psi.resolve.types.PhpType -import com.jetbrains.php.lang.psi.resolve.types.PhpTypeSignatureKey -import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor +import com.intellij.psi.util.parentOfType +import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag +import com.jetbrains.php.lang.psi.PhpFile import com.vk.kphpstorm.exphptype.ExPhpType -import com.vk.kphpstorm.exphptype.psi.ExPhpTypeInstancePsiImpl -import com.vk.kphpstorm.helpers.toExPhpType +import com.vk.kphpstorm.exphptype.ExPhpTypeTuple +import com.vk.kphpstorm.exphptype.PhpTypeToExPhpTypeParsing /** - * Комментарий вида `/**/` который пишется в вызове - * функции между именем функции и списком аргументов. + * Comment like `/**/` between function name and the + * argument list for a function call. * - * Данный комментарий хранит список явных шаблонных типов разделенных - * запятой. + * This comment stores a comma-separated list of explicit template types. + * Comment can have any types that can be represented in phpdoc. * - * В комментарии могут быть любимые типы которые могут быть представлены - * в phpdoc. - * - * Комментарий не имеет внутренней структуры, так как [PsiComment] не может - * иметь потомков, так как является листом дерева. В случае если не делать - * данный узел комментарием, то ломается парсинг вызова функции, так как - * грамматика не готова к тому что между именем функции и списком аргументов - * есть еще какой-то элемент. + * The comment has no internal structure, since [PsiComment] cannot + * have children, as it is a leaf of the tree. */ class GenericInstantiationPsiCommentImpl(type: IElementType, text: CharSequence) : PsiCommentImpl(type, text) { /** - * For `/**/` is `` - */ - val genericSpecs = text.substring(2 until text.length - 2) - - fun instantiationPartsTypes(): List { - val instantiationParts = instantiationParts() - val instantiationTypes = instantiationParts.mapNotNull { - PhpType().add(it.text).toExPhpType() - } - - return instantiationTypes - } - - private fun instantiationParts(): List { - val psi = PhpPsiElementFactory.createPsiFileFromText( - project, - "/** @return __ClassT$genericSpecs */class __ClassT {}" - ) - - val returnTag = PsiTreeUtil.findChildOfType(psi, PhpDocReturnTagImpl::class.java)!! - val genericPsi = returnTag.lastChild?.prevSibling!! - - genericPsi.accept(object : PhpElementVisitor() { - override fun visitElement(element: PsiElement) { - if (element is ExPhpTypeInstancePsiImpl && element.name != "__ClassT") { - val newName = resolveInstance(element.text) - val namePsi = element.firstChild - if (namePsi is LeafPsiElement) { - DebugUtil.performPsiModification(null) { - namePsi.rawReplaceWithText(newName) - } - } - } - - var child = element.firstChild - while (child != null) { - child.accept(this) - child = child.nextSibling - } - } - }) - - val firstElement = genericPsi.firstChild?.nextSibling?.nextSibling - val endElement = genericPsi.lastChild?.prevSibling - var curElement: PsiElement? = firstElement ?: return emptyList() - - if (firstElement == endElement) { - return listOf(endElement) - } - - val genericSpecElements = mutableListOf() - - while (curElement != endElement && curElement != null) { - if (curElement is LeafPsiElement || curElement is PsiWhiteSpace) { - curElement = curElement.nextSibling - continue - } - - genericSpecElements.add(curElement) - curElement = curElement.nextSibling - } - - if (endElement != null) { - genericSpecElements.add(endElement) - } - - return genericSpecElements - } - - private fun resolveInstance(rawName: String): String { - val (name, namespaceName) = splitAndResolveNamespace(this, rawName) - - val parent = prevSibling.parent - val reference = if (parent is FunctionReference) { - parent - } else if (parent is NewExpressionImpl) { - parent.classReference - } else { - null - } ?: return namespaceName + name - - return resolveLocal( - reference, - name, namespaceName - ) - } - - private fun resolveLocal(reference: PhpReference, name: String, namespaceName: String): String { - val aClass: String - val pluralisedType: String - if (name == "parent") { - aClass = "parent" - } else { - val elements = resolveLocalInner(reference, name, namespaceName) - if (elements.isNotEmpty()) { - val element = elements.iterator().next() as PhpNamedElement - pluralisedType = element.type.toString() - aClass = if (element is PhpClass && PhpDocTypeImpl.isPolymorphicClassReference(name, element)) - PhpTypeSignatureKey.POLYMORPHIC_CLASS.sign(pluralisedType) - else - pluralisedType - } else { - aClass = namespaceName + name - } - } - - val type = PhpType() - if (aClass.length > 1) { - type.add(aClass) - } - - return aClass - } - - private fun resolveLocalInner( - element: PhpReference, - name: String, - namespaceName: String - ): Set { - if ((PhpType.isPrimitiveType(name) || "\\callback".equals( - PhpLangUtil.toFQN(name), - ignoreCase = true - )) && !PhpLangUtil.mayBeReferenceToUserDefinedClass(name, project) - ) { - return emptySet() - } - - return ClassReferenceImpl.resolveLocal(element, name, namespaceName) - } - - /** - * Функция принимает текущий контекстный элемент и имя класса, которое - * может быть как FQN, так и просто именем. + * Returns the types defined in the comment. * - * В случае когда было передано FQN, она разделяет его на пространство - * имен и имя класса и возвращает их. + * Example: * - * В другом случае она находит текущее пространство имен в котором - * находится контекстный элемент и возвращает его вместе с переданным - * именем. + * `/**/` -> `[T, T2]` */ - private fun splitAndResolveNamespace(el: PsiElement, nameOrFqn: String): Pair { - // if fqn - if (nameOrFqn.startsWith('\\')) { - val lastSlash = nameOrFqn.lastIndexOf('\\') - val ns = nameOrFqn.substring(0..lastSlash) - val name = nameOrFqn.substring(lastSlash + 1) - return Pair(name, ns) - } + fun instantiationTypes(): List { + val outerFile = containingFile as PhpFile + val injected = InjectedLanguageManager.getInstance(project) + .findInjectedElementAt(outerFile, startOffset + 3) + ?: return emptyList() + + val docTagGenericsInstantiation = injected.parentOfType() + ?: return emptyList() + + val specList = PhpTypeToExPhpTypeParsing.parse(docTagGenericsInstantiation.type) as? ExPhpTypeTuple + ?: return emptyList() - return Pair(nameOrFqn, PhpReferenceImpl.findNamespaceName("", el)) + return specList.items } } diff --git a/src/main/kotlin/com/vk/kphpstorm/helpers/PsiTreeExtensions.kt b/src/main/kotlin/com/vk/kphpstorm/helpers/PsiTreeExtensions.kt index d49b7be..018269f 100644 --- a/src/main/kotlin/com/vk/kphpstorm/helpers/PsiTreeExtensions.kt +++ b/src/main/kotlin/com/vk/kphpstorm/helpers/PsiTreeExtensions.kt @@ -49,3 +49,12 @@ fun setSelectionInEditor(editor: Editor, elementToSelect: PsiElement) { editor.offsetToLogicalPosition(offset + elementToSelect.textLength) )) } + +fun setSelectionInEditor(editor: Editor, elementToSelect: PsiElement, shiftStart: Int, shiftEnd: Int) { + val offset = elementToSelect.textOffset + editor.caretModel.caretsAndSelections = listOf(CaretState( + editor.offsetToLogicalPosition(offset + shiftStart), + editor.offsetToLogicalPosition(offset + shiftStart), + editor.offsetToLogicalPosition(offset + shiftEnd), + )) +} diff --git a/src/main/kotlin/com/vk/kphpstorm/highlighting/hints/InlayHintsCollector.kt b/src/main/kotlin/com/vk/kphpstorm/highlighting/hints/InlayHintsCollector.kt index 6d38a6e..e768dee 100644 --- a/src/main/kotlin/com/vk/kphpstorm/highlighting/hints/InlayHintsCollector.kt +++ b/src/main/kotlin/com/vk/kphpstorm/highlighting/hints/InlayHintsCollector.kt @@ -34,7 +34,7 @@ class InlayHintsCollector( when { element is MethodReference && settings.showForFunctions -> { val call = GenericMethodCall(element) - showAnnotation(call, element.firstChild.nextSibling.nextSibling) + showAnnotation(call, element.firstChild?.nextSibling?.nextSibling) } element is FunctionReference && settings.showForFunctions -> { val call = GenericFunctionCall(element) @@ -42,15 +42,21 @@ class InlayHintsCollector( } element is NewExpression && settings.showForFunctions -> { val call = GenericConstructorCall(element) - showAnnotation(call, element.firstChild.nextSibling.nextSibling) + showAnnotation(call, element.firstChild?.nextSibling?.nextSibling) } } return true } - private fun showAnnotation(call: GenericCall, place: PsiElement) { - if (!call.isGeneric() || call.withExplicitSpecs()) { + private fun showAnnotation(call: GenericCall, place: PsiElement?) { + if (place == null || !call.isGeneric() || call.withExplicitSpecs()) { + return + } + + // Показываем хинт только если удалось вывести типы. + val decl = call.isNotEnoughInformation() + if (decl != null) { return } diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/GenericBoundViolationInspection.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/GenericBoundViolationInspection.kt deleted file mode 100644 index 51c85fd..0000000 --- a/src/main/kotlin/com/vk/kphpstorm/inspections/GenericBoundViolationInspection.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.vk.kphpstorm.inspections - -import com.intellij.codeInspection.ProblemHighlightType -import com.intellij.codeInspection.ProblemsHolder -import com.intellij.psi.PsiElementVisitor -import com.jetbrains.php.PhpIndex -import com.jetbrains.php.lang.inspections.PhpInspection -import com.jetbrains.php.lang.psi.elements.FunctionReference -import com.jetbrains.php.lang.psi.elements.MethodReference -import com.jetbrains.php.lang.psi.elements.NewExpression -import com.jetbrains.php.lang.psi.resolve.types.PhpType -import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor -import com.vk.kphpstorm.generics.GenericCall -import com.vk.kphpstorm.generics.GenericConstructorCall -import com.vk.kphpstorm.generics.GenericFunctionCall -import com.vk.kphpstorm.generics.GenericMethodCall -import com.vk.kphpstorm.helpers.toExPhpType - -class GenericBoundViolationInspection : PhpInspection() { - override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { - return object : PhpElementVisitor() { - override fun visitPhpNewExpression(expression: NewExpression) { - val call = GenericConstructorCall(expression) - checkGenericCall(call) - } - - override fun visitPhpMethodReference(reference: MethodReference) { - val call = GenericMethodCall(reference) - checkGenericCall(call) - } - - override fun visitPhpFunctionCall(reference: FunctionReference) { - val call = GenericFunctionCall(reference) - checkGenericCall(call) - } - - private fun checkGenericCall(call: GenericCall) { - if (!call.isResolved()) return - val genericNames = call.genericNames() - - genericNames.forEach { decl -> - val (resolvedType, isExplicit) = if (call.specializationNameMap[decl.name] != null) { - call.specializationNameMap[decl.name] to true - } else { - call.implicitSpecializationNameMap[decl.name] to false - } - - if (resolvedType == null) return@forEach - - val upperBoundClass = - PhpIndex.getInstance(call.project).getAnyByFQN(decl.extendsClass).firstOrNull() - ?: return@forEach - val upperBoundClassType = PhpType().add(upperBoundClass.fqn).toExPhpType() ?: return@forEach - - val errorPsi = call.explicitSpecsPsi ?: call.callArgs.firstOrNull() ?: return@forEach - - if (!upperBoundClassType.isAssignableFrom(resolvedType, call.project)) { - val extendsOrImplements = if (upperBoundClass.isInterface) "implements" else "extends" - - val message = - "${if (isExplicit) "Explicit" else "Reified"} generic type for ${decl.name} is not within its bounds (${resolvedType} not $extendsOrImplements ${upperBoundClass.fqn})" - - holder.registerProblem( - errorPsi, - message, - ProblemHighlightType.GENERIC_ERROR - ) - } - } - } - } - } -} diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/GenericInstantiationArgsCountMismatchInspection.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/GenericInstantiationArgsCountMismatchInspection.kt deleted file mode 100644 index 873cefa..0000000 --- a/src/main/kotlin/com/vk/kphpstorm/inspections/GenericInstantiationArgsCountMismatchInspection.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.vk.kphpstorm.inspections - -import com.intellij.codeInspection.ProblemHighlightType -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.MethodReference -import com.jetbrains.php.lang.psi.elements.NewExpression -import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor -import com.vk.kphpstorm.generics.GenericCall -import com.vk.kphpstorm.generics.GenericConstructorCall -import com.vk.kphpstorm.generics.GenericFunctionCall -import com.vk.kphpstorm.generics.GenericMethodCall - -class GenericInstantiationArgsCountMismatchInspection : PhpInspection() { - override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { - return object : PhpElementVisitor() { - override fun visitPhpNewExpression(expression: NewExpression) { - val call = GenericConstructorCall(expression) - checkGenericCall(call) - } - - override fun visitPhpMethodReference(reference: MethodReference) { - val call = GenericMethodCall(reference) - checkGenericCall(call) - } - - override fun visitPhpFunctionCall(reference: FunctionReference) { - val call = GenericFunctionCall(reference) - checkGenericCall(call) - } - - private fun checkGenericCall(call: GenericCall) { - if (!call.isResolved()) return - - val countGenericNames = call.genericNames().size - call.implicitClassSpecializationNameMap.size - val countExplicitSpecs = call.explicitSpecs.size - val explicitSpecsPsi = call.explicitSpecsPsi - - if (countGenericNames != countExplicitSpecs && explicitSpecsPsi != null) { - holder.registerProblem( - explicitSpecsPsi, - "$countGenericNames type arguments expected for ${call.function()!!.fqn}", - ProblemHighlightType.GENERIC_ERROR - ) - } - } - } - } -} diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/GenericNoEnoughInformationInspection.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/GenericNoEnoughInformationInspection.kt deleted file mode 100644 index 7a38a3d..0000000 --- a/src/main/kotlin/com/vk/kphpstorm/inspections/GenericNoEnoughInformationInspection.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.vk.kphpstorm.inspections - -import com.intellij.codeInspection.ProblemHighlightType -import com.intellij.codeInspection.ProblemsHolder -import com.intellij.psi.PsiElement -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.MethodReference -import com.jetbrains.php.lang.psi.elements.NewExpression -import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor -import com.vk.kphpstorm.generics.GenericCall -import com.vk.kphpstorm.generics.GenericConstructorCall -import com.vk.kphpstorm.generics.GenericFunctionCall -import com.vk.kphpstorm.generics.GenericMethodCall - -class GenericNoEnoughInformationInspection : PhpInspection() { - override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { - return object : PhpElementVisitor() { - override fun visitPhpNewExpression(expression: NewExpression) { - val call = GenericConstructorCall(expression) - checkGenericCall(call, expression) - } - - override fun visitPhpMethodReference(reference: MethodReference) { - val call = GenericMethodCall(reference) - checkGenericCall(call, reference.firstChild.nextSibling.nextSibling) - } - - override fun visitPhpFunctionCall(reference: FunctionReference) { - val call = GenericFunctionCall(reference) - checkGenericCall(call, reference.firstChild) - } - - private fun checkGenericCall(call: GenericCall, errorPsi: PsiElement) { - if (!call.isResolved()) return - val genericNames = call.genericNames() - - if (call.explicitSpecsPsi == null) { - genericNames.any { decl -> - val resolved = call.implicitSpecializationNameMap.contains(decl.name) - - if (!resolved) { - holder.registerProblem( - errorPsi, - "Not enough information to infer generic ${decl.name}", - ProblemHighlightType.GENERIC_ERROR - ) - - return@any true - } - - false - } - } - } - } - } -} diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/GenericSeveralGenericTypesInspection.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/GenericSeveralGenericTypesInspection.kt deleted file mode 100644 index 5441002..0000000 --- a/src/main/kotlin/com/vk/kphpstorm/inspections/GenericSeveralGenericTypesInspection.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.vk.kphpstorm.inspections - -import com.intellij.codeInspection.ProblemHighlightType -import com.intellij.codeInspection.ProblemsHolder -import com.intellij.psi.PsiElement -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.MethodReference -import com.jetbrains.php.lang.psi.elements.NewExpression -import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor -import com.jetbrains.rd.util.first -import com.vk.kphpstorm.generics.GenericCall -import com.vk.kphpstorm.generics.GenericConstructorCall -import com.vk.kphpstorm.generics.GenericFunctionCall -import com.vk.kphpstorm.generics.GenericMethodCall - -class GenericSeveralGenericTypesInspection : PhpInspection() { - override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { - return object : PhpElementVisitor() { - override fun visitPhpNewExpression(expression: NewExpression) { - val call = GenericConstructorCall(expression) - checkGenericCall(call, expression) - } - - override fun visitPhpMethodReference(reference: MethodReference) { - val call = GenericMethodCall(reference) - checkGenericCall(call, reference) - } - - override fun visitPhpFunctionCall(reference: FunctionReference) { - val call = GenericFunctionCall(reference) - checkGenericCall(call, reference) - } - - private fun checkGenericCall(call: GenericCall, element: PsiElement) { - if (!call.isResolved()) return - - // В случае даже если есть ошибки, то мы показываем их только - // в случае когда нет явного определения шаблона для вызова функции. - if (call.implicitSpecializationErrors.isNotEmpty() && !call.withExplicitSpecs()) { - val error = call.implicitSpecializationErrors.first() - val (type1, type2) = error.value - - val genericsTString = call.genericTs.joinToString(", ") - val callString = element.text - - val firstBracketIndex = callString.indexOf('(') - val beforeBracket = callString.substring(0, firstBracketIndex) - val afterBracket = callString.substring(firstBracketIndex + 1) - val callStingWithGenerics = "$beforeBracket/*<$genericsTString>*/($afterBracket" - - val explanation = - "Please, provide all generics types using following syntax: $callStingWithGenerics;" - - holder.registerProblem( - element, - "Couldn't reify generic <${error.key}> for call: it's both $type1 and $type2\n$explanation", - ProblemHighlightType.GENERIC_ERROR - ) - } - } - } - } -} diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/GenericUnnecessaryExplicitInstantiationListInspection.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/GenericUnnecessaryExplicitInstantiationListInspection.kt deleted file mode 100644 index d306e52..0000000 --- a/src/main/kotlin/com/vk/kphpstorm/inspections/GenericUnnecessaryExplicitInstantiationListInspection.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.vk.kphpstorm.inspections - -import com.intellij.codeInspection.ProblemHighlightType -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.MethodReference -import com.jetbrains.php.lang.psi.elements.NewExpression -import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor -import com.vk.kphpstorm.generics.GenericCall -import com.vk.kphpstorm.generics.GenericConstructorCall -import com.vk.kphpstorm.generics.GenericFunctionCall -import com.vk.kphpstorm.generics.GenericMethodCall -import com.vk.kphpstorm.inspections.quickfixes.RemoveExplicitGenericSpecsQuickFix - -class GenericUnnecessaryExplicitInstantiationListInspection : PhpInspection() { - override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { - return object : PhpElementVisitor() { - override fun visitPhpNewExpression(expression: NewExpression) { - val call = GenericConstructorCall(expression) - checkGenericCall(call) - } - - override fun visitPhpMethodReference(reference: MethodReference) { - val call = GenericMethodCall(reference) - checkGenericCall(call) - } - - override fun visitPhpFunctionCall(reference: FunctionReference) { - val call = GenericFunctionCall(reference) - checkGenericCall(call) - } - - private fun checkGenericCall(call: GenericCall) { - if (!call.isResolved()) return - if (call.explicitSpecsPsi == null) return - - if (call.isNoNeedExplicitSpec()) { - holder.registerProblem( - call.explicitSpecsPsi!!, - "Remove unnecessary explicit list of instantiation arguments", - ProblemHighlightType.WARNING, - RemoveExplicitGenericSpecsQuickFix() - ) - } - } - } - } -} diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/KphpGenericsInspection.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/KphpGenericsInspection.kt new file mode 100644 index 0000000..d81b290 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/KphpGenericsInspection.kt @@ -0,0 +1,150 @@ +package com.vk.kphpstorm.inspections + +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import com.jetbrains.php.PhpIndex +import com.jetbrains.php.lang.inspections.PhpInspection +import com.jetbrains.php.lang.psi.elements.FunctionReference +import com.jetbrains.php.lang.psi.elements.MethodReference +import com.jetbrains.php.lang.psi.elements.NewExpression +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor +import com.jetbrains.rd.util.first +import com.vk.kphpstorm.generics.GenericCall +import com.vk.kphpstorm.generics.GenericConstructorCall +import com.vk.kphpstorm.generics.GenericFunctionCall +import com.vk.kphpstorm.generics.GenericMethodCall +import com.vk.kphpstorm.helpers.toExPhpType +import com.vk.kphpstorm.inspections.quickfixes.AddExplicitInstantiationCommentQuickFix +import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl + +class KphpGenericsInspection : PhpInspection() { + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + return object : PhpElementVisitor() { + override fun visitPhpNewExpression(expression: NewExpression) { + val call = GenericConstructorCall(expression) + checkGenericCall(call, expression, expression) + } + + override fun visitPhpMethodReference(reference: MethodReference) { + val call = GenericMethodCall(reference) + checkGenericCall(call, reference, reference.firstChild.nextSibling.nextSibling) + } + + override fun visitPhpFunctionCall(reference: FunctionReference) { + val call = GenericFunctionCall(reference) + checkGenericCall(call, reference, reference.firstChild) + } + + private fun checkGenericCall(call: GenericCall, element: PsiElement, errorPsi: PsiElement) { + if (!call.isResolved()) return + val genericNames = call.genericNames() + + checkGenericTypesBounds(call, genericNames) + checkInstantiationArgsCount(call, genericNames) + checkReifiedGenericTypes(call, element, errorPsi) + checkReifiedSeveralGenericTypes(call, element) + } + + private fun checkReifiedSeveralGenericTypes(call: GenericCall, element: PsiElement) { + // В случае даже если есть ошибки, то мы показываем их только + // в случае когда нет явного определения шаблона для вызова функции. + if (call.implicitSpecializationErrors.isEmpty() || call.withExplicitSpecs()) return + + val error = call.implicitSpecializationErrors.first() + val (type1, type2) = error.value + + val genericsTString = call.genericTs.joinToString(", ") + val callString = element.text + + val firstBracketIndex = callString.indexOf('(') + val beforeBracket = callString.substring(0, firstBracketIndex) + val afterBracket = callString.substring(firstBracketIndex + 1) + val callStingWithGenerics = "$beforeBracket/*<$genericsTString>*/($afterBracket" + + val explanation = + "Please, provide all generics types using following syntax: $callStingWithGenerics;" + + holder.registerProblem( + element, + "Couldn't reify generic <${error.key}> for call: it's both $type1 and $type2\n$explanation", + ProblemHighlightType.GENERIC_ERROR + ) + } + + private fun checkReifiedGenericTypes( + call: GenericCall, + element: PsiElement, + errorPsi: PsiElement + ) { + val decl = call.isNotEnoughInformation() + if (decl != null) { + holder.registerProblem( + errorPsi, + "Not enough information to infer generic ${decl.name}", + ProblemHighlightType.GENERIC_ERROR, + AddExplicitInstantiationCommentQuickFix(element) + ) + } + } + + private fun checkInstantiationArgsCount( + call: GenericCall, + genericNames: List, + ) { + val countGenericNames = genericNames.size - call.implicitClassSpecializationNameMap.size + val countExplicitSpecs = call.explicitSpecs.size + val explicitSpecsPsi = call.explicitSpecsPsi + + if (countGenericNames != countExplicitSpecs && explicitSpecsPsi != null) { + holder.registerProblem( + explicitSpecsPsi, + "$countGenericNames type arguments expected for ${call.function()!!.fqn}", + ProblemHighlightType.GENERIC_ERROR + ) + } + } + + private fun checkGenericTypesBounds( + call: GenericCall, + genericNames: List, + ) { + genericNames.forEach { decl -> + val (resolvedType, isExplicit) = if (call.specializationNameMap[decl.name] != null) { + call.specializationNameMap[decl.name] to true + } else { + call.implicitSpecializationNameMap[decl.name] to false + } + + if (resolvedType == null) return@forEach + + val upperBoundClass = + PhpIndex.getInstance(call.project).getAnyByFQN(decl.extendsClass).firstOrNull() + ?: return@forEach + val upperBoundClassType = PhpType().add(upperBoundClass.fqn).toExPhpType() ?: return@forEach + + val errorPsi = + call.explicitSpecsPsi + ?: call.callArgs.firstOrNull() + ?: call.element() + ?: return@forEach + + if (!upperBoundClassType.isAssignableFrom(resolvedType, call.project)) { + val extendsOrImplements = if (upperBoundClass.isInterface) "implements" else "extends" + + val message = + "${if (isExplicit) "Explicit" else "Reified"} generic type for ${decl.name} is not within its bounds (${resolvedType} not $extendsOrImplements ${upperBoundClass.fqn})" + + holder.registerProblem( + errorPsi, + message, + ProblemHighlightType.GENERIC_ERROR + ) + } + } + } + } + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/KphpUndefinedClassInspection.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/KphpUndefinedClassInspection.kt index 2c74757..9b4d73b 100644 --- a/src/main/kotlin/com/vk/kphpstorm/inspections/KphpUndefinedClassInspection.kt +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/KphpUndefinedClassInspection.kt @@ -4,15 +4,18 @@ import com.intellij.codeInspection.ProblemHighlightType import com.intellij.codeInspection.ProblemsHolder import com.intellij.openapi.fileEditor.UniqueVFilePathBuilder import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.util.parentOfType import com.jetbrains.php.PhpIndex import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocType import com.jetbrains.php.lang.inspections.PhpInspection import com.jetbrains.php.lang.inspections.quickfix.PhpImportClassQuickFix import com.jetbrains.php.lang.psi.elements.* +import com.jetbrains.php.lang.psi.elements.Function import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor import com.vk.kphpstorm.exphptype.ExPhpTypeInstance import com.vk.kphpstorm.exphptype.PhpTypeToExPhpTypeParsing import com.vk.kphpstorm.exphptype.psi.ExPhpTypeInstancePsiImpl +import com.vk.kphpstorm.generics.GenericUtil.genericNames import com.vk.kphpstorm.inspections.helpers.KphpTypingAnalyzer /** @@ -75,8 +78,17 @@ class KphpUndefinedClassInspection : PhpInspection() { } val resolved = classReference.multiResolve(false) - if (resolved.isEmpty()) + if (resolved.isEmpty()) { + val containingFunction = classReference.parentOfType() + if (containingFunction != null) { + val names = containingFunction.genericNames() + val name = classReference.name + if (names.find { it.name == name } != null) { + return + } + } reportUndefinedClassUsage(classReference) + } } /** diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/helpers/KphpTypingAnalyzer.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/helpers/KphpTypingAnalyzer.kt index 52da809..6feaf63 100644 --- a/src/main/kotlin/com/vk/kphpstorm/inspections/helpers/KphpTypingAnalyzer.kt +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/helpers/KphpTypingAnalyzer.kt @@ -5,6 +5,7 @@ import com.jetbrains.php.lang.psi.elements.Field import com.jetbrains.php.lang.psi.elements.Function import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.vk.kphpstorm.exphptype.* +import com.vk.kphpstorm.generics.GenericUtil.getGenericPipeType /** * Helps check if something is statically typed @@ -43,10 +44,18 @@ object KphpTypingAnalyzer { fun isScalarTypeHint(s: String) = SCALAR_TYPE_HINTS.contains(s) - fun doesDocTypeMatchTypeHint(docType: ExPhpType, hintType: ExPhpType, project: Project): Boolean = - docType !is ExPhpTypePipe - && hintType.isAssignableFrom(docType, project) - && docType.isAssignableFrom(hintType, project) + fun doesDocTypeMatchTypeHint(docType: ExPhpType, hintType: ExPhpType, project: Project): Boolean { + if (docType is ExPhpTypePipe) { + val genericType = docType.getGenericPipeType() + if (genericType != null) { + return doesDocTypeMatchTypeHint(genericType, hintType, project) + } + } + + return docType !is ExPhpTypePipe + && hintType.isAssignableFrom(docType, project) + && docType.isAssignableFrom(hintType, project) + } fun doesDocTypeDuplicateTypeHint(docType: ExPhpType, hintType: ExPhpType): Boolean = docType.toString() == hintType.toString() diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/quickfixes/AddExplicitInstantiationCommentQuickFix.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/quickfixes/AddExplicitInstantiationCommentQuickFix.kt new file mode 100644 index 0000000..91a509d --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/quickfixes/AddExplicitInstantiationCommentQuickFix.kt @@ -0,0 +1,36 @@ +package com.vk.kphpstorm.inspections.quickfixes + +import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.jetbrains.php.lang.psi.PhpPsiElementFactory +import com.jetbrains.php.lang.psi.elements.FunctionReference +import com.jetbrains.php.lang.psi.elements.MethodReference +import com.jetbrains.php.lang.psi.elements.NewExpression +import com.vk.kphpstorm.generics.psi.GenericInstantiationPsiCommentImpl +import com.vk.kphpstorm.helpers.setSelectionInEditor + +class AddExplicitInstantiationCommentQuickFix(field: PsiElement) : LocalQuickFixAndIntentionActionOnPsiElement(field) { + + override fun getFamilyName() = "Add explicit generic instantiation tag" + override fun getText() = "Add explicit generic instantiation tag" + + override fun invoke(project: Project, file: PsiFile, editor: Editor?, startElement: PsiElement, endElement: PsiElement) { + val placeToInsert = when (startElement) { + is NewExpression -> startElement.classReference + is MethodReference -> startElement.nextSibling?.nextSibling?.nextSibling + is FunctionReference -> startElement.nextSibling + else -> null + } ?: return + + val comment = PhpPsiElementFactory.createFromText(project, GenericInstantiationPsiCommentImpl::class.java, "/**/") + if (comment != null) { + val insertedComment = startElement.addAfter(comment, placeToInsert) + if (editor != null) { + setSelectionInEditor(editor, insertedComment, 3, 4) + } + } + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagGenericPsiImpl.kt b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagGenericPsiImpl.kt index 1e40e39..089d153 100644 --- a/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagGenericPsiImpl.kt +++ b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagGenericPsiImpl.kt @@ -25,7 +25,7 @@ class KphpDocTagGenericPsiImpl : PhpDocTagImpl, KphpDocTagImpl { it.split(',').map { type -> val (name, extendsClass) = if (type.contains(':')) { val parts = type.split(':') - parts[0] to parts[1] + parts[0] to parts[1].ifBlank { null } } else { type to null } diff --git a/src/main/kotlin/com/vk/kphpstorm/typeProviders/CallableTypeProvider.kt b/src/main/kotlin/com/vk/kphpstorm/typeProviders/CallableTypeProvider.kt new file mode 100644 index 0000000..b913a9d --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/typeProviders/CallableTypeProvider.kt @@ -0,0 +1,70 @@ +package com.vk.kphpstorm.typeProviders + +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.jetbrains.php.lang.psi.elements.Function +import com.jetbrains.php.lang.psi.elements.PhpNamedElement +import com.jetbrains.php.lang.psi.resolve.types.PhpCharBasedTypeKey +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4 +import com.vk.kphpstorm.helpers.toStringAsNested + +class CallableTypeProvider : PhpTypeProvider4 { + private val KEY = object : PhpCharBasedTypeKey() { + override fun getKey(): Char = 'Λ' + } + + override fun getKey() = KEY.key + + override fun getType(el: PsiElement): PhpType? { + if (el !is Function) return null + if (el.name.isNotEmpty()) return null + + val params = el.parameters.joinToString("&&") { + it.type.toStringAsNested(";;") + } + + val returnType = el.typeDeclaration?.type?.toStringAsNested(";;") ?: "void" + + return PhpType().add(KEY.sign("$params,$returnType")) + } + + override fun complete(incompleteTypeStr: String, project: Project): PhpType? { + if (!KEY.signed(incompleteTypeStr)) { + return null + } + + val incompleteType = incompleteTypeStr.substring(2) + + val (paramsRawTypeString, returnRawTypeString) = incompleteType.split(",") + val params = paramsRawTypeString.split("&&") + val returnRawTypes = returnRawTypeString.split(";;") + + val paramTypes = params.map { param -> + if (param.isEmpty()) { + return@map PhpType.MIXED + } + val types = param.split(";;") + val type = PhpType() + types.forEach { type.add(it).global(project) } + type + } + val paramTypesString = paramTypes.joinToString(",") { it.toString() } + + val returnType = PhpType() + returnRawTypes.forEach { returnType.add(it).global(project) } + val returnTypeString = returnType.toString() + + val callableType = "\\Callable($paramTypesString):$returnTypeString" + return PhpType().add(callableType) + } + + override fun getBySignature( + typeStr: String, + visited: MutableSet?, + depth: Int, + project: Project? + ): MutableCollection? { + return null + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/vk/kphpstorm/typeProviders/ClassConstTypeProvider.kt b/src/main/kotlin/com/vk/kphpstorm/typeProviders/ClassConstTypeProvider.kt index 9a97cb1..c01c49d 100644 --- a/src/main/kotlin/com/vk/kphpstorm/typeProviders/ClassConstTypeProvider.kt +++ b/src/main/kotlin/com/vk/kphpstorm/typeProviders/ClassConstTypeProvider.kt @@ -25,7 +25,7 @@ class ClassConstTypeProvider : PhpTypeProvider4 { if (p is ClassConstantReference) { val classExType = p.classReference?.type?.toExPhpType() if (classExType != null) { - return PhpType().add("class-string(${classExType})") + return PhpType().add("force(class-string(${classExType}))") } } return null diff --git a/src/main/kotlin/com/vk/kphpstorm/typeProviders/ForeachTypeProvider.kt b/src/main/kotlin/com/vk/kphpstorm/typeProviders/ForeachTypeProvider.kt index 80db27c..ed76f3c 100644 --- a/src/main/kotlin/com/vk/kphpstorm/typeProviders/ForeachTypeProvider.kt +++ b/src/main/kotlin/com/vk/kphpstorm/typeProviders/ForeachTypeProvider.kt @@ -45,6 +45,14 @@ class ForeachTypeProvider : PhpTypeProvider4 { } override fun complete(incompleteTypeStr: String, project: Project): PhpType? { + if (!incompleteTypeStr.startsWith("#Ф")) { + return null + } + + if (incompleteTypeStr.contains("%Ф")) { + return null + } + val arrTypeStr = incompleteTypeStr.substring(2) val arrType = PhpType().add(arrTypeStr).global(project) diff --git a/src/main/kotlin/com/vk/kphpstorm/typeProviders/FunctionsTypeProvider.kt b/src/main/kotlin/com/vk/kphpstorm/typeProviders/FunctionsTypeProvider.kt index e29d206..51553e4 100644 --- a/src/main/kotlin/com/vk/kphpstorm/typeProviders/FunctionsTypeProvider.kt +++ b/src/main/kotlin/com/vk/kphpstorm/typeProviders/FunctionsTypeProvider.kt @@ -7,6 +7,7 @@ import com.jetbrains.php.lang.psi.elements.impl.ArrayCreationExpressionImpl import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4 import com.vk.kphpstorm.exphptype.* +import com.vk.kphpstorm.generics.GenericUtil.isGeneric import com.vk.kphpstorm.helpers.toExPhpType import com.vk.kphpstorm.helpers.toStringAsNested @@ -326,7 +327,13 @@ class FunctionsTypeProvider : PhpTypeProvider4 { private fun PhpType.force(): PhpType { - return when (this.toExPhpType()) { + val type = this.toExPhpType() ?: return this + + if (type.isGeneric()) { + return PhpType().add(this) + } + + return when(type) { is ExPhpTypeForcing -> this is ExPhpTypeInstance -> this else -> PhpType().add("force($this)") diff --git a/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericClassesTypeProvider.kt b/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericClassesTypeProvider.kt index 9ff8088..8f23090 100644 --- a/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericClassesTypeProvider.kt +++ b/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericClassesTypeProvider.kt @@ -2,25 +2,24 @@ package com.vk.kphpstorm.typeProviders import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement +import com.jetbrains.php.lang.psi.elements.NewExpression import com.jetbrains.php.lang.psi.elements.PhpNamedElement -import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl -import com.jetbrains.php.lang.psi.elements.impl.NewExpressionImpl import com.jetbrains.php.lang.psi.resolve.types.PhpCharBasedTypeKey import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4 import com.vk.kphpstorm.exphptype.ExPhpType +import com.vk.kphpstorm.exphptype.ExPhpTypeForcing import com.vk.kphpstorm.exphptype.ExPhpTypeGenericsT import com.vk.kphpstorm.exphptype.ExPhpTypeTplInstantiation import com.vk.kphpstorm.generics.GenericUtil.isGeneric import com.vk.kphpstorm.generics.IndexingGenericFunctionCall import com.vk.kphpstorm.generics.ResolvingGenericConstructorCall -import com.vk.kphpstorm.generics.ResolvingGenericMethodCall -import com.vk.kphpstorm.helpers.toExPhpType import kotlin.math.min class GenericClassesTypeProvider : PhpTypeProvider4 { companion object { - private val KEY = object : PhpCharBasedTypeKey() { + val SEP = "―" + val KEY = object : PhpCharBasedTypeKey() { override fun getKey(): Char { return '±' } @@ -30,29 +29,12 @@ class GenericClassesTypeProvider : PhpTypeProvider4 { override fun getKey() = KEY.key override fun getType(p: PsiElement?): PhpType? { - // $v->f() - if (p is MethodReferenceImpl && !p.isStatic) { - val methodName = p.name ?: return null - val lhs = p.classReference ?: return null - val lhsType = lhs.type - - val resultType = PhpType() - lhsType.types.forEach { type -> - val fqn = "$type.$methodName" - val data = IndexingGenericFunctionCall(fqn, p.parameters, p, "@MC@").pack() ?: return@forEach - - resultType.add("#±:$data") - } - - return resultType - } - // new A/*<...args>*/ - if (p is NewExpressionImpl) { + if (p is NewExpression) { val classRef = p.classReference ?: return null - val fqn = classRef.fqn + "__construct" - val data = IndexingGenericFunctionCall(fqn, p.parameters, p, "@CO@").pack() ?: return null - return PhpType().add("#±$data") + val fqn = classRef.fqn + ".__construct" + val data = IndexingGenericFunctionCall(fqn, p.parameters, p, SEP).pack() ?: return null + return PhpType().add(KEY.sign(data)) } return null @@ -65,18 +47,6 @@ class GenericClassesTypeProvider : PhpTypeProvider4 { val packedData = incompleteTypeStr.substring(2) - if (packedData.startsWith(":")) { - return completeMethodCall(project, packedData) - } - - if (packedData.contains("__construct")) { - return completeConstructCall(project, packedData) - } - - return null - } - - private fun completeConstructCall(project: Project, packedData: String): PhpType? { val call = ResolvingGenericConstructorCall(project) if (!call.unpack(packedData)) { return null @@ -98,34 +68,7 @@ class GenericClassesTypeProvider : PhpTypeProvider4 { val type = ExPhpTypeTplInstantiation(call.klass!!.fqn, genericsTypes) val methodTypeSpecialized = type.instantiateGeneric(specializationNameMap) - return methodTypeSpecialized.toPhpType() - } - - private fun completeMethodCall(project: Project, packedData: String): PhpType? { - val call = ResolvingGenericMethodCall(project) - if (!call.unpack(packedData)) { - return null - } - - val specialization = call.specialization() - - val specializationNameMap = mutableMapOf() - - for (i in 0 until min(call.genericTs.size, specialization.size)) { - specializationNameMap[call.genericTs[i].name] = specialization[i] - } - - if (call.classGenericType != null) { - for (i in 0 until min(call.classGenericTs.size, call.classGenericType!!.specializationList.size)) { - specializationNameMap[call.classGenericTs[i].name] = call.classGenericType!!.specializationList[i] - } - } - - val methodReturnTag = call.method?.docComment?.returnTag ?: return null - val methodTypeParsed = methodReturnTag.type.toExPhpType() ?: return null - val methodTypeSpecialized = methodTypeParsed.instantiateGeneric(specializationNameMap) - - return methodTypeSpecialized.toPhpType() + return ExPhpTypeForcing(methodTypeSpecialized).toPhpType() } override fun getBySignature( diff --git a/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericFunctionsTypeProvider.kt b/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericFunctionsTypeProvider.kt index 172ab95..0e462da 100644 --- a/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericFunctionsTypeProvider.kt +++ b/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericFunctionsTypeProvider.kt @@ -3,11 +3,13 @@ package com.vk.kphpstorm.typeProviders import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement import com.jetbrains.php.lang.psi.elements.FunctionReference +import com.jetbrains.php.lang.psi.elements.MethodReference import com.jetbrains.php.lang.psi.elements.PhpNamedElement import com.jetbrains.php.lang.psi.resolve.types.PhpCharBasedTypeKey import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4 import com.vk.kphpstorm.exphptype.ExPhpType +import com.vk.kphpstorm.exphptype.ExPhpTypeForcing import com.vk.kphpstorm.generics.GenericUtil.isReturnGeneric import com.vk.kphpstorm.generics.IndexingGenericFunctionCall import com.vk.kphpstorm.generics.ResolvingGenericFunctionCall @@ -26,7 +28,7 @@ class GenericFunctionsTypeProvider : PhpTypeProvider4 { override fun getKey() = KEY.key override fun getType(p: PsiElement): PhpType? { - if (p !is FunctionReference) { + if (p !is FunctionReference || p is MethodReference) { return null } @@ -64,7 +66,7 @@ class GenericFunctionsTypeProvider : PhpTypeProvider4 { val methodTypeParsed = methodReturnTag.type.toExPhpType() ?: return null val methodTypeSpecialized = methodTypeParsed.instantiateGeneric(specializationNameMap) - return methodTypeSpecialized.toPhpType() + return ExPhpTypeForcing(methodTypeSpecialized).toPhpType().add(methodTypeSpecialized.toPhpType()) } override fun getBySignature( diff --git a/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericMethodsTypeProvider.kt b/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericMethodsTypeProvider.kt new file mode 100644 index 0000000..439e36c --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericMethodsTypeProvider.kt @@ -0,0 +1,93 @@ +package com.vk.kphpstorm.typeProviders + +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.jetbrains.php.lang.psi.elements.MethodReference +import com.jetbrains.php.lang.psi.elements.PhpNamedElement +import com.jetbrains.php.lang.psi.resolve.types.PhpCharBasedTypeKey +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4 +import com.vk.kphpstorm.exphptype.ExPhpType +import com.vk.kphpstorm.generics.IndexingGenericFunctionCall +import com.vk.kphpstorm.generics.ResolvingGenericMethodCall +import com.vk.kphpstorm.helpers.toExPhpType +import kotlin.math.min + +class GenericMethodsTypeProvider : PhpTypeProvider4 { + companion object { + val SEP = "⁓" + val KEY = object : PhpCharBasedTypeKey() { + override fun getKey(): Char { + return 'ω' + } + } + } + + override fun getKey() = KEY.key + + override fun getType(p: PsiElement?): PhpType? { + // $v->f() + if (p is MethodReference && !p.isStatic) { + val methodName = p.name ?: return null + val lhs = p.classReference ?: return null + val lhsTypes = lhs.type.types.filter { type -> + GenericClassesTypeProvider.KEY.signed(type) || KEY.signed(type) || !type.startsWith("#") + } + + val resultType = PhpType() + lhsTypes.forEach { type -> + val fqn = "$type.$methodName" + val data = IndexingGenericFunctionCall(fqn, p.parameters, p, SEP).pack() ?: return@forEach + + resultType.add(KEY.sign(data)) + } + + return resultType + } + + return null + } + + override fun complete(incompleteTypeStr: String, project: Project): PhpType? { + if (!KEY.signed(incompleteTypeStr)) { + return null + } + + val packedData = incompleteTypeStr.substring(2) + + val call = ResolvingGenericMethodCall(project) + if (!call.unpack(packedData)) { + return null + } + + val specialization = call.specialization() + + val specializationNameMap = mutableMapOf() + + for (i in 0 until min(call.genericTs.size, specialization.size)) { + specializationNameMap[call.genericTs[i].name] = specialization[i] + } + + if (call.classGenericType != null) { + for (i in 0 until min(call.classGenericTs.size, call.classGenericType!!.specializationList.size)) { + specializationNameMap[call.classGenericTs[i].name] = call.classGenericType!!.specializationList[i] + } + } + + val methodReturnTag = call.method?.docComment?.returnTag ?: return null + val methodTypeParsed = methodReturnTag.type.toExPhpType() ?: return null + val methodTypeSpecialized = methodTypeParsed.instantiateGeneric(specializationNameMap) + + return methodTypeSpecialized.toPhpType() + } + + + override fun getBySignature( + typeStr: String, + visited: MutableSet?, + depth: Int, + project: Project? + ): MutableCollection? { + return null + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/typeProviders/TupleShapeTypeProvider.kt b/src/main/kotlin/com/vk/kphpstorm/typeProviders/TupleShapeTypeProvider.kt index 7c8a4ae..f7f4a74 100644 --- a/src/main/kotlin/com/vk/kphpstorm/typeProviders/TupleShapeTypeProvider.kt +++ b/src/main/kotlin/com/vk/kphpstorm/typeProviders/TupleShapeTypeProvider.kt @@ -83,7 +83,7 @@ class TupleShapeTypeProvider : PhpTypeProvider4 { // so, in complex scenarios like get()[1][2]->... combinations increase geometrically // to partially avoid this, use heruistics: // filter out subtypes detected by PhpStorm native type providers that are 100% useless here - if (!it.contains("#π") && !it.contains("#E")) + if (!it.contains("#π") && !it.contains("#E") && !it.contains("%")) resultType.add("#Й.$indexKey $it") } // println("type($lhs) = ${resultType.toString().replace("|", " | ")}") @@ -225,8 +225,9 @@ class TupleShapeTypeProvider : PhpTypeProvider4 { || it == "\\any" // any[*] is any, not undefined || it == "\\array" // array[*] is any (untyped arrays) } - if (!needsCustomIndexing) - return null + // TODO: Нам нужно тут выводить самим так как дженерики +// if (!needsCustomIndexing) +// return null return wholeType.toExPhpType()?.getSubkeyByIndex(indexKey)?.toPhpType() } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 4efc5f2..dadc7cd 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -64,26 +64,10 @@ implementationClass="com.vk.kphpstorm.inspections.RedundantCastInspection"/> - - - - - + implementationClass="com.vk.kphpstorm.inspections.KphpGenericsInspection"/> com.vk.kphpstorm.inspections.PrettifyPhpdocBlockIntention @@ -111,7 +95,6 @@ - @@ -125,10 +108,6 @@ implementationClass="com.vk.kphpstorm.highlighting.KphpGenericCommentFolderBuilder"/> - - - - @@ -139,6 +118,8 @@ + + diff --git a/src/main/resources/inspectionDescriptions/KphpGenericsInspection.html b/src/main/resources/inspectionDescriptions/KphpGenericsInspection.html new file mode 100644 index 0000000..92ddc5b --- /dev/null +++ b/src/main/resources/inspectionDescriptions/KphpGenericsInspection.html @@ -0,0 +1,8 @@ +Various generics checks: +
  • Reports if the generic type is not within its bounds
  • +
  • Reports if the number of explicit generic types passed during instantiation does not match the expected count
  • +
  • Reports if more than one possible type has been deduced for the generic type
  • +
  • Reports if the generic type could not be deduced from known information (e.g. argument types, types when the class + was created, etc.) +
  • + diff --git a/src/test/fixtures/generics/.meta/functions.php b/src/test/fixtures/generics/.meta/functions.php index 61b32f8..1e647ae 100644 --- a/src/test/fixtures/generics/.meta/functions.php +++ b/src/test/fixtures/generics/.meta/functions.php @@ -9,7 +9,7 @@ function expr_type($expr, string $string) { /** * @kphp-generic T * @param T $arg - * @return T + * @return ?T */ function nullable_of($arg) { if (0) { @@ -69,7 +69,7 @@ function combine($a1, $a2) { * @return T2[] */ function filter_is_instance($array, $class) { - return array_filter($array, fn($el) => is_a($el, $class));; + return array_filter($array, fn($el) => is_a($el, $class)); } /** diff --git a/src/test/fixtures/generics/Containers/MutableVectorList.php b/src/test/fixtures/generics/Containers/MutableVectorList.php index acbdc65..1de6322 100644 --- a/src/test/fixtures/generics/Containers/MutableVectorList.php +++ b/src/test/fixtures/generics/Containers/MutableVectorList.php @@ -5,7 +5,7 @@ * * @kphp-generic T */ -class MutableList extends VectorList { +class MutableVectorList extends VectorList { /** * @param T $data */ diff --git a/src/test/fixtures/generics/Containers/SimpleMap.php b/src/test/fixtures/generics/Containers/SimpleMap.php index b9b8f92..521ac0f 100644 --- a/src/test/fixtures/generics/Containers/SimpleMap.php +++ b/src/test/fixtures/generics/Containers/SimpleMap.php @@ -17,6 +17,7 @@ public function __construct(...$els) { } } + /** * @param TKey $key * @return TValue diff --git a/src/test/fixtures/generics/Containers/Vector.php b/src/test/fixtures/generics/Containers/Vector.php index 94c6f05..09ec965 100644 --- a/src/test/fixtures/generics/Containers/Vector.php +++ b/src/test/fixtures/generics/Containers/Vector.php @@ -40,7 +40,7 @@ function map($fn) { } /** - * @param callable(T):T $fn + * @param callable(T):bool $fn * @return SimpleVector */ function filter($fn) { @@ -50,8 +50,7 @@ function filter($fn) { } /** - * @kphp-generic T1 - * @param callable(T):T1 $fn + * @param callable(T): void $fn */ function foreach($fn) { foreach ($this->data as $el) { @@ -60,8 +59,7 @@ function foreach($fn) { } /** - * @kphp-generic T1 - * @param callable(string, T):T1 $fn + * @param callable(string, T): void $fn */ function foreach_key_value($fn) { foreach ($this->data as $key => $el) { @@ -86,6 +84,6 @@ function filter_is_instance($class) { */ function combine_with($other) { // TODO: replace Foo with T|T1 - return new SimpleVector/**/ (array_merge($this->data, $other->raw())); + return new SimpleVector/**/ (...array_merge($this->data, $other->raw())); } } diff --git a/src/test/fixtures/generics/all_ok/new_expr.fixture.php b/src/test/fixtures/generics/all_ok/new_expr.fixture.php new file mode 100644 index 0000000..d19eed8 --- /dev/null +++ b/src/test/fixtures/generics/all_ok/new_expr.fixture.php @@ -0,0 +1,7 @@ +*/(); + diff --git a/src/test/fixtures/generics/all_ok/vector_use.fixture.php b/src/test/fixtures/generics/all_ok/vector_use.fixture.php new file mode 100644 index 0000000..aaf6d6c --- /dev/null +++ b/src/test/fixtures/generics/all_ok/vector_use.fixture.php @@ -0,0 +1,83 @@ +*/([new Goo], function(Goo $a): Boo { +// return $a->boo(); +//}); +// +//$b = $a[0]; +//$b->foo(); + +$vec = new Vector/**/ (); + +//$vec->add(new Goo); +//$vec->add(null); +// +//$vec->filter(function(?Goo $el): bool { +// return $el != null; +//}); + +//$a = $vec->map/**/(function(Goo $a): Boo { +// return $a->boo(); +//}); + +// +//$b = $a->get(0); +// +//$c = $b->boo()->foo()->boo(); + +/** + * @return Vector> + */ +function returnVector() { + return new Vector; +} + + +$a = returnVector()->get(0); + +$b = $a->get(0)->get(0); + +/** + * @return Pair + */ +function returnPair(): Pair { + return new Pair(new Boo, new Goo); +} + +$x = returnPair()->second(); + + + + +//$vec->foreach(function(Goo $el) { +// var_dump($el); +//}); +// +//$vec->foreach(fn(Goo $el) => var_dump($el)); +// +//$vec->foreach_key_value(function(string $key, Goo $el) { +// var_dump($key); +// var_dump($el); +//}); diff --git a/src/test/fixtures/generics/inspections/unnecessary_explicit_instantiation_list.fixture.php b/src/test/fixtures/generics/inspections/unnecessary_explicit_instantiation_list.fixture.php deleted file mode 100644 index d3c1500..0000000 --- a/src/test/fixtures/generics/inspections/unnecessary_explicit_instantiation_list.fixture.php +++ /dev/null @@ -1,57 +0,0 @@ - $class - * @return T2[] - */ -function filter_is_instance_2($array, $class) { - return array_filter($array, fn($el) => is_a($el, $class));; -} - -/** - * @return Base[] - */ -function get_children_2() { - return [new Child1, new Child2()]; -} - -// TODO: fix it -//$a = mirror/**/ (new GlobalA()); -//expr_type($a, "\GlobalA|null"); - -$a = mirror_2/**/(""); -$a = mirror_2/**/ ([new GlobalA()]); -$a = combine_2/**/("", 1); -// TODO: fix it -//$children1_array = filter_is_instance_2/**/(get_children(), Child1::class); - -$a1 = mirror_2/**/(shape(["key1" => $a, "key2" => [new \GlobalD()]])); diff --git a/src/test/fixtures/generics/simple_functions.fixture.php b/src/test/fixtures/generics/simple_functions.fixture.php index 2eaeb96..61bfa95 100644 --- a/src/test/fixtures/generics/simple_functions.fixture.php +++ b/src/test/fixtures/generics/simple_functions.fixture.php @@ -123,16 +123,16 @@ "Nullable"; { $a1 = mirror/**/([new GlobalA()]); - expr_type($a1, "\GlobalA|null"); + expr_type($a1, "?\GlobalA"); $a2 = mirror/**/([new \Classes\A()]); - expr_type($a2, "\Classes\A|null"); + expr_type($a2, "?\Classes\A"); $a3 = mirror/**/([new B()]); - expr_type($a3, "\Classes\B|null"); + expr_type($a3, "?\Classes\B"); $a4 = mirror/**/([new GlobalC]); - expr_type($a4, "\Classes\C|null"); + expr_type($a4, "?\Classes\C"); } "Union"; { diff --git a/src/test/fixtures/generics/types/class-string/explicit.fixture.php b/src/test/fixtures/generics/types/class-string/explicit.fixture.php index c0d20d0..be8cc97 100644 --- a/src/test/fixtures/generics/types/class-string/explicit.fixture.php +++ b/src/test/fixtures/generics/types/class-string/explicit.fixture.php @@ -11,8 +11,12 @@ function get_children() { return [new Child1, new Child2()]; } -expr_type(Child1::class, "class-string(\Classes\Child1)|string"); +expr_type(Child1::class, "class-string(\Classes\Child1)"); $base_array = get_children(); $children1_array = filter_is_instance/**/($base_array, Child1::class); expr_type($children1_array, "\Classes\Child1[]"); + + + + diff --git a/src/test/fixtures/generics/types/class-string/implicit.fixture.php b/src/test/fixtures/generics/types/class-string/implicit.fixture.php index 4b0b747..25e74ba 100644 --- a/src/test/fixtures/generics/types/class-string/implicit.fixture.php +++ b/src/test/fixtures/generics/types/class-string/implicit.fixture.php @@ -14,3 +14,4 @@ function get_children() { $base_array = get_children(); $children2_array = filter_is_instance($base_array, Child2::class); expr_type($children2_array, "\Classes\Child2[]"); + diff --git a/src/test/fixtures/generics/types/classes/explicit/array.fixture.php b/src/test/fixtures/generics/types/classes-as-types/explicit/array.fixture.php similarity index 100% rename from src/test/fixtures/generics/types/classes/explicit/array.fixture.php rename to src/test/fixtures/generics/types/classes-as-types/explicit/array.fixture.php diff --git a/src/test/fixtures/generics/types/classes/explicit/mixed.fixture.php b/src/test/fixtures/generics/types/classes-as-types/explicit/mixed.fixture.php similarity index 54% rename from src/test/fixtures/generics/types/classes/explicit/mixed.fixture.php rename to src/test/fixtures/generics/types/classes-as-types/explicit/mixed.fixture.php index ea54811..d8db7ba 100644 --- a/src/test/fixtures/generics/types/classes/explicit/mixed.fixture.php +++ b/src/test/fixtures/generics/types/classes-as-types/explicit/mixed.fixture.php @@ -1,7 +1,7 @@ -*/(tuple([new GlobalA()], new \Classes\A())); -expr_type($a, "tuple(\GlobalA[],\Classes\C|\Classes\A)"); +expr_type($a, "tuple(\GlobalA[],\Classes\A|\Classes\C)"); $a1 = mirror/**/(shape(["key1" => $a, "key2" => [new \GlobalD()]])); -expr_type($a1, "shape(key1:tuple(\GlobalA[],\Classes\C|\Classes\A),key2:?\GlobalD[])"); +expr_type($a1, "shape(key1:tuple(\GlobalA[],\Classes\A|\Classes\C),key2:?\GlobalD[])"); diff --git a/src/test/fixtures/generics/types/classes/explicit/nullable.fixture.php b/src/test/fixtures/generics/types/classes-as-types/explicit/nullable.fixture.php similarity index 78% rename from src/test/fixtures/generics/types/classes/explicit/nullable.fixture.php rename to src/test/fixtures/generics/types/classes-as-types/explicit/nullable.fixture.php index 554179b..75fb979 100644 --- a/src/test/fixtures/generics/types/classes/explicit/nullable.fixture.php +++ b/src/test/fixtures/generics/types/classes-as-types/explicit/nullable.fixture.php @@ -9,25 +9,28 @@ // \GlobalD // Класс из глобального скоупа -$a = mirror/**/ (new GlobalA()); -expr_type($a, "\GlobalA|null"); +$a = mirror/**/ (null); +$a->methodGlobalA(); + + +expr_type($a, "?\GlobalA"); // Класс из пространства имен $a = mirror/**/ (new \Classes\A()); -expr_type($a, "\Classes\A|null"); +expr_type($a, "?\Classes\A"); // Импортированный класс из пространства имен $a = mirror/**/ (new B()); -expr_type($a, "\Classes\B|null"); +expr_type($a, "?\Classes\B"); // Импортированный класс из пространства имен с алиасом $a = mirror/**/ (new GlobalC); -expr_type($a, "\Classes\C|null"); +expr_type($a, "?\Classes\C"); // Импортированный класс из пространства имен с алиасом как у глобально класса $a = mirror/**/ (new GlobalD()); -expr_type($a, "\Classes\D|null"); +expr_type($a, "?\Classes\D"); // Глобальный класс с именем как у локального алиаса для другого класса $a = mirror/**/ (new \GlobalD()); -expr_type($a, "\GlobalD|null"); +expr_type($a, "?\GlobalD"); diff --git a/src/test/fixtures/generics/types/classes/explicit/shape.fixture.php b/src/test/fixtures/generics/types/classes-as-types/explicit/shape.fixture.php similarity index 100% rename from src/test/fixtures/generics/types/classes/explicit/shape.fixture.php rename to src/test/fixtures/generics/types/classes-as-types/explicit/shape.fixture.php diff --git a/src/test/fixtures/generics/types/classes/explicit/standalone.fixture.php b/src/test/fixtures/generics/types/classes-as-types/explicit/standalone.fixture.php similarity index 100% rename from src/test/fixtures/generics/types/classes/explicit/standalone.fixture.php rename to src/test/fixtures/generics/types/classes-as-types/explicit/standalone.fixture.php diff --git a/src/test/fixtures/generics/types/classes/explicit/tuple.fixture.php b/src/test/fixtures/generics/types/classes-as-types/explicit/tuple.fixture.php similarity index 100% rename from src/test/fixtures/generics/types/classes/explicit/tuple.fixture.php rename to src/test/fixtures/generics/types/classes-as-types/explicit/tuple.fixture.php diff --git a/src/test/fixtures/generics/types/classes/explicit/union.fixture.php b/src/test/fixtures/generics/types/classes-as-types/explicit/union.fixture.php similarity index 97% rename from src/test/fixtures/generics/types/classes/explicit/union.fixture.php rename to src/test/fixtures/generics/types/classes-as-types/explicit/union.fixture.php index cc9cf8c..2242daa 100644 --- a/src/test/fixtures/generics/types/classes/explicit/union.fixture.php +++ b/src/test/fixtures/generics/types/classes-as-types/explicit/union.fixture.php @@ -3,6 +3,7 @@ // \GlobalA // \Classes\A use Classes\B; +use Classes\C as GlobalC; use Classes\D as GlobalD; // \GlobalD diff --git a/src/test/fixtures/generics/types/classes/implicit/array.fixture.php b/src/test/fixtures/generics/types/classes-as-types/implicit/array.fixture.php similarity index 100% rename from src/test/fixtures/generics/types/classes/implicit/array.fixture.php rename to src/test/fixtures/generics/types/classes-as-types/implicit/array.fixture.php diff --git a/src/test/fixtures/generics/types/classes/implicit/mixed.fixture.php b/src/test/fixtures/generics/types/classes-as-types/implicit/mixed.fixture.php similarity index 100% rename from src/test/fixtures/generics/types/classes/implicit/mixed.fixture.php rename to src/test/fixtures/generics/types/classes-as-types/implicit/mixed.fixture.php diff --git a/src/test/fixtures/generics/types/classes/implicit/nullable.fixture.php b/src/test/fixtures/generics/types/classes-as-types/implicit/nullable.fixture.php similarity index 60% rename from src/test/fixtures/generics/types/classes/implicit/nullable.fixture.php rename to src/test/fixtures/generics/types/classes-as-types/implicit/nullable.fixture.php index 73f2a5d..66bacc5 100644 --- a/src/test/fixtures/generics/types/classes/implicit/nullable.fixture.php +++ b/src/test/fixtures/generics/types/classes-as-types/implicit/nullable.fixture.php @@ -1,5 +1,5 @@ */(); +//expr_type($obj, "\WithoutConstructor(?\TestA)"); +// +// +///** @kphp-generic T */ +//class OneArgumentConstructor { +// /** @param T $a */ +// public function __construct($a) {} +//} +// +//$obj = new OneArgumentConstructor/**/(null); +//expr_type($obj, "\OneArgumentConstructor(?\TestA)"); +// +//$obj = new OneArgumentConstructor(new TestA); +//expr_type($obj, "\OneArgumentConstructor(\TestA)"); + + +/** @kphp-generic T1, T2 */ +class TwoArgumentConstructor { + /** + * @param T1 $a + * @param T2 $b + */ + public function __construct($a, $b) {} +} + +$obj1 = new TwoArgumentConstructor(new TestA, "Hello World"); +expr_type($obj1, "\TwoArgumentConstructor(\TestA,string)"); + +// +///** @kphp-generic T1, T2 */ +//class TwoGenericTypes {} +// +//$obj = new TwoGenericTypes/**/(); +//expr_type($obj, "\TwoGenericTypes(?\TestA,string)"); +//$obj = new TwoGenericTypes/**/(); +//expr_type($obj, "\TwoGenericTypes(?\TestA,tuple(int,string))"); +// +// +///** @kphp-generic T1, T2, T3 */ +//class ThreeGenericTypes {} +// +//$obj = new ThreeGenericTypes/**/(); +//expr_type($obj, "\ThreeGenericTypes(?\TestA,string,int)"); +// +// +// +// +// +// +// + + + + + + + + + diff --git a/src/test/fixtures/generics/types/extended-reify/param.fixture.php b/src/test/fixtures/generics/types/extended-reify/param.fixture.php new file mode 100644 index 0000000..f7a2f66 --- /dev/null +++ b/src/test/fixtures/generics/types/extended-reify/param.fixture.php @@ -0,0 +1,52 @@ + $a + */ +function takeClassOfString($a) {} + +takeClassOfString(new GenericClass()); + + +/** + * @param GenericWithSeveralTypesClass $a + */ +function takeClassWithSeveralTypesOfStringInt($a) {} + +takeClassWithSeveralTypesOfStringInt(new GenericWithSeveralTypesClass); + + +/** + * @param GenericClass> $a + */ +function takeClassOfStringViaFunction($a) {} + +takeClassOfStringViaFunction(genericFunction()); + + +/** + * @param GenericClass $a + */ +function takeClassOfStringViaGenericMethod($a) {} + +$a = new ClassWithGenericMethod(); +takeClassOfStringViaGenericMethod($a->genericMethod()); + + +class TakeGeneric { + /** + * @param GenericClass $a + */ + function takeGenericFunction($a) {} + + /** + * @param GenericClass $a + */ + static function takeStaticGenericFunction($a) {} +} + +$g = new TakeGeneric; +$g->takeGenericFunction($a->genericMethod()); +$g->takeGenericFunction(new GenericClass()); +TakeGeneric::takeStaticGenericFunction(genericFunction()); +TakeGeneric::takeStaticGenericFunction(new GenericClass()); diff --git a/src/test/fixtures/generics/types/extended-reify/param_wrong.fixture.php b/src/test/fixtures/generics/types/extended-reify/param_wrong.fixture.php new file mode 100644 index 0000000..5e18869 --- /dev/null +++ b/src/test/fixtures/generics/types/extended-reify/param_wrong.fixture.php @@ -0,0 +1,18 @@ +new GenericClass()); + +class TakeWrongGeneric { + /** + * @param GenericClass $a + */ + static function takeStaticGenericFunction($a) {} +} + +TakeWrongGeneric::takeStaticGenericFunction(genericFunction()); + diff --git a/src/test/fixtures/generics/types/extended-reify/return.fixture.php b/src/test/fixtures/generics/types/extended-reify/return.fixture.php new file mode 100644 index 0000000..36afda7 --- /dev/null +++ b/src/test/fixtures/generics/types/extended-reify/return.fixture.php @@ -0,0 +1,59 @@ + + */ +function returnClassOfString() { + return new GenericClass(); +} + + +/** + * @kphp-generic T1, T2 + */ +class GenericWithSeveralTypesClass implements IGenericClass {} + +/** + * @return GenericWithSeveralTypesClass + */ +function returnClassWithSeveralTypesOfStringInt() { + return new GenericWithSeveralTypesClass(); +} + + +/** + * @kphp-generic T: IGenericClass + * @return GenericClass + */ +function genericFunction() { return null; } + +/** + * @return GenericClass> + */ +function returnClassOfStringViaFunction() { + return genericFunction(); +} + + +class ClassWithGenericMethod { + /** + * @kphp-generic T + * @return GenericClass + */ + function genericMethod() { return null; } +} + +/** + * @return GenericClass + */ +function returnClassOfStringViaGenericMethod() { + $a = new ClassWithGenericMethod(); + return $a->genericMethod(); +} diff --git a/src/test/fixtures/generics/types/extended-reify/return_wrong.fixture.php b/src/test/fixtures/generics/types/extended-reify/return_wrong.fixture.php new file mode 100644 index 0000000..39cd875 --- /dev/null +++ b/src/test/fixtures/generics/types/extended-reify/return_wrong.fixture.php @@ -0,0 +1,23 @@ + + */ +function returnClassOfString() { + return new OtherGenericClass(); +} + + +/** + * @return GenericClass + */ +function returnWrongClassOfString() { + return new GenericClass(); +} diff --git a/src/test/fixtures/generics/types/methods/chain.fixture.php b/src/test/fixtures/generics/types/methods/chain.fixture.php new file mode 100644 index 0000000..7d67afc --- /dev/null +++ b/src/test/fixtures/generics/types/methods/chain.fixture.php @@ -0,0 +1,39 @@ +>> + */ +function returnVector() { + return new Vector; +} + + +$a = returnVector()->get(0); +expr_type($a, "\Vector|\Vector(\Vector|\Vector(string))"); + +$b = $a->get(0)->get(0); +// TODO: должно быть string, а не null +//expr_type($b, "string"); + + +/** + * @return Pair + */ +function returnPair(): Pair { + return new Pair(new Boo, new Goo); +} + +$x = returnPair()->first(); +$y = returnPair()->second(); +expr_type($x, "\Boo"); +expr_type($y, "\Goo"); + +/** + * @return Vector> + */ +function returnVectorPair() { + return new Vector; +} + +$a = returnVectorPair()->get(0)->second(); +expr_type($a, "\Goo"); diff --git a/src/test/kotlin/com/vk/kphpstorm/testing/infrastructure/AllOkTestBase.kt b/src/test/kotlin/com/vk/kphpstorm/testing/infrastructure/AllOkTestBase.kt new file mode 100644 index 0000000..0f16cd6 --- /dev/null +++ b/src/test/kotlin/com/vk/kphpstorm/testing/infrastructure/AllOkTestBase.kt @@ -0,0 +1,41 @@ +package com.vk.kphpstorm.testing.infrastructure + +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.jetbrains.php.lang.inspections.PhpUndefinedFieldInspection +import com.jetbrains.php.lang.inspections.PhpUndefinedMethodInspection +import com.vk.kphpstorm.configuration.KphpStormConfiguration +import com.vk.kphpstorm.inspections.KphpGenericsInspection +import java.io.File + +abstract class AllOkTestBase : BasePlatformTestCase() { + + override fun getTestDataPath() = "src/test/fixtures" + + override fun setUp() { + super.setUp() + + myFixture.enableInspections(PhpUndefinedMethodInspection()) + myFixture.enableInspections(PhpUndefinedFieldInspection()) + myFixture.enableInspections(KphpGenericsInspection()) + } + + /** + * Run inspection on file.fixture.php and check that all and match + * If file.qf.php exists, apply quickfixes and compare result to file.qf.php + */ + protected fun runFixture(vararg fixtureFiles: String) { + // Highlighting test + KphpStormConfiguration.saveThatSetupForProjectDone(project) + myFixture.configureByFiles(*fixtureFiles) + myFixture.testHighlighting(true, false, true) + + // Quick-fix test + fixtureFiles.forEach { fixtureFile -> + val qfFile = fixtureFile.replace(".fixture.php", ".qf.php") + if (File(myFixture.testDataPath + "/" + qfFile).exists()) { + myFixture.getAllQuickFixes().forEach { myFixture.launchAction(it) } + myFixture.checkResultByFile(qfFile) + } + } + } +} diff --git a/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/all_ok/GenericsAllOkTest.kt b/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/all_ok/GenericsAllOkTest.kt new file mode 100644 index 0000000..4c9fcba --- /dev/null +++ b/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/all_ok/GenericsAllOkTest.kt @@ -0,0 +1,14 @@ +package com.vk.kphpstorm.testing.tests.generics.all_ok + +import com.vk.kphpstorm.testing.infrastructure.AllOkTestBase + +class GenericsAllOkTest : AllOkTestBase() { + fun testNewExpr() { + runFixture("generics/all_ok/new_expr.fixture.php") + } + + fun testVectorUse() { + runFixture("generics/Containers/Vector.php") + runFixture("generics/all_ok/vector_use.fixture.php") + } +} diff --git a/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/inspections/GenericBoundViolationInspectionsTest.kt b/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/inspections/GenericBoundViolationInspectionsTest.kt deleted file mode 100644 index 64098a1..0000000 --- a/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/inspections/GenericBoundViolationInspectionsTest.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.vk.kphpstorm.testing.tests.generics.inspections - -import com.vk.kphpstorm.inspections.GenericBoundViolationInspection -import com.vk.kphpstorm.testing.infrastructure.InspectionTestBase - -class GenericBoundViolationInspectionsTest : InspectionTestBase(GenericBoundViolationInspection()) { - fun test() { - runFixture("generics/inspections/bound_violation.fixture.php") - } -} diff --git a/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/inspections/GenericUnnecessaryExplicitInstantiationListInspectionTest.kt b/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/inspections/GenericUnnecessaryExplicitInstantiationListInspectionTest.kt deleted file mode 100644 index 2933afe..0000000 --- a/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/inspections/GenericUnnecessaryExplicitInstantiationListInspectionTest.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.vk.kphpstorm.testing.tests.generics.inspections - -import com.vk.kphpstorm.inspections.GenericUnnecessaryExplicitInstantiationListInspection -import com.vk.kphpstorm.testing.infrastructure.InspectionTestBase - -class GenericUnnecessaryExplicitInstantiationListInspectionTest - : InspectionTestBase(GenericUnnecessaryExplicitInstantiationListInspection()) { - - fun test() { - runFixture( - "generics/inspections/unnecessary_explicit_instantiation_list.fixture.php", - ) - } -} diff --git a/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/inspections/GenericsInstantiationArgsMismatchInspectionsTest.kt b/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/inspections/GenericsInstantiationArgsMismatchInspectionsTest.kt deleted file mode 100644 index 4334e65..0000000 --- a/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/inspections/GenericsInstantiationArgsMismatchInspectionsTest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.vk.kphpstorm.testing.tests.generics.inspections - -import com.vk.kphpstorm.inspections.GenericInstantiationArgsCountMismatchInspection -import com.vk.kphpstorm.testing.infrastructure.InspectionTestBase - -class GenericsInstantiationArgsMismatchInspectionsTest : InspectionTestBase(GenericInstantiationArgsCountMismatchInspection()) { - fun test() { - runFixture( - "generics/inspections/instantiation_args_mismatch.fixture.php", - ) - } -} diff --git a/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/inspections/GenericsNoEnoughInformationInspectionsTest.kt b/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/inspections/GenericsNoEnoughInformationInspectionsTest.kt deleted file mode 100644 index 7ace0b6..0000000 --- a/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/inspections/GenericsNoEnoughInformationInspectionsTest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.vk.kphpstorm.testing.tests.generics.inspections - -import com.vk.kphpstorm.inspections.GenericNoEnoughInformationInspection -import com.vk.kphpstorm.testing.infrastructure.InspectionTestBase - -class GenericsNoEnoughInformationInspectionsTest : InspectionTestBase(GenericNoEnoughInformationInspection()) { - fun test() { - runFixture( - "generics/inspections/no_enough_information.fixture.php", - ) - } -} diff --git a/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/inspections/KphpGenericsInspectionsTest.kt b/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/inspections/KphpGenericsInspectionsTest.kt new file mode 100644 index 0000000..4c66f35 --- /dev/null +++ b/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/inspections/KphpGenericsInspectionsTest.kt @@ -0,0 +1,21 @@ +package com.vk.kphpstorm.testing.tests.generics.inspections + +import com.vk.kphpstorm.inspections.KphpGenericsInspection +import com.vk.kphpstorm.testing.infrastructure.InspectionTestBase + +class KphpGenericsInspectionsTest : InspectionTestBase(KphpGenericsInspection()) { + fun testInspections() { + runFixture( + "generics/inspections/bound_violation.fixture.php", + "generics/inspections/no_enough_information.fixture.php", + "generics/inspections/instantiation_args_mismatch.fixture.php", + ) + } + + fun testReifyFromReturn() { + runFixture("generics/types/extended-reify/return.fixture.php",) + runFixture("generics/types/extended-reify/return_wrong.fixture.php") + runFixture("generics/types/extended-reify/param.fixture.php") + runFixture("generics/types/extended-reify/param_wrong.fixture.php") + } +} diff --git a/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/types/GenericsTypesTest.kt b/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/types/GenericsTypesTest.kt index 443de8b..ba6295d 100644 --- a/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/types/GenericsTypesTest.kt +++ b/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/types/GenericsTypesTest.kt @@ -29,30 +29,44 @@ class GenericsTypesTest : TypeTestBase() { fun testImplicitClass() { runFixture( - "generics/types/classes/implicit/standalone.fixture.php", - "generics/types/classes/implicit/array.fixture.php", - "generics/types/classes/implicit/nullable.fixture.php", - "generics/types/classes/implicit/union.fixture.php", - "generics/types/classes/implicit/tuple.fixture.php", - "generics/types/classes/implicit/shape.fixture.php", - "generics/types/classes/implicit/mixed.fixture.php", - "generics/.meta/functions.php" + "generics/types/classes-as-types/implicit/standalone.fixture.php", + "generics/types/classes-as-types/implicit/array.fixture.php", + "generics/types/classes-as-types/implicit/nullable.fixture.php", + "generics/types/classes-as-types/implicit/union.fixture.php", + "generics/types/classes-as-types/implicit/tuple.fixture.php", + "generics/types/classes-as-types/implicit/shape.fixture.php", + "generics/types/classes-as-types/implicit/mixed.fixture.php", + "generics/.meta/functions.php", ) } fun testExplicitClass() { runFixture( - "generics/types/classes/explicit/standalone.fixture.php", - "generics/types/classes/explicit/array.fixture.php", - "generics/types/classes/explicit/nullable.fixture.php", - "generics/types/classes/explicit/union.fixture.php", - "generics/types/classes/explicit/tuple.fixture.php", - "generics/types/classes/explicit/shape.fixture.php", - "generics/types/classes/explicit/mixed.fixture.php", + "generics/types/classes-as-types/explicit/standalone.fixture.php", + "generics/types/classes-as-types/explicit/array.fixture.php", + "generics/types/classes-as-types/explicit/nullable.fixture.php", + "generics/types/classes-as-types/explicit/union.fixture.php", + "generics/types/classes-as-types/explicit/tuple.fixture.php", + "generics/types/classes-as-types/explicit/shape.fixture.php", + "generics/types/classes-as-types/explicit/mixed.fixture.php", "generics/.meta/functions.php" ) } + fun testNexExpr() { + runFixture( + "generics/types/classes/new_expr.fixture.php", + ) + } + + fun testMethods() { + runFixture( + "generics/types/methods/chain.fixture.php", + "generics/Containers/Vector.php", + "generics/Containers/Pair.php", + ) + } + // TODO: // fun testSimpleFunctions() { // runFixture("generics/simple_functions.fixture.php")