diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTplInstantiation.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTplInstantiation.kt index c00fc78..482437f 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTplInstantiation.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTplInstantiation.kt @@ -29,6 +29,7 @@ class ExPhpTypeTplInstantiation(val classFqn: String, val specializationList: Li override fun isAssignableFrom(rhs: ExPhpType, project: Project): Boolean = when (rhs) { is ExPhpTypeAny -> true // not finished + is ExPhpTypePipe -> rhs.items.any { it.isAssignableFrom(this, project) } is ExPhpTypeTplInstantiation -> classFqn == rhs.classFqn && specializationList.size == rhs.specializationList.size else -> false } diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/PsiToExPhpType.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/PsiToExPhpType.kt index c8d5e04..94e2cf3 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/PsiToExPhpType.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/PsiToExPhpType.kt @@ -15,20 +15,31 @@ object PsiToExPhpType { else -> null } - fun dropGenerics(type: ExPhpType): ExPhpType { + fun dropGenerics(type: ExPhpType): ExPhpType? { // TODO: добавить все типы if (type is ExPhpTypePipe) { - return ExPhpTypePipe(type.items.filter { - it !is ExPhpTypeGenericsT && (it !is ExPhpTypeArray || it.inner !is ExPhpTypeGenericsT) - }.map { dropGenerics(it) }) + val items = type.items.mapNotNull { dropGenerics(it) } + if (items.isEmpty()) return null + + return ExPhpTypePipe(items) + } + + if (type is ExPhpTypeTplInstantiation) { + val list = type.specializationList.mapNotNull { dropGenerics(it) } + if (list.isEmpty()) return null + return ExPhpTypeTplInstantiation(type.classFqn, list) } if (type is ExPhpTypeNullable) { - return ExPhpTypeNullable(dropGenerics(type.inner)) + return dropGenerics(type.inner)?.let { ExPhpTypeNullable(it) } } if (type is ExPhpTypeArray) { - return ExPhpTypeArray(dropGenerics(type.inner)) + return dropGenerics(type.inner)?.let { ExPhpTypeArray(it) } + } + + if (type is ExPhpTypeGenericsT) { + return null } return type diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/GenericFunctionCall.kt b/src/main/kotlin/com/vk/kphpstorm/generics/GenericFunctionCall.kt index d61d8a9..8fd78e4 100644 --- a/src/main/kotlin/com/vk/kphpstorm/generics/GenericFunctionCall.kt +++ b/src/main/kotlin/com/vk/kphpstorm/generics/GenericFunctionCall.kt @@ -3,6 +3,7 @@ package com.vk.kphpstorm.generics import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement import com.jetbrains.php.PhpIndex +import com.jetbrains.php.lang.psi.PhpPsiElementFactory import com.jetbrains.php.lang.psi.elements.* import com.jetbrains.php.lang.psi.elements.Function import com.jetbrains.php.lang.psi.resolve.types.PhpType @@ -76,6 +77,25 @@ class GenericsReifier(val project: Project) { } 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) { @@ -377,6 +397,125 @@ class ResolvingGenericMethodCall(project: Project) : ResolvingGenericBase(projec } } +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 function = function() + 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 { +// val classTypes = call.classReference?.type?.global(project)?.toExPhpType() +// +// val classType = if (classTypes is ExPhpTypePipe) { +// classTypes.items.firstOrNull { it is ExPhpTypeInstance } as? ExPhpTypeInstance +// } else { +// classTypes as? ExPhpTypeInstance +// } + + method = call.resolve() as? Method + klass = method?.containingClass + + init() + } + + override fun function() = method + + override fun isResolved() = method != null && klass != null + + override fun genericNames() = method?.genericNames() ?: emptyList() + + 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) { + 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 function: Function? = call.resolve() as? Function + + init { + init() + } + + override fun function() = function + + override fun isResolved() = function != null + + override fun genericNames() = function?.genericNames() ?: emptyList() + + override fun isGeneric() = function()?.isGeneric() == true + + override fun toString(): String { + val function = function() + val explicit = explicitSpecs.joinToString(",") + val implicit = implicitSpecs.joinToString(",") + return "${function?.fqn ?: "UnknownFunction"}<$explicit>($implicit)" + } +} + /** * Ввиду причин описанных в [IndexingGenericFunctionCall], мы не можем использовать * объединенный класс для обработки вызова во время вывода типов. Однако в других @@ -413,41 +552,42 @@ class ResolvingGenericMethodCall(project: Project) : ResolvingGenericBase(projec * f/**/(new A, new B); // => T1 = C, T2 = D * ``` */ -class GenericFunctionCall(call: FunctionReference) { - val project = call.project - val function = call.resolve() as? Function +abstract class GenericCall(val project: Project) { + abstract val callArgs: Array + 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() - private val callArgs = call.parameters - private val argumentsTypes = - callArgs.filterIsInstance().map { it.type.global(project).toExPhpType() } - val explicitSpecsPsi = findInstantiationComment(call) - - private val extractor = GenericInstantiationExtractor() - private val reifier = GenericsReifier(call.project) + protected val extractor = GenericInstantiationExtractor() + protected val reifier = GenericsReifier(project) val explicitSpecs get() = extractor.explicitSpecs val implicitSpecs get() = reifier.implicitSpecs val implicitSpecializationNameMap get() = reifier.implicitSpecializationNameMap val implicitSpecializationErrors get() = reifier.implicitSpecializationErrors - init { - init() - } + protected fun init() { + val function = function() ?: return + if (!isGeneric()) return - private fun init() { - if (function == null || !isGeneric()) return + val genericNames = genericNames() parameters.addAll(function.parameters) - genericTs.addAll(function.genericNames()) + genericTs.addAll(genericNames) // Несмотря на то, что явный список является превалирующим над // типами выведенными из аргументов функций, нам все равно // необходимы обв списка для дальнейших инспекций // В первую очередь, выводим все типы шаблонов из аргументов функции (при наличии) - reifier.reifyAllGenericsT(function.parameters, function.genericNames(), argumentsTypes) + reifier.reifyAllGenericsT(function.parameters, genericNames, argumentsTypes) // Далее, выводим все типы шаблонов из явного списка типов (при наличии) extractor.extractExplicitGenericsT(function, explicitSpecsPsi) } @@ -456,10 +596,6 @@ class GenericFunctionCall(call: FunctionReference) { return explicitSpecsPsi != null } - fun isGeneric(): Boolean { - return function?.isGeneric() == true - } - /** * Функция проверяющая, что явно указанные шаблонные типы * соответствуют автоматически выведенным типам и их можно @@ -478,10 +614,9 @@ class GenericFunctionCall(call: FunctionReference) { * ``` */ fun isNoNeedExplicitSpec(): Boolean { - if (function == null) return false if (explicitSpecsPsi == null) return false - val countGenericNames = function.genericNames().size + val countGenericNames = genericNames().size val countExplicitSpecs = explicitSpecs.size val countImplicitSpecs = implicitSpecs.size @@ -523,7 +658,7 @@ class GenericFunctionCall(call: FunctionReference) { * В примере выше в результате будет возвращен тип `Foo`. */ fun typeOfParam(index: Int): ExPhpType? { - if (function == null) return null + val function = function() ?: return null val param = function.getParameter(index) ?: return null val paramType = param.type @@ -537,9 +672,5 @@ class GenericFunctionCall(call: FunctionReference) { return null } - override fun toString(): String { - val explicit = explicitSpecs.joinToString(",") - val implicit = implicitSpecs.joinToString(",") - return "${function?.fqn ?: "UnknownFunction"}<$explicit>($implicit)" - } + abstract override fun toString(): String } diff --git a/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpStormTypeInfoProvider.kt b/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpStormTypeInfoProvider.kt index 702ba65..45d59c6 100644 --- a/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpStormTypeInfoProvider.kt +++ b/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpStormTypeInfoProvider.kt @@ -18,7 +18,8 @@ class KphpStormTypeInfoProvider : ExpressionTypeProvider() { override fun getInformationHint(element: PhpTypedElement): String { val phpType = element.type.global(element.project) - return phpType.toExPhpType()?.let { PsiToExPhpType.dropGenerics(it).toHumanReadable(element) } ?: phpType.toString() + val exPhpType = phpType.toExPhpType() ?: return phpType.toString() + return PsiToExPhpType.dropGenerics(exPhpType)?.toHumanReadable(element) ?: phpType.toString() } override fun getExpressionsAt(elementAt: PsiElement): List { 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 0f81325..2128d47 100644 --- a/src/main/kotlin/com/vk/kphpstorm/highlighting/hints/InlayHintsCollector.kt +++ b/src/main/kotlin/com/vk/kphpstorm/highlighting/hints/InlayHintsCollector.kt @@ -9,8 +9,8 @@ import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.refactoring.suggested.endOffset import com.jetbrains.php.lang.psi.elements.FunctionReference +import com.jetbrains.php.lang.psi.elements.MethodReference import com.vk.kphpstorm.generics.GenericFunctionCall -import com.vk.kphpstorm.generics.GenericUtil.genericNames @Suppress("UnstableApiUsage") class InlayHintsCollector( @@ -41,11 +41,17 @@ class InlayHintsCollector( return } - val genericNames = call.function!!.genericNames().joinToString(", ") + val genericNames = call.genericNames().joinToString(", ") val simplePresentation = myHintsFactory.inlayHint("<$genericNames>") val namePsi = element.firstChild + val offset = if (element is MethodReference) { + // offset after "->method_name" + namePsi.nextSibling.nextSibling.endOffset + } else { + namePsi.endOffset + } - sink.addInlineElement(namePsi.endOffset, false, simplePresentation, false) + sink.addInlineElement(offset, false, simplePresentation, false) } } diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/FunctionGenericInstantiationArgsCountMismatchInspection.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/FunctionGenericInstantiationArgsCountMismatchInspection.kt deleted file mode 100644 index ae44934..0000000 --- a/src/main/kotlin/com/vk/kphpstorm/inspections/FunctionGenericInstantiationArgsCountMismatchInspection.kt +++ /dev/null @@ -1,32 +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.visitors.PhpElementVisitor -import com.vk.kphpstorm.generics.GenericFunctionCall -import com.vk.kphpstorm.generics.GenericUtil.genericNames - -class FunctionGenericInstantiationArgsCountMismatchInspection : PhpInspection() { - override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { - return object : PhpElementVisitor() { - override fun visitPhpFunctionCall(reference: FunctionReference) { - val call = GenericFunctionCall(reference) - if (call.function == null) return - - val countGenericNames = call.function.genericNames().size - val countExplicitSpecs = call.explicitSpecs.size - - if (countGenericNames != countExplicitSpecs && call.explicitSpecsPsi != null) { - holder.registerProblem( - call.explicitSpecsPsi, - "$countGenericNames type arguments expected for ${call.function.fqn}", - ProblemHighlightType.GENERIC_ERROR - ) - } - } - } - } -} diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/GenericInstantiationArgsCountMismatchInspection.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/GenericInstantiationArgsCountMismatchInspection.kt new file mode 100644 index 0000000..a63809d --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/GenericInstantiationArgsCountMismatchInspection.kt @@ -0,0 +1,51 @@ +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 + 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/FunctionGenericNoEnoughInformationInspection.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/GenericNoEnoughInformationInspection.kt similarity index 54% rename from src/main/kotlin/com/vk/kphpstorm/inspections/FunctionGenericNoEnoughInformationInspection.kt rename to src/main/kotlin/com/vk/kphpstorm/inspections/GenericNoEnoughInformationInspection.kt index dab4535..4e5ac11 100644 --- a/src/main/kotlin/com/vk/kphpstorm/inspections/FunctionGenericNoEnoughInformationInspection.kt +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/GenericNoEnoughInformationInspection.kt @@ -2,21 +2,39 @@ 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.GenericUtil.genericNames +import com.vk.kphpstorm.generics.GenericMethodCall -class FunctionGenericNoEnoughInformationInspection : PhpInspection() { +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) - if (call.function == null) return + checkGenericCall(call, reference.firstChild) + } - val genericNames = call.function.genericNames() + private fun checkGenericCall(call: GenericCall, errorPsi: PsiElement) { + if (!call.isResolved()) return + val genericNames = call.genericNames() if (call.explicitSpecsPsi == null) { genericNames.any { @@ -24,7 +42,7 @@ class FunctionGenericNoEnoughInformationInspection : PhpInspection() { if (!resolved) { holder.registerProblem( - reference.element, + errorPsi, "Not enough information to infer generic $it", ProblemHighlightType.GENERIC_ERROR ) diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/FunctionGenericSeveralGenericTypesInspection.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/GenericSeveralGenericTypesInspection.kt similarity index 55% rename from src/main/kotlin/com/vk/kphpstorm/inspections/FunctionGenericSeveralGenericTypesInspection.kt rename to src/main/kotlin/com/vk/kphpstorm/inspections/GenericSeveralGenericTypesInspection.kt index ccfc76b..5441002 100644 --- a/src/main/kotlin/com/vk/kphpstorm/inspections/FunctionGenericSeveralGenericTypesInspection.kt +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/GenericSeveralGenericTypesInspection.kt @@ -2,19 +2,39 @@ 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 FunctionGenericSeveralGenericTypesInspection : PhpInspection() { +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) - if (call.function == null) return + checkGenericCall(call, reference) + } + + private fun checkGenericCall(call: GenericCall, element: PsiElement) { + if (!call.isResolved()) return // В случае даже если есть ошибки, то мы показываем их только // в случае когда нет явного определения шаблона для вызова функции. @@ -23,15 +43,18 @@ class FunctionGenericSeveralGenericTypesInspection : PhpInspection() { val (type1, type2) = error.value val genericsTString = call.genericTs.joinToString(", ") - val callString = reference.element.text - val parts = callString.split("(") - val callStingWithGenerics = parts[0] + "<$genericsTString>(" + parts[1] + 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( - reference.parameterList ?: reference.element, + 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/FunctionUnnecessaryExplicitGenericInstantiationListInspection.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/GenericUnnecessaryExplicitInstantiationListInspection.kt similarity index 56% rename from src/main/kotlin/com/vk/kphpstorm/inspections/FunctionUnnecessaryExplicitGenericInstantiationListInspection.kt rename to src/main/kotlin/com/vk/kphpstorm/inspections/GenericUnnecessaryExplicitInstantiationListInspection.kt index 4c23bb5..c832aba 100644 --- a/src/main/kotlin/com/vk/kphpstorm/inspections/FunctionUnnecessaryExplicitGenericInstantiationListInspection.kt +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/GenericUnnecessaryExplicitInstantiationListInspection.kt @@ -5,21 +5,40 @@ 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 FunctionUnnecessaryExplicitGenericInstantiationListInspection : PhpInspection() { +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) - if (call.function == null) return + checkGenericCall(call) + } + + private fun checkGenericCall(call: GenericCall) { + if (!call.isResolved()) return if (call.explicitSpecsPsi == null) return if (call.isNoNeedExplicitSpec()) { holder.registerProblem( - call.explicitSpecsPsi, + call.explicitSpecsPsi!!, "Remove unnecessary explicit list of instantiation arguments", ProblemHighlightType.WEAK_WARNING, RemoveExplicitGenericSpecsQuickFix() diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 5091052..7bd34d2 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -64,22 +64,22 @@ implementationClass="com.vk.kphpstorm.inspections.RedundantCastInspection"/> - - + - + - + + implementationClass="com.vk.kphpstorm.inspections.GenericSeveralGenericTypesInspection"/> com.vk.kphpstorm.inspections.PrettifyPhpdocBlockIntention diff --git a/src/test/fixtures/generics/classes/Vector.fixture.php b/src/test/fixtures/generics/classes/Vector.fixture.php new file mode 100644 index 0000000..49085f1 --- /dev/null +++ b/src/test/fixtures/generics/classes/Vector.fixture.php @@ -0,0 +1,48 @@ +data = $els; + } + + /** + * @return T + */ + public function get(int $index) { + return $this->data[$index]; + } + + /** + * @param T $data + */ + public function add($data): void { + $this->data[] = $data; + } + + /** + * @kphp-generic T1 + * @param class-string $class + * @return T1[] + */ + function filter_is_instance($class) { + return array_filter($this->data, fn($el) => is_a($el, $class)); + } + + /** + * @kphp-generic T1 + * @param Vector $other + * @return Vector + */ + function combine_with($other) { + return new Vector/**/(array_merge($this->data, $other->data)); + } +} diff --git a/src/test/fixtures/generics/classes/instantiation_args_mismatch.inspection.fixture.php b/src/test/fixtures/generics/classes/instantiation_args_mismatch.inspection.fixture.php new file mode 100644 index 0000000..157c1c3 --- /dev/null +++ b/src/test/fixtures/generics/classes/instantiation_args_mismatch.inspection.fixture.php @@ -0,0 +1,45 @@ +/*<>*/(); + new GenericT/**/(); // ok +} + +"Класс с двумя шаблоннымм типом"; { + /** @kphp-generic T1, T2 */ + class GenericT1T2 {} + + new GenericT1T2/*<>*/(); + new GenericT1T2/**/(); + new GenericT1T2/**/(); // ok +} + +"Класс с одним шаблонным типом с конструктором с еще одним шаблонным типом"; { + /** @kphp-generic T */ + class GenericExplicitConstructorT1AndT { + /** + * @kphp-generic T1 + * @param T1 $el + */ + function __construct($el) {} + } + +// new GenericExplicitConstructorT1AndT/*<>*/(""); +// new GenericExplicitConstructorT1AndT/**/(""); + new GenericExplicitConstructorT1AndT/**/(""); +} + +"Методы"; { + +} \ No newline at end of file diff --git a/src/test/fixtures/generics/classes/no_enough_information.inspection.fixture.php b/src/test/fixtures/generics/classes/no_enough_information.inspection.fixture.php new file mode 100644 index 0000000..0dae705 --- /dev/null +++ b/src/test/fixtures/generics/classes/no_enough_information.inspection.fixture.php @@ -0,0 +1,85 @@ +new GenericT(); + new GenericT(100); // нет зависимости от типа аргумента +} + +"Класс с одним шаблонным типом с нестандартным именем"; { + /** @kphp-generic TKey */ + class GenericTKey {} + + new GenericTKey(); +} + +"Класс с двумя шаблоннымм типом"; { + /** @kphp-generic T1, T2 */ + class GenericT1T2 {} + + // Выводим ошибку только для первого шаблонного типа + new GenericT1T2(); +} + +"Класс с одним шаблонным типом с конструктором с аргументом шаблонного типа Т"; { + /** @kphp-generic T */ + class GenericExplicitConstructorT { + /** @param T $el */ + function __construct($el) {} + } + + new GenericExplicitConstructorT(); + new GenericExplicitConstructorT(100); // в отличии от класса GenericT тут зависимость есть +} + +"Класс с одним шаблонным типом с конструктором с еще одним шаблонным типом"; { + /** @kphp-generic T */ + class GenericExplicitConstructorT1AndT { + /** + * @kphp-generic T1 + * @param T1 $el + */ + function __construct($el) {} + } + + new GenericExplicitConstructorT1AndT(100); + new GenericExplicitConstructorT1AndT/**/(100); // ok +} + +"Класс с одним шаблонным типом с конструктором с еще одним шаблонным типом оба используемые в аргументах"; { + /** @kphp-generic T */ + class GenericExplicitConstructorT1AndExplicitT { + /** + * @kphp-generic T1 + * @param T $el + * @param T1 $el2 + */ + function __construct($el, $el2) {} + } + + new GenericExplicitConstructorT1AndExplicitT(100); + new GenericExplicitConstructorT1AndExplicitT(100, ""); // ok +} + +"Класс с двумя шаблоннымм типом один из которых используется в конструкторе"; { + /** @kphp-generic T1, T2 */ + class GenericExplicitConstructorT1AndImplicitT2 { + /** @param T1 $el */ + function __construct($el) {} + } + + // Выводим ошибку только для первого шаблонного типа + new GenericExplicitConstructorT1AndImplicitT2(); + new GenericExplicitConstructorT1AndImplicitT2(100); + new GenericExplicitConstructorT1AndImplicitT2/**/(100); +} diff --git a/src/test/fixtures/generics/classes/simple_classes.fixture.php b/src/test/fixtures/generics/classes/simple_classes.fixture.php new file mode 100644 index 0000000..c23dbd0 --- /dev/null +++ b/src/test/fixtures/generics/classes/simple_classes.fixture.php @@ -0,0 +1,33 @@ +*/(); +// $vec->add(new GlobalA); +// expr_type($vec->get(0), "\GlobalA"); +// +// $filtered = $vec->filter_is_instance(GlobalD::class); +// expr_type($filtered[0], "\GlobalD"); +} + +"Неявные типы"; { +// $vec1 = new Vector(new GlobalA, new GlobalA); +// $vec1->add(new GlobalA); +// expr_type($vec1->get(0), "\GlobalA"); +// +// $filtered1 = $vec1->filter_is_instance(GlobalA::class); +// expr_type($filtered1[0], "\GlobalA"); +// +// $vec2 = new Vector(new GlobalA()); + $vec3 = new Vector(new GlobalD()); +// +// $combine = $vec2->combine_with($vec3); +// expr_type($combine, "\Vector|\Vector(\GlobalD|\GlobalA)"); + + $vec4 = new Vector(100); + $combine1 = $vec4->combine_with($vec3); +} + + diff --git a/src/test/fixtures/generics/tests.php b/src/test/fixtures/generics/tests.php index 7841b48..21275f1 100644 --- a/src/test/fixtures/generics/tests.php +++ b/src/test/fixtures/generics/tests.php @@ -22,8 +22,44 @@ function nullable($arg) { function mirror($arg) { return $arg; } +// +//$a = mirror(tuple([new GlobalA()], new \Classes\A() ?? new \Classes\C)); +//expr_type($a, "tuple(\GlobalA[],\Classes\C|\Classes\A)"); +// + +class AAA { + /** + * @kphp-generic T + * @param T $arg + * @return T + */ + function mirror($arg) { + return $arg; + } + + /** + * @kphp-generic T1, T2 + * @param T1 $arg + * @param T2 $arg2 + * @return T1|T2 + */ + function combine($arg, $arg2) { + return $arg; + } +} + +//$a = new Vector(100); + +//$a1 = new Vector(""); +//$res = $a->combine_with/**/($a1); + +$aaa = new AAA(); +//$aaa->mirror/**/(""); + +$aaa->combine/*>*/("", new Vector("")); + + + -$a = mirror(tuple([new GlobalA()], new \Classes\A() ?? new \Classes\C)); -expr_type($a, "tuple(\GlobalA[],\Classes\C|\Classes\A)"); diff --git a/src/test/kotlin/com/vk/kphpstorm/testing/infrastructure/InspectionTestBase.kt b/src/test/kotlin/com/vk/kphpstorm/testing/infrastructure/InspectionTestBase.kt index b4f4aab..981f2d5 100644 --- a/src/test/kotlin/com/vk/kphpstorm/testing/infrastructure/InspectionTestBase.kt +++ b/src/test/kotlin/com/vk/kphpstorm/testing/infrastructure/InspectionTestBase.kt @@ -22,17 +22,19 @@ abstract class InspectionTestBase( * 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(fixtureFile: String) { + protected fun runFixture(vararg fixtureFiles: String) { // Highlighting test KphpStormConfiguration.saveThatSetupForProjectDone(project) - myFixture.configureByFile(fixtureFile) + myFixture.configureByFiles(*fixtureFiles) myFixture.testHighlighting(true, false, true) // Quick-fix test - val qfFile = fixtureFile.replace(".fixture.php", ".qf.php") - if (File(myFixture.testDataPath + "/" + qfFile).exists()) { - myFixture.getAllQuickFixes().forEach { myFixture.launchAction(it) } - myFixture.checkResultByFile(qfFile) + 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/infrastructure/TypeTestBase.kt b/src/test/kotlin/com/vk/kphpstorm/testing/infrastructure/TypeTestBase.kt index 06f932e..72102b3 100644 --- a/src/test/kotlin/com/vk/kphpstorm/testing/infrastructure/TypeTestBase.kt +++ b/src/test/kotlin/com/vk/kphpstorm/testing/infrastructure/TypeTestBase.kt @@ -32,9 +32,9 @@ abstract class TypeTestBase : BasePlatformTestCase() { return result } - protected fun runFixture(fixtureFile: String) { + protected fun runFixture(vararg fixtureFiles: String) { KphpStormConfiguration.saveThatSetupForProjectDone(project) - myFixture.configureByFile(fixtureFile) + myFixture.configureByFiles(*fixtureFiles) val exprTypeCalls = findExprTypeCalls() diff --git a/src/test/kotlin/com/vk/kphpstorm/testing/tests/GenericsInstantiationArgsMismatchInspectionsTest.kt b/src/test/kotlin/com/vk/kphpstorm/testing/tests/GenericsInstantiationArgsMismatchInspectionsTest.kt new file mode 100644 index 0000000..6fb749c --- /dev/null +++ b/src/test/kotlin/com/vk/kphpstorm/testing/tests/GenericsInstantiationArgsMismatchInspectionsTest.kt @@ -0,0 +1,13 @@ +package com.vk.kphpstorm.testing.tests + +import com.vk.kphpstorm.inspections.GenericInstantiationArgsCountMismatchInspection +import com.vk.kphpstorm.testing.infrastructure.InspectionTestBase + +class GenericsInstantiationArgsMismatchInspectionsTest : InspectionTestBase(GenericInstantiationArgsCountMismatchInspection()) { + fun testClasses() { + runFixture( + "generics/classes/instantiation_args_mismatch.inspection.fixture.php", + "generics/classes/Vector.fixture.php" + ) + } +} diff --git a/src/test/kotlin/com/vk/kphpstorm/testing/tests/GenericsNoEnoughInformationInspectionsTest.kt b/src/test/kotlin/com/vk/kphpstorm/testing/tests/GenericsNoEnoughInformationInspectionsTest.kt new file mode 100644 index 0000000..87aec59 --- /dev/null +++ b/src/test/kotlin/com/vk/kphpstorm/testing/tests/GenericsNoEnoughInformationInspectionsTest.kt @@ -0,0 +1,13 @@ +package com.vk.kphpstorm.testing.tests + +import com.vk.kphpstorm.inspections.GenericNoEnoughInformationInspection +import com.vk.kphpstorm.testing.infrastructure.InspectionTestBase + +class GenericsNoEnoughInformationInspectionsTest : InspectionTestBase(GenericNoEnoughInformationInspection()) { + fun testClasses() { + runFixture( + "generics/classes/no_enough_information.inspection.fixture.php", + "generics/classes/Vector.fixture.php" + ) + } +} diff --git a/src/test/kotlin/com/vk/kphpstorm/testing/tests/GenericsTypesTest.kt b/src/test/kotlin/com/vk/kphpstorm/testing/tests/GenericsTypesTest.kt index 1e0360f..6f26d20 100644 --- a/src/test/kotlin/com/vk/kphpstorm/testing/tests/GenericsTypesTest.kt +++ b/src/test/kotlin/com/vk/kphpstorm/testing/tests/GenericsTypesTest.kt @@ -26,4 +26,10 @@ class GenericsTypesTest : TypeTestBase() { fun testClassString() { runFixture("generics/class-string.fixture.php") } + + // Classes + + fun testSimpleClasses() { + runFixture("generics/classes/simple_classes.fixture.php", "generics/classes/Vector.fixture.php") + } }