From c01af26b53e6f2a4660bdd9d7e8e23d041a75b93 Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Mon, 14 Mar 2022 18:39:51 +0300 Subject: [PATCH] initial implementation --- build.gradle | 10 +- .../com/vk/kphpstorm/KphpStormASTFactory.kt | 17 + .../vk/kphpstorm/KphpStormParserDefinition.kt | 36 +- .../KphpGenericsReferenceContributor.kt | 95 +++++ .../com/vk/kphpstorm/exphptype/ExPhpType.kt | 2 +- .../vk/kphpstorm/exphptype/ExPhpTypeAny.kt | 2 +- .../vk/kphpstorm/exphptype/ExPhpTypeArray.kt | 4 +- .../kphpstorm/exphptype/ExPhpTypeCallable.kt | 4 +- .../exphptype/ExPhpTypeClassString.kt | 69 ++++ .../kphpstorm/exphptype/ExPhpTypeForcing.kt | 4 +- .../kphpstorm/exphptype/ExPhpTypeGenericT.kt | 33 ++ .../kphpstorm/exphptype/ExPhpTypeInstance.kt | 16 +- .../kphpstorm/exphptype/ExPhpTypeNullable.kt | 4 +- .../vk/kphpstorm/exphptype/ExPhpTypePipe.kt | 8 +- .../kphpstorm/exphptype/ExPhpTypePrimitive.kt | 19 +- .../vk/kphpstorm/exphptype/ExPhpTypeShape.kt | 4 +- .../exphptype/ExPhpTypeTplInstantiation.kt | 4 +- .../vk/kphpstorm/exphptype/ExPhpTypeTuple.kt | 4 +- .../exphptype/PhpTypeToExPhpTypeParsing.kt | 27 +- .../vk/kphpstorm/exphptype/PsiToExPhpType.kt | 19 +- .../psi/ExPhpTypeClassStringPsiImpl.kt | 30 ++ .../exphptype/psi/ExPhpTypeInstancePsiImpl.kt | 11 +- .../psi/ExPhpTypeTplInstantiationPsiImpl.kt | 10 +- .../psi/TokensToExPhpTypePsiParsing.kt | 15 +- .../kphpstorm/generics/GenericFunctionCall.kt | 367 ++++++++++++++++++ .../kphpstorm/generics/GenericFunctionUtil.kt | 69 ++++ .../psi/GenericInstantiationPsiCommentImpl.kt | 220 +++++++++++ .../highlighting/KphpColorsAndFontsPage.kt | 8 +- .../KphpGenericCommentFolderBuilder.kt | 51 +++ .../highlighting/KphpHighlightingData.kt | 3 + .../KphpStormGenericCommentsAnnotator.kt | 48 +++ .../highlighting/KphpStormTypeInfoProvider.kt | 4 +- .../hints/GenericsInlayTypeHintsProvider.kt | 38 ++ .../hints/InlayHintPresentationFactory.kt | 46 +++ .../highlighting/hints/InlayHintsCollector.kt | 52 +++ ...nstantiationArgsCountMismatchInspection.kt | 33 ++ ...ionGenericNoEnoughInformationInspection.kt | 42 ++ ...ionGenericSeveralGenericTypesInspection.kt | 43 ++ ...licitGenericInstantiationListInspection.kt | 32 ++ .../KphpParameterTypeMismatchInspection.kt | 16 +- .../KphpUndefinedClassInspection.kt | 12 +- .../RemoveExplicitGenericSpecsQuickFix.kt | 14 + .../kphpstorm/kphptags/AllKphpdocTagsList.kt | 2 +- .../kphpstorm/kphptags/KphpGenericDocTag.kt | 52 +++ .../vk/kphpstorm/kphptags/KphpReturnDocTag.kt | 8 +- .../kphpstorm/kphptags/KphpTemplateDocTag.kt | 32 -- .../kphptags/psi/KphpDocElementTypes.kt | 7 +- .../psi/KphpDocGenericParameterDeclPsiImpl.kt | 32 ++ .../psi/KphpDocTagGenericElementType.kt | 77 ++++ .../kphptags/psi/KphpDocTagGenericPsiImpl.kt | 37 ++ .../psi/KphpDocTagTemplateClassElementType.kt | 4 +- .../psi/KphpDocTagTemplateClassPsiImpl.kt | 4 +- .../psi/KphpDocTplParameterDeclPsiImpl.kt | 17 - .../typeProviders/ClassConstTypeProvider.kt | 41 ++ .../typeProviders/FunctionsTypeProvider.kt | 19 +- .../GenericFunctionsTypeProvider.kt | 95 +++++ .../GenericObjectAccessTypeProvider.kt | 271 +++++++++++++ .../TemplateObjectAccessTypeProvider.kt | 153 -------- src/main/resources/META-INF/plugin.xml | 38 +- 59 files changed, 2148 insertions(+), 286 deletions(-) create mode 100644 src/main/kotlin/com/vk/kphpstorm/KphpStormASTFactory.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/completion/KphpGenericsReferenceContributor.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeClassString.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeGenericT.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeClassStringPsiImpl.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/generics/GenericFunctionCall.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/generics/GenericFunctionUtil.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/generics/psi/GenericInstantiationPsiCommentImpl.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/highlighting/KphpGenericCommentFolderBuilder.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/highlighting/KphpStormGenericCommentsAnnotator.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/highlighting/hints/GenericsInlayTypeHintsProvider.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/highlighting/hints/InlayHintPresentationFactory.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/highlighting/hints/InlayHintsCollector.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/inspections/FunctionGenericInstantiationArgsCountMismatchInspection.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/inspections/FunctionGenericNoEnoughInformationInspection.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/inspections/FunctionGenericSeveralGenericTypesInspection.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/inspections/FunctionUnnecessaryExplicitGenericInstantiationListInspection.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/inspections/quickfixes/RemoveExplicitGenericSpecsQuickFix.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/kphptags/KphpGenericDocTag.kt delete mode 100644 src/main/kotlin/com/vk/kphpstorm/kphptags/KphpTemplateDocTag.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocGenericParameterDeclPsiImpl.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagGenericElementType.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagGenericPsiImpl.kt delete mode 100644 src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTplParameterDeclPsiImpl.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/typeProviders/ClassConstTypeProvider.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericFunctionsTypeProvider.kt create mode 100644 src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericObjectAccessTypeProvider.kt delete mode 100644 src/main/kotlin/com/vk/kphpstorm/typeProviders/TemplateObjectAccessTypeProvider.kt diff --git a/build.gradle b/build.gradle index 06a2de5..d2cf1d0 100644 --- a/build.gradle +++ b/build.gradle @@ -30,17 +30,23 @@ idea { } } +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + freeCompilerArgs += '-Xjvm-default=all' + } +} + intellij { type = 'IU' // for release versions: https://www.jetbrains.com/intellij-repository/releases (com.jetbrains.intellij.idea) // for EAPs: https://www.jetbrains.com/intellij-repository/snapshots - version = '2021.3' + version = '2021.3.1' plugins = [ 'com.jetbrains.php:213.5744.223', // https://plugins.jetbrains.com/plugin/6610-php/versions ] } runIde { - ideDir.set(file("/Users/alexandr.kirsanov/Library/Application Support/JetBrains/Toolbox/apps/PhpStorm/ch-0/213.5744.279/PhpStorm.app/Contents")) +// ideDir.set(file("/Users/alexandr.kirsanov/Library/Application Support/JetBrains/Toolbox/apps/PhpStorm/ch-0/213.5744.279/PhpStorm.app/Contents")) } patchPluginXml { diff --git a/src/main/kotlin/com/vk/kphpstorm/KphpStormASTFactory.kt b/src/main/kotlin/com/vk/kphpstorm/KphpStormASTFactory.kt new file mode 100644 index 0000000..6ae5d5f --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/KphpStormASTFactory.kt @@ -0,0 +1,17 @@ +package com.vk.kphpstorm + +import com.intellij.lang.DefaultASTFactoryImpl +import com.intellij.psi.impl.source.tree.LeafElement +import com.intellij.psi.impl.source.tree.PsiCommentImpl +import com.intellij.psi.tree.IElementType +import com.vk.kphpstorm.generics.psi.GenericInstantiationPsiCommentImpl + +class KphpStormASTFactory : DefaultASTFactoryImpl() { + override fun createComment(type: IElementType, text: CharSequence): LeafElement { + if (!text.startsWith("/*<") && !text.endsWith(">*/")) { + return PsiCommentImpl(type, text) + } + + return GenericInstantiationPsiCommentImpl(type, text) + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/KphpStormParserDefinition.kt b/src/main/kotlin/com/vk/kphpstorm/KphpStormParserDefinition.kt index 50f8442..11d1451 100644 --- a/src/main/kotlin/com/vk/kphpstorm/KphpStormParserDefinition.kt +++ b/src/main/kotlin/com/vk/kphpstorm/KphpStormParserDefinition.kt @@ -30,25 +30,27 @@ class KphpStormParserDefinition() : PhpParserDefinition() { */ override fun createElement(node: ASTNode): PsiElement { return when (node.elementType) { - KphpDocElementTypes.kphpDocTagSimple -> KphpDocTagSimplePsiImpl(node) - KphpDocElementTypes.kphpDocTagTemplateClass -> KphpDocTagTemplateClassPsiImpl(node) - KphpDocTplParameterDeclPsiImpl.elementType -> KphpDocTplParameterDeclPsiImpl(node) - KphpDocElementTypes.kphpDocTagWarnPerformance -> KphpDocTagWarnPerformancePsiImpl(node) - KphpDocWarnPerformanceItemPsiImpl.elementType -> KphpDocWarnPerformanceItemPsiImpl(node) + KphpDocElementTypes.kphpDocTagSimple -> KphpDocTagSimplePsiImpl(node) + KphpDocElementTypes.kphpDocTagTemplateClass -> KphpDocTagTemplateClassPsiImpl(node) + KphpDocElementTypes.kphpDocTagGeneric -> KphpDocTagGenericPsiImpl(node) + KphpDocGenericParameterDeclPsiImpl.elementType -> KphpDocGenericParameterDeclPsiImpl(node) + KphpDocElementTypes.kphpDocTagWarnPerformance -> KphpDocTagWarnPerformancePsiImpl(node) + KphpDocWarnPerformanceItemPsiImpl.elementType -> KphpDocWarnPerformanceItemPsiImpl(node) - ExPhpTypePrimitivePsiImpl.elementType -> ExPhpTypePrimitivePsiImpl(node) - ExPhpTypeInstancePsiImpl.elementType -> ExPhpTypeInstancePsiImpl(node) - ExPhpTypePipePsiImpl.elementType -> ExPhpTypePipePsiImpl(node) - ExPhpTypeAnyPsiImpl.elementType -> ExPhpTypeAnyPsiImpl(node) - ExPhpTypeArrayPsiImpl.elementType -> ExPhpTypeArrayPsiImpl(node) - ExPhpTypeTuplePsiImpl.elementType -> ExPhpTypeTuplePsiImpl(node) - ExPhpTypeShapePsiImpl.elementType -> ExPhpTypeShapePsiImpl(node) - ExPhpTypeNullablePsiImpl.elementType -> ExPhpTypeNullablePsiImpl(node) - ExPhpTypeTplInstantiationPsiImpl.elementType -> ExPhpTypeTplInstantiationPsiImpl(node) - ExPhpTypeCallablePsiImpl.elementType -> ExPhpTypeCallablePsiImpl(node) - ExPhpTypeForcingPsiImpl.elementType -> ExPhpTypeForcingPsiImpl(node) + ExPhpTypePrimitivePsiImpl.elementType -> ExPhpTypePrimitivePsiImpl(node) + ExPhpTypeInstancePsiImpl.elementType -> ExPhpTypeInstancePsiImpl(node) + ExPhpTypePipePsiImpl.elementType -> ExPhpTypePipePsiImpl(node) + ExPhpTypeAnyPsiImpl.elementType -> ExPhpTypeAnyPsiImpl(node) + ExPhpTypeArrayPsiImpl.elementType -> ExPhpTypeArrayPsiImpl(node) + ExPhpTypeTuplePsiImpl.elementType -> ExPhpTypeTuplePsiImpl(node) + ExPhpTypeShapePsiImpl.elementType -> ExPhpTypeShapePsiImpl(node) + ExPhpTypeNullablePsiImpl.elementType -> ExPhpTypeNullablePsiImpl(node) + ExPhpTypeTplInstantiationPsiImpl.elementType -> ExPhpTypeTplInstantiationPsiImpl(node) + ExPhpTypeCallablePsiImpl.elementType -> ExPhpTypeCallablePsiImpl(node) + ExPhpTypeClassStringPsiImpl.elementType -> ExPhpTypeClassStringPsiImpl(node) + ExPhpTypeForcingPsiImpl.elementType -> ExPhpTypeForcingPsiImpl(node) - else -> PhpPsiElementCreator.create(node) + else -> PhpPsiElementCreator.create(node) } } } diff --git a/src/main/kotlin/com/vk/kphpstorm/completion/KphpGenericsReferenceContributor.kt b/src/main/kotlin/com/vk/kphpstorm/completion/KphpGenericsReferenceContributor.kt new file mode 100644 index 0000000..33f43fa --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/completion/KphpGenericsReferenceContributor.kt @@ -0,0 +1,95 @@ +package com.vk.kphpstorm.completion + +import com.intellij.openapi.util.TextRange +import com.intellij.patterns.PlatformPatterns +import com.intellij.psi.* +import com.intellij.util.ProcessingContext +import com.jetbrains.php.lang.psi.PhpPsiElementFactory +import com.jetbrains.php.lang.psi.elements.FunctionReference +import com.jetbrains.php.lang.psi.elements.PhpNamedElement +import com.vk.kphpstorm.generics.psi.GenericInstantiationPsiCommentImpl + +/** + * Контрибьютор который создает ссылки на классы внутри комментария + * с описанием шаблонных типов при вызове функции. + * + * Подробнее в комментарии для [GenericInstantiationPsiCommentImpl]. + */ +class KphpGenericsReferenceContributor : PsiReferenceContributor() { + override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) { + registrar.registerReferenceProvider( + PlatformPatterns.psiComment(), + PhpPsiReferenceProvider() + ) + } + + class PhpPsiReferenceProvider : PsiReferenceProvider() { + override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array { + if (element !is GenericInstantiationPsiCommentImpl) { + return emptyArray() + } + + val instances = element.extractInstances() + + return instances.map { (_, instance) -> + instance.classes(element.project).map { klass -> + PhpElementReference(element, klass, instance.pos.shiftLeft(element.startOffset)) + } + }.flatten().toTypedArray() + } + + class PhpElementReference(element: PsiElement, result: PsiElement, private val myRange: TextRange? = null) : + PsiReference { + private val myElement: PsiElement + private val myResult: PsiElement + + init { + myElement = element + myResult = result + } + + override fun getElement() = myElement + + override fun getRangeInElement(): TextRange { + if (myRange != null) { + return myRange + } + val startOffset = 1 + return TextRange(startOffset, myElement.textLength - 1) + } + + override fun resolve() = myResult + + override fun getCanonicalText(): String = + if (myResult is PhpNamedElement) myResult.fqn + else myElement.parent.text + + override fun handleElementRename(newElementName: String): PsiElement { + val text = element.text + val start = text.slice(0 until rangeInElement.startOffset) + val end = text.slice(rangeInElement.endOffset until text.length) + + val specText = start + newElementName + end + + val psi = PhpPsiElementFactory.createPhpPsiFromText( + element.project, FunctionReference::class.java, "f$specText();" + ) + + val comment = psi.firstChild.nextSibling + + return myElement.replace(comment) + } + + override fun bindToElement(element: PsiElement): PsiElement { + throw UnsupportedOperationException() + } + + override fun isReferenceTo(element: PsiElement): Boolean { + return myResult === element + } + + override fun isSoft() = true + } + } +} + diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpType.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpType.kt index b3cf1ca..9f71a51 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpType.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpType.kt @@ -48,7 +48,7 @@ interface ExPhpType { fun toPhpType(): PhpType fun toHumanReadable(expr: PhpPsiElement): String fun getSubkeyByIndex(indexKey: String): ExPhpType? - fun instantiateTemplate(nameMap: Map): ExPhpType + fun instantiateGeneric(nameMap: Map): ExPhpType fun isAssignableFrom(rhs: ExPhpType, project: Project): Boolean companion object { diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeAny.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeAny.kt index 9b7f626..2c6f353 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeAny.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeAny.kt @@ -31,7 +31,7 @@ class ExPhpTypeAny : ExPhpType { return this } - override fun instantiateTemplate(nameMap: Map): ExPhpType { + override fun instantiateGeneric(nameMap: Map): ExPhpType { return this } diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeArray.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeArray.kt index 9c04282..44f5b0c 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeArray.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeArray.kt @@ -23,8 +23,8 @@ class ExPhpTypeArray(val inner: ExPhpType) : ExPhpType { return inner } - override fun instantiateTemplate(nameMap: Map): ExPhpType { - return ExPhpTypeArray(inner.instantiateTemplate(nameMap)) + override fun instantiateGeneric(nameMap: Map): ExPhpType { + return ExPhpTypeArray(inner.instantiateGeneric(nameMap)) } override fun isAssignableFrom(rhs: ExPhpType, project: Project): Boolean { diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeCallable.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeCallable.kt index 6760f01..5658843 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeCallable.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeCallable.kt @@ -21,8 +21,8 @@ class ExPhpTypeCallable(val argTypes: List, val returnType: ExPhpType return null } - override fun instantiateTemplate(nameMap: Map): ExPhpType { - return ExPhpTypeCallable(argTypes.map { it.instantiateTemplate(nameMap) }, returnType?.instantiateTemplate(nameMap)) + override fun instantiateGeneric(nameMap: Map): ExPhpType { + return ExPhpTypeCallable(argTypes.map { it.instantiateGeneric(nameMap) }, returnType?.instantiateGeneric(nameMap)) } override fun isAssignableFrom(rhs: ExPhpType, project: Project): Boolean = when (rhs) { diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeClassString.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeClassString.kt new file mode 100644 index 0000000..b1abdcc --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeClassString.kt @@ -0,0 +1,69 @@ +package com.vk.kphpstorm.exphptype + +import com.intellij.openapi.project.Project +import com.jetbrains.php.lang.psi.elements.PhpPsiElement +import com.jetbrains.php.lang.psi.resolve.types.PhpType + +/** + * Type of special class constant (`Foo::class`). + */ +class ExPhpTypeClassString(val inner: ExPhpType) : ExPhpType { + override fun toString() = "class-string($inner)" + + override fun toHumanReadable(expr: PhpPsiElement) = "class-string($inner)" + + override fun equals(other: Any?) = other is ExPhpTypeClassString && inner == other.inner + + override fun hashCode() = 35 + + override fun toPhpType(): PhpType { + return PhpType().add("class-string($inner)") + } + + override fun getSubkeyByIndex(indexKey: String): ExPhpType? { + return this + } + + override fun instantiateGeneric(nameMap: Map): ExPhpType { + // TODO: подумать тут + val fqn = when (inner) { + is ExPhpTypeInstance -> inner.fqn + is ExPhpTypeGenericsT -> inner.nameT + else -> "" + } + + return nameMap[fqn]?.let { ExPhpTypeClassString(ExPhpTypeInstance(it.toString())) } ?: this + } + + 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 inner.isAssignableFrom(otherType, project) + } else false + } + // class-string совместим только с class-string при условии + // что класс E является допустимым для класса 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/ExPhpTypeForcing.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeForcing.kt index 4155f8f..48e8e86 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeForcing.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeForcing.kt @@ -27,8 +27,8 @@ class ExPhpTypeForcing(val inner: ExPhpType) : ExPhpType { return ExPhpTypeForcing(inner.getSubkeyByIndex(indexKey) ?: return null) } - override fun instantiateTemplate(nameMap: Map): ExPhpType { - return ExPhpTypeForcing(inner.instantiateTemplate(nameMap)) + override fun instantiateGeneric(nameMap: Map): ExPhpType { + return ExPhpTypeForcing(inner.instantiateGeneric(nameMap)) } override fun isAssignableFrom(rhs: ExPhpType, project: Project): Boolean { diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeGenericT.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeGenericT.kt new file mode 100644 index 0000000..285c5fb --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeGenericT.kt @@ -0,0 +1,33 @@ +package com.vk.kphpstorm.exphptype + +import com.intellij.openapi.project.Project +import com.jetbrains.php.lang.psi.elements.PhpPsiElement +import com.jetbrains.php.lang.psi.resolve.types.PhpType + +/** + * 'T' — is genericsT when it's defined in @kphp-generic, then it's genericsT on resolve, not class T + * @see com.vk.kphpstorm.exphptype.psi.ExPhpTypeInstancePsiImpl.getType + */ +class ExPhpTypeGenericsT(val nameT: String) : ExPhpType { + override fun toString() = "%$nameT" + + override fun toHumanReadable(expr: PhpPsiElement) = "%$nameT" + + override fun toPhpType(): PhpType { + return PhpType().add("%$nameT") + } + + override fun getSubkeyByIndex(indexKey: String): ExPhpType? { + return null + } + + override fun instantiateGeneric(nameMap: Map): ExPhpType { + return nameMap[nameT] ?: this + } + + override fun isAssignableFrom(rhs: ExPhpType, project: Project): Boolean = when (rhs) { + is ExPhpTypeAny -> true + // todo shall we add any strict rules here? + else -> true + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeInstance.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeInstance.kt index f5bf861..ac48761 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeInstance.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeInstance.kt @@ -25,7 +25,7 @@ class ExPhpTypeInstance(val fqn: String) : ExPhpType { return null } - override fun instantiateTemplate(nameMap: Map): ExPhpType { + override fun instantiateGeneric(nameMap: Map): ExPhpType { return nameMap[fqn] ?: this } @@ -50,6 +50,20 @@ class ExPhpTypeInstance(val fqn: String) : ExPhpType { rhsIsChild } + is ExPhpTypeTplInstantiation -> rhs.classFqn == fqn || run { + val phpIndex = PhpIndex.getInstance(project) + val lhsClass = phpIndex.getAnyByFQN(fqn).firstOrNull() ?: return false + var rhsIsChild = false + phpIndex.getAnyByFQN(rhs.classFqn).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/ExPhpTypeNullable.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeNullable.kt index 2c281fd..34fc85f 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeNullable.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeNullable.kt @@ -20,8 +20,8 @@ class ExPhpTypeNullable(val inner: ExPhpType) : ExPhpType { return inner.getSubkeyByIndex(indexKey) } - override fun instantiateTemplate(nameMap: Map): ExPhpType { - return ExPhpTypeNullable(inner.instantiateTemplate(nameMap)) + override fun instantiateGeneric(nameMap: Map): ExPhpType { + return ExPhpTypeNullable(inner.instantiateGeneric(nameMap)) } override fun isAssignableFrom(rhs: ExPhpType, project: Project): Boolean = when (rhs) { diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePipe.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePipe.kt index a086cff..e2ee690 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePipe.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePipe.kt @@ -81,8 +81,8 @@ class ExPhpTypePipe(val items: List) : ExPhpType { } } - override fun instantiateTemplate(nameMap: Map): ExPhpType { - return ExPhpTypePipe(items.map { it.instantiateTemplate(nameMap) }) + override fun instantiateGeneric(nameMap: Map): ExPhpType { + return ExPhpTypePipe(items.map { it.instantiateGeneric(nameMap) }) } override fun isAssignableFrom(rhs: ExPhpType, project: Project): Boolean { @@ -134,6 +134,10 @@ 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/ExPhpTypePrimitive.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePrimitive.kt index 7f9c001..1e1a0e1 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePrimitive.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePrimitive.kt @@ -23,20 +23,21 @@ class ExPhpTypePrimitive(val typeStr: String) : ExPhpType { return if (this === ExPhpType.KMIXED || this === ExPhpType.STRING) this else null } - override fun instantiateTemplate(nameMap: Map): ExPhpType { + override fun instantiateGeneric(nameMap: Map): ExPhpType { return this } override fun isAssignableFrom(rhs: ExPhpType, project: Project): Boolean = when (rhs) { - is ExPhpTypeAny -> true - is ExPhpTypePipe -> rhs.isAssignableTo(this, project) - is ExPhpTypePrimitive -> canBeAssigned(this, rhs) - is ExPhpTypeNullable -> canBeAssigned(this, ExPhpType.NULL) && isAssignableFrom(rhs.inner, project) - is ExPhpTypeArray -> canBeAssigned(this, ExPhpType.KMIXED) && isAssignableFrom(rhs.inner, project) + is ExPhpTypeAny -> true + is ExPhpTypePipe -> rhs.isAssignableTo(this, project) + is ExPhpTypePrimitive -> canBeAssigned(this, rhs) + is ExPhpTypeNullable -> canBeAssigned(this, ExPhpType.NULL) && isAssignableFrom(rhs.inner, project) + is ExPhpTypeArray -> canBeAssigned(this, ExPhpType.KMIXED) && isAssignableFrom(rhs.inner, project) || this === ExPhpType.CALLABLE // ['class', 'name'] is assignable to "callable" :( - is ExPhpTypeInstance -> this === ExPhpType.OBJECT - is ExPhpTypeForcing -> isAssignableFrom(rhs.inner, project) - else -> false + is ExPhpTypeInstance -> this === ExPhpType.OBJECT + is ExPhpTypeForcing -> isAssignableFrom(rhs.inner, project) + is ExPhpTypeClassString -> this === ExPhpType.STRING + else -> false } companion object { diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeShape.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeShape.kt index 2b6efcf..1dfb0d8 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeShape.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeShape.kt @@ -29,8 +29,8 @@ class ExPhpTypeShape(val items: List) : ExPhpType { return items.find { it.keyName == indexKey }?.type } - override fun instantiateTemplate(nameMap: Map): ExPhpType { - return ExPhpTypeShape(items.map { ShapeItem(it.keyName, it.nullable, it.type.instantiateTemplate(nameMap)) }) + override fun instantiateGeneric(nameMap: Map): ExPhpType { + return ExPhpTypeShape(items.map { ShapeItem(it.keyName, it.nullable, it.type.instantiateGeneric(nameMap)) }) } override fun isAssignableFrom(rhs: ExPhpType, project: Project): Boolean = when (rhs) { diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTplInstantiation.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTplInstantiation.kt index e31667e..95bfbdd 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTplInstantiation.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTplInstantiation.kt @@ -21,8 +21,8 @@ class ExPhpTypeTplInstantiation(val classFqn: String, val specializationList: Li return null } - override fun instantiateTemplate(nameMap: Map): ExPhpType { - val replacedSpecialization = specializationList.map { it.instantiateTemplate(nameMap) } + override fun instantiateGeneric(nameMap: Map): ExPhpType { + val replacedSpecialization = specializationList.map { it.instantiateGeneric(nameMap) } return ExPhpTypeTplInstantiation(classFqn, replacedSpecialization) } diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTuple.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTuple.kt index 07ed945..e33a911 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTuple.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTuple.kt @@ -25,8 +25,8 @@ class ExPhpTypeTuple(val items: List) : ExPhpType { return if (idx >= 0 && idx < items.size) items[idx] else null } - override fun instantiateTemplate(nameMap: Map): ExPhpType { - return ExPhpTypeTuple(items.map { it.instantiateTemplate(nameMap) }) + override fun instantiateGeneric(nameMap: Map): ExPhpType { + return ExPhpTypeTuple(items.map { it.instantiateGeneric(nameMap) }) } override fun isAssignableFrom(rhs: ExPhpType, project: Project): Boolean = when (rhs) { diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/PhpTypeToExPhpTypeParsing.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/PhpTypeToExPhpTypeParsing.kt index b98406d..e42e9e1 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/PhpTypeToExPhpTypeParsing.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/PhpTypeToExPhpTypeParsing.kt @@ -8,7 +8,7 @@ import com.jetbrains.php.lang.psi.resolve.types.PhpType * Quite similar to phpdoc.cpp parsing logic in kphp. */ object PhpTypeToExPhpTypeParsing { - private val RE_VALID_FQN = Regex("[a-zA-Z0-9_\\\\]+") + private val RE_VALID_FQN = Regex("[a-zA-Z0-9-_\\\\]+") /** * Pre-cached parsed for primitives and common cases @@ -123,7 +123,7 @@ object PhpTypeToExPhpTypeParsing { fun parseFQN(): String? { skipWhitespace() val cur = if (offset < type.length) type[offset] else '\b' - if (!cur.isLetterOrDigit() && cur != '\\') + if (!cur.isLetterOrDigit() && cur != '\\' && cur != '-') return null val match = RE_VALID_FQN.find(type, offset) ?: return null offset = match.range.last + 1 @@ -179,7 +179,7 @@ object PhpTypeToExPhpTypeParsing { } } - private fun parseTemplateSpecialization(builder: ExPhpTypeBuilder): List? { + private fun parseGenericSpecialization(builder: ExPhpTypeBuilder): List? { if (!builder.compareAndEat('<')) return null @@ -242,6 +242,11 @@ object PhpTypeToExPhpTypeParsing { return ExPhpTypeNullable(expr) } + if (builder.compareAndEat('%')) { + val genericsT = builder.parseFQN() ?: return null + return ExPhpTypeGenericsT(genericsT) + } + val fqn = builder.parseFQN() ?: return null if (fqn == "tuple" && builder.compare('(')) { @@ -264,8 +269,22 @@ object PhpTypeToExPhpTypeParsing { return ExPhpTypeForcing(inner) } + if (fqn == "class-string" && (builder.compare('<') || builder.compare('('))) { + if (!builder.compareAndEat('(') && !builder.compareAndEat('<')) + return null + + val genericT = parseSimpleType(builder) ?: return null + + if (genericT !is ExPhpTypeInstance && genericT !is ExPhpTypeGenericsT) + return null + if (!builder.compareAndEat(')') && !builder.compareAndEat('>')) + return null + + return ExPhpTypeClassString(genericT) + } + if (builder.compare('<')) { - val specialization = parseTemplateSpecialization(builder) ?: return null + val specialization = parseGenericSpecialization(builder) ?: return null return ExPhpTypeTplInstantiation(fqn, specialization) } diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/PsiToExPhpType.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/PsiToExPhpType.kt index 7e36e71..644fa0c 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/PsiToExPhpType.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/PsiToExPhpType.kt @@ -11,10 +11,27 @@ import com.vk.kphpstorm.helpers.toExPhpType object PsiToExPhpType { fun getTypeOfExpr(e: PsiElement, project: Project): ExPhpType? = when (e) { - is PhpTypedElement -> e.type.toExPhpType(project) + is PhpTypedElement -> e.type.toExPhpType(project)?.let { dropGenerics(it) } else -> null } + fun dropGenerics(type: ExPhpType): ExPhpType { + // TODO: добавить все типы + if (type is ExPhpTypePipe) { + return ExPhpTypePipe(type.items.filter { it !is ExPhpTypeGenericsT }.map { dropGenerics(it) }) + } + + if (type is ExPhpTypeNullable) { + return ExPhpTypeNullable(dropGenerics(type.inner)) + } + + if (type is ExPhpTypeArray) { + return ExPhpTypeArray(dropGenerics(type.inner)) + } + + return type + } + fun getFieldDeclaredType(field: Field, project: Project): ExPhpType? { val fieldType = field.docType.takeIf { !it.isEmpty } ?: field.type return fieldType.global(project).toExPhpType() diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeClassStringPsiImpl.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeClassStringPsiImpl.kt new file mode 100644 index 0000000..3da4cf6 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeClassStringPsiImpl.kt @@ -0,0 +1,30 @@ +package com.vk.kphpstorm.exphptype.psi + +import com.intellij.lang.ASTNode +import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocElementType +import com.jetbrains.php.lang.documentation.phpdoc.psi.impl.PhpDocTypeImpl +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.vk.kphpstorm.generics.GenericFunctionUtil + +/** + * class-string — psi is class-string(Foo) corresponding type of Foo::class + * PhpType is "class-string(Foo)" + */ +class ExPhpTypeClassStringPsiImpl(node: ASTNode) : PhpDocTypeImpl(node) { + companion object { + val elementType = PhpDocElementType("exPhpTypeClassString") + } + + override fun getNameNode(): ASTNode? = null + + override fun getType(): PhpType { + val brace = if (text.contains('(')) listOf('(', ')') else listOf('<', '>') + val genericType = text.substring(text.indexOf(brace[0]) + 1 until text.indexOf(brace[1])) + + // В случае когда класс на самом деле является шаблонным типом нам нужно мимикрировать тип + // и добавить знак процента к имени типа, чтобы в дальнейшем работать с ним как с шаблоном. + val genericMark = if (GenericFunctionUtil.nameIsGeneric(this, genericType)) "%" else "" + + return PhpType().add("class-string($genericMark$genericType)") + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeInstancePsiImpl.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeInstancePsiImpl.kt index c33120e..c0391f3 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeInstancePsiImpl.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeInstancePsiImpl.kt @@ -4,6 +4,7 @@ import com.intellij.lang.ASTNode import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocElementType import com.jetbrains.php.lang.documentation.phpdoc.psi.impl.PhpDocTypeImpl import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.vk.kphpstorm.generics.GenericFunctionUtil /** * 'A', 'asdf\Instance', '\VK\Memcache' — instances (not primitives!) — psi is just PhpDocType @@ -16,7 +17,15 @@ class ExPhpTypeInstancePsiImpl(node: ASTNode) : PhpDocTypeImpl(node) { override fun getType(): PhpType { // for "future" don't invoke getType(), because it will be treated as relative class name in namespace - return if (isKphpBuiltinClass()) PhpType().add(text) else getType(this, text) + if (isKphpBuiltinClass()) { + PhpType().add(text) + } + + if (GenericFunctionUtil.nameIsGeneric(this, text)) { + return PhpType().add("%$text") + } + + return getType(this, text) } fun isKphpBuiltinClass() = text.let { diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeTplInstantiationPsiImpl.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeTplInstantiationPsiImpl.kt index b320f54..ac49c59 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeTplInstantiationPsiImpl.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeTplInstantiationPsiImpl.kt @@ -22,12 +22,12 @@ class ExPhpTypeTplInstantiationPsiImpl(node: ASTNode) : PhpDocTypeImpl(node) { override fun getNameNode(): ASTNode? = null override fun getType(): PhpType { - // kphp as built-in future and future_queue, which are ints an runtime and in php code, not classes - val templateClassName = (firstChild as? PhpDocType)?.type?.types?.firstOrNull() ?: return PhpType.EMPTY + // kphp as built-in future and future_queue, which are ints a runtime and in php code, not classes + val genericClassName = (firstChild as? PhpDocType)?.type?.types?.firstOrNull() ?: return PhpType.EMPTY - if (templateClassName == "future") + if (genericClassName == "future") return PhpType.INT - if (templateClassName == "future_queue") + if (genericClassName == "future_queue") return KphpPrimitiveTypes.PHP_TYPE_ARRAY_OF_ANY var innerTypesStr = "" @@ -41,6 +41,6 @@ class ExPhpTypeTplInstantiationPsiImpl(node: ASTNode) : PhpDocTypeImpl(node) { child = child.nextSibling } - return PhpType().add(templateClassName).add("$templateClassName<$innerTypesStr>") + return PhpType().add(genericClassName).add("$genericClassName<$innerTypesStr>") } } diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/TokensToExPhpTypePsiParsing.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/TokensToExPhpTypePsiParsing.kt index e06c2b1..afce1d8 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/TokensToExPhpTypePsiParsing.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/TokensToExPhpTypePsiParsing.kt @@ -73,7 +73,7 @@ internal object TokensToExPhpTypePsiParsing { } } - private fun parseTemplateSpecialization(builder: PhpPsiBuilder): Boolean { + private fun parseGenericSpecialization(builder: PhpPsiBuilder): Boolean { if (!builder.compareAndEat(PhpDocTokenTypes.DOC_LAB)) return !builder.expected("<") @@ -183,6 +183,17 @@ internal object TokensToExPhpTypePsiParsing { return true } + if (builder.compare(PhpDocTokenTypes.DOC_IDENTIFIER) && builder.tokenText == "class-string") { + val marker = builder.mark() + builder.advanceLexer() + if (!parseGenericSpecialization(builder)) { + marker.drop() + return false + } + marker.done(ExPhpTypeClassStringPsiImpl.elementType) + return true + } + if (builder.compare(PhpDocTokenTypes.DOC_IDENTIFIER) && builder.tokenText == "callable" && builder.rawLookup(1) == PhpDocTokenTypes.DOC_LPAREN) { val marker = builder.mark() builder.advanceLexer() @@ -220,7 +231,7 @@ internal object TokensToExPhpTypePsiParsing { if (builder.compare(PhpDocTokenTypes.DOC_LAB)) { val instantiationMarker = marker.precede() - if (!parseTemplateSpecialization(builder)) + if (!parseGenericSpecialization(builder)) instantiationMarker.drop() else instantiationMarker.done(ExPhpTypeTplInstantiationPsiImpl.elementType) diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/GenericFunctionCall.kt b/src/main/kotlin/com/vk/kphpstorm/generics/GenericFunctionCall.kt new file mode 100644 index 0000000..c8bc31b --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/GenericFunctionCall.kt @@ -0,0 +1,367 @@ +package com.vk.kphpstorm.generics + +import com.intellij.psi.PsiElement +import com.jetbrains.php.lang.psi.elements.* +import com.jetbrains.php.lang.psi.elements.Function +import com.jetbrains.php.lang.psi.elements.impl.ArrayCreationExpressionImpl +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.vk.kphpstorm.exphptype.* +import com.vk.kphpstorm.generics.GenericFunctionUtil.genericNames +import com.vk.kphpstorm.generics.GenericFunctionUtil.isGeneric +import com.vk.kphpstorm.generics.psi.GenericInstantiationPsiCommentImpl +import com.vk.kphpstorm.helpers.toExPhpType +import kotlin.math.min + +/** + * Класс инкапсулирующий в себе всю логику обработки шаблонных вызовов. + * + * Он выводит неявные типы, когда нет явного определения списка типов при инстанциации. + * + * Например: + * + * ```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 + * ``` + */ +class GenericFunctionCall(val call: FunctionReference) { + val project = call.project + var function: Function? = null + val callArgs = call.parameters + val parameters = mutableListOf() + val genericTs = mutableListOf() + + val explicitSpecs = mutableListOf() + val implicitSpecs = mutableListOf() + val specializationNameMap = mutableMapOf() + val implicitSpecializationNameMap = mutableMapOf() + val implicitSpecializationErrors = mutableMapOf>() + + val explicitSpecsPsi = call.firstChild?.nextSibling?.takeIf { + it is GenericInstantiationPsiCommentImpl + } as? GenericInstantiationPsiCommentImpl + + init { + init() + } + + private fun init() { + resolveFunction() + if (function == null || !isGeneric()) return + +// function = call.resolve() as? Function +// parameters.addAll(function!!.parameters) +// genericTs.addAll(function!!.genericNames()) + + // Несмотря на то, что явный список является превалирующим над + // типами выведенными из аргументов функций, нам все равно + // необходимы обв списка для дальнейших инспекций + + // В первую очередь, выводим все типы шаблонов из аргументов функции (при наличии) + reifyAllGenericsT() + // Далее, выводим все типы шаблонов из явного списка типов (при наличии) + extractExplicitGenericsT() + } + + /** + * Having a call `f/**/(...)`, where `f` is `f`, deduce T1 and T2 from + * comment `/**/`. + */ + private fun extractExplicitGenericsT() { + resolveFunction() + + if (function == null) return + if (explicitSpecsPsi == null) return + + val instances = explicitSpecsPsi.extractInstances() + val specTypesString = explicitSpecsPsi.genericSpecs + + val lhsType = PhpType().add("PseudoClass$specTypesString").global(project) + val parsed = lhsType.toExPhpType() + + val instantiation = when (parsed) { + is ExPhpTypePipe -> parsed.items.firstOrNull { it is ExPhpTypeTplInstantiation } + is ExPhpTypeNullable -> parsed.inner + else -> parsed + } as? ExPhpTypeTplInstantiation ?: return + + for (i in 0 until min(genericTs.size, instantiation.specializationList.size)) { + val type = instantiation.specializationList[i] + if (type is ExPhpTypeInstance) { + val resolvedInstance = instances[type.fqn + i.toString()] + val resolvedClasses = resolvedInstance?.classes(project) + if (resolvedClasses != null && resolvedClasses.isNotEmpty()) { + val exType = if (resolvedClasses.size > 1) { + ExPhpTypePipe(resolvedClasses.map { ExPhpTypeInstance(it.fqn) }) + } else { + ExPhpTypeInstance(resolvedClasses.first().fqn) + } + + explicitSpecs.add(exType) + specializationNameMap[genericTs[i]] = exType + continue + } + } + + explicitSpecs.add(instantiation.specializationList[i]) + specializationNameMap[genericTs[i]] = instantiation.specializationList[i] + } + } + + /** + * 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(arg: PsiElement, argExType: ExPhpType, paramExType: ExPhpType) { + if (arg !is PhpTypedElement) return + + 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(arg, argExType.inner, paramExType.inner) + } else { + reifyArgumentGenericsT(arg, argExType, paramExType.inner) + } + } + + if (paramExType is ExPhpTypePipe) { + // если случай 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(arg, argTypeWithoutFalse, paramTypeWithoutFalse) + } + // TODO: подумать над пайпами, так как не все так очевидно + } + } + + if (paramExType is ExPhpTypeClassString) { + if (arg is ClassConstantReference) { + val classExType = arg.classReference?.type?.toExPhpType() + if (classExType != null) { + reifyArgumentGenericsT(arg, classExType, paramExType.inner) + } + } + + 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(arg, classStringType.inner, paramExType.inner) + } + } + + if (paramExType is ExPhpTypeArray) { + if (arg is ArrayCreationExpression) { + ArrayCreationExpressionImpl.children(arg).forEach { el -> + if (el is ArrayHashElement) { + if (el.value != null && el.value is PhpTypedElement) { + val elExType = (el.value as PhpTypedElement).type.toExPhpType() ?: return@forEach + reifyArgumentGenericsT(el.value!!, elExType, paramExType.inner) + } + } else if (el.firstChild is PhpTypedElement) { + val elExType = (el.firstChild as PhpTypedElement).type.toExPhpType() ?: return@forEach + reifyArgumentGenericsT(el.firstChild, elExType, paramExType.inner) + } + } + } + } + + if (paramExType is ExPhpTypeTuple) { + if (argExType is ExPhpTypeTuple) { + for (i in 0 until min(argExType.items.size, paramExType.items.size)) { + reifyArgumentGenericsT(arg, 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(arg, argShapeItem.type, correspondingParamShapeItem.type) + } + } + } + } + + /** + * Having a call `f(...)` of a template function `f(...)`, deduce T1 and T2 + * "auto deducing" for generics arguments is typically called "reification". + */ + private fun reifyAllGenericsT() { + for (i in 0 until min(callArgs.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 arg = callArgs[i] as? PhpTypedElement ?: continue + val argExType = arg.type.global(project).toExPhpType() ?: continue + reifyArgumentGenericsT(arg, argExType, paramExType) + } + } + + fun resolveFunction() { + if (function == null) { + function = call.resolve() as? Function ?: return + parameters.addAll(function!!.parameters) + genericTs.addAll(function!!.genericNames()) + } + } + + fun withExplicitSpecs(): Boolean { + return explicitSpecsPsi != null + } + + fun isGeneric(): Boolean { + return function?.isGeneric() == true + } + + /** + * Функция проверяющая, что явно указанные шаблонные типы + * соответствуют автоматически выведенным типам и их можно + * безопасно удалить. + * + * Например: + * + * ```php + * /** + * * @kphp-generic T + * * @param T $arg + * */ + * function f($arg) {} + * + * f/**/(new Foo); // === f(new Foo); + * ``` + */ + fun isNoNeedExplicitSpec(): Boolean { + if (function == null) return false + if (explicitSpecsPsi == null) return false + + val countGenericNames = function!!.genericNames().size + val countExplicitSpecs = explicitSpecs.size + val countImplicitSpecs = implicitSpecs.size + + if (countGenericNames == countExplicitSpecs && countExplicitSpecs == countImplicitSpecs) { + var isEqual = true + explicitSpecs.forEachIndexed { index, explicitSpec -> + 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? { + if (function == null) return null + + val param = function!!.getParameter(index) ?: return null + val paramType = param.type + if (paramType.isGeneric(function!!)) { + val usedNameMap = specializationNameMap.ifEmpty { + implicitSpecializationNameMap + } + return paramType.toExPhpType()?.instantiateGeneric(usedNameMap) + } + + return null + } + + override fun toString(): String { + val specs = explicitSpecs.ifEmpty { implicitSpecs } + return "${function?.fqn ?: "UnknownFunction"}<${specs.joinToString(",")}>" + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/GenericFunctionUtil.kt b/src/main/kotlin/com/vk/kphpstorm/generics/GenericFunctionUtil.kt new file mode 100644 index 0000000..b491bc6 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/GenericFunctionUtil.kt @@ -0,0 +1,69 @@ +package com.vk.kphpstorm.generics + +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.util.PsiTreeUtil +import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment +import com.jetbrains.php.lang.psi.elements.Function +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.vk.kphpstorm.kphptags.psi.KphpDocTagGenericPsiImpl + +object GenericFunctionUtil { + fun Function.isGeneric(): Boolean { + return docComment?.getTagElementsByName("@kphp-generic")?.firstOrNull() != null + } + + fun PhpType.isGeneric(f: Function): Boolean { + val genericNames = f.genericNames() + return isGeneric(genericNames) + } + + fun PhpType.isGeneric(genericNames: List): Boolean { + // TODO: Подумать как сделать улучшить + return genericNames.any { name -> types.contains("%$name") } || + genericNames.any { types.first().contains("%$it") } + } + + fun Function.isReturnGeneric(): Boolean { + if (docComment == null) return false + + return docComment!!.getTagElementsByName("@return").any { + it.type.isGeneric(this) + } + } + + fun Function.genericNames(): List { + val docT = docComment?.getTagElementsByName("@kphp-generic")?.firstOrNull() as? KphpDocTagGenericPsiImpl + ?: return emptyList() + return docT.getGenericArguments() + } + + fun nameIsGeneric(el: PsiElement, text: String): Boolean { + var parent = el.parent + while (parent !is PsiFile) { + // например, когда мы внутри @param T, то когда-то дойдём до doc comment, где написано @kphp-generic + if (parent is PhpDocComment) { + val doc = parent + for (child in doc.children) { + if (child is KphpDocTagGenericPsiImpl && child.getGenericArguments().contains(text)) { + return true + } + } + } + // например, когда мы внутри @var T, то когда дойдём до функции/класса вверх, @kphp-generic будет в нём + if (parent is Function) { + val doc = PsiTreeUtil.skipWhitespacesBackward(parent) as? PhpDocComment + if (doc != null) { + for (child in doc.children) { + if (child is KphpDocTagGenericPsiImpl && child.getGenericArguments().contains(text)) { + return true + } + } + } + } + parent = parent.parent + } + + return false + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/psi/GenericInstantiationPsiCommentImpl.kt b/src/main/kotlin/com/vk/kphpstorm/generics/psi/GenericInstantiationPsiCommentImpl.kt new file mode 100644 index 0000000..9a12a4d --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/psi/GenericInstantiationPsiCommentImpl.kt @@ -0,0 +1,220 @@ +package com.vk.kphpstorm.generics.psi + +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiElement +import com.intellij.psi.impl.source.tree.PsiCommentImpl +import com.intellij.psi.tree.IElementType +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.refactoring.suggested.endOffset +import com.intellij.refactoring.suggested.startOffset +import com.jetbrains.php.PhpIndex +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.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.vk.kphpstorm.completion.KphpGenericsReferenceContributor +import com.vk.kphpstorm.exphptype.psi.ExPhpTypeInstancePsiImpl + +/** + * Комментарий вида `/**/` который пишется в вызове + * функции между именем функции и списком аргументов. + * + * Данный комментарий хранит список явных шаблонных типов разделенных + * запятой. + * + * В комментарии могут быть любимые типы которые могут быть представлены + * в phpdoc. + * + * Комментарий не имеет внутренней структуры, так как [PsiComment] не может + * иметь потомков, так как является листом дерева. В случае если не делать + * данный узел комментарием, то ломается парсинг вызова функции, так как + * грамматика не готова к тому что между именем функции и списком аргументов + * есть еще какой-то элемент. + */ +class GenericInstantiationPsiCommentImpl(type: IElementType, text: CharSequence) : PsiCommentImpl(type, text) { + /** + * For `/**/` is `` + */ + val genericSpecs = text.substring(2 until text.length - 2) + + class Instance(val fqn: String, val pos: TextRange) { + fun classes(project: Project): List { + return PhpIndex.getInstance(project).getClassesByFQN(fqn).toList() + } + } + + /** + * Предпосылки: + * + * Ввиду того, что комментарий не имеет структуры причины чего описаны в + * комментарии над классом, нам необходимо по-другому вычленять составные части. + * + * Так как в комментарии описываются типы, то там могут быть и классы + * для которых должно быть доступны переход к определению, переименование, поиск + * использований и т.д. + * + * Для этого мы добавляем в комментарий ссылки (см. [KphpGenericsReferenceContributor]). + * Для того чтобы смочь добавить эти ссылки, нам нужно для начала вычленить их + * из комментария. + * + * + * Описание: + * + * Данная функция находит все классы описанные в комментарии. + * Для каждого класса она: + * 1. Резолвит их имена в текущем контексте + * 2. Находит их стартовую и конечную позицию + * + * Функция возвращает мапу где ключом является имя класса из комментария, + * а значением список классов на которые ссылается это имя. + * + * Однако в комментарии один класс может появляться несколько раз, например: + * + * + * + * + * Поэтому в результирующей мапе ключом является не просто имя класса + * как в комментарии, а имя + порядковый номер в комментарии. + * + * Таким образом в результате для примера выше мы получим следующую мапу: + * + * Foo0 -> [...] + * Boo1 -> [...] + * Foo2 -> [...] + */ + fun extractInstances(): Map { + val result = mutableMapOf() + + val psi = PhpPsiElementFactory.createPsiFileFromText( + project, + "use \\Some\\Doo; /** @return __ClassT$genericSpecs */class __ClassT {}" + ) + + val returnTag = PsiTreeUtil.findChildOfType(psi, PhpDocReturnTagImpl::class.java)!! + + val pseudoReturnTag = returnTag.firstChild + val startOfTemplate = pseudoReturnTag.startOffset + "@return __ClassT".length + 1 + val templatePsi = returnTag.lastChild?.prevSibling!! + + val startInRealCode = startOffset + 3 + var instanceIndex = 0 + templatePsi.accept(object : PhpElementVisitor() { + private fun resolveInstanceByName(name: String, startOffset: Int, endOffset: Int) { + val fqn = resolveInstance(name) + result["$name$instanceIndex"] = Instance( + fqn, + TextRange(startOffset, endOffset) + ) + instanceIndex++ + } + + override fun visitElement(element: PsiElement) { + val startOffset = startInRealCode + (element.startOffset - startOfTemplate) + val endOffset = startInRealCode + (element.endOffset - startOfTemplate) + + if (element is ExPhpTypeInstancePsiImpl && element.name != "__ClassT") { + resolveInstanceByName(element.text, startOffset, endOffset) + } + + var child = element.firstChild + while (child != null) { + child.accept(this) + child = child.nextSibling + } + } + }) + + return result + } + + private fun resolveInstance(rawName: String): String { + val (name, namespaceName) = splitAndResolveNamespace(this, rawName) + + return resolveLocal( + prevSibling.parent as FunctionReference, + 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 = name + reference + } + } + + 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) + } + + // TODO: удалить + private fun resolveGlobal(element: PhpReference, name: String, namespaceName: String): Collection { + return ClassReferenceImpl.resolveGlobal(element, name, namespaceName, false) + } + + /** + * Функция принимает текущий контекстный элемент и имя класса, которое + * может быть как FQN, так и просто именем. + * + * В случае когда было передано FQN, она разделяет его на пространство + * имен и имя класса и возвращает их. + * + * В другом случае она находит текущее пространство имен в котором + * находится контекстный элемент и возвращает его вместе с переданным + * именем. + */ + 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) + } + + return Pair(nameOrFqn, PhpReferenceImpl.findNamespaceName("", el)) + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpColorsAndFontsPage.kt b/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpColorsAndFontsPage.kt index f445754..e16aef0 100644 --- a/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpColorsAndFontsPage.kt +++ b/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpColorsAndFontsPage.kt @@ -20,7 +20,8 @@ class KphpColorsAndFontsPage : ColorSettingsPage, DisplayPrioritySortable { AttributesDescriptor("phpdoc: variable of @param", PhpHighlightingData.DOC_PARAMETER), AttributesDescriptor("function call: php predefined (array_pop / ini_get / etc)", PhpHighlightingData.PREDEFINED_SYMBOL), AttributesDescriptor("function call: kphp native (wait / instance_cast / etc)", KphpHighlightingData.FUNC_CALL_KPHP_NATIVE), - AttributesDescriptor("function call: regular (not instance)", KphpHighlightingData.FUNC_CALL_REGULAR) + AttributesDescriptor("function call: regular (not instance)", KphpHighlightingData.FUNC_CALL_REGULAR), + AttributesDescriptor("generic specification: function and classes", KphpHighlightingData.GENERIC_SPECS) ) override fun getHighlighter() = PhpColorPageHighlighter(mapOf()) @@ -36,7 +37,9 @@ class KphpColorsAndFontsPage : ColorSettingsPage, DisplayPrioritySortable { "f_reg" to KphpHighlightingData.FUNC_CALL_REGULAR, "f_php" to PhpHighlightingData.PREDEFINED_SYMBOL, - "f_native" to KphpHighlightingData.FUNC_CALL_KPHP_NATIVE + "f_native" to KphpHighlightingData.FUNC_CALL_KPHP_NATIVE, + + "generic" to KphpHighlightingData.GENERIC_SPECS ) override fun getIcon() = PhpFileType.INSTANCE.icon @@ -71,6 +74,7 @@ class KphpColorsAndFontsPage : ColorSettingsPage, DisplayPrioritySortable { instance_to_array(${'$'}user); ini_get('memory_limit'); + acceptor/**/(new User); """.trimIndent() override fun getPriority() = DisplayPriority.KEY_LANGUAGE_SETTINGS diff --git a/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpGenericCommentFolderBuilder.kt b/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpGenericCommentFolderBuilder.kt new file mode 100644 index 0000000..467b1b2 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpGenericCommentFolderBuilder.kt @@ -0,0 +1,51 @@ +package com.vk.kphpstorm.highlighting + +import com.intellij.lang.ASTNode +import com.intellij.lang.folding.FoldingBuilderEx +import com.intellij.lang.folding.FoldingDescriptor +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.FoldingGroup +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement +import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor +import com.vk.kphpstorm.generics.psi.GenericInstantiationPsiCommentImpl + +class KphpGenericCommentFolderBuilder : FoldingBuilderEx(), DumbAware { + override fun buildFoldRegions(root: PsiElement, document: Document, quick: Boolean): Array { + val group = FoldingGroup.newGroup("KPHP Generic") + val descriptors = mutableListOf() + + root.accept(object : PhpElementVisitor() { + override fun visitElement(element: PsiElement) { + if (element is GenericInstantiationPsiCommentImpl) { + descriptors.add( + FoldingDescriptor( + element.node, + TextRange( + element.textRange.startOffset, + element.textRange.endOffset + ), + group + ) + ) + } + + var child = element.firstChild + while (child != null) { + child.accept(this) + child = child.nextSibling + } + } + }) + + return descriptors.toTypedArray() + } + + override fun getPlaceholderText(node: ASTNode): String { + val text = node.text + return text.substring(2, text.length - 2) + } + + override fun isCollapsedByDefault(node: ASTNode): Boolean = true +} diff --git a/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpHighlightingData.kt b/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpHighlightingData.kt index 8dbf374..1e6e194 100644 --- a/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpHighlightingData.kt +++ b/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpHighlightingData.kt @@ -16,4 +16,7 @@ object KphpHighlightingData { val FUNC_CALL_REGULAR = TextAttributesKey.createTextAttributesKey("FUNC_CALL_REGULAR", PhpHighlightingData.FUNCTION_CALL) val FUNC_CALL_KPHP_NATIVE = TextAttributesKey.createTextAttributesKey("FUNC_CALL_KPHP_NATIVE", PhpHighlightingData.FUNCTION_CALL) + + val GENERIC_SPECS = TextAttributesKey.createTextAttributesKey("GENERIC_SPECS", PhpHighlightingData.CLASS) + val UNNECESSARY_GENERIC_SPECS = TextAttributesKey.createTextAttributesKey("UNNECESSARY_GENERIC_SPECS", PhpHighlightingData.COMMENT) } diff --git a/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpStormGenericCommentsAnnotator.kt b/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpStormGenericCommentsAnnotator.kt new file mode 100644 index 0000000..30f91cd --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpStormGenericCommentsAnnotator.kt @@ -0,0 +1,48 @@ +package com.vk.kphpstorm.highlighting + +import com.intellij.lang.annotation.AnnotationHolder +import com.intellij.lang.annotation.Annotator +import com.intellij.lang.annotation.HighlightSeverity +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement +import com.jetbrains.php.lang.psi.elements.FunctionReference +import com.vk.kphpstorm.generics.GenericFunctionCall +import com.vk.kphpstorm.generics.psi.GenericInstantiationPsiCommentImpl + +class KphpStormGenericCommentsAnnotator : Annotator { + override fun annotate(element: PsiElement, holder: AnnotationHolder) { + if (element !is GenericInstantiationPsiCommentImpl) { + return + } + + var unnecessarySpec = false + + val parent = element.parent + if (parent is FunctionReference) { + val call = GenericFunctionCall(parent) + call.resolveFunction() + if (call.isNoNeedExplicitSpec()) { + holder.newSilentAnnotation(HighlightSeverity.INFORMATION) + .range(TextRange(element.textRange.startOffset, element.textRange.endOffset)) + .textAttributes(KphpHighlightingData.UNNECESSARY_GENERIC_SPECS).create() + unnecessarySpec = true + } + } + + if (!unnecessarySpec) { + holder.newSilentAnnotation(HighlightSeverity.INFORMATION) + .range(TextRange(element.textRange.startOffset + 2, element.textRange.endOffset - 2)) + .textAttributes(KphpHighlightingData.GENERIC_SPECS).create() + } + + val instances = element.extractInstances() + instances.forEach { (_, instance) -> + instance.classes(element.project).forEach { _ -> + holder.newSilentAnnotation(HighlightSeverity.INFORMATION) + .range(instance.pos) + .textAttributes(KphpHighlightingData.PHPDOC_TAG_KPHP) + .create() + } + } + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpStormTypeInfoProvider.kt b/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpStormTypeInfoProvider.kt index 3d32b6e..702ba65 100644 --- a/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpStormTypeInfoProvider.kt +++ b/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpStormTypeInfoProvider.kt @@ -3,6 +3,7 @@ package com.vk.kphpstorm.highlighting import com.intellij.lang.ExpressionTypeProvider import com.intellij.psi.PsiElement import com.jetbrains.php.lang.psi.elements.PhpTypedElement +import com.vk.kphpstorm.exphptype.PsiToExPhpType import com.vk.kphpstorm.helpers.toExPhpType /** @@ -15,10 +16,9 @@ import com.vk.kphpstorm.helpers.toExPhpType class KphpStormTypeInfoProvider : ExpressionTypeProvider() { private val delegate = com.jetbrains.php.actions.PhpExpressionTypeProvider() - override fun getInformationHint(element: PhpTypedElement): String { val phpType = element.type.global(element.project) - return phpType.toExPhpType()?.toHumanReadable(element) ?: phpType.toString() + return phpType.toExPhpType()?.let { PsiToExPhpType.dropGenerics(it).toHumanReadable(element) } ?: phpType.toString() } override fun getExpressionsAt(elementAt: PsiElement): List { diff --git a/src/main/kotlin/com/vk/kphpstorm/highlighting/hints/GenericsInlayTypeHintsProvider.kt b/src/main/kotlin/com/vk/kphpstorm/highlighting/hints/GenericsInlayTypeHintsProvider.kt new file mode 100644 index 0000000..ec8bf0d --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/highlighting/hints/GenericsInlayTypeHintsProvider.kt @@ -0,0 +1,38 @@ +package com.vk.kphpstorm.highlighting.hints + +import com.intellij.codeInsight.hints.* +import com.intellij.codeInsight.hints.ImmediateConfigurable.Case +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiFile +import javax.swing.JComponent +import javax.swing.JPanel + +@Suppress("UnstableApiUsage") +class GenericsInlayTypeHintsProvider : InlayHintsProvider { + data class Settings( + var showForFunctions: Boolean = true, + ) + + override val key: SettingsKey = KEY + override val name: String = "KPHP generic annotations" + override val previewText: String = "" + + override fun createConfigurable(settings: Settings) = object : ImmediateConfigurable { + override val mainCheckboxText: String = "Use inline hints for visibility" + + override val cases: List = listOf( + Case("Show for functions", "functions", settings::showForFunctions), + ) + + override fun createComponent(listener: ChangeListener): JComponent = JPanel() + } + + override fun createSettings() = Settings() + + override fun getCollectorFor(file: PsiFile, editor: Editor, settings: Settings, sink: InlayHintsSink) = + InlayHintsCollector(editor, file, settings, sink) + + companion object { + private val KEY: SettingsKey = SettingsKey("kphp.generic.annotations") + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/highlighting/hints/InlayHintPresentationFactory.kt b/src/main/kotlin/com/vk/kphpstorm/highlighting/hints/InlayHintPresentationFactory.kt new file mode 100644 index 0000000..8c8e2eb --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/highlighting/hints/InlayHintPresentationFactory.kt @@ -0,0 +1,46 @@ +package com.vk.kphpstorm.highlighting.hints + +import com.intellij.codeInsight.hints.presentation.* +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.openapi.editor.impl.EditorImpl + +@Suppress("UnstableApiUsage") +class InlayHintPresentationFactory(private val myEditor: Editor) { + private val myTextMetricsStorage = InlayTextMetricsStorage(myEditor as EditorImpl) + private val myOffsetFromTopProvider = object : InsetValueProvider { + override val top = myTextMetricsStorage.getFontMetrics(true).offsetFromTop() + } + + fun inlayHint(value: String): InlayPresentation { + return roundWithBackground(text(value)) + } + + private fun text(text: String): InlayPresentation { + return withInlayAttributes(TextInlayPresentation(myTextMetricsStorage, false, text), + DefaultLanguageHighlighterColors.INLINE_PARAMETER_HINT_HIGHLIGHTED) + } + + private fun roundWithBackground(base: InlayPresentation): InlayPresentation { + val rounding = RoundWithBackgroundPresentation( + InsetPresentation( + base, left = 7, right = 7, + top = 0, down = 0 + ), + 0, 0, backgroundAlpha = 0f + ) + + return DynamicInsetPresentation(rounding, myOffsetFromTopProvider) + } + + private fun withInlayAttributes( + base: InlayPresentation, + attributes: TextAttributesKey = DefaultLanguageHighlighterColors.INLAY_DEFAULT + ): InlayPresentation { + return WithAttributesPresentation( + base, attributes, myEditor, + WithAttributesPresentation.AttributesFlags().withIsDefault(true) + ) + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/highlighting/hints/InlayHintsCollector.kt b/src/main/kotlin/com/vk/kphpstorm/highlighting/hints/InlayHintsCollector.kt new file mode 100644 index 0000000..46dcedd --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/highlighting/hints/InlayHintsCollector.kt @@ -0,0 +1,52 @@ +package com.vk.kphpstorm.highlighting.hints + +import com.intellij.codeInsight.hints.FactoryInlayHintsCollector +import com.intellij.codeInsight.hints.InlayHintsSink +import com.intellij.openapi.components.service +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.DumbService +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.vk.kphpstorm.generics.GenericFunctionCall +import com.vk.kphpstorm.generics.GenericFunctionUtil.genericNames + +@Suppress("UnstableApiUsage") +class InlayHintsCollector( + editor: Editor, + private val file: PsiFile, + private val settings: GenericsInlayTypeHintsProvider.Settings, + private val sink: InlayHintsSink +) : FactoryInlayHintsCollector(editor) { + + private val myHintsFactory = InlayHintPresentationFactory(editor) + + override fun collect(element: PsiElement, editor: Editor, sink: InlayHintsSink): Boolean { + // If the indexing process is in progress. + if (file.project.service().isDumb) return true + + when { + element is FunctionReference && settings.showForFunctions -> { + showAnnotation(element) + } + } + + return true + } + + private fun showAnnotation(element: FunctionReference) { + val call = GenericFunctionCall(element) + call.resolveFunction() + if (!call.isGeneric() || call.withExplicitSpecs()) { + return + } + + val genericNames = call.function!!.genericNames().joinToString(", ") + val simplePresentation = myHintsFactory.inlayHint("<$genericNames>") + + val namePsi = element.firstChild + + sink.addInlineElement(namePsi.endOffset, 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 new file mode 100644 index 0000000..b6b704f --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/FunctionGenericInstantiationArgsCountMismatchInspection.kt @@ -0,0 +1,33 @@ +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.GenericFunctionUtil.genericNames + +class FunctionGenericInstantiationArgsCountMismatchInspection : PhpInspection() { + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + return object : PhpElementVisitor() { + override fun visitPhpFunctionCall(reference: FunctionReference) { + val call = GenericFunctionCall(reference) + call.resolveFunction() + 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/FunctionGenericNoEnoughInformationInspection.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/FunctionGenericNoEnoughInformationInspection.kt new file mode 100644 index 0000000..1f96363 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/FunctionGenericNoEnoughInformationInspection.kt @@ -0,0 +1,42 @@ +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.GenericFunctionUtil.genericNames + +class FunctionGenericNoEnoughInformationInspection : PhpInspection() { + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + return object : PhpElementVisitor() { + override fun visitPhpFunctionCall(reference: FunctionReference) { + val call = GenericFunctionCall(reference) + call.resolveFunction() + if (call.function == null) return + + val genericNames = call.function!!.genericNames() + + if (call.explicitSpecsPsi == null) { + genericNames.any { + val resolved = call.implicitSpecializationNameMap.contains(it) + + if (!resolved) { + holder.registerProblem( + reference.element, + "Not enough information to infer generic $it", + ProblemHighlightType.GENERIC_ERROR + ) + + return@any true + } + + false + } + } + } + } + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/FunctionGenericSeveralGenericTypesInspection.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/FunctionGenericSeveralGenericTypesInspection.kt new file mode 100644 index 0000000..1504dff --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/FunctionGenericSeveralGenericTypesInspection.kt @@ -0,0 +1,43 @@ +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.jetbrains.rd.util.first +import com.vk.kphpstorm.generics.GenericFunctionCall + +class FunctionGenericSeveralGenericTypesInspection : PhpInspection() { + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + return object : PhpElementVisitor() { + override fun visitPhpFunctionCall(reference: FunctionReference) { + val call = GenericFunctionCall(reference) + call.resolveFunction() + if (call.function == null) return + + // В случае даже если есть ошибки, то мы показываем их только + // в случае когда нет явного определения шаблона для вызова функции. + if (call.implicitSpecializationErrors.isNotEmpty() && !call.withExplicitSpecs()) { + val error = call.implicitSpecializationErrors.first() + 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 explanation = + "Please, provide all generics types using following syntax: $callStingWithGenerics;" + + holder.registerProblem( + reference.parameterList ?: reference.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/FunctionUnnecessaryExplicitGenericInstantiationListInspection.kt new file mode 100644 index 0000000..1ed77d2 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/FunctionUnnecessaryExplicitGenericInstantiationListInspection.kt @@ -0,0 +1,32 @@ +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.inspections.quickfixes.RemoveExplicitGenericSpecsQuickFix + +class FunctionUnnecessaryExplicitGenericInstantiationListInspection : PhpInspection() { + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + return object : PhpElementVisitor() { + override fun visitPhpFunctionCall(reference: FunctionReference) { + val call = GenericFunctionCall(reference) + call.resolveFunction() + if (call.function == null) return + if (call.explicitSpecsPsi == null) return + + if (call.isNoNeedExplicitSpec()) { + holder.registerProblem( + call.explicitSpecsPsi, + "Remove unnecessary explicit list of instantiation arguments", + ProblemHighlightType.WEAK_WARNING, + RemoveExplicitGenericSpecsQuickFix() + ) + } + } + } + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/KphpParameterTypeMismatchInspection.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/KphpParameterTypeMismatchInspection.kt index 435d372..bfb8e71 100644 --- a/src/main/kotlin/com/vk/kphpstorm/inspections/KphpParameterTypeMismatchInspection.kt +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/KphpParameterTypeMismatchInspection.kt @@ -14,6 +14,7 @@ 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.PsiToExPhpType +import com.vk.kphpstorm.generics.GenericFunctionCall import com.vk.kphpstorm.helpers.KPHP_NATIVE_FUNCTIONS import com.vk.kphpstorm.kphptags.KphpInferDocTag @@ -76,8 +77,19 @@ class KphpParameterTypeMismatchInspection : PhpInspection() { val callType = PsiToExPhpType.getTypeOfExpr(callParam, project) ?: continue val argType = PsiToExPhpType.getArgumentDeclaredType(fArg, project) ?: continue - if (!argType.isAssignableFrom(callType, project) && needsReporting(f)) { - holder.registerProblem(callParam, "Can't pass '${callType.toHumanReadable(call)}' to '${argType.toHumanReadable(call)}' \$${fArg.name}", ProblemHighlightType.GENERIC_ERROR_OR_WARNING) + val actualArgType = if (call is FunctionReference) { + val genericCall = GenericFunctionCall(call) + genericCall.resolveFunction() + if (genericCall.isGeneric()) + genericCall.typeOfParam(argIdx) ?: argType + else + argType + } else { + argType + } + + if (!actualArgType.isAssignableFrom(callType, project) && needsReporting(f)) { + holder.registerProblem(callParam, "Can't pass '${callType.toHumanReadable(call)}' to '${actualArgType.toHumanReadable(call)}' \$${fArg.name}", ProblemHighlightType.GENERIC_ERROR_OR_WARNING) } } diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/KphpUndefinedClassInspection.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/KphpUndefinedClassInspection.kt index c5ef94c..2c74757 100644 --- a/src/main/kotlin/com/vk/kphpstorm/inspections/KphpUndefinedClassInspection.kt +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/KphpUndefinedClassInspection.kt @@ -10,6 +10,8 @@ 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.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.inspections.helpers.KphpTypingAnalyzer @@ -82,12 +84,14 @@ class KphpUndefinedClassInspection : PhpInspection() { * report if this class is unknown (primitives 'int', 'mixed', etc have another psi impl) */ override fun visitPhpDocType(type: PhpDocType) { - if (type !is ExPhpTypeInstancePsiImpl || type.isKphpBuiltinClass()) + if (type !is ExPhpTypeInstancePsiImpl) return - val resolved = type.multiResolve(false) - if (resolved.isEmpty()) - reportUndefinedClassUsage(type) + val resolvedType = PhpTypeToExPhpTypeParsing.parse(type.type) + if (resolvedType is ExPhpTypeInstance) { + if (PhpIndex.getInstance(type.project).getAnyByFQN(resolvedType.fqn).isEmpty()) + reportUndefinedClassUsage(type) + } } /** diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/quickfixes/RemoveExplicitGenericSpecsQuickFix.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/quickfixes/RemoveExplicitGenericSpecsQuickFix.kt new file mode 100644 index 0000000..8b4c7d3 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/quickfixes/RemoveExplicitGenericSpecsQuickFix.kt @@ -0,0 +1,14 @@ +package com.vk.kphpstorm.inspections.quickfixes + +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.openapi.project.Project + +class RemoveExplicitGenericSpecsQuickFix : LocalQuickFix { + override fun getFamilyName() = "Remove explicit specs" + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val comment = descriptor.psiElement ?: return + comment.delete() + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/kphptags/AllKphpdocTagsList.kt b/src/main/kotlin/com/vk/kphpstorm/kphptags/AllKphpdocTagsList.kt index b3bb849..1696b75 100644 --- a/src/main/kotlin/com/vk/kphpstorm/kphptags/AllKphpdocTagsList.kt +++ b/src/main/kotlin/com/vk/kphpstorm/kphptags/AllKphpdocTagsList.kt @@ -11,7 +11,6 @@ val ALL_KPHPDOC_TAGS: List = listOf( KphpFlattenDocTag, KphpRequiredDocTag, KphpSyncDocTag, - KphpTemplateDocTag, KphpReturnDocTag, KphpShouldNotThrowDocTag, KphpThrowsDocTag, @@ -27,6 +26,7 @@ val ALL_KPHPDOC_TAGS: List = listOf( KphpSerializableDocTag, KphpReservedFieldsDocTag, KphpTemplateClassDocTag, + KphpGenericDocTag, KphpMemcacheClassDocTag, KphpImmutableClassDocTag, KphpTlClassDocTag, diff --git a/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpGenericDocTag.kt b/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpGenericDocTag.kt new file mode 100644 index 0000000..5e33c4d --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpGenericDocTag.kt @@ -0,0 +1,52 @@ +package com.vk.kphpstorm.kphptags + +import com.intellij.lang.annotation.AnnotationHolder +import com.intellij.psi.PsiElement +import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment +import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag +import com.jetbrains.php.lang.psi.elements.Function +import com.jetbrains.php.lang.psi.elements.PhpClass +import com.vk.kphpstorm.kphptags.psi.KphpDocElementTypes +import com.vk.kphpstorm.kphptags.psi.KphpDocTagElementType +import com.vk.kphpstorm.kphptags.psi.KphpDocTagGenericPsiImpl + +object KphpGenericDocTag : KphpDocTag("@kphp-generic") { + override val description: String + get() = "Describes generic types for a function and makes it generic" + + override val elementType: KphpDocTagElementType + get() = KphpDocElementTypes.kphpDocTagGeneric + + override fun isApplicableFor(owner: PsiElement): Boolean { + return owner is Function || owner is PhpClass + } + + override fun needsAutoCompleteOnTyping(docComment: PhpDocComment, owner: PsiElement?): Boolean { + return true + } + + override fun areDuplicatesAllowed(): Boolean { + return false + } + + override fun onAutoCompleted(docComment: PhpDocComment): String? { + return "|T" + } + + override fun annotate(docTag: PhpDocTag, rhs: PsiElement?, holder: AnnotationHolder) { + if (rhs == null) { + holder.errTag(docTag, "Expected: T [, T1, ...]") + return + } + + if (docTag is KphpDocTagGenericPsiImpl) { + val names = mutableSetOf() + docTag.getGenericArguments().forEach { name -> + if (names.contains(name)) { + holder.errTag(docTag, "Duplicate generic type $name in declaration") + } + names.add(name) + } + } + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpReturnDocTag.kt b/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpReturnDocTag.kt index f77a63f..b8b664d 100644 --- a/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpReturnDocTag.kt +++ b/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpReturnDocTag.kt @@ -8,18 +8,18 @@ import com.jetbrains.php.lang.psi.elements.Function object KphpReturnDocTag : KphpDocTag("@kphp-return") { override val description: String - get() = "Can be used if @kphp-template was specified, to provide information of returning class/fields based on template argument." + get() = "Can be used if @kphp-generic was specified, to provide information of returning class/fields based on generic argument." override fun isApplicableFor(owner: PsiElement): Boolean { - return owner is Function && KphpTemplateDocTag.existsInDocComment(owner) + return owner is Function && KphpGenericDocTag.existsInDocComment(owner) } override fun needsAutoCompleteOnTyping(docComment: PhpDocComment, owner: PsiElement?): Boolean { - return KphpTemplateDocTag.existsInDocComment(docComment) + return KphpGenericDocTag.existsInDocComment(docComment) } override fun annotate(docTag: PhpDocTag, rhs: PsiElement?, holder: AnnotationHolder) { if (rhs == null) - holder.errTag(docTag, "Specify returning class/fields based on template argument.") + holder.errTag(docTag, "Specify returning class/fields based on generic argument.") } } diff --git a/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpTemplateDocTag.kt b/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpTemplateDocTag.kt deleted file mode 100644 index 7de7a7a..0000000 --- a/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpTemplateDocTag.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.vk.kphpstorm.kphptags - -import com.intellij.lang.annotation.AnnotationHolder -import com.intellij.psi.PsiElement -import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment -import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag -import com.jetbrains.php.lang.psi.elements.Function - -object KphpTemplateDocTag : KphpDocTag("@kphp-template") { - override val description: String - get() = "Used above functions to implement duck typing: such functions are 'overloaded' and can accept variuos OOP-incompatible instances." - - override fun isApplicableFor(owner: PsiElement): Boolean { - return owner is Function - } - - override fun needsAutoCompleteOnTyping(docComment: PhpDocComment, owner: PsiElement?): Boolean { - // this tag is very rare; IDE recognizes it, but does not auto-suggest - return false - } - - override fun areDuplicatesAllowed(): Boolean { - // @kphp-template can meet several times, for each parameter - return true - } - - override fun annotate(docTag: PhpDocTag, rhs: PsiElement?, holder: AnnotationHolder) { - if (rhs == null) - holder.errTag(docTag, "Expected: [T] \$arg [,\$arg2,...]") - // do not complicate rhs verification logic, because this tag is very rare - } -} diff --git a/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocElementTypes.kt b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocElementTypes.kt index e210f2c..706d804 100644 --- a/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocElementTypes.kt +++ b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocElementTypes.kt @@ -36,6 +36,9 @@ object KphpDocElementTypes { */ val kphpDocTagTemplateClass = KphpDocTagTemplateClassElementType + /** + * '@kphp-generic T1, T2' + * This tag stores "T1,T2" in stubs and has custom psi for them, therefore is not simple + */ + val kphpDocTagGeneric = KphpDocTagGenericElementType } - - diff --git a/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocGenericParameterDeclPsiImpl.kt b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocGenericParameterDeclPsiImpl.kt new file mode 100644 index 0000000..ef03d3b --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocGenericParameterDeclPsiImpl.kt @@ -0,0 +1,32 @@ +package com.vk.kphpstorm.kphptags.psi + +import com.intellij.lang.ASTNode +import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocElementType +import com.jetbrains.php.lang.documentation.phpdoc.psi.impl.PhpDocPsiElementImpl + +/** + * Inside '@kphp-generic T1, T2: ExtendsClass' — 'T1' and 'T2: ExtendsClass' are separate psi elements of this impl + * @see KphpDocTagGenericElementType.getTagParser + */ +class KphpDocGenericParameterDeclPsiImpl(node: ASTNode) : PhpDocPsiElementImpl(node) { + companion object { + val elementType = PhpDocElementType("phpdocGenericParameterDecl") + } + + override fun getName(): String { + val text = text + if (text.contains(':')) { + return text.substring(0 until text.indexOf(':')) + } + + return text + } + + fun getExtendsClass(): String? { + if (text.contains(':')) { + return text.substring(text.indexOf(':') + 1) + } + + return null + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagGenericElementType.kt b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagGenericElementType.kt new file mode 100644 index 0000000..ed47ef4 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagGenericElementType.kt @@ -0,0 +1,77 @@ +package com.vk.kphpstorm.kphptags.psi + +import com.intellij.psi.stubs.StubElement +import com.intellij.psi.stubs.StubInputStream +import com.intellij.psi.stubs.StubOutputStream +import com.jetbrains.php.lang.documentation.phpdoc.lexer.PhpDocTokenTypes +import com.jetbrains.php.lang.documentation.phpdoc.parser.tags.PhpDocTagParser +import com.jetbrains.php.lang.documentation.phpdoc.psi.stubs.PhpDocTagStub +import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag +import com.jetbrains.php.lang.parser.PhpParserErrors +import com.jetbrains.php.lang.parser.PhpPsiBuilder +import com.jetbrains.php.lang.psi.stubs.PhpStubElementType + +/** + * '@kphp-generic T1, T2: ExtendsClass' has a separate elementType, + * psi for 'T1' and 'T2: ExtendsClass' and stub contents. + * + * @see KphpDocElementTypes.kphpDocTagGeneric + */ +object KphpDocTagGenericElementType : PhpStubElementType("@kphp-generic"), + KphpDocTagElementType { + override fun createPsi(stub: PhpDocTagStub): PhpDocTag { + return KphpDocTagGenericPsiImpl(stub, stub.stubType) + } + + override fun createStub(psi: PhpDocTag, parentStub: StubElement<*>?): PhpDocTagStub { + // TODO: Add `: ExtendsName` + // stub value is 'T1,T2' — without spaces + val stubValue = (psi as KphpDocTagGenericPsiImpl).getGenericArguments().joinToString(",") + return KphpDocTagStubImpl(parentStub, this, psi.name, stubValue) + } + + override fun serialize(stub: PhpDocTagStub, dataStream: StubOutputStream) { + dataStream.writeName(stub.name) + dataStream.writeName(stub.value) + } + + override fun deserialize(dataStream: StubInputStream, parentStub: StubElement<*>?): PhpDocTagStub { + val name = dataStream.readName()?.toString() ?: throw NullPointerException() + val stubValue = dataStream.readName()?.toString() + return KphpDocTagStubImpl(parentStub, this, name, stubValue) + } + + + /** + * Parse tag argument - 'T1, T2' — making T1 and T2 separate psi elements + * @see KphpDocGenericParameterDeclPsiImpl + */ + override fun getTagParser() = object : PhpDocTagParser() { + override fun getElementType() = KphpDocTagGenericElementType + + override fun parseContents(builder: PhpPsiBuilder): Boolean { + do { + val marker = builder.mark() + if (!builder.compareAndEat(PhpDocTokenTypes.DOC_IDENTIFIER)) { + marker.drop() + builder.error(PhpParserErrors.expected("Generic argument name (like T)")) + break + } + + if (builder.compare(PhpDocTokenTypes.DOC_TEXT)) { + val text = builder.tokenText?.trim() + builder.advanceLexer() + if (text == ":") { + if (!builder.compareAndEat(PhpDocTokenTypes.DOC_IDENTIFIER)) { + marker.drop() + builder.error(PhpParserErrors.expected("Extends class name")) + break + } + } + } + marker.done(KphpDocGenericParameterDeclPsiImpl.elementType) + } while (builder.compareAndEat(PhpDocTokenTypes.DOC_COMMA)) + return true + } + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagGenericPsiImpl.kt b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagGenericPsiImpl.kt new file mode 100644 index 0000000..d3601d9 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagGenericPsiImpl.kt @@ -0,0 +1,37 @@ +package com.vk.kphpstorm.kphptags.psi + +import com.intellij.lang.ASTNode +import com.intellij.psi.stubs.IStubElementType +import com.jetbrains.php.lang.documentation.phpdoc.psi.impl.tags.PhpDocTagImpl +import com.jetbrains.php.lang.documentation.phpdoc.psi.stubs.PhpDocTagStub + +/** + * Implementation of '@kphp-generic' tag — created either from stub or from ast + * @see KphpDocElementTypes.kphpDocTagGeneric + */ +class KphpDocTagGenericPsiImpl : PhpDocTagImpl, KphpDocTagImpl { + constructor(node: ASTNode) : super(node) + constructor(stub: PhpDocTagStub, nodeType: IStubElementType<*, *>) : super(stub, nodeType) + + // important! this function can be called when current file is not loaded, + // but we store all necessary information in stub + fun getGenericArguments(): List = + when (val stub = this.greenStub) { + null -> getGenericArgumentsFromAst() + else -> stub.value.let { // stub value is 'T1,T2' + if (it != null && it.isNotEmpty()) it.split(',') + else listOf() + } + } + + private fun getGenericArgumentsFromAst(): List { + val args = mutableListOf() + var child = this.firstChild + while (child != null) { + if (child is KphpDocGenericParameterDeclPsiImpl) + args.add(child.name) + child = child.nextSibling + } + return args + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagTemplateClassElementType.kt b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagTemplateClassElementType.kt index 2efa9df..1005c06 100644 --- a/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagTemplateClassElementType.kt +++ b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagTemplateClassElementType.kt @@ -40,7 +40,7 @@ object KphpDocTagTemplateClassElementType : PhpStubElementType() var child = this.firstChild while (child != null) { - if (child is KphpDocTplParameterDeclPsiImpl) + if (child is KphpDocGenericParameterDeclPsiImpl) args.add(child.text) child = child.nextSibling } diff --git a/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTplParameterDeclPsiImpl.kt b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTplParameterDeclPsiImpl.kt deleted file mode 100644 index 61556b5..0000000 --- a/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTplParameterDeclPsiImpl.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.vk.kphpstorm.kphptags.psi - -import com.intellij.lang.ASTNode -import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocElementType -import com.jetbrains.php.lang.documentation.phpdoc.psi.impl.PhpDocPsiElementImpl - -/** - * Inside '@kphp-template-class T1, T2' — 'T1' and 'T2' are separate psi elements of this impl - * @see KphpDocTagTemplateClassElementType.getTagParser - */ -class KphpDocTplParameterDeclPsiImpl(node: ASTNode) : PhpDocPsiElementImpl(node) { - companion object { - val elementType = PhpDocElementType("phpdocTplParameterDecl") - } - - override fun getName(): String? = text -} diff --git a/src/main/kotlin/com/vk/kphpstorm/typeProviders/ClassConstTypeProvider.kt b/src/main/kotlin/com/vk/kphpstorm/typeProviders/ClassConstTypeProvider.kt new file mode 100644 index 0000000..9a97cb1 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/typeProviders/ClassConstTypeProvider.kt @@ -0,0 +1,41 @@ +package com.vk.kphpstorm.typeProviders + +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.jetbrains.php.lang.psi.elements.ClassConstantReference +import com.jetbrains.php.lang.psi.elements.PhpNamedElement +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4 +import com.vk.kphpstorm.helpers.toExPhpType + +/** + * Нативный вывод типов PhpStorm считает что `Foo::class` это строка и + * это верно, однако ввиду такого упрощения мы теряем информацию и из-за + * этого могут быть проблемы с выводом шаблонных параметров. + * + * Поэтому данный провайдер добавляет новый тип `class-string(T)` для + * каждого выражения `Foo::class`. + */ +class ClassConstTypeProvider : PhpTypeProvider4 { + override fun getKey(): Char { + return '§' + } + + override fun getType(p: PsiElement): PhpType? { + if (p is ClassConstantReference) { + val classExType = p.classReference?.type?.toExPhpType() + if (classExType != null) { + return PhpType().add("class-string(${classExType})") + } + } + return null + } + + override fun complete(incompleteTypeStr: String, project: Project): PhpType? { + return null + } + + override fun getBySignature(typeStr: String, visited: MutableSet?, depth: Int, project: Project?): MutableCollection? { + return null + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/typeProviders/FunctionsTypeProvider.kt b/src/main/kotlin/com/vk/kphpstorm/typeProviders/FunctionsTypeProvider.kt index 030b047..19355a9 100644 --- a/src/main/kotlin/com/vk/kphpstorm/typeProviders/FunctionsTypeProvider.kt +++ b/src/main/kotlin/com/vk/kphpstorm/typeProviders/FunctionsTypeProvider.kt @@ -3,6 +3,7 @@ package com.vk.kphpstorm.typeProviders import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement import com.jetbrains.php.lang.psi.elements.* +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.* @@ -245,9 +246,23 @@ class FunctionsTypeProvider : PhpTypeProvider4 { } } - // for shape([...]) let it be just shape(), without detalization; all shapes are compatible with each other + // ввиду шаблонов нам может понадобиться точный тип для шейпов, поэтому мы выводим его здесь if (funcName == "shape") { - return PhpType().add("shape()") + val innerArray = p.parameters.firstOrNull() as? ArrayCreationExpression ?: return null + + val types = ArrayCreationExpressionImpl.children(innerArray).mapNotNull { + if (it !is ArrayHashElement) return@mapNotNull null + if (it.value !is PhpTypedElement) return@mapNotNull null + if (it.key == null) return@mapNotNull null + if (it.key !is StringLiteralExpression) return@mapNotNull null + + val key = (it.key as StringLiteralExpression).contents + val type = (it.value as PhpTypedElement).type.global(p.project).toStringAsNested() + + "$key:$type" + }.joinToString(",") + + return PhpType().add("shape($types)") } // println("unhandled function: $funcName") diff --git a/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericFunctionsTypeProvider.kt b/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericFunctionsTypeProvider.kt new file mode 100644 index 0000000..532c659 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericFunctionsTypeProvider.kt @@ -0,0 +1,95 @@ +package com.vk.kphpstorm.typeProviders + +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.jetbrains.php.PhpIndex +import com.jetbrains.php.lang.psi.elements.FunctionReference +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.ExPhpTypeNullable +import com.vk.kphpstorm.exphptype.ExPhpTypePipe +import com.vk.kphpstorm.exphptype.ExPhpTypeTplInstantiation +import com.vk.kphpstorm.generics.GenericFunctionCall +import com.vk.kphpstorm.generics.GenericFunctionUtil.genericNames +import com.vk.kphpstorm.generics.GenericFunctionUtil.isReturnGeneric +import com.vk.kphpstorm.helpers.toExPhpType +import kotlin.math.min + +class GenericFunctionsTypeProvider : PhpTypeProvider4 { + companion object { + private val KEY = object : PhpCharBasedTypeKey() { + override fun getKey(): Char { + return 'П' + } + } + } + + override fun getKey() = KEY.key + + override fun getType(p: PsiElement): PhpType? { + if (p !is FunctionReference) { + return null + } + + val call = GenericFunctionCall(p) + call.resolveFunction() + if (call.function == null) return null + + // Если возвращаемый тип функции не зависит от шаблонного типа, + // то нет смысла как-то уточнять ее тип. + if (!call.function!!.isReturnGeneric()) { + return null + } + + val specs = call.explicitSpecs.ifEmpty { call.implicitSpecs } + + val specTypes = specs.joinToString(",") + + return PhpType().add("#П${call.function!!.fqn}<$specTypes>") + } + + override fun complete(incompleteTypeStr: String, project: Project): PhpType? { + if (!KEY.signed(incompleteTypeStr)) { + return null + } + + val functionName = incompleteTypeStr.substring(2 until incompleteTypeStr.indexOf('<')) + val function = PhpIndex.getInstance(project).getFunctionsByFQN(functionName).firstOrNull() ?: return null + + val lhsTypeStr = incompleteTypeStr.substring(2) + val lhsType = PhpType().add(lhsTypeStr).global(project) + val parsed = lhsType.toExPhpType() + + val instantiation = when (parsed) { + is ExPhpTypePipe -> parsed.items.firstOrNull { it is ExPhpTypeTplInstantiation } + is ExPhpTypeNullable -> parsed.inner + else -> parsed + } as? ExPhpTypeTplInstantiation ?: return null + + val docTNames = function.genericNames() + + val specializationNameMap = mutableMapOf() + + for (i in 0 until min(docTNames.size, instantiation.specializationList.size)) { + specializationNameMap[docTNames[i]] = instantiation.specializationList[i] + } + + val methodReturnTag = function.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/GenericObjectAccessTypeProvider.kt b/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericObjectAccessTypeProvider.kt new file mode 100644 index 0000000..1b2dd43 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericObjectAccessTypeProvider.kt @@ -0,0 +1,271 @@ +package com.vk.kphpstorm.typeProviders + +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.search.searches.ReferencesSearch +import com.intellij.util.applyIf +import com.jetbrains.php.PhpIndex +import com.jetbrains.php.lang.psi.PhpPsiUtil +import com.jetbrains.php.lang.psi.elements.Function +import com.jetbrains.php.lang.psi.elements.PhpNamedElement +import com.jetbrains.php.lang.psi.elements.PhpTypedElement +import com.jetbrains.php.lang.psi.elements.impl.* +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.ExPhpTypeNullable +import com.vk.kphpstorm.exphptype.ExPhpTypePipe +import com.vk.kphpstorm.exphptype.ExPhpTypeTplInstantiation +import com.vk.kphpstorm.generics.psi.GenericInstantiationPsiCommentImpl +import com.vk.kphpstorm.helpers.toExPhpType +import com.vk.kphpstorm.kphptags.psi.KphpDocTagTemplateClassPsiImpl +import kotlin.math.min + +/** + * Experimental! + * Do not enable it in plugin.xml for production!!! Will hang working IDE. + * Will be reconsidered later. + * + * Support for generics, i.e. template class ItemWrapper, @param ItemWrapper + * KPHP has no support for them now, it's just experiments with IDE for future. + * + * TODO: Переписать + */ +class GenericObjectAccessTypeProvider : PhpTypeProvider4 { + override fun getKey(): Char { + return 'Щ' + } + + override fun getType(p: PsiElement): PhpType? { + // detect type only for $v->p and $v->f(), but NOT for A::$p, + // because $v::$p is ugly and unsupported (only A::$p), but A::* in case of template classes is useless imho + // (as we need A::*, I didn't provide such syntax, doesn't make sense) + + if (p is VariableImpl) { + val containingFunction = PhpPsiUtil.getParentByCondition(p) { + it is Function + } as? Function + + val docT = containingFunction?.docComment?.getTagElementsByName("@kphp-param")?.firstOrNull() + if (docT != null) { + val classGenericName = docT.tagValue.split(" ")[0] + if (classGenericName.contains("<") && classGenericName.contains(">")) { +// val templateParams = classTemplateName.slice( +// classTemplateName.indexOf("<") + 1 until classTemplateName.indexOf(">") +// ) + + val templateParams = classGenericName.slice( + classGenericName.indexOf("<") until classGenericName.length + ).replace(" ", "") + + val lhsType = p.inferredType + if (lhsType.isEmpty) { + return PhpType().add("$classGenericName in ${containingFunction.fqn}") + } + + val resultType = PhpType() + lhsType.types.forEach { + resultType.add("#Щ$it$templateParams in ${containingFunction.fqn}") + } + return resultType + } + } + } + + // $v->p + if (p is FieldReferenceImpl && !p.isStatic) { + val propertyName = p.name ?: return null + val lhs = p.classReference ?: return null + val lhsType = lhs.type + + // optimization isComplete not done + + val resultType = PhpType() + lhsType.types.forEach { + resultType.add("#Щ.$propertyName $it") + } +// println("type($lhs) = ${resultType.toString().replace("|", " | ")}") + return resultType + } + + // $v->f() + if (p is MethodReferenceImpl && !p.isStatic) { + val methodName = p.name ?: return null + val lhs = p.classReference ?: return null + val lhsType = lhs.type + + // optimization isComplete not done + + val method = PhpIndex.getInstance(p.project).getClassesByFQN(p.fqn).firstOrNull() + + val docT = method?.docComment?.getTagElementsByName("@kphp-generic")?.firstOrNull() + val docTNames = docT?.tagValue?.let { listOf(it) } ?: emptyList() + + val paramList = + when (val element = p.element) { + is FunctionReferenceImpl -> element.parameterList + is MethodReferenceImpl -> element.parameterList + else -> null + } + + val specializationList = paramList?.parameters?.mapNotNull { + if (it is PhpTypedElement) { + it.type.toExPhpType() + } else { + null + } + } + + if (specializationList != null) { + val specializationNameMap = mutableMapOf() + for (i in 0 until min(docTNames.size, specializationList.size)) + specializationNameMap["\\" + docTNames[i]] = specializationList[i] + } + + val resultType = PhpType() + lhsType.types.forEach { + resultType.add("#Щ:$methodName $it") + } +// println("type($lhs) = ${resultType.toString().replace("|", " | ")}") + return resultType + } + + // new A/*<...args>*/ + if (p is NewExpressionImpl) { + // todo there is no psi inside C-style comment for now, just parse from string and suppose in has only 1 arg for demo + // and this arg, if it's a class, must be fqn, not relative + // (this will be simplified after having psi in C-style comment) + val specComment = p.firstPsiChild?.nextSibling?.takeIf { + it is GenericInstantiationPsiCommentImpl + } ?: return null + val specTypeStr = specComment.text.substring(3, specComment.text.length - 3) + val specTypesStr = specTypeStr.split(",").map { it.trim() } + + val specTypes = specTypesStr.mapNotNull { + PhpType().add(it).toExPhpType()?.toString() + }.joinToString(",") + + val classRef = p.classReference ?: return null + +// println("type(new) = ${classRef.fqn}<$specType>") + return PhpType().add("${classRef.fqn}<$specTypes>") + } + + return null + } + + override fun complete(incompleteTypeStr: String, project: Project): PhpType? { + // optimization searching for "<" (not to parse otherwise) not done + val isMethod = incompleteTypeStr[2] == ':' + val spacePos = incompleteTypeStr.indexOf(' ') + val inPos = incompleteTypeStr.indexOf(" in ").applyIf(incompleteTypeStr.indexOf(" in ") == -1) { + incompleteTypeStr.length + } + val memberName = if (spacePos != -1) incompleteTypeStr.substring(3, spacePos) else "" + val lhsTypeStr = if (spacePos + 1 < inPos) incompleteTypeStr.substring(spacePos + 1, inPos) else "" + + val lhsType = PhpType().add(lhsTypeStr).global(project) + val parsed = lhsType.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 null + + val specializationList = mutableListOf() + specializationList.addAll(instantiation.specializationList) + + // TODO: удалить + val containsUnresolvedTemplateTypes = specializationList.any { + it != null + } + + if (containsUnresolvedTemplateTypes && incompleteTypeStr.contains(" in ")) { + val containingFunctionName = incompleteTypeStr.substring(inPos + " in ".length) + val containingFunctions = PhpIndex.getInstance(project).getFunctionsByFQN(containingFunctionName) + + val callParamsTypes = mutableMapOf() + + containingFunctions.forEach { + val refs = ReferencesSearch.search(it) + refs.forEach forEachRefs@{ ref -> + if (ref !is FunctionReferenceImpl) { + return@forEachRefs + } + + val params = ref.parameterList + params?.parameters?.forEach { paramType -> + if (paramType is PhpTypedElement) { + val type = paramType.type.toExPhpType() + + val instant = when (type) { + is ExPhpTypePipe -> type.items.firstOrNull { it is ExPhpTypeTplInstantiation } + is ExPhpTypeNullable -> type.inner + else -> type + } as? ExPhpTypeTplInstantiation + + instant?.specializationList?.forEachIndexed { index, exPhpType -> + val paramTypes = callParamsTypes[index]?.add(exPhpType.toPhpType()) + ?: exPhpType.toPhpType() + callParamsTypes[index] = paramTypes + } + } + } + + } + } + + specializationList.forEachIndexed { index, spec -> +// if (spec is ExPhpTypeUnresolved) { +// val typeFromCalls = callParamsTypes[index] ?: return@forEachIndexed +// +// specializationList[index] = typeFromCalls.toExPhpType()!! +// } + } + } + + val phpClass = PhpIndex.getInstance(project).getClassesByFQN(instantiation.classFqn).firstOrNull() + ?: return null + val docT = phpClass.docComment?.getTagElementsByName("@kphp-template-class")?.firstOrNull() as? KphpDocTagTemplateClassPsiImpl + ?: return null + val docTNames = docT.getTemplateArguments() + val specializationNameMap = mutableMapOf() + for (i in 0 until min(docTNames.size, specializationList.size)) + specializationNameMap["\\" + docTNames[i]] = specializationList[i] + + if (isMethod) { + val classMethod = phpClass.findMethodByName(memberName) ?: return null + val methodReturnTag = classMethod.docComment?.returnTag ?: return null + val methodTypeParsed = methodReturnTag.type.toExPhpType() ?: return null + val methodTypeSpecialized = methodTypeParsed.instantiateGeneric(specializationNameMap) +// println("specialized ->$memberName() as $methodTypeSpecialized") + + return methodTypeSpecialized.toPhpType() + } + else { + val classField = phpClass.findFieldByName(memberName, false) ?: return null + val fieldVarTag = classField.docComment?.varTag ?: return null + val fieldTypeParsed = fieldVarTag.type.toExPhpType() ?: return null + val fieldTypeSpecialized = fieldTypeParsed.instantiateGeneric(specializationNameMap) +// println("specialized ->$memberName as $fieldTypeSpecialized") + + return fieldTypeSpecialized.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/TemplateObjectAccessTypeProvider.kt b/src/main/kotlin/com/vk/kphpstorm/typeProviders/TemplateObjectAccessTypeProvider.kt deleted file mode 100644 index 0ca5e4c..0000000 --- a/src/main/kotlin/com/vk/kphpstorm/typeProviders/TemplateObjectAccessTypeProvider.kt +++ /dev/null @@ -1,153 +0,0 @@ -package com.vk.kphpstorm.typeProviders - -import com.intellij.openapi.project.Project -import com.intellij.psi.PsiElement -import com.intellij.psi.util.elementType -import com.jetbrains.php.PhpIndex -import com.jetbrains.php.lang.lexer.PhpTokenTypes -import com.jetbrains.php.lang.psi.elements.PhpNamedElement -import com.jetbrains.php.lang.psi.elements.impl.FieldReferenceImpl -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.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4 -import com.vk.kphpstorm.exphptype.ExPhpType -import com.vk.kphpstorm.exphptype.ExPhpTypeNullable -import com.vk.kphpstorm.exphptype.ExPhpTypePipe -import com.vk.kphpstorm.exphptype.ExPhpTypeTplInstantiation -import com.vk.kphpstorm.helpers.toExPhpType -import com.vk.kphpstorm.kphptags.psi.KphpDocTagTemplateClassPsiImpl -import kotlin.math.min - - -/** - * Experimental! - * Do not enable it in plugin.xml for production!!! Will hang working IDE. - * Will be reconsidered later. - * - * Support for generics, i.e. template class ItemWrapper, @param ItemWrapper - * KPHP has no support for them now, it's just experiments with IDE for future. - */ -class TemplateObjectAccessTypeProvider : PhpTypeProvider4 { - override fun getKey(): Char { - return 'Щ' - } - - override fun getType(p: PsiElement): PhpType? { - // detect type only for $v->p and $v->f(), but NOT for A::$p, - // because $v::$p is ugly and unsupported (only A::$p), but A::* in case of template classes is useless imho - // (as we need A::*, I didn't provide such syntax, doesn't make sense) - - // $v->p - if (p is FieldReferenceImpl && !p.isStatic) { - val propertyName = p.name ?: return null - val lhs = p.classReference ?: return null - val lhsType = lhs.type - - // optimization isComplete not done - - val resultType = PhpType() - lhsType.types.forEach { - resultType.add("#Щ.$propertyName $it") - } -// println("type($lhs) = ${resultType.toString().replace("|", " | ")}") - return resultType - } - - // $v->f() - if (p is MethodReferenceImpl && !p.isStatic) { - val methodName = p.name ?: return null - val lhs = p.classReference ?: return null - val lhsType = lhs.type - - // optimization isComplete not done - - val resultType = PhpType() - lhsType.types.forEach { - resultType.add("#Щ:$methodName $it") - } -// println("type($lhs) = ${resultType.toString().replace("|", " | ")}") - return resultType - } - - // new A/*<...args>*/ - if (p is NewExpressionImpl) { - // todo there is no psi inside C-style comment for now, just parse from string and suppose in has only 1 arg for demo - // and this arg, if it's a class, must be fqn, not relative - // (this will be simplified after having psi in C-style comment) - val specComment = p.firstPsiChild?.nextSibling?.takeIf { - it.elementType == PhpTokenTypes.C_STYLE_COMMENT && - it.text.startsWith("/*<") && - it.text.endsWith(">*/") - } ?: return null - val specTypeStr = specComment.text.substring(3, specComment.text.length - 3) - val specType = PhpType().add(specTypeStr).toExPhpType() ?: return null - val classRef = p.classReference ?: return null - -// println("type(new) = ${classRef.fqn}<$specType>") - return PhpType().add("${classRef.fqn}<$specType>") - } - - return null - } - - override fun complete(incompleteTypeStr: String, project: Project): PhpType? { - // optimization searching for "<" (not to parse otherwise) not done - val isMethod = incompleteTypeStr[2] == ':' - val spacePos = incompleteTypeStr.indexOf(' ') - val memberName = incompleteTypeStr.substring(3, spacePos) - val lhsTypeStr = incompleteTypeStr.substring(spacePos + 1) - - val lhsType = PhpType().add(lhsTypeStr).global(project) - val parsed = lhsType.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 null - - val phpClass = PhpIndex.getInstance(project).getClassesByFQN(instantiation.classFqn).firstOrNull() - ?: return null - val docT = phpClass.docComment?.getTagElementsByName("@kphp-template-class")?.firstOrNull() as? KphpDocTagTemplateClassPsiImpl - ?: return null - val docTNames = docT.getTemplateArguments() - val specializationNameMap = mutableMapOf() - for (i in 0 until min(docTNames.size, instantiation.specializationList.size)) - specializationNameMap["\\" + docTNames[i]] = instantiation.specializationList[i] - - if (isMethod) { - val classMethod = phpClass.findMethodByName(memberName) ?: return null - val methodReturnTag = classMethod.docComment?.returnTag ?: return null - val methodTypeParsed = methodReturnTag.type.toExPhpType() ?: return null - val methodTypeSpecialized = methodTypeParsed.instantiateTemplate(specializationNameMap) -// println("specialized ->$memberName() as $methodTypeSpecialized") - - return methodTypeSpecialized.toPhpType() - } - else { - val classField = phpClass.findFieldByName(memberName, false) ?: return null - val fieldVarTag = classField.docComment?.varTag ?: return null - val fieldTypeParsed = fieldVarTag.type.toExPhpType() ?: return null - val fieldTypeSpecialized = fieldTypeParsed.instantiateTemplate(specializationNameMap) -// println("specialized ->$memberName as $fieldTypeSpecialized") - - return fieldTypeSpecialized.toPhpType() - } - - } - - override fun getBySignature(typeStr: String, visited: MutableSet?, depth: Int, project: Project?): MutableCollection? { - return null - } -} - - - - - - - - diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 840713e..b9a0e36 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -63,6 +63,24 @@ enabledByDefault="false" level="WARNING" implementationClass="com.vk.kphpstorm.inspections.RedundantCastInspection"/> + + + + + + com.vk.kphpstorm.inspections.PrettifyPhpdocBlockIntention PHP @@ -77,8 +95,14 @@ implementationClass="com.vk.kphpstorm.completion.KphpStormCompletionContributor"/> + + + @@ -86,13 +110,25 @@ implementationClass="com.vk.kphpstorm.highlighting.KphpStormTypeInfoProvider"/> + + + + + - + + +