diff --git a/build.gradle b/build.gradle index 3539063..b6a4ad6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ plugins { id 'java' id 'idea' - id 'org.jetbrains.intellij' version '1.5.3' // https://github.com/JetBrains/gradle-intellij-plugin - id 'org.jetbrains.kotlin.jvm' version '1.6.10' // https://plugins.gradle.org/plugin/org.jetbrains.kotlin.jvm + id 'org.jetbrains.intellij' version '1.6.0' // https://github.com/JetBrains/gradle-intellij-plugin + id 'org.jetbrains.kotlin.jvm' version "1.7.0-RC" // https://plugins.gradle.org/plugin/org.jetbrains.kotlin.jvm } group 'com.vk' @@ -15,8 +15,12 @@ repositories { url 'https://www.jetbrains.com/intellij-repository/snapshots' } } + dependencies { - testImplementation group: 'junit', name: 'junit', version: '4.13.2' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' + testRuntimeOnly 'org.junit.vintage:junit-vintage-engine:5.8.2' + testCompileOnly 'junit:junit:4.13.2' } idea { @@ -30,17 +34,37 @@ idea { } } +compileKotlin { + sourceCompatibility = 11 + targetCompatibility = 11 +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + freeCompilerArgs += '-Xjvm-default=all' + jvmTarget = '11' + } +} + +wrapper { + gradleVersion = "7.4" +} + 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 = '2022.1' + version = '222.2964-EAP-CANDIDATE-SNAPSHOT' +// version = '2022.1.1' +// version = '2021.3.1' plugins = [ - 'com.jetbrains.php:221.5080.224', // https://plugins.jetbrains.com/plugin/6610-php/versions + 'com.jetbrains.php:222.2964.55', // https://plugins.jetbrains.com/plugin/6610-php/versions +// 'com.jetbrains.php:221.5591.58', +// 'com.jetbrains.php:213.5744.223' ] } 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 { @@ -48,8 +72,7 @@ patchPluginXml { // is only for LATEST version and should be updated on every version bump changeNotes = """ """ } @@ -65,6 +88,8 @@ sourceSets { } } test { + useJUnitPlatform() + exclude 'com/vk/kphpstorm/testing/infrastructure/**' include "**/*Test.class" scanForTestClasses false 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..cf38dfa --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/KphpStormASTFactory.kt @@ -0,0 +1,16 @@ +package com.vk.kphpstorm + +import com.intellij.lang.DefaultASTFactoryImpl +import com.intellij.psi.impl.source.tree.LeafElement +import com.intellij.psi.tree.IElementType +import com.vk.kphpstorm.generics.psi.GenericInstantiationPsiCommentImpl + +class KphpStormASTFactory : DefaultASTFactoryImpl() { + override fun createComment(type: IElementType, text: CharSequence): LeafElement { + if (text.startsWith("/*<") && text.endsWith(">*/")) { + return GenericInstantiationPsiCommentImpl(type, text) + } + + return super.createComment(type, text) + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/KphpStormParserDefinition.kt b/src/main/kotlin/com/vk/kphpstorm/KphpStormParserDefinition.kt index 50f8442..4d58935 100644 --- a/src/main/kotlin/com/vk/kphpstorm/KphpStormParserDefinition.kt +++ b/src/main/kotlin/com/vk/kphpstorm/KphpStormParserDefinition.kt @@ -1,11 +1,23 @@ package com.vk.kphpstorm import com.intellij.lang.ASTNode +import com.intellij.lang.injection.MultiHostInjector +import com.intellij.lang.injection.MultiHostRegistrar +import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiElement +import com.intellij.psi.impl.source.tree.PsiCommentImpl +import com.intellij.psi.util.parentOfType +import com.jetbrains.php.lang.PhpLanguage import com.jetbrains.php.lang.documentation.phpdoc.parser.tags.PhpDocTagParserRegistry import com.jetbrains.php.lang.parser.PhpParserDefinition import com.jetbrains.php.lang.parser.PhpPsiElementCreator +import com.jetbrains.php.lang.psi.PhpFile +import com.jetbrains.php.lang.psi.elements.Function +import com.jetbrains.php.lang.psi.elements.PhpClass +import com.jetbrains.php.lang.psi.elements.PhpUse import com.vk.kphpstorm.exphptype.psi.* +import com.vk.kphpstorm.generics.GenericUtil.genericNames +import com.vk.kphpstorm.generics.psi.GenericInstantiationPsiCommentImpl import com.vk.kphpstorm.kphptags.ALL_KPHPDOC_TAGS import com.vk.kphpstorm.kphptags.psi.* @@ -30,25 +42,28 @@ 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) - - 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) - - else -> PhpPsiElementCreator.create(node) + KphpDocElementTypes.kphpDocTagSimple -> KphpDocTagSimplePsiImpl(node) + KphpDocElementTypes.kphpDocTagGeneric -> KphpDocTagGenericPsiImpl(node) + KphpDocGenericParameterDeclPsiImpl.elementType -> KphpDocGenericParameterDeclPsiImpl(node) + KphpDocInheritParameterDeclPsiImpl.elementType -> KphpDocInheritParameterDeclPsiImpl(node) + KphpDocElementTypes.kphpDocTagInherit -> KphpDocTagInheritPsiImpl(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) + ExPhpTypeClassStringPsiImpl.elementType -> ExPhpTypeClassStringPsiImpl(node) + ExPhpTypeForcingPsiImpl.elementType -> ExPhpTypeForcingPsiImpl(node) + + else -> PhpPsiElementCreator.create(node) } } } @@ -58,3 +73,35 @@ class KphpStormParserDefinition() : PhpParserDefinition() { * This has no sense but correct plugin.xml validity while development */ class FakePhpLanguage : com.intellij.lang.Language("PHP") + +class GenericsInstantiationInjector : MultiHostInjector { + override fun getLanguagesToInject(registrar: MultiHostRegistrar, context: PsiElement) { + if (context is GenericInstantiationPsiCommentImpl) { + val file = context.containingFile as? PhpFile ?: return + + val namespace = file.mainNamespaceName?.trim('\\') ?: "" + val usesText = file.topLevelDefs.values().filterIsInstance().joinToString("\n") { it.parent.text } + + val parentFunctionGenericT = context.parentOfType()?.genericNames() ?: emptyList() + val parentClassGenericT = context.parentOfType()?.genericNames() ?: emptyList() + val genericT = (parentFunctionGenericT + parentClassGenericT).joinToString(", ") { it.name } + + val start = context.startOffset - context.textOffset + val range = TextRange(start + 3, start + context.textLength - 3) + registrar.startInjecting(PhpLanguage.INSTANCE) + .addPlace( + """): 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..5654d4d 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeCallable.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeCallable.kt @@ -21,13 +21,15 @@ 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) { is ExPhpTypeAny -> true + // TODO: добавить полноценную проверку? is ExPhpTypeCallable -> true + is ExPhpTypePrimitive -> rhs == ExPhpType.CALLABLE || rhs == ExPhpType.STRING is ExPhpTypeNullable -> isAssignableFrom(rhs.inner, project) else -> false } 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..fadd642 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeClassString.kt @@ -0,0 +1,45 @@ +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 (e.g. `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().add("class-string(${inner.toPhpType()})") + + override fun getSubkeyByIndex(indexKey: String) = this + + override fun instantiateGeneric(nameMap: Map): ExPhpType { + // TODO: подумать тут + val fqn = when (inner) { + is ExPhpTypeInstance -> inner.fqn + is ExPhpTypeGenericsT -> inner.nameT + else -> "" + } + + val name = nameMap[fqn] ?: return this + if (name is ExPhpTypeGenericsT) { + return ExPhpTypeClassString(ExPhpTypeGenericsT(name.toString())) + } + + return ExPhpTypeClassString(ExPhpTypeInstance(name.toString())) + } + + override fun isAssignableFrom(rhs: ExPhpType, project: Project): Boolean = when (rhs) { + // class-string is only compatible with class-string + // if class E is compatible with class T. + is ExPhpTypeClassString -> inner.isAssignableFrom(rhs.inner, project) + else -> false + } +} 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/ExPhpTypeGenericsT.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeGenericsT.kt new file mode 100644 index 0000000..6e3fd6e --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeGenericsT.kt @@ -0,0 +1,29 @@ +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 = PhpType.EMPTY + + override fun getSubkeyByIndex(indexKey: String) = 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..15cdd54 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 { diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePrimitive.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePrimitive.kt index 7f9c001..0f8dd33 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePrimitive.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePrimitive.kt @@ -23,20 +23,22 @@ 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 + is ExPhpTypeCallable -> this === ExPhpType.CALLABLE || 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..37c2afd 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTplInstantiation.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTplInstantiation.kt @@ -1,6 +1,8 @@ package com.vk.kphpstorm.exphptype import com.intellij.openapi.project.Project +import com.jetbrains.php.PhpClassHierarchyUtils +import com.jetbrains.php.PhpIndex import com.jetbrains.php.codeInsight.PhpCodeInsightUtil import com.jetbrains.php.lang.psi.elements.PhpPsiElement import com.jetbrains.php.lang.psi.resolve.types.PhpType @@ -9,27 +11,42 @@ import com.jetbrains.php.lang.psi.resolve.types.PhpType * C, e.g. Wrapper, Container[]>, future */ class ExPhpTypeTplInstantiation(val classFqn: String, val specializationList: List) : ExPhpType { - override fun toString() = "$classFqn<${specializationList.joinToString(",")}>" + override fun toString() = "$classFqn(${specializationList.joinToString(",")})" - override fun toHumanReadable(expr: PhpPsiElement) = "${PhpCodeInsightUtil.createQualifiedName(PhpCodeInsightUtil.findScopeForUseOperator(expr)!!, classFqn)}<${specializationList.joinToString(",") { it.toHumanReadable(expr) }}>" + override fun toHumanReadable(expr: PhpPsiElement) = "${PhpCodeInsightUtil.createQualifiedName(PhpCodeInsightUtil.findScopeForUseOperator(expr)!!, classFqn)}(${specializationList.joinToString(",") { it.toHumanReadable(expr) }})" override fun toPhpType(): PhpType { - return PhpType().add("$classFqn<${specializationList.joinToString(",")}>") + return PhpType().add("$classFqn(${specializationList.joinToString(",")})") } override fun getSubkeyByIndex(indexKey: String): ExPhpType? { 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) } override fun isAssignableFrom(rhs: ExPhpType, project: Project): Boolean = when (rhs) { is ExPhpTypeAny -> true // not finished + is ExPhpTypePipe -> rhs.items.any { it.isAssignableFrom(this, project) } is ExPhpTypeTplInstantiation -> classFqn == rhs.classFqn && specializationList.size == rhs.specializationList.size + + is ExPhpTypeInstance -> rhs.fqn == classFqn || run { + val phpIndex = PhpIndex.getInstance(project) + val lhsClass = phpIndex.getAnyByFQN(classFqn).firstOrNull() ?: return false + var rhsIsChild = false + phpIndex.getAnyByFQN(rhs.fqn).forEach { rhsClass -> + PhpClassHierarchyUtils.processSuperWithoutMixins(rhsClass, true, true) { clazz -> + if (PhpClassHierarchyUtils.classesEqual(lhsClass, clazz)) + rhsIsChild = true + !rhsIsChild + } + } + rhsIsChild + } else -> false } } diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/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..32516a2 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 @@ -18,86 +18,85 @@ object PhpTypeToExPhpTypeParsing { * 2) optimization for widely used types and primitive arrays, not to invoke regexp and objects allocation */ private val FQN_PREPARSED = sortedMapOf( - // synonyms are also listed here, like in mapStringToPhpType - "int" to ExPhpType.INT, - "float" to ExPhpType.FLOAT, - "double" to ExPhpType.FLOAT, - "string" to ExPhpType.STRING, - "bool" to ExPhpType.BOOL, - "boolean" to ExPhpType.BOOL, - "true" to ExPhpType.BOOL, - "false" to ExPhpType.FALSE, - "null" to ExPhpType.NULL, - "object" to ExPhpType.OBJECT, - "callable" to ExPhpType.CALLABLE, - "Closure" to ExPhpType.CALLABLE, - "void" to ExPhpType.VOID, - "resource" to ExPhpType.INT, - "kmixed" to ExPhpType.KMIXED, - - // PhpType().add("int") — types[0] is "\int", so we need everything with leading slash - "\\int" to ExPhpType.INT, - "\\float" to ExPhpType.FLOAT, - "\\double" to ExPhpType.FLOAT, - "\\string" to ExPhpType.STRING, - "\\bool" to ExPhpType.BOOL, - "\\boolean" to ExPhpType.BOOL, - "\\true" to ExPhpType.BOOL, - "\\false" to ExPhpType.FALSE, - "\\null" to ExPhpType.NULL, - "\\object" to ExPhpType.OBJECT, - "\\callable" to ExPhpType.CALLABLE, - "\\Closure" to ExPhpType.CALLABLE, - "\\void" to ExPhpType.VOID, - "\\resource" to ExPhpType.INT, - "\\kmixed" to ExPhpType.KMIXED, - - // arrays of primitives also meet quite often, make them preparsed - "int[]" to ExPhpTypeArray(ExPhpType.INT), - "float[]" to ExPhpTypeArray(ExPhpType.FLOAT), - "string[]" to ExPhpTypeArray(ExPhpType.STRING), - "bool[]" to ExPhpTypeArray(ExPhpType.BOOL), - "false[]" to ExPhpTypeArray(ExPhpType.FALSE), - "null[]" to ExPhpTypeArray(ExPhpType.NULL), - "object[]" to ExPhpTypeArray(ExPhpType.OBJECT), - "callable[]" to ExPhpTypeArray(ExPhpType.CALLABLE), - "kmixed[]" to ExPhpTypeArray(ExPhpType.KMIXED), - - // same arrays of primitives with leading slash - "\\int[]" to ExPhpTypeArray(ExPhpType.INT), - "\\float[]" to ExPhpTypeArray(ExPhpType.FLOAT), - "\\string[]" to ExPhpTypeArray(ExPhpType.STRING), - "\\bool[]" to ExPhpTypeArray(ExPhpType.BOOL), - "\\false[]" to ExPhpTypeArray(ExPhpType.FALSE), - "\\null[]" to ExPhpTypeArray(ExPhpType.NULL), - "\\object[]" to ExPhpTypeArray(ExPhpType.OBJECT), - "\\callable[]" to ExPhpTypeArray(ExPhpType.CALLABLE), - "\\kmixed[]" to ExPhpTypeArray(ExPhpType.KMIXED), - - // 'any' from phpdoc has a special instantiation - "any" to ExPhpType.ANY, - "\\any" to ExPhpType.ANY, - - // "array" from phpdoc and "array" emerged by PhpStorm internals is "any[]" - "array" to ExPhpType.ARRAY_OF_ANY, - "\\array" to ExPhpType.ARRAY_OF_ANY, - "any[]" to ExPhpType.ARRAY_OF_ANY, - "\\any[]" to ExPhpType.ARRAY_OF_ANY, - - // Important! - // 'mixed' in phpdoc is treated as 'kmixed', that's why - // 'mixed' can emerge only by PhpStorm internal inferring, when it couldn't detect the type or types are really mixed - // (for example, [1,'2'] is mixed[] and [new A, new ADevired] is mixed[] in native PhpStorm inferring) - // so, if PhpStorm couldn't detect, it can be really anything, - // and we don't have any reason to produce errors and assume whether it is compatible with mixed or not - "mixed" to ExPhpType.ANY, - "\\mixed" to ExPhpType.ANY, - - // some "forced" that can occur often, @see [ForcingTypeProvider], \\ not needed - "force(string)" to ExPhpTypeForcing(ExPhpType.STRING), - "force(int)" to ExPhpTypeForcing(ExPhpType.INT), - "force(kmixed)" to ExPhpTypeForcing(ExPhpType.KMIXED), - "force(any)" to ExPhpTypeForcing(ExPhpType.ANY) + // synonyms are also listed here, like in mapStringToPhpType + "int" to ExPhpType.INT, + "float" to ExPhpType.FLOAT, + "double" to ExPhpType.FLOAT, + "string" to ExPhpType.STRING, + "bool" to ExPhpType.BOOL, + "boolean" to ExPhpType.BOOL, + "true" to ExPhpType.BOOL, + "false" to ExPhpType.FALSE, + "null" to ExPhpType.NULL, + "object" to ExPhpType.OBJECT, + "callable" to ExPhpType.CALLABLE, + "Closure" to ExPhpType.CALLABLE, + "void" to ExPhpType.VOID, + "resource" to ExPhpType.INT, + "kmixed" to ExPhpType.KMIXED, + + // PhpType().add("int") — types[0] is "\int", so we need everything with leading slash + "\\int" to ExPhpType.INT, + "\\float" to ExPhpType.FLOAT, + "\\double" to ExPhpType.FLOAT, + "\\string" to ExPhpType.STRING, + "\\bool" to ExPhpType.BOOL, + "\\boolean" to ExPhpType.BOOL, + "\\true" to ExPhpType.BOOL, + "\\false" to ExPhpType.FALSE, + "\\null" to ExPhpType.NULL, + "\\object" to ExPhpType.OBJECT, + "\\callable" to ExPhpType.CALLABLE, + "\\void" to ExPhpType.VOID, + "\\resource" to ExPhpType.INT, + "\\kmixed" to ExPhpType.KMIXED, + + // arrays of primitives also meet quite often, make them preparsed + "int[]" to ExPhpTypeArray(ExPhpType.INT), + "float[]" to ExPhpTypeArray(ExPhpType.FLOAT), + "string[]" to ExPhpTypeArray(ExPhpType.STRING), + "bool[]" to ExPhpTypeArray(ExPhpType.BOOL), + "false[]" to ExPhpTypeArray(ExPhpType.FALSE), + "null[]" to ExPhpTypeArray(ExPhpType.NULL), + "object[]" to ExPhpTypeArray(ExPhpType.OBJECT), + "callable[]" to ExPhpTypeArray(ExPhpType.CALLABLE), + "kmixed[]" to ExPhpTypeArray(ExPhpType.KMIXED), + + // same arrays of primitives with leading slash + "\\int[]" to ExPhpTypeArray(ExPhpType.INT), + "\\float[]" to ExPhpTypeArray(ExPhpType.FLOAT), + "\\string[]" to ExPhpTypeArray(ExPhpType.STRING), + "\\bool[]" to ExPhpTypeArray(ExPhpType.BOOL), + "\\false[]" to ExPhpTypeArray(ExPhpType.FALSE), + "\\null[]" to ExPhpTypeArray(ExPhpType.NULL), + "\\object[]" to ExPhpTypeArray(ExPhpType.OBJECT), + "\\callable[]" to ExPhpTypeArray(ExPhpType.CALLABLE), + "\\kmixed[]" to ExPhpTypeArray(ExPhpType.KMIXED), + + // 'any' from phpdoc has a special instantiation + "any" to ExPhpType.ANY, + "\\any" to ExPhpType.ANY, + + // "array" from phpdoc and "array" emerged by PhpStorm internals is "any[]" + "array" to ExPhpType.ARRAY_OF_ANY, + "\\array" to ExPhpType.ARRAY_OF_ANY, + "any[]" to ExPhpType.ARRAY_OF_ANY, + "\\any[]" to ExPhpType.ARRAY_OF_ANY, + + // Important! + // 'mixed' in phpdoc is treated as 'kmixed', that's why + // 'mixed' can emerge only by PhpStorm internal inferring, when it couldn't detect the type or types are really mixed + // (for example, [1,'2'] is mixed[] and [new A, new ADevired] is mixed[] in native PhpStorm inferring) + // so, if PhpStorm couldn't detect, it can be really anything, + // and we don't have any reason to produce errors and assume whether it is compatible with mixed or not + "mixed" to ExPhpType.ANY, + "\\mixed" to ExPhpType.ANY, + + // some "forced" that can occur often, @see [ForcingTypeProvider], \\ not needed + "force(string)" to ExPhpTypeForcing(ExPhpType.STRING), + "force(int)" to ExPhpTypeForcing(ExPhpType.INT), + "force(kmixed)" to ExPhpTypeForcing(ExPhpType.KMIXED), + "force(any)" to ExPhpTypeForcing(ExPhpType.ANY) ) private class ExPhpTypeBuilder(private val type: String) { @@ -120,10 +119,19 @@ object PhpTypeToExPhpTypeParsing { return eq } + fun advance(): Boolean { + skipWhitespace() + if (offset >= type.length) { + return false + } + offset++ + return true + } + 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,14 +187,14 @@ object PhpTypeToExPhpTypeParsing { } } - private fun parseTemplateSpecialization(builder: ExPhpTypeBuilder): List? { - if (!builder.compareAndEat('<')) + private fun parseGenericSpecialization(builder: ExPhpTypeBuilder): List? { + if (!builder.compareAndEat('<') && !builder.compareAndEat('(')) return null val specialization = mutableListOf() while (true) { specialization.add(parseTypeExpression(builder) ?: return null) - if (builder.compareAndEat('>')) + if (builder.compareAndEat('>') || builder.compareAndEat(')')) return specialization if (builder.compareAndEat(',')) @@ -212,12 +220,63 @@ object PhpTypeToExPhpTypeParsing { } val returnType: ExPhpType? = - if (builder.compareAndEat(':')) parseTypeExpression(builder) ?: return null - else null + if (builder.compareAndEat(':')) parseTypeExpression(builder) ?: return null + else null return Pair(argTypes, returnType) } + private fun parseClosureTypesListContents(builder: ExPhpTypeBuilder): List? { + val argTypes = mutableListOf() + + if (builder.compare(',')) + return argTypes + + while (true) { + argTypes.add(parseTypeExpression(builder) ?: return null) + + if (builder.compareAndEat('ᤓ')) + continue + + if (builder.compare(',')) + return argTypes + if (builder.compare('>')) + return argTypes + } + } + + private fun parseClosureContents(builder: ExPhpTypeBuilder): Pair, ExPhpType?>? { + if (!builder.compareAndEat('<')) + return null + + val argTypes = mutableListOf() + while (true) { + if (builder.compareAndEat('>')) + break + + val types = parseClosureTypesListContents(builder) ?: return null + val type = if (types.isEmpty()) + null + else if (types.size == 1) + types[0] + else + ExPhpTypePipe(types) + + if (type != null) { + argTypes.add(type) + } + + if (builder.compareAndEat(',')) + continue + if (!builder.compare('>')) + return null + } + + val returnType = argTypes.lastOrNull() + + return Pair( argTypes.dropLast(1), returnType) + } + private fun parseForcingTypeContents(builder: ExPhpTypeBuilder): ExPhpType? { if (!builder.compareAndEat('(')) return null @@ -242,8 +301,25 @@ object PhpTypeToExPhpTypeParsing { return ExPhpTypeNullable(expr) } + if (builder.compareAndEat('%')) { + val genericsT = builder.parseFQN() ?: return null + return ExPhpTypeGenericsT(genericsT) + } + val fqn = builder.parseFQN() ?: return null + // TODO: Так как в 2022.2 добилась поддержка типа int<0, 100>, нам нужно + // вернуть для него всегда просто \int, а не \int<0, 100> + // Если использовать не typesWithParametrisedParts, а types + // то это не нужно, но тогда не будет работать вывод типов callable. + if (fqn == "\\int" && builder.compare('<')) { + for (i in 0..20) { + if (builder.compareAndEat('>')) break + builder.advance() + } + return FQN_PREPARSED[fqn] + } + if (fqn == "tuple" && builder.compare('(')) { val items = parseTupleContents(builder) ?: return null return ExPhpTypeTuple(items) @@ -259,13 +335,32 @@ object PhpTypeToExPhpTypeParsing { return ExPhpTypeCallable(argTypes, returnType) } + if (fqn == "\\Closure" && builder.compare('<')) { + val (argTypes, returnType) = parseClosureContents(builder) ?: return null + return ExPhpTypeCallable(argTypes, returnType) + } + if (fqn == "force" && builder.compare('(')) { val inner = parseForcingTypeContents(builder) ?: return null return ExPhpTypeForcing(inner) } - if (builder.compare('<')) { - val specialization = parseTemplateSpecialization(builder) ?: return null + 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('<') || builder.compare('(')) { + val specialization = parseGenericSpecialization(builder) ?: return null return ExPhpTypeTplInstantiation(fqn, specialization) } @@ -283,9 +378,11 @@ object PhpTypeToExPhpTypeParsing { private fun parseTypeExpression(builder: ExPhpTypeBuilder): ExPhpType? { val lhs = parseTypeArray(builder) ?: return null // wrap with ExPhpTypePipe only 'T1|T2', leaving 'T' being as is - if (!builder.compare('|') && !builder.compare('/')) - return if (lhs is ExPhpTypeForcing) lhs.inner else lhs - + if (!builder.compare('|') && !builder.compare('/')) { + // TODO: здесь была строчка, точно ли она не нужна? + // return if (lhs is ExPhpTypeForcing) lhs.inner else lhs + return lhs + } val pipeItems = mutableListOf(lhs) while (builder.compareAndEat('|') || builder.compareAndEat('/')) { val rhs = parseTypeArray(builder) ?: break @@ -313,25 +410,31 @@ object PhpTypeToExPhpTypeParsing { return createNullableOrSimplified(pipeItems[0]) // T1|T2|...|force(T) will be just T - for (item in pipeItems) - if (item is ExPhpTypeForcing) + for (item in pipeItems) { + if (item is ExPhpTypeForcing) { return item.inner + } else if (item is ExPhpTypeArray && item.inner is ExPhpTypeForcing) { + // TODO: подумать тут + return ExPhpTypeArray(item.inner.inner) + } + } return ExPhpTypePipe(pipeItems) } private fun createNullableOrSimplified(nullableType: ExPhpType): ExPhpType = when { nullableType === ExPhpType.KMIXED -> ExPhpType.KMIXED - nullableType === ExPhpType.ANY -> ExPhpType.ANY + nullableType === ExPhpType.ANY -> ExPhpType.ANY nullableType is ExPhpTypeForcing -> nullableType - else -> ExPhpTypeNullable(nullableType) + else -> ExPhpTypeNullable(nullableType) } fun parse(phpType: PhpType): ExPhpType? { return when (phpType.types.size) { - 0 -> null - 1 -> - phpType.types.first().let { str -> + 0 -> null + 1 -> +// TODO: здесь не все так просто + phpType.typesWithParametrisedParts.first().let { str -> FQN_PREPARSED[str] ?: parseTypeExpression(ExPhpTypeBuilder(str)) } else -> { // optimization: not phpType.toString(), not to concatenate strings diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/PsiToExPhpType.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/PsiToExPhpType.kt index 7e36e71..5f0d40e 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/PsiToExPhpType.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/PsiToExPhpType.kt @@ -11,10 +11,41 @@ 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) { + val items = type.items.mapNotNull { dropGenerics(it) } + if (items.isEmpty()) return null + if (items.size == 1) return items.first() + + return ExPhpTypePipe(items) + } + + if (type is ExPhpTypeTplInstantiation) { + val list = type.specializationList.mapNotNull { dropGenerics(it) } + if (list.isEmpty()) return null + return ExPhpTypeTplInstantiation(type.classFqn, list) + } + + if (type is ExPhpTypeNullable) { + return dropGenerics(type.inner)?.let { ExPhpTypeNullable(it) } + } + + if (type is ExPhpTypeArray) { + return dropGenerics(type.inner)?.let { ExPhpTypeArray(it) } + } + + if (type is ExPhpTypeGenericsT) { + return null + } + + 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/CustomVarParamReturnDocTags.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/CustomVarParamReturnDocTags.kt index 557a6d5..244d05d 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/CustomVarParamReturnDocTags.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/CustomVarParamReturnDocTags.kt @@ -35,7 +35,7 @@ class PhpDocParamTagParserEx : PhpDocTagParser() { */ class PhpDocVarTagParserEx : PhpDocTagParser() { override fun getElementType(): IElementType = - PhpDocElementTypes.phpDocTag + PhpDocElementTypes.phpDocParam override fun parseContents(builder: PhpPsiBuilder) = TokensToExPhpTypePsiParsing.parseVarAndType(builder) diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeCallablePsiImpl.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeCallablePsiImpl.kt index ab74635..f0f1c94 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeCallablePsiImpl.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeCallablePsiImpl.kt @@ -1,9 +1,13 @@ package com.vk.kphpstorm.exphptype.psi import com.intellij.lang.ASTNode +import com.intellij.psi.util.elementType +import com.jetbrains.php.lang.documentation.phpdoc.lexer.PhpDocTokenTypes import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocElementType +import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocType import com.jetbrains.php.lang.documentation.phpdoc.psi.impl.PhpDocTypeImpl import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.vk.kphpstorm.helpers.toStringAsNested /** * callable(int, float):int - psi is callable(PhpType, PhpType):PhpType @@ -19,6 +23,26 @@ class ExPhpTypeCallablePsiImpl(node: ASTNode) : PhpDocTypeImpl(node) { override fun getNameNode(): ASTNode? = null override fun getType(): PhpType { - return PhpType.CALLABLE + val argTypes = mutableListOf() + var returnType = "" + var nextReturnType = false + var child = firstChild.nextSibling + while (child != null) { + if (child is PhpDocType) { + val type = child.type.toStringAsNested() + if (nextReturnType) { + returnType = type + break + } + argTypes.add(type) + } + if (child.elementType == PhpDocTokenTypes.DOC_TEXT && child.text == ":") { + nextReturnType = true + } + child = child.nextSibling + } + + val argTypesStr = argTypes.joinToString(",") + return PhpType().add("callable").add("force(callable($argTypesStr):$returnType)") } } 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..d46c9c6 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeClassStringPsiImpl.kt @@ -0,0 +1,47 @@ +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.exphptype.KphpPrimitiveTypes +import com.vk.kphpstorm.generics.GenericUtil +import com.vk.kphpstorm.helpers.toExPhpType + +/** + * class-string — psi is class-string(Foo) corresponding type of Foo::class + * 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 text = text + val brace = if (text.contains('(')) listOf('(', ')') else listOf('<', '>') + // Во время написания типа если он уже завершен. + val className = if (text.contains(brace[0]) && text.contains(brace[1])) { + val genericType = text.substring(text.indexOf(brace[0]) + 1 until text.indexOf(brace[1])) + + // В случае когда класс на самом деле является шаблонным типом нам нужно мимикрировать тип + // и добавить знак процента к имени типа, чтобы в дальнейшем работать с ним как с шаблоном. + val genericMark = if (GenericUtil.nameIsGeneric(this, genericType)) "%" else "" + + "$genericMark$genericType" + } else { + "" + } + + if (className.isEmpty()) return KphpPrimitiveTypes.PHP_TYPE_ANY + if (className.startsWith("%")) return PhpType().add("class-string($className)") + + val classPsi = firstChild?.nextSibling?.nextSibling as? ExPhpTypeInstancePsiImpl + ?: return KphpPrimitiveTypes.PHP_TYPE_ANY + + val innerType = getType(classPsi, text).toExPhpType() + return PhpType().add("class-string($innerType)") + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeInstancePsiImpl.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeInstancePsiImpl.kt index c33120e..c239070 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeInstancePsiImpl.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeInstancePsiImpl.kt @@ -1,9 +1,13 @@ package com.vk.kphpstorm.exphptype.psi import com.intellij.lang.ASTNode +import com.intellij.psi.PsiWhiteSpace +import com.intellij.psi.util.PsiTreeUtil 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.elements.PhpClassFieldsList import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.vk.kphpstorm.generics.GenericUtil /** * 'A', 'asdf\Instance', '\VK\Memcache' — instances (not primitives!) — psi is just PhpDocType @@ -15,8 +19,33 @@ class ExPhpTypeInstancePsiImpl(node: ASTNode) : PhpDocTypeImpl(node) { } override fun getType(): PhpType { + val text = text + // 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()) { + return PhpType().add(text) + } + + if (isGenericT()) { + return PhpType().add("%$text") + } + + return getType(this, text) + } + + fun isGenericT(): Boolean { + val isGenericT = GenericUtil.nameIsGeneric(this, text) + + val phpDoc = parent.parent ?: return isGenericT + val element = PsiTreeUtil.skipSiblingsForward(phpDoc, PsiWhiteSpace::class.java) + if (element is PhpClassFieldsList) { + if (element.modifierList?.hasStatic() == true) { + // For static fields is always not genericT + return false + } + } + + return isGenericT } 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..381cf64 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/ExPhpTypeTuplePsiImpl.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeTuplePsiImpl.kt index 81d9b9e..9c01b8a 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeTuplePsiImpl.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/psi/ExPhpTypeTuplePsiImpl.kt @@ -28,6 +28,7 @@ class ExPhpTypeTuplePsiImpl(node: ASTNode) : PhpDocTypeImpl(node) { if (child is PhpDocType) { if (itemsStr.length > 1) itemsStr += ',' + // TODO: подумать itemsStr += child.type.toStringAsNested() } child = child.nextSibling 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 e27387e..6e7b249 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("<") @@ -201,6 +201,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 == "ffi_cdata" || builder.tokenText == "ffi_scope")) { val marker = builder.mark() builder.advanceLexer() @@ -249,7 +260,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/GenericCall.kt b/src/main/kotlin/com/vk/kphpstorm/generics/GenericCall.kt new file mode 100644 index 0000000..cb12cb5 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/GenericCall.kt @@ -0,0 +1,192 @@ +package com.vk.kphpstorm.generics + +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.util.parentOfType +import com.jetbrains.php.lang.psi.elements.* +import com.jetbrains.php.lang.psi.elements.Function +import com.vk.kphpstorm.exphptype.ExPhpType +import com.vk.kphpstorm.generics.GenericUtil.isGeneric +import com.vk.kphpstorm.generics.psi.GenericInstantiationPsiCommentImpl +import com.vk.kphpstorm.helpers.toExPhpType +import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl + +/** + * Is a union of the [IndexingGenericCall] and [ResolvingGenericCallBase] classes, which + * can be used for checks after PhpStorm has completed the indexing. + * + * Note: can't be used during indexing! + * + * [GenericCall] encapsulates all the logic for processing generic calls. + * + * It infers implicit types when no explicit type list definition on instantiation. + * + * For example: + * + * ```php + * /** + * * @kphp-generic T1, T2 + * * @param T1 $a + * * @param T2 $b + * */ + * function f($a, $b) {} + * + * f(new A, new B); // => T1 = A, T2 = B + * ``` + * + * In the case where an explicit instantiation list of types, it collects the types from it. + * + * For example: + * + * ```php + * f/**/(new A, new B); // => T1 = C, T2 = D + * ``` + */ +abstract class GenericCall(val project: Project) { + abstract val element: PsiElement + abstract val arguments: Array + abstract val argumentTypes: List + abstract val klass: PhpClass? + + val explicitSpecsPsi: GenericInstantiationPsiCommentImpl? by lazy { + GenericUtil.findInstantiationComment(element) + } + private var contextType: ExPhpType? = null + + abstract fun function(): Function? + abstract fun isResolved(): Boolean + + /** + * Returns generic parameters belonging to the current element, + * and, if it's part of a class, class parameter as well. + */ + abstract fun genericNames(): List + + /** + * Returns generic parameters belonging only to the current element. + * If it's a method, for example, then only the method parameters are + * returned without the class parameters. + */ + abstract fun ownGenericNames(): List + + abstract fun isGeneric(): Boolean + + private val genericTs = mutableListOf() + private val parameters = mutableListOf() + + private val extractor = GenericInstantiationExtractor() + protected val reifier = GenericReifier(project) + + val explicitSpecs get() = extractor.explicitSpecs + val specializationNameMap get() = extractor.specializationNameMap + val implicitSpecs get() = reifier.implicitSpecs + val implicitSpecializationNameMap get() = reifier.implicitSpecializationNameMap + val implicitSpecializationErrors get() = reifier.implicitSpecializationErrors + + protected fun init() { + val function = function() ?: return + if (!isGeneric()) return + + val genericNames = genericNames() + + parameters.addAll(function.parameters) + genericTs.addAll(genericNames) + + // If the current call is in return or is a function argument, + // then we can extract additional type hints. + contextType = calcContextType(element) + + // Even though the explicit list takes precedence over types inferred + // from function arguments, we still need both lists for further inspections. + + // First, we reify all generic types from the function arguments, if any. + reifier.reifyAllGenericsT(klass, function.parameters, genericNames, argumentTypes, contextType) + // Next, we extract all explicit generic types from the explicit list of types, if any. + extractor.extractExplicitGenericsT(genericNames(), explicitSpecsPsi) + } + + private fun calcContextType(element: PsiElement): ExPhpType? { + val parent = element.parent ?: return null + if (parent is PhpReturn) { + val parentFunction = parent.parentOfType() ?: return null + val returnType = parentFunction.docComment?.returnTag?.type + return returnType?.toExPhpType() + } + + if (parent is ParameterList) { + val calledInFunctionCall = parent.parentOfType() ?: return null + + val calledFunction = calledInFunctionCall.resolve() as? Function ?: return null + val index = parent.parameters.indexOf(element) + + calledFunction.getParameter(index)?.let { + return it.type.toExPhpType() + } + } + + return null + } + + fun withExplicitSpecs() = explicitSpecsPsi != null + + /** + * Имея следующую функцию: + * + * ```php + * /** + * * @kphp-generic T + * * @param T $arg + * */ + * function f($arg) {} + * ``` + * + * И следующий вызов: + * + * ```php + * f/**/(new Foo); + * ``` + * + * Нам необходимо вывести тип `$arg`, для того чтобы проверить, что + * переданное выражение `new Foo` имеет правильный тип. + * + * Так как функция может вызываться с разными шаблонными типа, нам + * необходимо найти тип `$arg` для каждого конкретного вызова. + * В примере выше в результате будет возвращен тип `Foo`. + */ + fun typeOfParam(index: Int): ExPhpType? { + val function = function() ?: return null + + val param = function.getParameter(index) ?: return null + val paramType = param.type + if (paramType.isGeneric(genericNames())) { + val usedNameMap = extractor.specializationNameMap.ifEmpty { + reifier.implicitSpecializationNameMap + } + return paramType.toExPhpType()?.instantiateGeneric(usedNameMap) + } + + return null + } + + fun isNotEnoughInformation(): KphpDocGenericParameterDecl? { + if (explicitSpecsPsi != null) return null + + val genericNames = if (this is GenericMethodCall && isStatic()) { + ownGenericNames() + } else { + genericNames() + } + + genericNames.forEach { decl -> + val resolved = implicitSpecializationNameMap.contains(decl.name) + + if (!resolved) { + return decl + } + } + + return null + } + + abstract override fun toString(): String +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/GenericConstructorCall.kt b/src/main/kotlin/com/vk/kphpstorm/generics/GenericConstructorCall.kt new file mode 100644 index 0000000..29ad492 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/GenericConstructorCall.kt @@ -0,0 +1,64 @@ +package com.vk.kphpstorm.generics + +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.jetbrains.php.PhpIndex +import com.jetbrains.php.lang.psi.PhpPsiElementFactory +import com.jetbrains.php.lang.psi.elements.Method +import com.jetbrains.php.lang.psi.elements.NewExpression +import com.jetbrains.php.lang.psi.elements.PhpClass +import com.jetbrains.php.lang.psi.elements.PhpTypedElement +import com.vk.kphpstorm.exphptype.ExPhpType +import com.vk.kphpstorm.generics.GenericUtil.genericNames +import com.vk.kphpstorm.helpers.toExPhpType +import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl + +class GenericConstructorCall(call: NewExpression) : GenericCall(call.project) { + override val element = call + override val arguments: Array = call.parameters + override val argumentTypes: List = arguments + .filterIsInstance().map { it.type.global(project).toExPhpType() } + + override val klass: PhpClass? + private val method: Method? + + init { + val className = call.classReference?.fqn + klass = PhpIndex.getInstance(project).getClassesByFQN(className).firstOrNull() + // If the class doesn't have a constructor, then we create its pseudo version. + method = klass?.constructor ?: createPseudoConstructor(project, klass?.name ?: "UnknownClass") + + init() + } + + override fun function() = method + + override fun isResolved() = method != null && klass != null + + override fun genericNames(): List { + val methodNames = method?.genericNames() ?: emptyList() + val classNames = klass?.genericNames() ?: emptyList() + + return mutableListOf() + .apply { addAll(methodNames) } + .apply { addAll(classNames) } + .toList() + } + + override fun ownGenericNames() = genericNames() + + override fun isGeneric() = genericNames().isNotEmpty() + + override fun toString(): String { + val explicit = explicitSpecs.joinToString(",") + val implicit = implicitSpecs.joinToString(",") + return "${klass?.fqn ?: "UnknownClass"}->__construct<$explicit>($implicit)" + } + + private fun createPseudoConstructor(project: Project, className: String): Method { + return PhpPsiElementFactory.createPhpPsiFromText( + project, + Method::class.java, "class $className { public function __construct() {} }" + ) + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/GenericFunctionCall.kt b/src/main/kotlin/com/vk/kphpstorm/generics/GenericFunctionCall.kt new file mode 100644 index 0000000..d7452c1 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/GenericFunctionCall.kt @@ -0,0 +1,42 @@ +package com.vk.kphpstorm.generics + +import com.intellij.psi.PsiElement +import com.jetbrains.php.lang.psi.elements.Function +import com.jetbrains.php.lang.psi.elements.FunctionReference +import com.jetbrains.php.lang.psi.elements.PhpTypedElement +import com.vk.kphpstorm.exphptype.ExPhpType +import com.vk.kphpstorm.generics.GenericUtil.genericNames +import com.vk.kphpstorm.generics.GenericUtil.isGeneric +import com.vk.kphpstorm.helpers.toExPhpType + +class GenericFunctionCall(call: FunctionReference) : GenericCall(call.project) { + override val element = call + override val klass = null + override val arguments: Array = call.parameters + override val argumentTypes: List = arguments + .filterIsInstance().map { it.type.global(project).toExPhpType() } + + private val function: Function? = call.resolve() as? Function + + init { + init() + } + + override fun function() = function + + override fun isResolved() = function != null + + override fun genericNames() = function?.genericNames() ?: emptyList() + + override fun ownGenericNames() = genericNames() + + override fun isGeneric() = function()?.isGeneric() == true + + override fun toString(): String { + val function = function() + val explicit = explicitSpecs.joinToString(",") + val implicit = implicitSpecs.joinToString(",") + return "${function?.fqn ?: "UnknownFunction"}<$explicit>($implicit)" + } +} + diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/GenericInstantiationExtractor.kt b/src/main/kotlin/com/vk/kphpstorm/generics/GenericInstantiationExtractor.kt new file mode 100644 index 0000000..4596df7 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/GenericInstantiationExtractor.kt @@ -0,0 +1,33 @@ +package com.vk.kphpstorm.generics + +import com.vk.kphpstorm.exphptype.ExPhpType +import com.vk.kphpstorm.generics.psi.GenericInstantiationPsiCommentImpl +import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl +import kotlin.math.min + +/** + * Encapsulates the logic for extracting generic types from the instantiation list. + */ +class GenericInstantiationExtractor { + val explicitSpecs = mutableListOf() + val specializationNameMap = mutableMapOf() + + /** + * Having a call `f/**/(...)`, where `f` is `f`, deduce T1 and T2 from + * comment `/**/`. + */ + fun extractExplicitGenericsT( + genericsNames: List, + explicitSpecsPsi: GenericInstantiationPsiCommentImpl? + ) { + if (explicitSpecsPsi == null) return + + val explicitSpecsTypes = explicitSpecsPsi.instantiationTypes() + + explicitSpecs.addAll(explicitSpecsTypes) + + for (i in 0 until min(genericsNames.size, explicitSpecsTypes.size)) { + specializationNameMap[genericsNames[i].name] = explicitSpecsTypes[i] + } + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/GenericMethodCall.kt b/src/main/kotlin/com/vk/kphpstorm/generics/GenericMethodCall.kt new file mode 100644 index 0000000..9d4cfac --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/GenericMethodCall.kt @@ -0,0 +1,84 @@ +package com.vk.kphpstorm.generics + +import com.intellij.psi.PsiElement +import com.jetbrains.php.PhpIndex +import com.jetbrains.php.lang.psi.elements.Method +import com.jetbrains.php.lang.psi.elements.MethodReference +import com.jetbrains.php.lang.psi.elements.PhpClass +import com.jetbrains.php.lang.psi.elements.PhpTypedElement +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.vk.kphpstorm.exphptype.ExPhpType +import com.vk.kphpstorm.generics.GenericUtil.genericNames +import com.vk.kphpstorm.generics.GenericUtil.getInstantiations +import com.vk.kphpstorm.helpers.toExPhpType +import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl +import kotlin.math.min + +class GenericMethodCall(call: MethodReference) : GenericCall(call.project) { + override val element = call + override val arguments: Array = call.parameters + override val argumentTypes: List = arguments + .filterIsInstance().map { it.type.global(project).toExPhpType() } + + private val method = call.resolve() as? Method + // TODO + private val containingClass = method?.containingClass + + override val klass: PhpClass? + + init { + val callType = call.classReference?.type?.global(project) + + val classType = PhpType().add(callType).global(project) + val parsed = classType.toExPhpType() + + val instantiation = parsed?.getInstantiations()?.firstOrNull() + + if (instantiation != null) { + klass = PhpIndex.getInstance(project).getAnyByFQN(instantiation.classFqn).firstOrNull() + + val specialization = instantiation.specializationList + val classSpecializationNameMap = mutableMapOf() + val genericNames = klass?.genericNames() ?: emptyList() + + for (i in 0 until min(genericNames.size, specialization.size)) { + classSpecializationNameMap[genericNames[i].name] = specialization[i] + } + + classSpecializationNameMap.forEach { (name, type) -> + reifier.implicitClassSpecializationNameMap[name] = type + } + } else { + klass = null + } + + init() + } + + fun isStatic() = method?.isStatic ?: false + + override fun function() = method + + override fun isResolved() = method != null && klass != null + + override fun genericNames(): List { + val methodsNames = method?.genericNames() ?: emptyList() + val classesNames = klass?.genericNames() ?: emptyList() + + return mutableListOf() + .apply { addAll(methodsNames) } + .apply { addAll(classesNames) } + .toList() + } + + override fun ownGenericNames() = method?.genericNames() ?: emptyList() + + override fun isGeneric() = genericNames().isNotEmpty() + + override fun toString(): String { + val function = function() + val explicit = explicitSpecs.joinToString(",") + val implicit = implicitSpecs.joinToString(",") + return "${klass?.fqn ?: "UnknownClass"}->${function?.name ?: "UnknownMethod"}<$explicit>($implicit)" + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/GenericReifier.kt b/src/main/kotlin/com/vk/kphpstorm/generics/GenericReifier.kt new file mode 100644 index 0000000..8ee4061 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/GenericReifier.kt @@ -0,0 +1,256 @@ +package com.vk.kphpstorm.generics + +import com.intellij.openapi.project.Project +import com.jetbrains.php.lang.psi.elements.Parameter +import com.jetbrains.php.lang.psi.elements.PhpClass +import com.jetbrains.php.lang.psi.elements.PhpTypedElement +import com.vk.kphpstorm.exphptype.* +import com.vk.kphpstorm.generics.GenericUtil.genericInheritInstantiation +import com.vk.kphpstorm.generics.GenericUtil.genericNames +import com.vk.kphpstorm.generics.GenericUtil.genericParents +import com.vk.kphpstorm.generics.GenericUtil.getGenericTypeOrSelf +import com.vk.kphpstorm.generics.GenericUtil.getInstantiation +import com.vk.kphpstorm.generics.GenericUtil.isGeneric +import com.vk.kphpstorm.helpers.toExPhpType +import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl +import kotlin.math.min + +/** + * Encapsulates generic type inference logic from function parameters. + */ +class GenericReifier(val project: Project) { + val implicitSpecs = mutableListOf() + val implicitSpecializationNameMap = mutableMapOf() + val implicitClassSpecializationNameMap = mutableMapOf() + val implicitSpecializationErrors = mutableMapOf>() + + /** + * Having a call `f(...)` of a generic function `f(...)`, deduce T1 and T2 + * "auto deducing" for generics arguments is typically called "reification". + */ + fun reifyAllGenericsT( + klass: PhpClass?, + parameters: Array, + genericTs: List, + argumentTypes: List, + contextType: ExPhpType?, + ) { + for (i in 0 until min(argumentTypes.size, parameters.size)) { + val param = parameters[i] as? PhpTypedElement ?: continue + val paramType = param.type.global(project) + // Когда мы примешиваем extends или default тип, то появляется pipe тип, + // поэтому нам необходимо достать из него generic тип для разрешения. + val paramExType = paramType.toExPhpType()?.getGenericTypeOrSelf() ?: continue + + // Если параметр не является шаблонным, то мы его пропускаем + if (!paramType.isGeneric(genericTs) && paramType.toString() != KphpPrimitiveTypes.CALLABLE) { + continue + } + + val argExType = argumentTypes[i] ?: continue + reifyArgumentGenericsT(argExType, paramExType) + } + + // TODO: А что если тут несколько инстанциаций в типе? + val instantiation = contextType?.getInstantiation() + if (instantiation != null) { + val specList = instantiation.specializationList + for (i in 0 until min(specList.size, genericTs.size)) { + val type = specList[i] + val genericT = genericTs[i] + + implicitSpecializationNameMap[genericT.name] = type + } + } + + genericTs.forEach { + if (it.defaultType != null) { + // Если тип для параметра уже выведен, то пропускаем его. + if (implicitSpecializationNameMap.containsKey(it.name)) { + return@forEach + } + + val defaultType = if (it.defaultType.isGeneric()) { + it.defaultType.instantiateGeneric(implicitSpecializationNameMap) + } else { + it.defaultType + } + + implicitSpecializationNameMap[it.name] = defaultType + } + } + + implicitSpecializationNameMap.forEach { (_, type) -> + implicitSpecs.add(type) + } + implicitSpecializationNameMap.putAll(implicitClassSpecializationNameMap) + + if (klass != null) { + val parentsList = klass.genericParents() + parentsList.forEach { parent -> + val extendsName = parent.fqn + val genericNames = parent.genericNames() + val inheritInstantiation = klass.genericInheritInstantiation(extendsName) + if (inheritInstantiation != null) { + val specList = inheritInstantiation.specializationList() + + val classSpecializationMap = genericNames.associate { + it.name to it.defaultType + }.toMutableMap() + + for (i in 0 until Integer.min(genericNames.size, specList.size)) { + val genericT = genericNames[i] + val spec = specList[i] + + classSpecializationMap[genericT.name] = spec + } + + classSpecializationMap.forEach classForEach@{ (name, type) -> + if (type == null) { + return@classForEach + } + implicitSpecializationNameMap[name] = type.instantiateGeneric(implicitSpecializationNameMap) + } + } + } + } + } + + /** + * 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 generic argument of `f()` invocation. + */ + private fun reifyArgumentGenericsT(argExType: ExPhpType, paramExType: ExPhpType) { + if (paramExType is ExPhpTypeGenericsT) { + val prevReifiedType = implicitSpecializationNameMap[paramExType.nameT] + // Особые случаи, когда несколько типов это не ошибка + val isAny = argExType == ExPhpType.ANY || prevReifiedType == ExPhpType.ANY + val isNull = argExType == ExPhpType.NULL || prevReifiedType == ExPhpType.NULL + if (prevReifiedType != null && !isAny && !isNull && prevReifiedType.toString() != argExType.toString()) { + // В таком случае мы получаем ситуацию когда один шаблонный тип + // имеет несколько возможных вариантов типа, что является ошибкой. + implicitSpecializationErrors[paramExType.nameT] = Pair(argExType, prevReifiedType) + } + + if (prevReifiedType == null || argExType != ExPhpType.ANY && argExType != ExPhpType.NULL) { + implicitSpecializationNameMap[paramExType.nameT] = argExType + } + } + + if (paramExType is ExPhpTypeNullable) { + if (argExType is ExPhpTypeNullable) { + reifyArgumentGenericsT(argExType.inner, paramExType.inner) + } else { + reifyArgumentGenericsT(argExType, paramExType.inner) + } + } + + if (paramExType is ExPhpTypePipe) { + // если случай paramExType это Vector|Vector<%T> и argExType это Vector|Vector + val instantiationParamType = + paramExType.items.find { it is ExPhpTypeTplInstantiation } as? ExPhpTypeTplInstantiation + val instantiationArgType = + if (argExType is ExPhpTypePipe) + argExType.items.find { it is ExPhpTypeTplInstantiation } as? ExPhpTypeTplInstantiation + else + argExType as? ExPhpTypeTplInstantiation + + if (instantiationParamType != null && instantiationArgType != null) { + for (i in 0 until min( + instantiationArgType.specializationList.size, + instantiationParamType.specializationList.size + )) { + reifyArgumentGenericsT( + instantiationArgType.specializationList[i], + instantiationParamType.specializationList[i] + ) + } + } + + // если случай T|false + if (paramExType.items.size == 2 && paramExType.items.any { it == ExPhpType.FALSE }) { + if (argExType is ExPhpTypePipe) { + val argTypeWithoutFalse = ExPhpTypePipe(argExType.items.filter { it != ExPhpType.FALSE }) + val paramTypeWithoutFalse = paramExType.items.first { it != ExPhpType.FALSE } + reifyArgumentGenericsT(argTypeWithoutFalse, paramTypeWithoutFalse) + } + // TODO: подумать над пайпами, так как не все так очевидно + } + } + + if (paramExType is ExPhpTypeCallable) { + if (argExType is ExPhpTypeCallable) { + if (argExType.returnType != null && paramExType.returnType != null) { + reifyArgumentGenericsT(argExType.returnType, paramExType.returnType) + } + for (i in 0 until min(argExType.argTypes.size, paramExType.argTypes.size)) { + reifyArgumentGenericsT(argExType.argTypes[i], paramExType.argTypes[i]) + } + } + } + + if (paramExType is ExPhpTypeClassString) { + val isPipeWithClassString = argExType is ExPhpTypePipe && + argExType.items.any { it is ExPhpTypeClassString } + + // Для случаев когда нативный вывод типов дает в результате string|class-string + // В таком случае нам необходимо найти более точный тип. + val classStringType = if (isPipeWithClassString) { + (argExType as ExPhpTypePipe).items.find { it is ExPhpTypeClassString } + } else if (argExType is ExPhpTypeClassString) { + argExType + } else { + null + } + + if (classStringType is ExPhpTypeClassString) { + reifyArgumentGenericsT(classStringType.inner, paramExType.inner) + } + } + + if (paramExType is ExPhpTypeArray) { + if (argExType is ExPhpTypeArray) { + reifyArgumentGenericsT(argExType.inner, paramExType.inner) + } + } + + if (paramExType is ExPhpTypeTuple) { + if (argExType is ExPhpTypeTuple) { + for (i in 0 until min(argExType.items.size, paramExType.items.size)) { + reifyArgumentGenericsT(argExType.items[i], paramExType.items[i]) + } + } + } + + if (paramExType is ExPhpTypeShape) { + val isPipeWithShapes = argExType is ExPhpTypePipe && + argExType.items.any { it is ExPhpTypeShape && it.items.isNotEmpty() } + + // Для случаев когда нативный вывод типов дает в результате shape()|shape(key1:Foo...) + // В таком случае нам необходимо вычленить более точный шейп. + val shapeWithKeys = if (isPipeWithShapes) { + (argExType as ExPhpTypePipe).items.find { it is ExPhpTypeShape && it.items.isNotEmpty() } + } else if (argExType is ExPhpTypeShape) { + argExType + } else { + null + } + + if (shapeWithKeys is ExPhpTypeShape) { + shapeWithKeys.items.forEach { argShapeItem -> + val correspondingParamShapeItem = paramExType.items.find { paramShapeItem -> + argShapeItem.keyName == paramShapeItem.keyName + } ?: return@forEach + + reifyArgumentGenericsT(argShapeItem.type, correspondingParamShapeItem.type) + } + } + } + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/GenericUtil.kt b/src/main/kotlin/com/vk/kphpstorm/generics/GenericUtil.kt new file mode 100644 index 0000000..549028b --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/GenericUtil.kt @@ -0,0 +1,251 @@ +package com.vk.kphpstorm.generics + +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.util.containers.Stack +import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment +import com.jetbrains.php.lang.psi.elements.Field +import com.jetbrains.php.lang.psi.elements.Function +import com.jetbrains.php.lang.psi.elements.PhpClass +import com.jetbrains.php.lang.psi.elements.PhpNamedElement +import com.jetbrains.php.lang.psi.elements.impl.MethodReferenceImpl +import com.jetbrains.php.lang.psi.elements.impl.NewExpressionImpl +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.vk.kphpstorm.exphptype.* +import com.vk.kphpstorm.generics.psi.GenericInstantiationPsiCommentImpl +import com.vk.kphpstorm.helpers.toExPhpType +import com.vk.kphpstorm.kphptags.psi.* + +object GenericUtil { + fun PhpNamedElement.isGeneric() = docComment?.getTagElementsByName("@kphp-generic")?.firstOrNull() != null + + fun PhpClass.isGeneric(): Boolean { + val withGenericTag = docComment?.getTagElementsByName("@kphp-generic")?.firstOrNull() != null + val withInheritTag = docComment?.getTagElementsByName("@kphp-inherit")?.firstOrNull() != null + return withGenericTag || withInheritTag + } + + fun ExPhpType.isGeneric() = toString().contains("%") + + fun PhpType.isGeneric(el: PhpNamedElement) = isGeneric(el.genericNames()) + + fun PhpType.isGeneric(genericNames: List): Boolean { + // TODO: Подумать как сделать улучшить + return genericNames.any { decl -> types.contains("%${decl.name}") } || + genericNames.any { decl -> types.any { type -> type.contains("%${decl.name}") } } + } + + fun Field.isGeneric(): Boolean { + if (docComment == null) return false + val varTag = docComment?.varTag ?: return false + val klass = containingClass ?: return false + return varTag.type.isGeneric(klass) + } + + fun Function.isReturnGeneric(): Boolean { + if (docComment == null) return false + + return docComment!!.getTagElementsByName("@return").any { tag -> + // TODO: think about it + tag.type.toExPhpType(project)?.isGeneric() == true + } + } + + fun PhpNamedElement.genericNames(): List = genericNames(docComment) + fun PhpNamedElement.genericNonDefaultNames(): List = + genericNames(docComment).filter { it.defaultType == null } + + private fun genericNames(docComment: PhpDocComment?): List { + val docT = docComment?.getTagElementsByName("@kphp-generic")?.firstOrNull() as? KphpDocTagGenericPsiImpl + ?: return emptyList() + return docT.getFullGenericParameters() + } + + fun PhpClass.genericParents(): List { + val extendsList = extendsList.referenceElements.mapNotNull { it.resolve() as? PhpClass } + val implementsList = implementsList.referenceElements.mapNotNull { it.resolve() as? PhpClass } + + return extendsList.filter { it.isGeneric() } + implementsList.filter { it.isGeneric() } + } + + fun PhpClass.getAllGenericParents(): Stack>> { + var currentClass = this + val superClasses = Stack>>() + while (currentClass.superClass != null) { + val superClass = currentClass.superClass ?: continue + val classes = currentClass.implementedInterfaces.toMutableList() + classes.add(superClass) + superClasses.add(currentClass to classes) + currentClass = superClass + } + + for (i in 0 until superClasses.size) { + if (!superClasses.last().second.any { it.isGeneric() }) { + superClasses.pop() + } + } + + superClasses.reverse() + + return superClasses + } + + fun PhpClass.genericInheritInstantiation(className: String): KphpDocInheritParameterDecl? { + val docT = docComment?.getTagElementsByName("@kphp-inherit")?.firstOrNull() as? KphpDocTagInheritPsiImpl + ?: return null + + return docT.getParameters().find { + it.name == className + } + } + + fun PhpClass.genericInheritInstantiationPsi(className: String): KphpDocInheritParameterDeclPsiImpl? { + val docT = docComment?.getTagElementsByName("@kphp-inherit")?.firstOrNull() as? KphpDocTagInheritPsiImpl + ?: return null + + return docT.getParametersPsi().find { + it.className() == className + } + } + + fun ExPhpType.isGenericPipe(): Boolean { + if (this is ExPhpTypePipe) { + if (this.items.size != 2) return false + return this.items.any { + it is ExPhpTypeTplInstantiation || (it is ExPhpTypeArray && it.inner is ExPhpTypeTplInstantiation) + } + } + + return false + } + + fun ExPhpType.getGenericPipeType(): ExPhpType? { + if (!isGenericPipe()) { + return null + } + + return (this as ExPhpTypePipe).items.firstOrNull { + // TODO: + it is ExPhpTypeTplInstantiation || (it is ExPhpTypeArray && it.inner is ExPhpTypeTplInstantiation) + } + } + + fun ExPhpType.getGenericTypeOrSelf(): ExPhpType { + if (this is ExPhpTypePipe) { + // If T|false. + if (this.items.size == 2 && this.items.any { it is ExPhpTypePrimitive && it.typeStr == "false" }) { + return this + } + + return this.items.firstOrNull { + it is ExPhpTypeGenericsT + } as? ExPhpTypeGenericsT ?: return this + } + + return this + } + + fun ExPhpTypePipe.isStringableStringUnion(): Boolean { + if (items.size == 2) { + return items.find { it is ExPhpTypeInstance && it.fqn.endsWith("\\Stringable") } != null && + items.find { it is ExPhpTypePrimitive && it.typeStr == "string" } != null + } + + return false + } + + fun generateUniqueGenericName(names: List?): String { + if (names == null || names.isEmpty()) return "T" + + for (i in 1..100) { + val name = "T$i" + if (names.none { it.name == name }) { + return name + } + } + + return "T" + } + + /** + * for IDE, we return PhpType "A"|"A", that's why + * A> is resolved as "A"|"A>", so if pipe — search for instantiation + */ + fun ExPhpType.getInstantiation(): ExPhpTypeTplInstantiation? { + return when (this) { + is ExPhpTypePipe -> items.firstOrNull { it is ExPhpTypeTplInstantiation } + is ExPhpTypeNullable -> inner + is ExPhpTypeForcing -> inner + else -> this + } as? ExPhpTypeTplInstantiation + } + + fun ExPhpType.getInstantiations(): List { + if (this is ExPhpTypePipe) { + return items.filterIsInstance() + } + val inner = when (this) { + is ExPhpTypeNullable -> inner + is ExPhpTypeForcing -> inner + else -> this + } as? ExPhpTypeTplInstantiation ?: return emptyList() + + return listOf(inner) + } + + fun findInstantiationComment(el: PsiElement): GenericInstantiationPsiCommentImpl? { + if (el is Field) return null + + val candidate = when (el) { + is NewExpressionImpl -> { + el.classReference?.nextSibling + } + is MethodReferenceImpl -> { + el.firstChild?.nextSibling?.nextSibling?.nextSibling + } + else -> { + el.firstChild?.nextSibling + } + } ?: return null + + if (candidate !is GenericInstantiationPsiCommentImpl) return null + return candidate + } + + fun nameIsGeneric(el: PsiElement, text: String): Boolean { + var parent = el.parent + while (parent !is PsiFile) { + if (parent is PhpDocComment) { + if (nameIsGenericInDoc(parent, text)) { + return true + } + } + if (parent is Function) { + if (nameIsGenericInDoc(parent.docComment, text)) { + return true + } + } + + if (parent is PhpClass) { + if (nameIsGenericInDoc(parent.docComment, text)) { + return true + } + } + + parent = parent.parent + } + + return false + } + + private fun nameIsGenericInDoc(doc: PhpDocComment?, text: String): Boolean { + if (doc == null) return false + + for (child in doc.children) { + if (child is KphpDocTagGenericPsiImpl && child.getOnlyNameGenericParameters().contains(text)) { + return true + } + } + return false + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/IndexingGenericCall.kt b/src/main/kotlin/com/vk/kphpstorm/generics/IndexingGenericCall.kt new file mode 100644 index 0000000..425a577 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/IndexingGenericCall.kt @@ -0,0 +1,84 @@ +package com.vk.kphpstorm.generics + +import com.intellij.psi.PsiElement +import com.jetbrains.php.lang.psi.elements.PhpTypedElement +import com.jetbrains.php.lang.psi.resolve.types.PhpType + +/** + * Type inference in PhpStorm has two steps: + * + * - Indexing, collection of types based only on local information from the file, without access to others. + * Because of this, for complex types that depend on information from other files, + * we need to pack all the necessary information into a string, what class [IndexingGenericCall] does. + * PhpStorm calls these types Incomplete, it requires additional processing in the second stage. + * + * - Resolving Incomplete types. + * At this point, PhpStorm converts the resulting Incomplete to Complete types. + * This is done by the [ResolvingGenericCallBase] class. + * + * A typical example of using this class with [ResolvingGenericCallBase] is calling the [pack] method in the + * [com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4.getType] method. + * + * Then calling the [ResolvingGenericCallBase.resolve] method in the method + * [com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4.complete]. + * + * Note that [separator] must be unique for different types of calls (e.g. functions, methods, constructors). + */ +class IndexingGenericCall( + private val fqn: String, + private val arguments: Array, + reference: PsiElement, + private val separator: String, +) { + companion object { + const val START_TYPE = '⋙' + const val END_TYPE = '⋘' + } + + private val explicitSpecsPsi = GenericUtil.findInstantiationComment(reference) + + /** + * Packs all data into a string so that [ResolvingGenericCallBase.resolve] can then unpack + * this data and infer types when indexing is complete. + * + * @return packed call data + */ + fun pack(): String { + val explicitSpecsString = extractExplicitGenericsT().joinToString("$$") + val callArgsString = argumentTypes().joinToString("$$") { + // Это необходимо здесь так как например для выражения [new Boo] тип будет #_\int и \Boo + // и если мы сохраним его как #_\int|\Boo, то в дальнейшем тип будет "#_\int|\Boo", и + // этот тип не разрешится верно, поэтому сохраняем типы через стрелочку, таким образом + // внутри PhpType типы будут также разделены, как были на момент сохранения здесь + + // TODO: Добавить поддержку Incomplete типов? Чтобы работало даже без тайпхинтов. + if (it.typesWithParametrisedParts.firstOrNull()?.startsWith("\\Closure<") == true) { + val rawType = it.typesWithParametrisedParts.first() + val parts = PhpType.getParametrizedParts(rawType).map { type -> type.replace("ᤓ", "/") } + .map {type -> + if (type.startsWith("\\int<") && type.endsWith("int")) { + "int" + } else { + type + } + } + + val returnType = parts.last() + val paramTypes = parts.dropLast(1).map { type -> type.ifEmpty { "mixed" } } + + return@joinToString "callable(${paramTypes.joinToString(",")}):$returnType" + } + + if (it.types.size == 1) { + it.toString() + } else { + it.types.joinToString("→") + } + } + + return "$START_TYPE$fqn$separator$explicitSpecsString$separator$callArgsString$END_TYPE" + } + + private fun argumentTypes() = arguments.filterIsInstance().map { it.type } + private fun extractExplicitGenericsT() = explicitSpecsPsi?.instantiationTypes() ?: emptyList() +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericCallBase.kt b/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericCallBase.kt new file mode 100644 index 0000000..b535246 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericCallBase.kt @@ -0,0 +1,200 @@ +package com.vk.kphpstorm.generics + +import com.intellij.openapi.project.Project +import com.jetbrains.php.lang.psi.elements.Parameter +import com.jetbrains.php.lang.psi.elements.PhpClass +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.vk.kphpstorm.exphptype.ExPhpType +import com.vk.kphpstorm.exphptype.ExPhpTypeTplInstantiation +import com.vk.kphpstorm.helpers.toExPhpType +import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl +import org.apache.commons.lang3.tuple.MutablePair +import java.util.* +import kotlin.math.min + +/** + * Responsible for processing data from [IndexingGenericCall.pack]. + * + * Override [instantiate] and [unpack] methods for each case you need. + * + * @see IndexingGenericCall + */ +abstract class ResolvingGenericCallBase(protected val project: Project) { + protected var parameters: Array = emptyArray() + protected var genericTs: List = emptyList() + protected var classGenericTs: List = emptyList() + + protected var klass: PhpClass? = null + protected var classGenericType: ExPhpTypeTplInstantiation? = null + + protected var argumentTypes: List = emptyList() + protected var explicitGenericsT: List = emptyList() + + private val reifier = GenericReifier(project) + + /** + * By the results from [unpack] instantiate template types and + * returns the final Complete type. + * + * @return Complete type + */ + protected abstract fun instantiate(): PhpType? + + /** + * Unpacking data from [IndexingGenericCall.pack]. + * + * @return true if data successfully unpacked, false otherwise + */ + protected abstract fun unpack(packedData: String): Boolean + + /** + * Resolves the given [incompleteType] and returns the Complete type. + * If it can't resolve the received incomplete type, returns null. + * + * @return Complete type or null + */ + fun resolve(incompleteType: String): PhpType? { + val packedData = incompleteType.substring(2) + if (!unpack(packedData)) { + return null + } + + reifier.reifyAllGenericsT(klass, parameters, genericTs, argumentTypes, null) + return instantiate() + } + + protected fun specialization(): MutableMap { + val specialization = specializationList() + + val specializationNameMap = mutableMapOf() + + for (i in 0 until min(genericTs.size, specialization.size)) { + specializationNameMap[genericTs[i].name] = specialization[i] + } + + if (classGenericType != null) { + for (i in 0 until min(classGenericTs.size, classGenericType!!.specializationList.size)) { + specializationNameMap[classGenericTs[i].name] = classGenericType!!.specializationList[i] + } + } + + return specializationNameMap + } + + private fun explicitGenericTypes(): List { + if (explicitGenericsT.isEmpty()) return emptyList() + + val specMap = mutableMapOf() + + genericTs.forEachIndexed { index, genericT -> + val type = explicitGenericsT.getOrNull(index) ?: genericT.defaultType ?: return@forEachIndexed + specMap[genericT.name] = type.instantiateGeneric(specMap) + } + + return genericTs.mapNotNull { specMap[it.name] } + } + + private fun specializationList() = explicitGenericTypes().ifEmpty { reifier.implicitSpecs } + + protected fun beginCompleted(packedData: String): Boolean { + return packedData.startsWith(IndexingGenericCall.START_TYPE + "\\") + } + + protected fun safeSplit(data: String, count: Int, separator: String): List? { + val parts = data.split(separator) + if (parts.size != count) return null + return parts + } + + protected fun resolveSubTypes(packedData: String): String { + var data = packedData + .removePrefix(IndexingGenericCall.START_TYPE.toString()) + .removeSuffix(IndexingGenericCall.END_TYPE.toString()) + + val pairsParts = mutableListOf>>() + + val pairStack = Stack>() + for (i in data.indices) { + + if (data[i] == IndexingGenericCall.START_TYPE) { + if (pairsParts.isEmpty()) { + pairsParts.add(mutableListOf()) + } + + val pair = MutablePair(i - 2, -1) + pairsParts.last().add(pair) + + pairStack.add(pair) + } + + if (data[i] == IndexingGenericCall.END_TYPE) { + if (pairStack.isNotEmpty()) { + val pair = pairStack.pop() + pair.right = i + 1 + } else { + print("") + } + + if (pairStack.isEmpty()) { + pairsParts.add(mutableListOf()) + } + } + } + + pairsParts.reverse() + pairsParts.forEach { pairs -> + pairs.reverse() + var rightShift = 0 + + var prevPair: MutablePair? = null + for (pair in pairs) { + val startIndex = pair.left + val endIndex = pair.right - if (prevPair != null) { + if (pair.right < prevPair.left) { + 0 + } else { + rightShift + } + } else { + 0 + } + + val subType = try { + data.substring(startIndex, endIndex) + } catch (e: IndexOutOfBoundsException) { + "" + } + + val resolvedSubType = PhpType().add(subType).global(project).toExPhpType()?.toString() ?: "" + + rightShift += subType.length - resolvedSubType.length + + try { + data = data.replaceRange(startIndex until endIndex, resolvedSubType) + } catch (e: Exception) { + print("") + } + + prevPair = pair + } + } + + return data + } + + protected fun unpackTypeArray(text: String) = if (text.isNotEmpty()) + text.split("$$").mapNotNull { + val types = it.split("→") + val type = PhpType() + types.forEach { singleType -> + type.add(singleType) + } + try { + type.global(project).toExPhpType() + } catch (e: Exception) { + null + } + } + else + emptyList() +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericConstructorCall.kt b/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericConstructorCall.kt new file mode 100644 index 0000000..a643265 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericConstructorCall.kt @@ -0,0 +1,59 @@ +package com.vk.kphpstorm.generics + +import com.intellij.openapi.project.Project +import com.jetbrains.php.PhpIndex +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.vk.kphpstorm.exphptype.ExPhpTypeForcing +import com.vk.kphpstorm.exphptype.ExPhpTypeGenericsT +import com.vk.kphpstorm.exphptype.ExPhpTypeTplInstantiation +import com.vk.kphpstorm.generics.GenericUtil.genericNames +import com.vk.kphpstorm.generics.GenericUtil.isGeneric +import com.vk.kphpstorm.typeProviders.GenericClassesTypeProvider + +class ResolvingGenericConstructorCall(project: Project) : ResolvingGenericCallBase(project) { + override fun instantiate(): PhpType { + val specializationNameMap = specialization() + + val genericsTypes = genericTs.map { ExPhpTypeGenericsT(it.name) } + val instantiationType = ExPhpTypeTplInstantiation(klass!!.fqn, genericsTypes) + val specializedType = instantiationType.instantiateGeneric(specializationNameMap) + + return ExPhpTypeForcing(specializedType).toPhpType() + } + + override fun unpack(packedData: String): Boolean { + if (beginCompleted(packedData)) { + val firstSeparator = packedData.indexOf(".__construct") + if (firstSeparator != -1) { + val className = packedData.substring(1, firstSeparator) + klass = PhpIndex.getInstance(project).getClassesByFQN(className).firstOrNull() + if (klass?.isGeneric() == false) + return false + } + } + + val data = resolveSubTypes(packedData) + val parts = safeSplit(data, 3, GenericClassesTypeProvider.SEP) ?: return false + + val fqn = parts[0] + val explicitGenericsString = parts[1] + val argumentsTypesString = parts[2] + + val className = fqn.split(".").first() + + if (klass == null) { + klass = PhpIndex.getInstance(project).getClassesByFQN(className).firstOrNull() ?: return false + } + + val method = klass!!.constructor + + // Если у класса есть конструктор, получаем из него параметры, если нет, то считаем что параметров нет + parameters = method?.parameters ?: emptyArray() + genericTs = klass!!.genericNames() + + explicitGenericsT = unpackTypeArray(explicitGenericsString) + argumentTypes = unpackTypeArray(argumentsTypesString) + + return true + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericFieldFetch.kt b/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericFieldFetch.kt new file mode 100644 index 0000000..6225edf --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericFieldFetch.kt @@ -0,0 +1,72 @@ +package com.vk.kphpstorm.generics + +import com.intellij.openapi.project.Project +import com.jetbrains.php.PhpIndex +import com.jetbrains.php.lang.psi.elements.Field +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.vk.kphpstorm.exphptype.ExPhpTypeForcing +import com.vk.kphpstorm.generics.GenericUtil.genericNames +import com.vk.kphpstorm.generics.GenericUtil.getInstantiations +import com.vk.kphpstorm.helpers.toExPhpType +import com.vk.kphpstorm.typeProviders.GenericFieldsTypeProvider + +class ResolvingGenericFieldFetch(project: Project) : ResolvingGenericCallBase(project) { + private var field: Field? = null + + override fun instantiate(): PhpType? { + val specializationNameMap = specialization() + + val varTag = field?.docComment?.varTag ?: return null + val exType = varTag.type.toExPhpType(project) ?: return null + val specializedType = exType.instantiateGeneric(specializationNameMap) + + return PhpType().add(specializedType.toPhpType()).add(ExPhpTypeForcing(specializedType).toPhpType()) + } + + override fun unpack(packedData: String): Boolean { + // If PhpStorm resolved className and fieldName: + // \SomeName(int).field... + if (beginCompleted(packedData)) { + val firstSeparator = packedData.indexOf(GenericFieldsTypeProvider.SEP) + val fqn = packedData.substring(1, firstSeparator) + val className = fqn.split('.').first() + // Если имя класса не содержит скобок, значит вывести + // тип поля мы не сможем, поэтому заканчиваем распаковку. + if (!className.contains("(")) { + return false + } + } + + val data = resolveSubTypes(packedData) + val parts = safeSplit(data, 3, GenericFieldsTypeProvider.SEP) ?: return false + + val fqn = parts[0] + + val dotIndex = fqn.lastIndexOf('.') + val classRawName = fqn.substring(0, dotIndex) + val methodName = fqn.substring(dotIndex + 1) + + val classType = PhpType().add(classRawName).global(project) + val parsed = classType.toExPhpType() + + val instantiations = parsed?.getInstantiations() + val foundInstantiation = instantiations?.firstOrNull { + val klass = PhpIndex.getInstance(project).getClassesByFQN(it.classFqn).firstOrNull() + val field = klass?.findFieldByName(methodName, false) + + field != null + } ?: return false + + classGenericType = foundInstantiation + + klass = PhpIndex.getInstance(project).getClassesByFQN(foundInstantiation.classFqn).firstOrNull() ?: return false + field = klass?.findFieldByName(methodName, false) ?: return false + + classGenericTs = klass!!.genericNames() + + argumentTypes = emptyList() + explicitGenericsT = emptyList() + + return true + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericFunctionCall.kt b/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericFunctionCall.kt new file mode 100644 index 0000000..0100106 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericFunctionCall.kt @@ -0,0 +1,61 @@ +package com.vk.kphpstorm.generics + +import com.intellij.openapi.project.Project +import com.jetbrains.php.PhpIndex +import com.jetbrains.php.lang.psi.elements.Function +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.vk.kphpstorm.exphptype.ExPhpTypeForcing +import com.vk.kphpstorm.generics.GenericUtil.genericNames +import com.vk.kphpstorm.generics.GenericUtil.isReturnGeneric +import com.vk.kphpstorm.helpers.toExPhpType +import com.vk.kphpstorm.typeProviders.GenericFunctionsTypeProvider + +class ResolvingGenericFunctionCall(project: Project) : ResolvingGenericCallBase(project) { + private var function: Function? = null + + override fun instantiate(): PhpType? { + val specializationNameMap = specialization() + + val returnTag = function!!.docComment?.returnTag ?: return null + val exType = returnTag.type.toExPhpType(project) ?: return null + val specializedType = exType.instantiateGeneric(specializationNameMap) + + return ExPhpTypeForcing(specializedType).toPhpType().add(specializedType.toPhpType()) + } + + override fun unpack(packedData: String): Boolean { + if (beginCompleted(packedData)) { + val firstSeparator = packedData.indexOf(GenericFunctionsTypeProvider.SEP) + if (firstSeparator != -1) { + val functionName = packedData.substring(1, firstSeparator) + function = PhpIndex.getInstance(project).getFunctionsByFQN(functionName).firstOrNull() + if (function?.isReturnGeneric() == false) + return false + } + } + + val data = resolveSubTypes(packedData) + val parts = safeSplit(data, 3, GenericFunctionsTypeProvider.SEP) ?: return false + val functionName = parts[0] + val explicitGenericsString = parts[1] + val argumentsTypesString = parts[2] + + if (function == null) { + function = PhpIndex.getInstance(project).getFunctionsByFQN(functionName).firstOrNull() ?: return false + } + + // Если возвращаемый тип функции не зависит от шаблонного типа, + // то нет смысла как-то уточнять ее тип. + if (!function!!.isReturnGeneric()) { + return false + } + + genericTs = function!!.genericNames() + parameters = function!!.parameters + + explicitGenericsT = unpackTypeArray(explicitGenericsString) + argumentTypes = unpackTypeArray(argumentsTypesString) + + return true + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericMethodCall.kt b/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericMethodCall.kt new file mode 100644 index 0000000..d10082f --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/ResolvingGenericMethodCall.kt @@ -0,0 +1,154 @@ +package com.vk.kphpstorm.generics + +import com.intellij.openapi.project.Project +import com.jetbrains.php.PhpIndex +import com.jetbrains.php.lang.psi.elements.Function +import com.jetbrains.php.lang.psi.elements.Method +import com.jetbrains.php.lang.psi.resolve.types.PhpDocTypeFromSuperMemberTP +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.vk.kphpstorm.exphptype.ExPhpType +import com.vk.kphpstorm.exphptype.ExPhpTypeGenericsT +import com.vk.kphpstorm.generics.GenericUtil.genericInheritInstantiation +import com.vk.kphpstorm.generics.GenericUtil.genericNames +import com.vk.kphpstorm.generics.GenericUtil.getAllGenericParents +import com.vk.kphpstorm.generics.GenericUtil.getInstantiations +import com.vk.kphpstorm.generics.GenericUtil.isReturnGeneric +import com.vk.kphpstorm.helpers.toExPhpType +import com.vk.kphpstorm.typeProviders.GenericMethodsTypeProvider +import java.lang.Integer.min + +class ResolvingGenericMethodCall(project: Project) : ResolvingGenericCallBase(project) { + private var method: Method? = null + + override fun instantiate(): PhpType? { + val klass = klass ?: return null + val method = method ?: return null + + val specializationNameMap = specialization() + val superClasses = klass.getAllGenericParents() + + val specializationMaps = mutableListOf>() + superClasses.forEach { (childClass, classes) -> + classes.forEach byClasses@{ superClass -> + val name = superClass.fqn + val genericNames = superClass.genericNames() + val inheritInstantiation = childClass.genericInheritInstantiation(name) ?: return@byClasses + + val specList = inheritInstantiation.specializationList() + + val classSpecializationMap = genericNames.associate { + it.name to it.defaultType + }.toMutableMap() + + for (i in 0 until min(genericNames.size, specList.size)) { + val genericT = genericNames[i] + val spec = specList[i] + + classSpecializationMap[genericT.name] = spec + } + + specializationMaps.add(classSpecializationMap) + } + } + + specializationMaps.reverse() + + specializationMaps.forEach { map -> + map.forEach { (name, type) -> + map[name] = type?.instantiateGeneric(specializationNameMap) + } + + map.forEach { (name, type) -> + specializationNameMap[name] = type!! + } + } + + val returnType = getReturnDocType(method, project) ?: return null + val specializedType = returnType.instantiateGeneric(specializationNameMap) + + return specializedType.toPhpType() + } + + private fun getReturnDocType(function: Function, project: Project): ExPhpType? { + var returnType: PhpType? = null + var curFunction: Function? = function + while (returnType == null && curFunction != null) { + val curReturnType = curFunction.docType + if (!curReturnType.isEmpty) + returnType = curReturnType + else if (curFunction is Method) + curFunction = PhpDocTypeFromSuperMemberTP.superMethods(curFunction).firstOrNull() + else + break + } + + return returnType?.toExPhpType(project) + } + + override fun unpack(packedData: String): Boolean { + // If PhpStorm resolved className and methodName: + // \SomeName.method... + if (beginCompleted(packedData)) { + val firstSeparator = packedData.indexOf(GenericMethodsTypeProvider.SEP) + val fqn = packedData.substring(1, firstSeparator) + val (className, methodName) = fqn.split('.') + if (!className.contains("(") && !className.contains("|")) { + klass = PhpIndex.getInstance(project).getClassesByFQN(className).firstOrNull() + method = klass?.findMethodByName(methodName) + if (method?.isReturnGeneric() == false) + return false + } + } + + val data = resolveSubTypes(packedData) + val parts = safeSplit(data, 3, GenericMethodsTypeProvider.SEP) ?: return false + + val fqn = parts[0] + + val dotIndex = fqn.lastIndexOf('.') + val classRawName = fqn.substring(0, dotIndex) + val methodName = fqn.substring(dotIndex + 1) + + val classType = PhpType().add(classRawName).global(project) + val parsed = classType.toExPhpType() + val instantiations = parsed?.getInstantiations() + + val foundInstantiation = instantiations?.firstOrNull { + val klass = PhpIndex.getInstance(project).getClassesByFQN(it.classFqn).firstOrNull() + val method = klass?.findMethodByName(methodName) + + method != null + } + + val className = + if (foundInstantiation != null && foundInstantiation.specializationList.first() !is ExPhpTypeGenericsT) { + classGenericType = foundInstantiation + foundInstantiation.classFqn + } else { + classRawName + } + + if (klass == null) { + klass = PhpIndex.getInstance(project).getClassesByFQN(className).firstOrNull() ?: return false + } + if (method == null) { + method = klass!!.findMethodByName(methodName) + } + if (method == null) { + return false + } + + parameters = method!!.parameters + genericTs = method!!.genericNames() + + // Не устанавливаем параметры класса, так как это статический вызов + if (!method!!.isStatic) { + classGenericTs = klass!!.genericNames() + } + + explicitGenericsT = unpackTypeArray(parts[1]) + argumentTypes = unpackTypeArray(parts[2]) + + return true + } +} 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..9228408 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/generics/psi/GenericInstantiationPsiCommentImpl.kt @@ -0,0 +1,46 @@ +package com.vk.kphpstorm.generics.psi + +import com.intellij.lang.injection.InjectedLanguageManager +import com.intellij.psi.PsiComment +import com.intellij.psi.impl.source.tree.PsiCommentImpl +import com.intellij.psi.tree.IElementType +import com.intellij.psi.util.parentOfType +import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag +import com.jetbrains.php.lang.psi.PhpFile +import com.vk.kphpstorm.exphptype.ExPhpType +import com.vk.kphpstorm.exphptype.ExPhpTypeTuple +import com.vk.kphpstorm.exphptype.PhpTypeToExPhpTypeParsing + +/** + * Comment like `/**/` between function/method/class name, and the + * argument list for a call. + * + * This comment stores a comma-separated list of explicit generic types. + * Comment can have any types that PhpStorm can represent in phpdoc. + * + * The comment has no internal structure, since [PsiComment] can't + * have children, as it's a leaf of the tree. + */ +class GenericInstantiationPsiCommentImpl(type: IElementType, text: CharSequence) : PsiCommentImpl(type, text) { + /** + * Returns the types defined in the comment. + * + * Example: + * + * `/**/` -> `[T, T2]` + */ + fun instantiationTypes(): List { + val outerFile = containingFile as PhpFile + val injected = InjectedLanguageManager.getInstance(project) + .findInjectedElementAt(outerFile, startOffset + 3) + ?: return emptyList() + + val docTagGenericsInstantiation = injected.parentOfType() + ?: return emptyList() + + val specList = PhpTypeToExPhpTypeParsing.parse(docTagGenericsInstantiation.type) as? ExPhpTypeTuple + ?: return emptyList() + + return specList.items + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/helpers/PsiTreeExtensions.kt b/src/main/kotlin/com/vk/kphpstorm/helpers/PsiTreeExtensions.kt index d49b7be..018269f 100644 --- a/src/main/kotlin/com/vk/kphpstorm/helpers/PsiTreeExtensions.kt +++ b/src/main/kotlin/com/vk/kphpstorm/helpers/PsiTreeExtensions.kt @@ -49,3 +49,12 @@ fun setSelectionInEditor(editor: Editor, elementToSelect: PsiElement) { editor.offsetToLogicalPosition(offset + elementToSelect.textLength) )) } + +fun setSelectionInEditor(editor: Editor, elementToSelect: PsiElement, shiftStart: Int, shiftEnd: Int) { + val offset = elementToSelect.textOffset + editor.caretModel.caretsAndSelections = listOf(CaretState( + editor.offsetToLogicalPosition(offset + shiftStart), + editor.offsetToLogicalPosition(offset + shiftStart), + editor.offsetToLogicalPosition(offset + shiftEnd), + )) +} diff --git a/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpColorsAndFontsPage.kt b/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpColorsAndFontsPage.kt index f445754..c19e86b 100644 --- a/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpColorsAndFontsPage.kt +++ b/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpColorsAndFontsPage.kt @@ -18,9 +18,10 @@ class KphpColorsAndFontsPage : ColorSettingsPage, DisplayPrioritySortable { AttributesDescriptor("phpdoc: other tags (@param / @see / etc)", KphpHighlightingData.PHPDOC_TAG_REGULAR), AttributesDescriptor("phpdoc: type inside @var / @param / @return", KphpHighlightingData.PHPDOC_TYPE_INSIDE), AttributesDescriptor("phpdoc: variable of @param", PhpHighlightingData.DOC_PARAMETER), + AttributesDescriptor("phpdoc: generic T names", KphpHighlightingData.PHPDOC_GENERIC_TYPE_T), 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), ) override fun getHighlighter() = PhpColorPageHighlighter(mapOf()) @@ -36,7 +37,10 @@ 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, + + "f_generic_t" to KphpHighlightingData.PHPDOC_TYPE_INSIDE, + "f_generic_extends" to PhpHighlightingData.DOC_COMMENT, ) override fun getIcon() = PhpFileType.INSTANCE.icon @@ -71,6 +75,13 @@ class KphpColorsAndFontsPage : ColorSettingsPage, DisplayPrioritySortable { instance_to_array(${'$'}user); ini_get('memory_limit'); + /** + * @kphp-generic T: Field + */ + function demo_generic(): bool { + return true; + } + """.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..e8e9014 100644 --- a/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpHighlightingData.kt +++ b/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpHighlightingData.kt @@ -13,6 +13,7 @@ object KphpHighlightingData { val PHPDOC_TAG_KPHP = TextAttributesKey.createTextAttributesKey("PHPDOC_TAG_KPHP", PhpHighlightingData.DOC_TAG) val PHPDOC_TAG_REGULAR = TextAttributesKey.createTextAttributesKey("PHPDOC_TAG_REGULAR", PhpHighlightingData.DOC_TAG) val PHPDOC_TYPE_INSIDE = TextAttributesKey.createTextAttributesKey("PHPDOC_TYPE_INSIDE", PhpHighlightingData.DOC_IDENTIFIER) + val PHPDOC_GENERIC_TYPE_T = TextAttributesKey.createTextAttributesKey("PHPDOC_GENERIC_TYPE_T", PhpHighlightingData.DOC_IDENTIFIER) 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) diff --git a/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpStormAnnotator.kt b/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpStormAnnotator.kt index f5b3415..a59b875 100644 --- a/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpStormAnnotator.kt +++ b/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpStormAnnotator.kt @@ -15,14 +15,12 @@ import com.jetbrains.php.lang.documentation.phpdoc.psi.impl.PhpDocTypeImpl import com.jetbrains.php.lang.documentation.phpdoc.psi.impl.tags.PhpDocTagImpl import com.jetbrains.php.lang.highlighter.PhpHighlightingData import com.jetbrains.php.lang.lexer.PhpTokenTypes -import com.jetbrains.php.lang.psi.elements.Function -import com.jetbrains.php.lang.psi.elements.Method import com.jetbrains.php.lang.psi.elements.NewExpression import com.jetbrains.php.lang.psi.elements.PhpClass import com.jetbrains.php.lang.psi.elements.impl.ClassReferenceImpl -import com.jetbrains.php.lang.psi.elements.impl.FunctionImpl import com.jetbrains.php.lang.psi.elements.impl.FunctionReferenceImpl import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.vk.kphpstorm.exphptype.psi.ExPhpTypeInstancePsiImpl import com.vk.kphpstorm.helpers.KPHP_NATIVE_FUNCTIONS import com.vk.kphpstorm.kphptags.ALL_KPHPDOC_TAGS import com.vk.kphpstorm.kphptags.psi.KphpDocTagImpl @@ -125,6 +123,9 @@ class KphpStormAnnotator : Annotator { @Suppress("UNUSED_PARAMETER") private fun onTypeInsidePhpdocTag(element: PhpDocTypeImpl, holder: AnnotationHolder) { // for future: highlight primitives and classes, template args and shape keys + if (element is ExPhpTypeInstancePsiImpl && element.isGenericT()) { + holder.textAttributes(element, KphpHighlightingData.PHPDOC_GENERIC_TYPE_T) + } } /** diff --git a/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpStormTypeInfoProvider.kt b/src/main/kotlin/com/vk/kphpstorm/highlighting/KphpStormTypeInfoProvider.kt index 3d32b6e..45d59c6 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,10 @@ 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() + val exPhpType = phpType.toExPhpType() ?: return phpType.toString() + return PsiToExPhpType.dropGenerics(exPhpType)?.toHumanReadable(element) ?: phpType.toString() } override fun getExpressionsAt(elementAt: PsiElement): List { diff --git a/src/main/kotlin/com/vk/kphpstorm/highlighting/hints/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..419fca6 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/highlighting/hints/InlayHintsCollector.kt @@ -0,0 +1,72 @@ +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.jetbrains.php.lang.psi.elements.MethodReference +import com.jetbrains.php.lang.psi.elements.NewExpression +import com.vk.kphpstorm.generics.GenericCall +import com.vk.kphpstorm.generics.GenericConstructorCall +import com.vk.kphpstorm.generics.GenericFunctionCall +import com.vk.kphpstorm.generics.GenericMethodCall + +@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 MethodReference && settings.showForFunctions -> { + val call = GenericMethodCall(element) + showAnnotation(call, element.firstChild?.nextSibling?.nextSibling) + } + element is FunctionReference && settings.showForFunctions -> { + val call = GenericFunctionCall(element) + showAnnotation(call, element.firstChild) + } + element is NewExpression && settings.showForFunctions -> { + val call = GenericConstructorCall(element) + showAnnotation(call, element.firstChild?.nextSibling?.nextSibling) + } + } + + return true + } + + private fun showAnnotation(call: GenericCall, place: PsiElement?) { + if (place == null || !call.isGeneric() || call.withExplicitSpecs()) { + return + } + + // Показываем хинт только если удалось вывести типы. + val decl = call.isNotEnoughInformation() + if (decl != null) { + return + } + + val genericNames = call.ownGenericNames() + + if (genericNames.isEmpty()) { + return + } + + val simplePresentation = myHintsFactory.inlayHint("") + + sink.addInlineElement(place.endOffset, false, simplePresentation, false) + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/CollapseGenericsInstantiationIntention.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/CollapseGenericsInstantiationIntention.kt new file mode 100644 index 0000000..baf70f3 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/CollapseGenericsInstantiationIntention.kt @@ -0,0 +1,24 @@ +package com.vk.kphpstorm.inspections + +import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionPlaces +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.playback.commands.ActionCommand +import com.intellij.psi.PsiElement +import com.vk.kphpstorm.generics.psi.GenericInstantiationPsiCommentImpl + +class CollapseGenericsInstantiationIntention : PsiElementBaseIntentionAction() { + override fun getText() = "Collapse generics instantiation" + override fun getFamilyName() = "Collapse generics instantiation" + override fun isAvailable(project: Project, editor: Editor?, element: PsiElement) = + element is GenericInstantiationPsiCommentImpl + + override fun invoke(project: Project, editor: Editor?, element: PsiElement) { + val actionId = "CollapseRegion" + val action = ActionManager.getInstance().getAction(actionId) + ActionManager.getInstance() + .tryToExecute(action, ActionCommand.getInputEvent(actionId), null, ActionPlaces.UNKNOWN, true) + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/KphpGenericsInspection.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/KphpGenericsInspection.kt new file mode 100644 index 0000000..6dd350a --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/KphpGenericsInspection.kt @@ -0,0 +1,359 @@ +package com.vk.kphpstorm.inspections + +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.SmartPointerManager +import com.intellij.psi.util.findParentOfType +import com.jetbrains.php.PhpIndex +import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocType +import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag +import com.jetbrains.php.lang.inspections.PhpInspection +import com.jetbrains.php.lang.psi.elements.* +import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor +import com.jetbrains.rd.util.first +import com.vk.kphpstorm.exphptype.* +import com.vk.kphpstorm.exphptype.psi.ExPhpTypeInstancePsiImpl +import com.vk.kphpstorm.exphptype.psi.ExPhpTypeTplInstantiationPsiImpl +import com.vk.kphpstorm.generics.GenericCall +import com.vk.kphpstorm.generics.GenericConstructorCall +import com.vk.kphpstorm.generics.GenericFunctionCall +import com.vk.kphpstorm.generics.GenericMethodCall +import com.vk.kphpstorm.generics.GenericUtil.genericNames +import com.vk.kphpstorm.generics.GenericUtil.getInstantiation +import com.vk.kphpstorm.generics.GenericUtil.isGeneric +import com.vk.kphpstorm.generics.GenericUtil.isStringableStringUnion +import com.vk.kphpstorm.inspections.quickfixes.AddExplicitInstantiationCommentQuickFix +import com.vk.kphpstorm.inspections.quickfixes.RegenerateKphpInheritQuickFix +import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl +import com.vk.kphpstorm.kphptags.psi.KphpDocTagGenericPsiImpl +import com.vk.kphpstorm.kphptags.psi.KphpDocTagInheritPsiImpl + +class KphpGenericsInspection : PhpInspection() { + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + return object : PhpElementVisitor() { + override fun visitPhpNewExpression(expression: NewExpression) { + val call = GenericConstructorCall(expression) + checkGenericCall(call, expression, expression) + } + + override fun visitPhpMethodReference(reference: MethodReference) { + val call = GenericMethodCall(reference) + checkGenericCall(call, reference, reference.firstChild.nextSibling.nextSibling) + } + + override fun visitPhpFunctionCall(reference: FunctionReference) { + if (reference.parent is PhpUse) { + return + } + val call = GenericFunctionCall(reference) + checkGenericCall(call, reference, reference.firstChild) + } + + override fun visitPhpClass(klass: PhpClass) { + checkInheritTag(klass) + } + + private fun checkInheritTag(klass: PhpClass) { + val extendsList = klass.extendsList.referenceElements.mapNotNull { it to (it.resolve() as? PhpClass) } + val implementsList = + klass.implementsList.referenceElements.mapNotNull { it to (it.resolve() as? PhpClass) } + + val allParents = extendsList + implementsList + + val extendsGenericList = extendsList.filter { it.second?.isGeneric() ?: false } + val implementsGenericList = implementsList.filter { it.second?.isGeneric() ?: false } + + val allGenericParents = extendsGenericList + implementsGenericList + + val inheritTag = klass.docComment?.getTagElementsByName("@kphp-inherit") + ?.firstOrNull() as? KphpDocTagInheritPsiImpl + if (inheritTag == null && allGenericParents.isNotEmpty()) { + holder.registerProblem( + klass.nameIdentifier ?: klass, + "Class extends or implements generic class/interface, please specify @kphp-inherit", + ProblemHighlightType.GENERIC_ERROR, + RegenerateKphpInheritQuickFix( + SmartPointerManager.getInstance(klass.project).createSmartPsiElementPointer(klass), + needKeepExistent = false, + "Generate @kphp-inherit tag" + ) + ) + return + } + + val tagParents = inheritTag?.getParametersPsi()?.associateBy { it.decl().name } ?: emptyMap() + allGenericParents.forEach { (ref, parentCLass) -> + if (parentCLass == null) return@forEach + + if (!tagParents.containsKey(parentCLass.fqn)) { + holder.registerProblem( + ref.element, + "Class extends generic class/interface, but this class not specified in @kphp-inherit", + ProblemHighlightType.GENERIC_ERROR, + RegenerateKphpInheritQuickFix( + SmartPointerManager.getInstance(klass.project).createSmartPsiElementPointer(klass), + needKeepExistent = true, + ) + ) + } + } + + tagParents.forEach { (name, decl) -> + val parent = allParents.find { it.second?.fqn == name } + if (parent == null && decl.decl().name != null) { + return holder.registerProblem( + decl, + "Class/interface $name not extended or implemented class/interface ${klass.name}", + ProblemHighlightType.GENERIC_ERROR, + RegenerateKphpInheritQuickFix( + SmartPointerManager.getInstance(klass.project).createSmartPsiElementPointer(klass), + needKeepExistent = true, + ) + ) + } + + if (parent?.second?.isGeneric() == false) { + return holder.registerProblem( + decl, + "It is not necessary to specify not generic class/interface $name in @kphp-inherit", + ProblemHighlightType.GENERIC_ERROR, + RegenerateKphpInheritQuickFix( + SmartPointerManager.getInstance(klass.project).createSmartPsiElementPointer(klass), + needKeepExistent = true, + ) + ) + } + } + } + + override fun visitPhpDocType(type: PhpDocType) { + checkPhpDocType(type) + } + + override fun visitPhpDocTag(tag: PhpDocTag) { + checkGenericTag(tag) + } + + private fun checkGenericTag(tag: PhpDocTag) { + if (tag !is KphpDocTagGenericPsiImpl) { + return + } + + var wasDefault = false + tag.getFullGenericParameters().forEach { + if (it.defaultType != null) { + wasDefault = true + } + + if (it.defaultType == null && wasDefault) { + holder.registerProblem( + tag, + "Generic parameters with a default type cannot come before parameters without a default type", + ProblemHighlightType.GENERIC_ERROR + ) + } + } + } + + private fun checkPhpDocType(type: PhpDocType) { + // If it's ExPhpTypeInstancePsiImpl (Vector) in Vector. + if (type is ExPhpTypeInstancePsiImpl && type.parent is ExPhpTypeTplInstantiationPsiImpl) { + return + } + + val instanceType = when (type) { + is ExPhpTypeInstancePsiImpl, is ExPhpTypeTplInstantiationPsiImpl -> type + else -> null + } ?: return + + val inKphpGenericTag = instanceType.findParentOfType() != null + + val resolvedType = PhpTypeToExPhpTypeParsing.parse(instanceType.type) ?: return + if (resolvedType is ExPhpTypeInstance && inKphpGenericTag) { + // Don't check instances in @kphp-generic tags. + return + } + + val (className, countSpecs) = if (resolvedType is ExPhpTypeInstance) { + resolvedType.fqn to 0 + } else if (resolvedType.getInstantiation() != null) { + val instantiation = resolvedType.getInstantiation()!! + val countExplicitSpecs = instantiation.specializationList.size + + instantiation.classFqn to countExplicitSpecs + } else { + return + } + + val klass = PhpIndex.getInstance(type.project).getAnyByFQN(className).firstOrNull() ?: return + if (!klass.isGeneric()) { + return + } + + val genericNames = klass.genericNames() + + reportParamsCountMismatch(genericNames, countSpecs, type) + } + + private fun checkGenericCall(call: GenericCall, element: PsiElement, errorPsi: PsiElement) { + if (!call.isResolved()) return + val genericNames = call.genericNames() + + checkGenericTypesBounds(call, genericNames) + checkInstantiationParamsCount(call) + checkReifiedGenericTypes(call, element, errorPsi) + checkReifiedSeveralGenericTypes(call, element, errorPsi) + } + + private fun checkReifiedSeveralGenericTypes(call: GenericCall, element: PsiElement, errorPsi: PsiElement) { + // В случае даже если есть ошибки, то мы показываем их только + // в случае когда нет явного определения шаблона для вызова функции. + if (call.implicitSpecializationErrors.isEmpty() || call.withExplicitSpecs()) return + + val error = call.implicitSpecializationErrors.first() + val (type1, type2) = error.value + + holder.registerProblem( + errorPsi, + "Couldn't reify generic <${error.key}>: it's both $type1 and $type2", + ProblemHighlightType.GENERIC_ERROR, + AddExplicitInstantiationCommentQuickFix(element), + ) + } + + private fun checkReifiedGenericTypes( + call: GenericCall, + element: PsiElement, + errorPsi: PsiElement + ) { + val decl = call.isNotEnoughInformation() + if (decl != null) { + holder.registerProblem( + errorPsi, + "Not enough information to infer generic ${decl.name}", + ProblemHighlightType.GENERIC_ERROR, + AddExplicitInstantiationCommentQuickFix(element) + ) + } + } + + private fun checkInstantiationParamsCount(call: GenericCall) { + val genericNames = call.ownGenericNames() + + val countExplicitSpecs = call.explicitSpecs.size + val explicitSpecsPsi = call.explicitSpecsPsi + + reportParamsCountMismatch(genericNames, countExplicitSpecs, explicitSpecsPsi) + } + + private fun reportParamsCountMismatch( + genericNames: List, + countSpecs: Int, + errorPsi: PsiElement? + ) { + val minCount = genericNames.filter { it.defaultType == null }.size + val maxCount = genericNames.size + + if (minCount == maxCount && minCount != countSpecs && errorPsi != null) { + holder.registerProblem( + errorPsi, + "$minCount generic parameters expected, but $countSpecs passed", + ProblemHighlightType.GENERIC_ERROR + ) + return + } + + if (countSpecs < minCount && errorPsi != null) { + holder.registerProblem( + errorPsi, + "Not enough generic parameters, expected at least $minCount", + ProblemHighlightType.GENERIC_ERROR, + ) + return + } + + if (countSpecs > maxCount && errorPsi != null) { + holder.registerProblem( + errorPsi, + "Too many generic parameters, expected at most $maxCount", + ProblemHighlightType.GENERIC_ERROR, + ) + } + } + + private fun checkGenericTypesBounds( + call: GenericCall, + genericNames: List, + ) { + genericNames.forEach { decl -> + val (resolvedType, isExplicit) = if (call.specializationNameMap[decl.name] != null) { + call.specializationNameMap[decl.name] to true + } else { + call.implicitSpecializationNameMap[decl.name] to false + } + + if (resolvedType == null) return@forEach + if (resolvedType is ExPhpTypeGenericsT) return@forEach + + val upperBoundType = decl.extendsType ?: return@forEach + + val errorPsi = + call.explicitSpecsPsi + ?: call.arguments.firstOrNull() + ?: call.element + + if (!upperBoundType.isAssignableFrom(resolvedType, call.project)) { + val violationMessage = generateViolationMessage(call.project, upperBoundType, resolvedType) + val message = + "${if (isExplicit) "Explicit" else "Reified"} generic type for ${decl.name} is not within its bounds ($violationMessage)" + + holder.registerProblem( + errorPsi, + message, + ProblemHighlightType.GENERIC_ERROR + ) + } + } + } + + private fun generateViolationMessage( + project: Project, + upperBoundType: ExPhpType, + resolvedType: ExPhpType + ): String { + return when (upperBoundType) { + is ExPhpTypeInstance -> { + val klass = PhpIndex.getInstance(project).getAnyByFQN(upperBoundType.fqn).firstOrNull() + val extendsOrImplements = if (klass != null && klass.isInterface) "implement" else "extend" + "$resolvedType is not $extendsOrImplements ${upperBoundType.fqn}" + } + is ExPhpTypePrimitive -> { + "$resolvedType is not ${upperBoundType.typeStr})" + } + is ExPhpTypePipe -> { + val allInstance = upperBoundType.items.all { it is ExPhpTypeInstance } + val allPrimitives = upperBoundType.items.all { it is ExPhpTypePrimitive } + + if (allInstance) { + val itemsString = upperBoundType.items.joinToString(" or ") { it.toString() } + "$resolvedType is none extend/implement any of $itemsString" + } else if (allPrimitives) { + val itemsString = upperBoundType.items.joinToString(" nor ") { it.toString() } + "$resolvedType is neither $itemsString" + } else if (upperBoundType.isStringableStringUnion()) { + "$resolvedType is not implement a \\Stringable and not a string" + } else { + "" + } + } + else -> { + "" + } + } + } + } + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/KphpParameterTypeMismatchInspection.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/KphpParameterTypeMismatchInspection.kt index 435d372..9c17ada 100644 --- a/src/main/kotlin/com/vk/kphpstorm/inspections/KphpParameterTypeMismatchInspection.kt +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/KphpParameterTypeMismatchInspection.kt @@ -14,6 +14,9 @@ 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.GenericConstructorCall +import com.vk.kphpstorm.generics.GenericFunctionCall +import com.vk.kphpstorm.generics.GenericMethodCall import com.vk.kphpstorm.helpers.KPHP_NATIVE_FUNCTIONS import com.vk.kphpstorm.kphptags.KphpInferDocTag @@ -76,8 +79,34 @@ 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 genericCall = when (call) { + is MethodReference -> { + GenericMethodCall(call) + } + is FunctionReference -> { + GenericFunctionCall(call) + } + is NewExpression -> { + GenericConstructorCall(call) + } + else -> null + } + + val actualArgType = if (genericCall != null) { + 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..cf9138c 100644 --- a/src/main/kotlin/com/vk/kphpstorm/inspections/KphpUndefinedClassInspection.kt +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/KphpUndefinedClassInspection.kt @@ -2,15 +2,21 @@ package com.vk.kphpstorm.inspections import com.intellij.codeInspection.ProblemHighlightType import com.intellij.codeInspection.ProblemsHolder +import com.intellij.lang.injection.InjectedLanguageManager import com.intellij.openapi.fileEditor.UniqueVFilePathBuilder import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.util.parentOfType import com.jetbrains.php.PhpIndex import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocType import com.jetbrains.php.lang.inspections.PhpInspection import com.jetbrains.php.lang.inspections.quickfix.PhpImportClassQuickFix import com.jetbrains.php.lang.psi.elements.* +import com.jetbrains.php.lang.psi.elements.Function import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor +import com.vk.kphpstorm.exphptype.ExPhpTypeInstance +import com.vk.kphpstorm.exphptype.PhpTypeToExPhpTypeParsing import com.vk.kphpstorm.exphptype.psi.ExPhpTypeInstancePsiImpl +import com.vk.kphpstorm.generics.GenericUtil.genericNames import com.vk.kphpstorm.inspections.helpers.KphpTypingAnalyzer /** @@ -39,8 +45,13 @@ class KphpUndefinedClassInspection : PhpInspection() { it != clazz && it.containingFile != clazz.containingFile } ?: return - val relativePath = UniqueVFilePathBuilder.getInstance().getUniqueVirtualFilePath(clazz.project, another.containingFile.virtualFile) - holder.registerProblem(identifier, "Another declaration of class '#ref' exists in $relativePath", ProblemHighlightType.WEAK_WARNING) + val relativePath = UniqueVFilePathBuilder.getInstance() + .getUniqueVirtualFilePath(clazz.project, another.containingFile.virtualFile) + holder.registerProblem( + identifier, + "Another declaration of class '#ref' exists in $relativePath", + ProblemHighlightType.WEAK_WARNING + ) } /** @@ -57,7 +68,11 @@ class KphpUndefinedClassInspection : PhpInspection() { if (isPrimitive) { if (classReference.parent !is PhpTypeDeclaration) - holder.registerProblem(nameNode.psi, "Incorrect primitive type usage", ProblemHighlightType.ERROR) + holder.registerProblem( + nameNode.psi, + "Incorrect primitive type usage", + ProblemHighlightType.ERROR + ) return } @@ -85,9 +100,41 @@ class KphpUndefinedClassInspection : PhpInspection() { if (type !is ExPhpTypeInstancePsiImpl || type.isKphpBuiltinClass()) return - val resolved = type.multiResolve(false) - if (resolved.isEmpty()) - reportUndefinedClassUsage(type) + val resolvedType = PhpTypeToExPhpTypeParsing.parse(type.type) + if (resolvedType is ExPhpTypeInstance) { + if (isGenericType(type, resolvedType)) + return + + if (PhpIndex.getInstance(type.project).getAnyByFQN(resolvedType.fqn).isEmpty()) + reportUndefinedClassUsage(type) + } + } + + private fun isGenericType( + type: ExPhpTypeInstancePsiImpl, + resolvedType: ExPhpTypeInstance + ): Boolean { + val placeWithInjection = + InjectedLanguageManager.getInstance(type.project).getInjectionHost(type) ?: return false + + val name = resolvedType.fqn.substring(resolvedType.fqn.lastIndexOf('\\') + 1) + + val containingFunction = placeWithInjection.parentOfType() + if (containingFunction != null) { + val names = containingFunction.genericNames() + if (names.find { it.name == name } != null) { + return true + } + } + val containingClass = placeWithInjection.parentOfType() + if (containingClass != null) { + val names = containingClass.genericNames() + if (names.find { it.name == name } != null) { + return true + } + } + + return false } /** @@ -104,7 +151,12 @@ class KphpUndefinedClassInspection : PhpInspection() { val importAvailable = candidates.isNotEmpty() && (isOnTheFly || candidates.size == 1) if (importAvailable) - holder.registerProblem(psi, "Undefined class '#ref'", ProblemHighlightType.ERROR, PhpImportClassQuickFix.INSTANCE) + holder.registerProblem( + psi, + "Undefined class '#ref'", + ProblemHighlightType.ERROR, + PhpImportClassQuickFix.INSTANCE + ) else holder.registerProblem(psi, "Undefined class '#ref'", ProblemHighlightType.ERROR) } diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/helpers/KphpTypingAnalyzer.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/helpers/KphpTypingAnalyzer.kt index 52da809..4c97bb7 100644 --- a/src/main/kotlin/com/vk/kphpstorm/inspections/helpers/KphpTypingAnalyzer.kt +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/helpers/KphpTypingAnalyzer.kt @@ -5,6 +5,7 @@ import com.jetbrains.php.lang.psi.elements.Field import com.jetbrains.php.lang.psi.elements.Function import com.jetbrains.php.lang.psi.resolve.types.PhpType import com.vk.kphpstorm.exphptype.* +import com.vk.kphpstorm.generics.GenericUtil.getGenericPipeType /** * Helps check if something is statically typed @@ -43,10 +44,19 @@ object KphpTypingAnalyzer { fun isScalarTypeHint(s: String) = SCALAR_TYPE_HINTS.contains(s) - fun doesDocTypeMatchTypeHint(docType: ExPhpType, hintType: ExPhpType, project: Project): Boolean = - docType !is ExPhpTypePipe - && hintType.isAssignableFrom(docType, project) - && docType.isAssignableFrom(hintType, project) + fun doesDocTypeMatchTypeHint(docType: ExPhpType, hintType: ExPhpType, project: Project): Boolean { + if (docType is ExPhpTypePipe) { + // TODO: redone + val genericType = docType.getGenericPipeType() + if (genericType != null) { + return doesDocTypeMatchTypeHint(genericType, hintType, project) + } + } + + return docType !is ExPhpTypePipe + && hintType.isAssignableFrom(docType, project) + && docType.isAssignableFrom(hintType, project) + } fun doesDocTypeDuplicateTypeHint(docType: ExPhpType, hintType: ExPhpType): Boolean = docType.toString() == hintType.toString() diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/helpers/PhpDocPsiBuilder.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/helpers/PhpDocPsiBuilder.kt index 8d77eec..e4239d4 100644 --- a/src/main/kotlin/com/vk/kphpstorm/inspections/helpers/PhpDocPsiBuilder.kt +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/helpers/PhpDocPsiBuilder.kt @@ -72,7 +72,7 @@ object PhpDocPsiBuilder { /** * Create empty phpdoc and insert it before function/class/field */ - private fun createDocComment(project: Project, element: PhpNamedElement, contents: String = "\n"): PhpDocComment { + fun createDocComment(project: Project, element: PhpNamedElement, contents: String = "\n"): PhpDocComment { val docComment = PhpPsiElementFactory.createFromText(project, PhpDocElementTypes.DOC_COMMENT, "/**$contents*/") val insLevel = if (element is Field) element.parent else element return insLevel.parent.addBefore(docComment, insLevel) as PhpDocComment @@ -81,7 +81,7 @@ object PhpDocPsiBuilder { /** * Having '/** @var int */', transform to '/** \n * @var int \n */' */ - private fun PhpDocComment.transformToMultiline(project: Project): PhpDocComment { + fun PhpDocComment.transformToMultiline(project: Project): PhpDocComment { val firstTag = children.find { it is PhpDocTag } var item = PsiTreeUtil.skipWhitespacesBackward(firstTag) @@ -102,7 +102,7 @@ object PhpDocPsiBuilder { return this } - private fun PhpDocComment.addTag(project: Project, nameWithAt: String, tagValue: String = "", afterTag: PhpDocTag? = null): PhpDocTag { + fun PhpDocComment.addTag(project: Project, nameWithAt: String, tagValue: String = "", afterTag: PhpDocTag? = null): PhpDocTag { val firstTagStart = PsiTreeUtil.skipWhitespacesBackward(children.find { it is PhpDocTag }) val firstTagAnchor = when (firstTagStart?.elementType) { PhpTokenTypes.DOC_LEADING_ASTERISK -> firstTagStart diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/prettifier/PrettificationFinder.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/prettifier/PrettificationFinder.kt index b1ba606..509bab7 100644 --- a/src/main/kotlin/com/vk/kphpstorm/inspections/prettifier/PrettificationFinder.kt +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/prettifier/PrettificationFinder.kt @@ -74,7 +74,9 @@ private fun findPhpDocPrettifications(docComment: PhpDocComment, stopOnFirst: Bo phpTypes.size == 2 && phpTypes.last() == "\\null" -> phpTypes.first() else -> return } - val replacementStr = "?" + PhpTypeToExPhpTypeParsing.parseFromString(replacementPart)?.toHumanReadable(type) + val replacementStr = "?" + PhpTypeToExPhpTypeParsing.parseFromString(replacementPart) + ?.toHumanReadable(type) + ?.replace("%", "") list.add(NullableTypePrettification(type, replacementStr)) } } diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/quickfixes/AddExplicitInstantiationCommentQuickFix.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/quickfixes/AddExplicitInstantiationCommentQuickFix.kt new file mode 100644 index 0000000..8cbd0c9 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/quickfixes/AddExplicitInstantiationCommentQuickFix.kt @@ -0,0 +1,73 @@ +package com.vk.kphpstorm.inspections.quickfixes + +import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.jetbrains.php.lang.psi.PhpPsiElementFactory +import com.jetbrains.php.lang.psi.elements.FunctionReference +import com.jetbrains.php.lang.psi.elements.MethodReference +import com.jetbrains.php.lang.psi.elements.NewExpression +import com.vk.kphpstorm.generics.GenericConstructorCall +import com.vk.kphpstorm.generics.GenericFunctionCall +import com.vk.kphpstorm.generics.GenericMethodCall +import com.vk.kphpstorm.generics.psi.GenericInstantiationPsiCommentImpl +import com.vk.kphpstorm.helpers.setSelectionInEditor + +class AddExplicitInstantiationCommentQuickFix(element: PsiElement) : LocalQuickFixAndIntentionActionOnPsiElement(element) { + override fun getFamilyName() = "Add explicit generic instantiation tag" + override fun getText() = "Add explicit generic instantiation tag" + + override fun invoke( + project: Project, + file: PsiFile, + editor: Editor?, + startElement: PsiElement, + endElement: PsiElement + ) { + val call = when (startElement) { + is NewExpression -> GenericConstructorCall(startElement) + is MethodReference -> GenericMethodCall(startElement) + is FunctionReference -> GenericFunctionCall(startElement) + else -> return + } + + if (!call.isResolved()) { + return + } + + val genericTs = call.ownGenericNames() + val genericTsString = genericTs.joinToString(", ") { it.name } + val comment = + PhpPsiElementFactory.createFromText(project, GenericInstantiationPsiCommentImpl::class.java, "/*<$genericTsString>*/") + ?: return + + val insertedComment = when (startElement) { + is NewExpression -> { + val text = startElement.text + if (!text.endsWith(")")) { + val newNewExpr = + PhpPsiElementFactory.createFromText(project, NewExpression::class.java, "$text/*<$genericTsString>*/()") + ?: return + startElement.replace(newNewExpr) + null + } else { + startElement.addAfter(comment, startElement.classReference) + } + } + is MethodReference -> { + val after = startElement.firstChild?.nextSibling?.nextSibling ?: return + startElement.addAfter(comment, after) + } + is FunctionReference -> { + startElement.addAfter(comment, startElement.firstChild) + } + else -> return + } + + if (editor != null && insertedComment != null) { + setSelectionInEditor(editor, insertedComment, 3, 3 + genericTs.first().name.length) + } + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/inspections/quickfixes/RegenerateKphpInheritQuickFix.kt b/src/main/kotlin/com/vk/kphpstorm/inspections/quickfixes/RegenerateKphpInheritQuickFix.kt new file mode 100644 index 0000000..763bb20 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/inspections/quickfixes/RegenerateKphpInheritQuickFix.kt @@ -0,0 +1,71 @@ +package com.vk.kphpstorm.inspections.quickfixes + +import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.SmartPsiElementPointer +import com.jetbrains.php.lang.psi.elements.PhpClass +import com.vk.kphpstorm.generics.GenericUtil.genericNonDefaultNames +import com.vk.kphpstorm.generics.GenericUtil.genericParents +import com.vk.kphpstorm.inspections.helpers.PhpDocPsiBuilder +import com.vk.kphpstorm.inspections.helpers.PhpDocPsiBuilder.addTag +import com.vk.kphpstorm.inspections.helpers.PhpDocPsiBuilder.removeTagFromDocComment +import com.vk.kphpstorm.inspections.helpers.PhpDocPsiBuilder.transformToMultiline +import com.vk.kphpstorm.kphptags.KphpInheritDocTag +import com.vk.kphpstorm.kphptags.psi.KphpDocTagInheritPsiImpl + +class RegenerateKphpInheritQuickFix( + private val element: SmartPsiElementPointer, + private val needKeepExistent: Boolean = false, + private val text: String = "Regenerate @kphp-inherit tag" +) : LocalQuickFixAndIntentionActionOnPsiElement(element.element) { + + override fun getFamilyName() = "Regenerate @kphp-inherit tag" + override fun getText() = text + + override fun invoke(project: Project, file: PsiFile, editor: Editor?, start: PsiElement, end: PsiElement) { + val klass = element.element as? PhpClass ?: return + val docComment = klass.docComment ?: PhpDocPsiBuilder.createDocComment(project, klass, "\n * @kphp-generic T\n") + val genericTag = docComment.getTagElementsByName("@kphp-generic").lastOrNull() + + val newParents = if (needKeepExistent) { + val containingNamespace = klass.namespaceName + val inheritTag = docComment.getTagElementsByName("@kphp-inherit").firstOrNull() as? KphpDocTagInheritPsiImpl + ?: return + val tagParents = inheritTag.getParameters().associateBy { it.name } + + val definedParent = klass.genericParents() + + val newParents = mutableListOf() + definedParent.forEach { + if (tagParents.containsKey(it.fqn)) { + newParents.add(tagParents[it.fqn].toString()) + } else { + val genericTs = it.genericNonDefaultNames().joinToString(", ") { "T" } + newParents.add(it.fqn + "<$genericTs>") + } + } + + newParents.joinToString(", ") { + it.removePrefix(containingNamespace) + } + } else { + val parentsList = klass.genericParents() + + parentsList.joinToString(", ") { + val genericTs = it.genericNonDefaultNames().joinToString(", ") { "T" } + it.name + "<$genericTs>" + } + } + + removeTagFromDocComment(docComment, "@kphp-inherit") + + if (newParents.isNotEmpty()) { + docComment + .transformToMultiline(project) + .addTag(project, KphpInheritDocTag.nameWithAt, newParents, genericTag) + } + } +} 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..ef47c55 100644 --- a/src/main/kotlin/com/vk/kphpstorm/kphptags/AllKphpdocTagsList.kt +++ b/src/main/kotlin/com/vk/kphpstorm/kphptags/AllKphpdocTagsList.kt @@ -11,8 +11,6 @@ val ALL_KPHPDOC_TAGS: List = listOf( KphpFlattenDocTag, KphpRequiredDocTag, KphpSyncDocTag, - KphpTemplateDocTag, - KphpReturnDocTag, KphpShouldNotThrowDocTag, KphpThrowsDocTag, KphpDisableWarningsDocTag, @@ -26,7 +24,8 @@ val ALL_KPHPDOC_TAGS: List = listOf( KphpSerializableDocTag, KphpReservedFieldsDocTag, - KphpTemplateClassDocTag, + KphpGenericDocTag, + KphpInheritDocTag, KphpMemcacheClassDocTag, KphpImmutableClassDocTag, KphpTlClassDocTag, diff --git a/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpDocTag.kt b/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpDocTag.kt index 9d27a99..4159da6 100644 --- a/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpDocTag.kt +++ b/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpDocTag.kt @@ -2,6 +2,7 @@ package com.vk.kphpstorm.kphptags import com.intellij.lang.annotation.AnnotationHolder import com.intellij.lang.annotation.HighlightSeverity +import com.intellij.openapi.editor.colors.TextAttributesKey import com.intellij.psi.PsiElement import com.jetbrains.php.lang.documentation.phpdoc.PhpDocUtil import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment @@ -88,6 +89,20 @@ abstract class KphpDocTag( this.newAnnotation(HighlightSeverity.ERROR, errorMessage).range(docTag).create() } + /** + * Helper for use inside annotate() on any error + */ + protected fun AnnotationHolder.error(tagElement: PsiElement, errorMessage: String) { + this.newAnnotation(HighlightSeverity.ERROR, errorMessage).range(tagElement).create() + } + + /** + * Helper for use inside annotate() for highlighting parts + */ + protected fun AnnotationHolder.highlight(element: PsiElement, textAttributes: TextAttributesKey) { + newSilentAnnotation(HighlightSeverity.INFORMATION).range(element).textAttributes(textAttributes).create() + } + /** * Helper for use inside annotate() for simple tags, accepting no parameters */ 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..18ac973 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpGenericDocTag.kt @@ -0,0 +1,115 @@ +package com.vk.kphpstorm.kphptags + +import com.intellij.lang.annotation.AnnotationHolder +import com.intellij.psi.PsiElement +import com.intellij.psi.util.parentOfType +import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment +import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag +import com.jetbrains.php.lang.highlighter.PhpHighlightingData +import com.jetbrains.php.lang.psi.elements.Function +import com.jetbrains.php.lang.psi.elements.PhpClass +import com.vk.kphpstorm.exphptype.* +import com.vk.kphpstorm.generics.GenericUtil +import com.vk.kphpstorm.generics.GenericUtil.genericNames +import com.vk.kphpstorm.generics.GenericUtil.isStringableStringUnion +import com.vk.kphpstorm.highlighting.KphpHighlightingData +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) = owner is Function || owner is PhpClass + + override fun needsAutoCompleteOnTyping(docComment: PhpDocComment, owner: PsiElement?) = true + + override fun areDuplicatesAllowed() = false + + override fun onAutoCompleted(docComment: PhpDocComment): String { + val parentClass = docComment.parentOfType() + return "|" + GenericUtil.generateUniqueGenericName(parentClass?.genericNames()) + } + + override fun annotate(docTag: PhpDocTag, rhs: PsiElement?, holder: AnnotationHolder) { + if (rhs == null) { + holder.errTag(docTag, "Expected: T[: ExtendsClass] [, T1[: ExtendsClass] [= DefaultType], ...]") + return + } + + if (docTag is KphpDocTagGenericPsiImpl) { + val genericArguments = docTag.getFullGenericParameters() + val names = mutableSetOf() + genericArguments.forEach { decl -> + checkExtendsType(decl.extendsType, holder, docTag) + + if (names.contains(decl.name)) { + holder.errTag(docTag, "Duplicate generic type ${decl.name} in declaration") + } + names.add(decl.name) + } + + val parentClass = docTag.parentOfType() + parentClass?.genericNames()?.forEach { decl -> + if (names.contains(decl.name)) { + holder.errTag(docTag, "Duplicate generic type ${decl.name} (first seen in class declaration)") + } + } + } + + if (docTag is KphpDocTagGenericPsiImpl) { + docTag.getParametersPsi().forEach { psi -> + if (psi.namePsi != null) { + holder.highlight(psi.namePsi, KphpHighlightingData.PHPDOC_TYPE_INSIDE) + } + if (psi.extendsTypePsi != null) { + holder.highlight(psi.extendsTypePsi!!, PhpHighlightingData.DOC_COMMENT) + } + if (psi.defaultTypePsi != null) { + holder.highlight(psi.defaultTypePsi!!, PhpHighlightingData.DOC_COMMENT) + } + } + } + } + + private fun checkExtendsType( + extendsType: ExPhpType?, + holder: AnnotationHolder, + docTag: PhpDocTag + ) { + if (extendsType == null) { + return + } + + if (extendsType is ExPhpTypePrimitive || extendsType is ExPhpTypeCallable || extendsType is ExPhpTypeInstance) { + return + } + + if (extendsType is ExPhpTypePipe) { + if (extendsType.isStringableStringUnion()) { + return + } + + val allInstance = extendsType.items.all { + it is ExPhpTypeInstance || it is ExPhpTypeTplInstantiation || it is ExPhpTypeGenericsT + } + val allPrimitives = extendsType.items.all { it is ExPhpTypePrimitive } + + if (!allInstance && !allPrimitives) { + holder.errTag( + docTag, + "Union type can contain either only instances or only primitives (except '\\Stringable|string')" + ) + return + } + + return + } + + holder.errTag(docTag, "Type '$extendsType' is not allowed here") + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpInheritDocTag.kt b/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpInheritDocTag.kt new file mode 100644 index 0000000..a1a107e --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpInheritDocTag.kt @@ -0,0 +1,63 @@ +package com.vk.kphpstorm.kphptags + +import com.intellij.lang.annotation.AnnotationHolder +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiTreeUtil +import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment +import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag +import com.jetbrains.php.lang.highlighter.PhpHighlightingData +import com.jetbrains.php.lang.psi.elements.PhpClass +import com.vk.kphpstorm.exphptype.psi.ExPhpTypeInstancePsiImpl +import com.vk.kphpstorm.exphptype.psi.ExPhpTypeTplInstantiationPsiImpl +import com.vk.kphpstorm.generics.GenericUtil.genericNonDefaultNames +import com.vk.kphpstorm.generics.GenericUtil.genericParents +import com.vk.kphpstorm.kphptags.psi.KphpDocElementTypes +import com.vk.kphpstorm.kphptags.psi.KphpDocInheritParameterDeclPsiImpl +import com.vk.kphpstorm.kphptags.psi.KphpDocTagElementType +import com.vk.kphpstorm.kphptags.psi.KphpDocTagInheritPsiImpl + +object KphpInheritDocTag : KphpDocTag("@kphp-inherit") { + override val description: String + get() = "Describes generic inherit for a class" + + override val elementType: KphpDocTagElementType + get() = KphpDocElementTypes.kphpDocTagInherit + + override fun isApplicableFor(owner: PsiElement) = owner is PhpClass + + override fun needsAutoCompleteOnTyping(docComment: PhpDocComment, owner: PsiElement?) = true + + override fun areDuplicatesAllowed() = false + + override fun onAutoCompleted(docComment: PhpDocComment): String { + val klass = docComment.owner as? PhpClass ?: return "" + val parentsList = klass.genericParents() + + val parentsString = parentsList.joinToString(", ") { + val genericTs = it.genericNonDefaultNames().joinToString(", ") { "T" } + it.name + "<$genericTs>" + } + + return "$parentsString|" + } + + override fun annotate(docTag: PhpDocTag, rhs: PsiElement?, holder: AnnotationHolder) { + if (rhs == null) { + holder.errTag(docTag, "Expected: ExtendsClass[, ExtendsClass2, ImplementsClass]") + return + } + + if (docTag is KphpDocTagInheritPsiImpl) { + holder.highlight(docTag, PhpHighlightingData.DOC_COMMENT) + } + + val parameters = PsiTreeUtil.findChildrenOfType(rhs.parent, KphpDocInheritParameterDeclPsiImpl::class.java) + parameters.forEach { + it as KphpDocInheritParameterDeclPsiImpl + val child = it.type() + if (child !is ExPhpTypeTplInstantiationPsiImpl && child !is ExPhpTypeInstancePsiImpl) { + holder.error(it.firstChild, "Expected ClassName, got ${it.firstChild.text}") + } + } + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpReturnDocTag.kt b/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpReturnDocTag.kt deleted file mode 100644 index f77a63f..0000000 --- a/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpReturnDocTag.kt +++ /dev/null @@ -1,25 +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 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." - - override fun isApplicableFor(owner: PsiElement): Boolean { - return owner is Function && KphpTemplateDocTag.existsInDocComment(owner) - } - - override fun needsAutoCompleteOnTyping(docComment: PhpDocComment, owner: PsiElement?): Boolean { - return KphpTemplateDocTag.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.") - } -} diff --git a/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpTemplateClassDocTag.kt b/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpTemplateClassDocTag.kt deleted file mode 100644 index 50ccc93..0000000 --- a/src/main/kotlin/com/vk/kphpstorm/kphptags/KphpTemplateClassDocTag.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.PhpClass -import com.vk.kphpstorm.kphptags.psi.KphpDocElementTypes -import com.vk.kphpstorm.kphptags.psi.KphpDocTagElementType - -object KphpTemplateClassDocTag : KphpDocTag("@kphp-template-class") { - override val description: String - get() = "Experiments for future, try to implement concept of template classes in php" - - override val elementType: KphpDocTagElementType - get() = KphpDocElementTypes.kphpDocTagTemplateClass - - override fun isApplicableFor(owner: PsiElement): Boolean { - return owner is PhpClass - } - - override fun needsAutoCompleteOnTyping(docComment: PhpDocComment, owner: PsiElement?): Boolean { - // this is experimental for IDE only, not supported in kphp for now - return false - } - - override fun annotate(docTag: PhpDocTag, rhs: PsiElement?, holder: AnnotationHolder) { - // rhs is the first template tag name, other can be accessed with nextSibling etc - if (rhs == null) - holder.errTag(docTag, "Template arguments not specified") - } -} 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..0a455ea 100644 --- a/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocElementTypes.kt +++ b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocElementTypes.kt @@ -30,12 +30,15 @@ object KphpDocElementTypes { val kphpDocTagWarnPerformance = KphpDocTagWarnPerformanceElementType /** - * '@kphp-template-class T1, T2' - * (NOTE! This is not working in KPHP for now, it is just a matter of IDE experiments for future) - * This tag stores "T1,T2" in stubs and has custom psi for them, therefore is not simple + * '@kphp-generic T1, T2: ExtendsClass, T3 = default' + * This tag stores "T1,T2:ExtendsClass,T3=default" in stubs and has custom psi for them, therefore is not simple */ - val kphpDocTagTemplateClass = KphpDocTagTemplateClassElementType + val kphpDocTagGeneric = KphpDocTagGenericElementType + /** + * '@kphp-inherit ExtendsClass, ImplementsClass' + * This tag stores "ExtendsClass, ImplementsClass" + * in stubs and has custom psi for them, therefore is not simple + */ + val kphpDocTagInherit = KphpDocTagInheritElementType } - - 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..1ce8396 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocGenericParameterDeclPsiImpl.kt @@ -0,0 +1,107 @@ +package com.vk.kphpstorm.kphptags.psi + +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiTreeUtil +import com.jetbrains.php.codeInsight.PhpCodeInsightUtil +import com.jetbrains.php.lang.documentation.phpdoc.lexer.PhpDocTokenTypes +import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocElementType +import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocRef +import com.jetbrains.php.lang.documentation.phpdoc.psi.impl.PhpDocPsiElementImpl +import com.jetbrains.php.lang.documentation.phpdoc.psi.impl.PhpDocTypeImpl +import com.vk.kphpstorm.exphptype.ExPhpTypeInstance +import com.vk.kphpstorm.exphptype.ExPhpTypePipe +import com.vk.kphpstorm.exphptype.PhpTypeToExPhpTypeParsing +import com.vk.kphpstorm.helpers.toExPhpType + +data class KphpDocGenericParameterDecl( + val name: String, + private val extendsTypeString: String?, + private val defaultTypeString: String?, +) { + val extendsType = extendsTypeString?.let { PhpTypeToExPhpTypeParsing.parseFromString(it) } + val defaultType = defaultTypeString?.let { PhpTypeToExPhpTypeParsing.parseFromString(it) } + + fun toHumanReadable(context: PsiElement): String { + val type = StringBuilder() + type.append(name) + + if (extendsTypeString != null) { + type.append(": ") + + when (extendsType) { + is ExPhpTypeInstance -> { + type.append(instanceToString(context, extendsType)) + } + is ExPhpTypePipe -> { + val pipe = extendsType.items.joinToString(" | ") { + if (it is ExPhpTypeInstance) { + instanceToString(context, it) + } else { + it.toString() + } + } + type.append(pipe) + } + else -> { + type.append(extendsTypeString.removePrefix("\\")) + } + } + } + + return type.toString() + } + + private fun instanceToString( + context: PsiElement, + extendsType: ExPhpTypeInstance + ): String { + val part = + PhpCodeInsightUtil.findScopeForUseOperator(context)?.let { + PhpCodeInsightUtil.createQualifiedName( + it, + extendsType.fqn + ) + } + return part ?: extendsType.fqn + } +} + +/** + * Inside '@kphp-generic T1, T2: ExtendsClass, T3 = default' — 'T1', 'T2: ExtendsClass' and 'T3 = default' + * are separate psi elements of this impl. + * + * @see KphpDocTagGenericElementType.getTagParser + */ +class KphpDocGenericParameterDeclPsiImpl(node: ASTNode) : PhpDocPsiElementImpl(node), PhpDocRef { + companion object { + val elementType = PhpDocElementType("phpdocGenericParameterDecl") + } + + val namePsi: PsiElement? = firstChild + var extendsTypePsi: PhpDocTypeImpl? = null + var defaultTypePsi: PhpDocTypeImpl? = null + + init { + val types = findChildrenByClass(PhpDocTypeImpl::class.java) + + types.forEach { + val textBefore = PsiTreeUtil.findSiblingBackward(it, PhpDocTokenTypes.DOC_TEXT, null) + if (textBefore?.text == ":") { + extendsTypePsi = it + } else if (textBefore?.text == "=") { + defaultTypePsi = it + } + } + } + + override fun getName() = namePsi?.text ?: "" + + fun decl(): KphpDocGenericParameterDecl { + return KphpDocGenericParameterDecl( + name, + extendsTypePsi?.type?.toExPhpType()?.toString(), + defaultTypePsi?.type?.toExPhpType()?.toString() + ) + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocInheritParameterDeclPsiImpl.kt b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocInheritParameterDeclPsiImpl.kt new file mode 100644 index 0000000..6596753 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocInheritParameterDeclPsiImpl.kt @@ -0,0 +1,74 @@ +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.PhpDocRef +import com.jetbrains.php.lang.documentation.phpdoc.psi.impl.PhpDocPsiElementImpl +import com.jetbrains.php.lang.documentation.phpdoc.psi.impl.PhpDocTypeImpl +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.vk.kphpstorm.exphptype.ExPhpType +import com.vk.kphpstorm.exphptype.ExPhpTypeInstance +import com.vk.kphpstorm.exphptype.ExPhpTypeTplInstantiation +import com.vk.kphpstorm.exphptype.psi.ExPhpTypeInstancePsiImpl +import com.vk.kphpstorm.exphptype.psi.ExPhpTypeTplInstantiationPsiImpl +import com.vk.kphpstorm.generics.GenericUtil.getInstantiation +import com.vk.kphpstorm.helpers.toExPhpType + +data class KphpDocInheritParameterDecl( + val name: String?, + val specializationList: List, + val text: String, +) { + fun specializationList(): List { + return specializationList.map { PhpType().add(it).toExPhpType() ?: ExPhpType.ANY } + } + + override fun toString() = text +} + +/** + * Inside '@kphp-inherit ExtendsClass, ImplementsClass' — 'ExtendsClass' and 'ImplementsClass { + val instantiationPsi = findChildByClass(PhpDocTypeImpl::class.java) ?: return emptyList() + if (instantiationPsi !is ExPhpTypeTplInstantiationPsiImpl) + return emptyList() + + val exType = instantiationPsi.type.toExPhpType() ?: return emptyList() + val instantiation = exType.getInstantiation() ?: return emptyList() + + return instantiation.specializationList + } +} 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..29ee791 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagGenericElementType.kt @@ -0,0 +1,113 @@ +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 +import com.vk.kphpstorm.exphptype.psi.TokensToExPhpTypePsiParsing + +/** + * '@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 { + // stub value is 'T1,T2:ExtendsClass,T2=default' — without spaces + val stubValue = (psi as KphpDocTagGenericPsiImpl).getFullGenericParameters() + .joinToString(",") { + val type = StringBuilder() + type.append(it.name) + + if (it.extendsType != null) { + type.append(":") + type.append(it.extendsType.toString()) + } + + if (it.defaultType != null) { + type.append("=") + type.append(it.defaultType.toString()) + } + + type.toString() + } + 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)) { + var text = builder.tokenText?.trim() + builder.advanceLexer() + + var withExtends = false + if (text == ":") { + if (!TokensToExPhpTypePsiParsing.parseTypeExpression(builder)) { + marker.drop() + builder.error(PhpParserErrors.expected("Extends class name")) + break + } + + withExtends = true + } + + if (withExtends) { + text = builder.tokenText?.trim() + } + + if (text == "=") { + if (withExtends) { + builder.advanceLexer() + } + if (!TokensToExPhpTypePsiParsing.parseTypeExpression(builder)) { + marker.drop() + builder.error(PhpParserErrors.expected("Default type 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..0a4d43d --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagGenericPsiImpl.kt @@ -0,0 +1,97 @@ +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) + + fun getParametersPsi(): List { + val args = mutableListOf() + var child = this.firstChild + while (child != null) { + if (child is KphpDocGenericParameterDeclPsiImpl) + args.add(child) + child = child.nextSibling + } + return args + } + + /** + * Функция возвращающая только имена шаблонных аргументов, без extends и default типов. + * + * Эта функция необходима, чтобы в [KphpDocGenericParameterDeclPsiImpl.decl] мы могли + * вывести типы. Если использовать [getFullGenericParameters] вместо, то + * будет рекурсия, так как для вывода типа класса, нам нужно знать не шаблонный ли это + * аргумент, а для этого используется [getFullGenericParameters] которая вызывает + * в себе [KphpDocGenericParameterDeclPsiImpl.decl].которая в себе пытается вывести типы. + */ + fun getOnlyNameGenericParameters(): List = + when (val stub = this.greenStub) { + null -> getOnlyNameGenericParametersFromAst() + else -> fromStubs(stub).map { it.name } + } + + fun getFullGenericParameters(): List = + when (val stub = this.greenStub) { + null -> getFullGenericParametersFromAst() + else -> fromStubs(stub) + } + + private fun fromStubs(stub: PhpDocTagStub): List { + val value = stub.value ?: return emptyList() + if (value.isEmpty()) { + return emptyList() + } + + return value.split(',').mapNotNull { type -> + val colonIndex = type.indexOf(':') + val parts = type.split(':', '=') + if (parts.isEmpty()) return@mapNotNull null + val name = parts[0] + + if (parts.size == 1) { + KphpDocGenericParameterDecl(name, null, null) + } else if (parts.size == 2) { + if (colonIndex != -1) { + KphpDocGenericParameterDecl(name, parts[1], null) + } else { + KphpDocGenericParameterDecl(name, null, parts[1]) + } + } else if (parts.size == 3) { + KphpDocGenericParameterDecl(name, parts[1], parts[2]) + } else { + null + } + } + } + + private fun getFullGenericParametersFromAst(): List { + val args = mutableListOf() + var child = this.firstChild + while (child != null) { + if (child is KphpDocGenericParameterDeclPsiImpl) + args.add(child.decl()) + child = child.nextSibling + } + return args + } + + private fun getOnlyNameGenericParametersFromAst(): 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/KphpDocTagInheritElementType.kt similarity index 54% rename from src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagTemplateClassElementType.kt rename to src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagInheritElementType.kt index 2efa9df..80c9aee 100644 --- a/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagTemplateClassElementType.kt +++ b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagInheritElementType.kt @@ -10,19 +10,37 @@ 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 +import com.vk.kphpstorm.exphptype.psi.TokensToExPhpTypePsiParsing /** - * '@kphp-template-class T1, T2' has a separate elementType, psi for 'T1' and 'T2' and stub contents - * @see KphpDocElementTypes.kphpDocTagTemplateClass + * '@kphp-inherit ExtendsClass' has a separate elementType, + * psi for ExtendsClass and stub contents. + * + * @see KphpDocElementTypes.kphpDocTagInherit */ -object KphpDocTagTemplateClassElementType : PhpStubElementType("@kphp-template-class"), KphpDocTagElementType { +object KphpDocTagInheritElementType : + PhpStubElementType("@kphp-inherit"), KphpDocTagElementType { + override fun createPsi(stub: PhpDocTagStub): PhpDocTag { - return KphpDocTagTemplateClassPsiImpl(stub, stub.stubType) + return KphpDocTagInheritPsiImpl(stub, stub.stubType) } override fun createStub(psi: PhpDocTag, parentStub: StubElement<*>?): PhpDocTagStub { - // stub value is 'T1,T2' — without spaces - val stubValue = (psi as KphpDocTagTemplateClassPsiImpl).getTemplateArguments().joinToString(",") + val stubValue = (psi as KphpDocTagInheritPsiImpl).getParameters() + .joinToString(",") { + val type = StringBuilder() + + type.append(it.name.toString()) + type.append(':') + + val specsList = it.specializationList().joinToString(".") { specType -> specType.toString() } + type.append(specsList) + + type.append(':') + type.append(it.text.replace(',', ';')) + + type.toString() + } return KphpDocTagStubImpl(parentStub, this, psi.name, stubValue) } @@ -39,21 +57,25 @@ object KphpDocTagTemplateClassElementType : PhpStubElementType, ImplementsClass' — making + * ExtendsClass and ImplementsClass separate psi elements + * @see KphpDocInheritParameterDeclPsiImpl */ + @Suppress("UnstableApiUsage") override fun getTagParser() = object : PhpDocTagParser() { - override fun getElementType() = KphpDocTagTemplateClassElementType + override fun getElementType() = KphpDocTagInheritElementType override fun parseContents(builder: PhpPsiBuilder): Boolean { do { val marker = builder.mark() - if (!builder.compareAndEat(PhpDocTokenTypes.DOC_IDENTIFIER)) { + + if (!TokensToExPhpTypePsiParsing.parseTypeExpression(builder)) { marker.drop() - builder.error(PhpParserErrors.expected("Template argument name (like T)")) + builder.error(PhpParserErrors.expected("Extends/implements class name")) break } - marker.done(KphpDocTplParameterDeclPsiImpl.elementType) + + marker.done(KphpDocInheritParameterDeclPsiImpl.elementType) } while (builder.compareAndEat(PhpDocTokenTypes.DOC_COMMA)) return true } diff --git a/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagInheritPsiImpl.kt b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagInheritPsiImpl.kt new file mode 100644 index 0000000..289c508 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagInheritPsiImpl.kt @@ -0,0 +1,54 @@ +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-inherit' tag — created either from stub or from ast + * @see KphpDocElementTypes.kphpDocTagGeneric + */ +class KphpDocTagInheritPsiImpl : PhpDocTagImpl, KphpDocTagImpl { + constructor(node: ASTNode) : super(node) + constructor(stub: PhpDocTagStub, nodeType: IStubElementType<*, *>) : super(stub, nodeType) + + fun getParametersPsi(): List = + children.filterIsInstance() + + fun getParameters(): List = + when (val stub = this.greenStub) { + null -> getParametersFromAst() + else -> fromStubs(stub) + } + + private fun fromStubs(stub: PhpDocTagStub): List { + val value = stub.value ?: return emptyList() + if (value.isEmpty()) { + return emptyList() + } + + return value.split(',').map { type -> + val parts = type.split(':') + if (parts.size != 3) { + return@map KphpDocInheritParameterDecl(name, emptyList(), name) + } + + val (name, typesList, text) = parts + val types = typesList.split(".") + + KphpDocInheritParameterDecl(name, types, text.replace(';', ',')) + } + } + + private fun getParametersFromAst(): List { + val args = mutableListOf() + var child = this.firstChild + while (child != null) { + if (child is KphpDocInheritParameterDeclPsiImpl) + args.add(child.decl()) + child = child.nextSibling + } + return args + } +} diff --git a/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagTemplateClassPsiImpl.kt b/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagTemplateClassPsiImpl.kt deleted file mode 100644 index 15c02fe..0000000 --- a/src/main/kotlin/com/vk/kphpstorm/kphptags/psi/KphpDocTagTemplateClassPsiImpl.kt +++ /dev/null @@ -1,37 +0,0 @@ -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 - -/** - * Implemetation of '@kphp-template-class' tag — created either from stub or from ast - * @see KphpDocElementTypes.kphpDocTagTemplateClass - */ -class KphpDocTagTemplateClassPsiImpl : 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 getTemplateArguments(): List = - when (val stub = this.greenStub) { - null -> getTemplateArgumentsFromAst() - else -> stub.value.let { // stub value is 'T1,T2' - if (it != null && it.isNotEmpty()) it.split(',') - else listOf() - } - } - - private fun getTemplateArgumentsFromAst(): List { - val args = mutableListOf() - var child = this.firstChild - while (child != null) { - if (child is KphpDocTplParameterDeclPsiImpl) - args.add(child.text) - child = child.nextSibling - } - return args - } -} 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..da03b0e --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/typeProviders/ClassConstTypeProvider.kt @@ -0,0 +1,34 @@ +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.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() = '§' + + override fun getType(p: PsiElement): PhpType? { + if (p is ClassConstantReference) { + val classExType = p.classReference?.type?.toExPhpType() + if (classExType != null) { + return PhpType().add("force(class-string($classExType))") + } + } + return null + } + + override fun complete(incompleteType: String, project: Project) = null + + override fun getBySignature(t: String, v: MutableSet, d: Int, p: Project) = 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..e1f6cfc 100644 --- a/src/main/kotlin/com/vk/kphpstorm/typeProviders/FunctionsTypeProvider.kt +++ b/src/main/kotlin/com/vk/kphpstorm/typeProviders/FunctionsTypeProvider.kt @@ -3,9 +3,11 @@ 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.* +import com.vk.kphpstorm.generics.GenericUtil.isGeneric import com.vk.kphpstorm.helpers.toExPhpType import com.vk.kphpstorm.helpers.toStringAsNested @@ -245,9 +247,33 @@ 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 + + var containsUnresolved = false + 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 + if (!type.isComplete) { + containsUnresolved = true + } + + Pair(key, type) + } + + if (containsUnresolved) { + return PhpType().add( + "#!h ${types.joinToString("ꄴ") { it.first + ":" + it.second.toStringAsNested("ꄶ") }}" + ) + } + + return inferShape(types) } // println("unhandled function: $funcName") @@ -262,10 +288,35 @@ class FunctionsTypeProvider : PhpTypeProvider4 { if (funcChar == 't') { val parameterTypes = argTypeStr.split('ꄳ').map { PhpType().apply { - it.split('⎋').forEach { add(it) } + it.split('⎋').forEach { + val subType = PhpType().add(it).global(project) + // TODO: add more tests + if (!subType.isAmbiguous) { + add(PhpType().add(it).global(project)) + } + } + } + } + return inferTuple(parameterTypes.map { it.global(project) }) + } + + // inferring for "shape(...)" needs special decoding: any arguments are encoded to a single string + if (funcChar == 'h') { + val parameterTypes = argTypeStr.split('ꄴ').map { + val (key, unresolvedType) = it.split(':') + val type = PhpType().apply { + unresolvedType.split('ꄶ').forEach { unresolvedSubType -> + val subType = PhpType().add(unresolvedSubType).global(project) + // TODO: add more tests + if (!subType.isAmbiguous) { + add(PhpType().add(unresolvedSubType).global(project)) + } + } }.global(project) + + Pair(key, type) } - return inferTuple(parameterTypes) + return inferShape(parameterTypes) } // for all other cases, just a single argument is encoded @@ -286,9 +337,14 @@ class FunctionsTypeProvider : PhpTypeProvider4 { return null } - private fun PhpType.force(): PhpType { - return when (this.toExPhpType()) { + val type = this.toExPhpType() ?: return this + + if (type.isGeneric()) { + return PhpType().add(this) + } + + return when(type) { is ExPhpTypeForcing -> this is ExPhpTypeInstance -> this else -> PhpType().add("force($this)") @@ -300,7 +356,6 @@ class FunctionsTypeProvider : PhpTypeProvider4 { val argType = (arg as? PhpTypedElement)?.type ?: return null return if (argType.isEmpty) KphpPrimitiveTypes.PHP_TYPE_ANY else argType } - private fun inferTypeSameAs(argType: PhpType): PhpType { return argType.force() @@ -338,4 +393,10 @@ class FunctionsTypeProvider : PhpTypeProvider4 { parameterTypes.joinToString(",", "tuple(", ")") { it.toStringAsNested() } ) } + + private fun inferShape(parameterTypes: List>): PhpType { + return PhpType().add( + parameterTypes.joinToString(",", "shape(", ")") { it.first + ":" + it.second.toStringAsNested() } + ) + } } diff --git a/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericClassesTypeProvider.kt b/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericClassesTypeProvider.kt new file mode 100644 index 0000000..3093a45 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericClassesTypeProvider.kt @@ -0,0 +1,54 @@ +package com.vk.kphpstorm.typeProviders + +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.util.parentOfType +import com.jetbrains.php.lang.psi.elements.ClassReference +import com.jetbrains.php.lang.psi.elements.NewExpression +import com.jetbrains.php.lang.psi.elements.PhpClass +import com.jetbrains.php.lang.psi.resolve.types.PhpCharTypeKey +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4 +import com.vk.kphpstorm.exphptype.psi.ExPhpTypeTplInstantiationPsiImpl +import com.vk.kphpstorm.generics.GenericUtil.genericInheritInstantiationPsi +import com.vk.kphpstorm.generics.IndexingGenericCall +import com.vk.kphpstorm.generics.ResolvingGenericConstructorCall + +class GenericClassesTypeProvider : PhpTypeProvider4 { + companion object { + const val SEP = "―" + val KEY = PhpCharTypeKey('±') + } + + override fun getKey() = KEY.key + + override fun getType(p: PsiElement?): PhpType? { + // new A/*<...args>*/() + if (p is NewExpression) { + val classRef = p.classReference ?: return null + val fqn = classRef.fqn + ".__construct" + val data = IndexingGenericCall(fqn, p.parameters, p, SEP).pack() + return PhpType().add(KEY.sign(data)) + } + + if (p is ClassReference && p.name == "parent") { + val containingClass = p.parentOfType() + if (containingClass != null) { + val superClass = containingClass.extendsList.referenceElements.firstOrNull() ?: return null + val superClassName = superClass.fqn ?: return null + val instantiationParameter = containingClass.genericInheritInstantiationPsi(superClassName) + val instantiation = instantiationParameter?.firstChild as? ExPhpTypeTplInstantiationPsiImpl + return instantiation?.type + } + } + + return null + } + + override fun complete(incompleteType: String, project: Project) = + ResolvingGenericConstructorCall(project).resolve(incompleteType) + + override fun getBySignature(t: String, v: MutableSet, d: Int, p: Project) = null + + override fun emptyResultIsComplete() = true +} diff --git a/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericFieldsTypeProvider.kt b/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericFieldsTypeProvider.kt new file mode 100644 index 0000000..b153057 --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericFieldsTypeProvider.kt @@ -0,0 +1,53 @@ +package com.vk.kphpstorm.typeProviders + +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.jetbrains.php.lang.psi.elements.FieldReference +import com.jetbrains.php.lang.psi.resolve.types.PhpCharTypeKey +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4 +import com.vk.kphpstorm.generics.IndexingGenericCall +import com.vk.kphpstorm.generics.ResolvingGenericFieldFetch + +class GenericFieldsTypeProvider : PhpTypeProvider4 { + companion object { + const val SEP = "≠" + val KEY = PhpCharTypeKey('μ') + } + + override fun getKey() = KEY.key + + override fun getType(p: PsiElement?): PhpType? { + // $v->a + if (p is FieldReference && !p.isStatic) { + val fieldName = p.name ?: return null + val lhs = p.classReference ?: return null + val lhsTypes = lhs.type.types.filter { type -> + GenericClassesTypeProvider.KEY.signed(type) || + GenericFunctionsTypeProvider.KEY.signed(type) || + GenericMethodsTypeProvider.KEY.signed(type) || + KEY.signed(type) || + !type.startsWith("#") + } + + val resultType = PhpType() + lhsTypes.forEach { type -> + val fqn = "$type.$fieldName" + val data = IndexingGenericCall(fqn, emptyArray(), p, SEP).pack() + + resultType.add(KEY.sign(data)) + } + + return resultType + } + + return null + } + + override fun complete(incompleteType: String, project: Project) = + ResolvingGenericFieldFetch(project).resolve(incompleteType) + + override fun getBySignature(t: String, v: MutableSet, d: Int, p: Project) = null + + override fun emptyResultIsComplete() = true +} 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..3a77a3e --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericFunctionsTypeProvider.kt @@ -0,0 +1,36 @@ +package com.vk.kphpstorm.typeProviders + +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.jetbrains.php.lang.psi.elements.FunctionReference +import com.jetbrains.php.lang.psi.elements.MethodReference +import com.jetbrains.php.lang.psi.resolve.types.PhpCharTypeKey +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4 +import com.vk.kphpstorm.generics.IndexingGenericCall +import com.vk.kphpstorm.generics.ResolvingGenericFunctionCall + +class GenericFunctionsTypeProvider : PhpTypeProvider4 { + companion object { + const val SEP = "∃" + val KEY = PhpCharTypeKey('П') + } + + override fun getKey() = KEY.key + + override fun getType(p: PsiElement): PhpType? { + if (p !is FunctionReference || p is MethodReference) { + return null + } + + val data = IndexingGenericCall(p.fqn!!, p.parameters, p, SEP).pack() + return PhpType().add(KEY.sign(data)) + } + + override fun complete(incompleteType: String, project: Project) = + ResolvingGenericFunctionCall(project).resolve(incompleteType) + + override fun getBySignature(t: String, v: MutableSet, d: Int, p: Project) = null + + override fun emptyResultIsComplete() = true +} diff --git a/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericMethodsTypeProvider.kt b/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericMethodsTypeProvider.kt new file mode 100644 index 0000000..c4dba5d --- /dev/null +++ b/src/main/kotlin/com/vk/kphpstorm/typeProviders/GenericMethodsTypeProvider.kt @@ -0,0 +1,98 @@ +package com.vk.kphpstorm.typeProviders + +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.util.parentOfType +import com.jetbrains.php.lang.psi.elements.* +import com.jetbrains.php.lang.psi.elements.Function +import com.jetbrains.php.lang.psi.resolve.types.PhpCharTypeKey +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4 +import com.vk.kphpstorm.exphptype.ExPhpType +import com.vk.kphpstorm.generics.GenericUtil.genericNames +import com.vk.kphpstorm.generics.GenericUtil.isGeneric +import com.vk.kphpstorm.generics.IndexingGenericCall +import com.vk.kphpstorm.generics.ResolvingGenericMethodCall +import com.vk.kphpstorm.helpers.toExPhpType +import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl + +class GenericMethodsTypeProvider : PhpTypeProvider4 { + companion object { + const val SEP = "⁓" + val KEY = PhpCharTypeKey('ω') + } + + override fun getKey() = KEY.key + + override fun getType(p: PsiElement?): PhpType? { + if (p is Parameter) { + // Для параметров если они шаблонные и имеют дефолтный тип или тип extends, то + // возвращаем здесь этот тип тем самым типизируя частично код внутри функции. + val parentFunction = p.parentOfType() ?: return null + val parentClass = p.parentOfType() + val paramTag = parentFunction.docComment?.getParamTagByName(p.name) ?: return null + val docType = paramTag.type.toExPhpType() ?: return null + val genericNames = parentFunction.genericNames() + (parentClass?.genericNames() ?: emptyList()) + + return instantiateDocType(docType, genericNames) + } + + if (p is Field) { + val parentClass = p.containingClass ?: return null + val paramTag = p.docComment?.varTag ?: return null + val docType = paramTag.type.toExPhpType() ?: return null + val genericNames = parentClass.genericNames() + + return instantiateDocType(docType, genericNames) + } + + // $v->f() or ClassName::f() + if (p is MethodReference) { + val methodName = p.name ?: return null + val lhs = p.classReference ?: return null + val lhsTypes = lhs.type.types.filter { type -> + GenericClassesTypeProvider.KEY.signed(type) || + GenericFunctionsTypeProvider.KEY.signed(type) || + GenericFieldsTypeProvider.KEY.signed(type) || + KEY.signed(type) || + (!type.startsWith("#") && !type.startsWith("%")) + } + + val resultType = PhpType() + lhsTypes.forEach { type -> + val fqn = "$type.$methodName" + val data = IndexingGenericCall(fqn, p.parameters, p, SEP).pack() + + resultType.add(KEY.sign(data)) + } + + return resultType + } + + return null + } + + private fun instantiateDocType(docType: ExPhpType, genericNames: List): PhpType? { + if (!docType.isGeneric()) { + return null + } + + val instantiationMap = genericNames.mapNotNull { + it.name to (it.extendsType ?: it.defaultType ?: return@mapNotNull null) + }.toMap() + + if (instantiationMap.isEmpty()) { + return null + } + + val type = docType.instantiateGeneric(instantiationMap) + return PhpType().add(type.toPhpType()).add(docType.toPhpType()) + } + + override fun complete(incompleteType: String, project: Project) = + ResolvingGenericMethodCall(project).resolve(incompleteType) + + override fun getBySignature(t: String, v: MutableSet, d: Int, p: Project) = null + + override fun emptyResultIsComplete() = true +} 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/kotlin/com/vk/kphpstorm/typeProviders/TupleShapeTypeProvider.kt b/src/main/kotlin/com/vk/kphpstorm/typeProviders/TupleShapeTypeProvider.kt index bd52e4d..74c7afc 100644 --- a/src/main/kotlin/com/vk/kphpstorm/typeProviders/TupleShapeTypeProvider.kt +++ b/src/main/kotlin/com/vk/kphpstorm/typeProviders/TupleShapeTypeProvider.kt @@ -83,8 +83,18 @@ class TupleShapeTypeProvider : PhpTypeProvider4 { // so, in complex scenarios like get()[1][2]->... combinations increase geometrically // to partially avoid this, use heruistics: // filter out subtypes detected by PhpStorm native type providers that are 100% useless here - if (!it.contains("#π") && !it.contains("#E")) + if (!it.contains("#π") && !it.contains("#E") && !it.contains("%")) { resultType.add("#Й.$indexKey $it") + return@forEach + } + + if (it.contains(GenericFunctionsTypeProvider.KEY.key) || + it.contains(GenericMethodsTypeProvider.KEY.key) || + it.contains(GenericClassesTypeProvider.KEY.key) || + it.contains(GenericFieldsTypeProvider.KEY.key) + ) { + resultType.add("#Й.$indexKey $it") + } } // println("type($lhs) = ${resultType.toString().replace("|", " | ")}") @@ -121,6 +131,10 @@ class TupleShapeTypeProvider : PhpTypeProvider4 { */ override fun complete(incompleteTypeStr: String, project: Project): PhpType? { val spacePos = incompleteTypeStr.indexOf(' ') + if (spacePos == -1) { + return null + } + val indexKey = incompleteTypeStr.substring(3, spacePos) val lhsTypeStr = incompleteTypeStr.substring(spacePos + 1) val wholeType = PhpType().add(lhsTypeStr).global(project) @@ -218,8 +232,9 @@ class TupleShapeTypeProvider : PhpTypeProvider4 { || it == "\\any" // any[*] is any, not undefined || it == "\\array" // array[*] is any (untyped arrays) } - if (!needsCustomIndexing) - return null + // TODO: Нам нужно тут выводить самим так как дженерики +// if (!needsCustomIndexing) +// return null return wholeType.toExPhpType()?.getSubkeyByIndex(indexKey)?.toPhpType() } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index a6eefa0..2b4b858 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -67,11 +67,22 @@ enabledByDefault="false" level="ERROR" implementationClass="com.vk.kphpstorm.inspections.KphpUnsupportedFunctionCallInspection"/> + + + com.vk.kphpstorm.inspections.PrettifyPhpdocBlockIntention PHP PrettifyPhpdocBlockIntention + + com.vk.kphpstorm.inspections.CollapseGenericsInstantiationIntention + PHP + CollapseGenericsInstantiationIntention + @@ -81,6 +92,11 @@ implementationClass="com.vk.kphpstorm.completion.KphpStormCompletionContributor"/> + + @@ -90,13 +106,24 @@ implementationClass="com.vk.kphpstorm.highlighting.KphpStormTypeInfoProvider"/> + + + + + - + + + + + diff --git a/src/main/resources/colorSchemes/KphpAddonsDarcula.xml b/src/main/resources/colorSchemes/KphpAddonsDarcula.xml index c3aeb6a..089c7bd 100644 --- a/src/main/resources/colorSchemes/KphpAddonsDarcula.xml +++ b/src/main/resources/colorSchemes/KphpAddonsDarcula.xml @@ -21,6 +21,12 @@ + + */(new A); +take_instance/**/(new B); + + +/** + * @kphp-generic T: string + * @param T $a + */ +function take_string($a) {} + +take_string(""); +take_string(10); +take_string/**/(""); +take_string/**/(10); + + +/** + * @kphp-generic T: SomeI | B + * @param T $a + */ +function take_instance_or_other($a) {} + +take_instance_or_other(new A); +take_instance_or_other(new B); +take_instance_or_other(new C); +take_instance_or_other/**/(new A); +take_instance_or_other/**/(new B); +take_instance_or_other/**/(new C); + + +/** + * @kphp-generic T: string | int + * @param T $a + */ +function take_string_or_int($a) {} + +take_string_or_int(""); +take_string_or_int(100); +take_string_or_int(true); +take_string_or_int/**/(""); +take_string_or_int/**/(100); +take_string_or_int/**/(true); + + +/** + * @kphp-generic T: string | Stringable + * @param T $a + */ +function take_stringable_or_string($a) {} + +take_stringable_or_string(""); +take_stringable_or_string(new DStringable); +take_stringable_or_string(new C); +take_stringable_or_string/**/(""); +take_stringable_or_string/**/(new DStringable); +take_stringable_or_string/**/(new C); diff --git a/src/test/fixtures/generics/general/reifier/reify.fixture.php b/src/test/fixtures/generics/general/reifier/reify.fixture.php new file mode 100644 index 0000000..779ae26 --- /dev/null +++ b/src/test/fixtures/generics/general/reifier/reify.fixture.php @@ -0,0 +1,151 @@ +fooMethod(); +f8(new Boo, new Boo)->booMethod(); +f8(new Goo)->gooMethod(); + +class F10 { + /** + * @kphp-generic T + * @param T ...$all + * @return T + */ + function f11(...$all) { + return $all[0]; + } +} + +$f10 = new F10; +(function() use ($f10) { + $f10->f11(new Foo, new Foo, new Foo)->fooMethod(); + $f10->f11(new Boo)->booMethod(); + $f10->f11/**/ ()->gooMethod(); +})(); + +/** + * @kphp-generic T + * @param callable(T,T): bool $gt + * @param T ...$arr + * @return T + */ +function maxBy($gt, ...$arr) { + $max = array_first_value($arr); + for ($i = 1; $i < count($arr); ++$i) { + if ($gt($arr[$i], $max)) { + $max = $arr[$i]; + } + } + return $max; +} + +echo maxBy/**/ (fn($a, $b) => $a > $b, 1, 2, 9, 3), "\n"; +echo maxBy/**/ (fn($a, $b) => ord($a) > ord($b), 'a', 'z', 'd'), "\n"; + +/** + * @kphp-generic T, DstClass + * @param T $obj + * @param class-string $to_classname + * @return DstClass + */ +function my_cast($obj, $to_classname) { + return instance_cast($obj, $to_classname); +} + +/** + * @kphp-generic T, DstClass + * @param T $obj + * @param class-string $if_classname + */ +function callDMethodIfNotNull($obj, $if_classname) { + $casted = my_cast($obj, $if_classname); + if ($casted) { + $casted->dMethod(); + } else { + echo "cast to $if_classname is null: obj is ", get_class($obj), "\n"; + } +} + +/** + * @kphp-generic T + * @param T|false $a + * @return T + */ +function foo1($a) { + return $a; +} + +$a = new Foo() ?? false; + +$b = foo1($a); +expr_type($b, "\Reify\Foo"); + + +/** @kphp-generic T */ +class GenericT {} + +/** + * @kphp-generic T + * @param T $a + * @return T + */ +function foo2($a) { + return $a; +} + +$a = new GenericT/**/(); + +$b = foo2($a); +expr_type($b, "\Reify\GenericT(int)"); + + +/** + * @kphp-generic TKey, TValue + */ +class Map {} +class Exception {} + +class SomeFoo { + /** + * @kphp-generic Tk, Tv + * @param Map $containers + * @return Tv + */ + function unwrap($containers) { + return; + } + + /** + * @param Map $typed_containers + */ + function takesDifferentTypes($typed_containers): void { + $value = $this->unwrap($typed_containers); + expr_type($value, "\Reify\Exception"); + } +} diff --git a/src/test/fixtures/generics/general/vector_use.fixture.php b/src/test/fixtures/generics/general/vector_use.fixture.php new file mode 100644 index 0000000..e4425ef --- /dev/null +++ b/src/test/fixtures/generics/general/vector_use.fixture.php @@ -0,0 +1,101 @@ +*/([new Goo], function(Goo $a): Boo { + return $a->gooMethod(); +}); + +$b = $a[0]; +$b->booMethod(); + +$vec = new Vector/**/ (); + +$vec->add(new Goo); +$vec->add(null); + +$vec->filter(function(?Goo $el): bool { + return $el != null; +}); + +$vec->foreach(function(Goo $el) { + var_dump($el); +}); + +$vec->foreach(fn(Goo $el) => $el->getName()); + +$vec->foreach_key_value(function(string $key, Goo $el) { + var_dump($key); + var_dump($el); +}); + +$a = $vec->map/**/(function(Goo $a): Boo { + return $a->gooMethod(); +}); + +$b = $a->get(0); + +$c = $b->gooMethod()->booMethod()->fooMethod(); + +/** + * @return Vector> + */ +function returnVector() { + return new Vector; +} + +$a = returnVector()->get(0); + +$a->get(0)->fooMethod(); + +/** + * @return Pair + */ +function returnPair(): Pair { + return new Pair(new Boo, new Goo); +} + +$x = returnPair()->second(); +$x->gooMethod(); + +$vecGoo = new Vector/**/(); +$vecBoo = new Vector/**/(); + +$combinedVec = $vecGoo->combine_with($vecBoo); +$combinedVec->get(0)->booMethod(); +$combinedVec->get(0)->gooMethod(); diff --git a/src/test/fixtures/generics/inspections/bound_violation.fixture.php b/src/test/fixtures/generics/inspections/bound_violation.fixture.php new file mode 100644 index 0000000..252d175 --- /dev/null +++ b/src/test/fixtures/generics/inspections/bound_violation.fixture.php @@ -0,0 +1,24 @@ +f(); +} + +take_some_i(new A); +take_some_i(new B); +take_some_i(new C); +take_some_i/**/(new A); +take_some_i/**/(new B); +take_some_i/**/(new C); +take_some_i/**/(null); diff --git a/src/test/fixtures/generics/inspections/duplicate_generic_params.fixture.php b/src/test/fixtures/generics/inspections/duplicate_generic_params.fixture.php new file mode 100644 index 0000000..d098829 --- /dev/null +++ b/src/test/fixtures/generics/inspections/duplicate_generic_params.fixture.php @@ -0,0 +1,37 @@ +@kphp-generic */ +function f() {} + +/** @kphp-generic T: */ +function f1() {} + +/** @kphp-generic T: Foo */ +function f2() {} + +/** @kphp-generic T = */ +function f3() {} + +/** @kphp-generic T: Foo = */ +function f4() {} + +/** @kphp-generic T: Foo = Foo */ +function f5() {} + +/** @kphp-generic T, */ +function f6() {} + +/** + * @kphp-generic T, T + */ +function takeSomethingOther() {} + +/** + * @kphp-generic T, T1, T1 + */ +class SomeClass { + /** + * @kphp-generic T + */ + function takeSomethingOther() {} +} diff --git a/src/test/fixtures/generics/inspections/extends/ok.fixture.php b/src/test/fixtures/generics/inspections/extends/ok.fixture.php new file mode 100644 index 0000000..c0bc6bb --- /dev/null +++ b/src/test/fixtures/generics/inspections/extends/ok.fixture.php @@ -0,0 +1,51 @@ + */ +function take10() {} + +// Special allowed instance + primitive case +/** @kphp-generic T: Stringable|string */ +function take11() {} +/** @kphp-generic T: \Stringable|string */ +function take12() {} +/** @kphp-generic T: string|Stringable */ +function take13() {} +/** @kphp-generic T: string|\Stringable */ +function take14() {} diff --git a/src/test/fixtures/generics/inspections/extends/wrong.fixture.php b/src/test/fixtures/generics/inspections/extends/wrong.fixture.php new file mode 100644 index 0000000..9e7280e --- /dev/null +++ b/src/test/fixtures/generics/inspections/extends/wrong.fixture.php @@ -0,0 +1,29 @@ +@kphp-generic T: int|Foo */ +function take1() {} + +/** @kphp-generic T: ?Foo */ +function take2() {} + +/** @kphp-generic T: force(int) */ +function take3() {} + +/** @kphp-generic T: tuple(int, Foo) */ +function take4() {} + +/** @kphp-generic T: shape(key1: int, key2: Foo) */ +function take5() {} + +/** @kphp-generic T: class-string */ +function take6() {} + +/** @kphp-generic T: Foo[] */ +function take7() {} + +/** @kphp-generic T: any */ +function take8() {} diff --git a/src/test/fixtures/generics/inspections/instantiation_args_mismatch.fixture.php b/src/test/fixtures/generics/inspections/instantiation_args_mismatch.fixture.php new file mode 100644 index 0000000..1134a14 --- /dev/null +++ b/src/test/fixtures/generics/inspections/instantiation_args_mismatch.fixture.php @@ -0,0 +1,85 @@ +/*<>*/(); +new GenericT/**/(); // ok + + +// Класс с двумя шаблоннымм типом +/** @kphp-generic T1, T2 */ +class GenericT1T2 {} + +new GenericT1T2/*<>*/(); +new GenericT1T2/**/(); +new GenericT1T2/**/(); // ok + + +// Класс с одним шаблонным типом с конструктором с еще одним шаблонным типом +/** @kphp-generic T */ +class GenericExplicitConstructorT1AndT { + /** + * @kphp-generic T1 + * @param T1 $el + */ + function __construct($el) {} +} + +new GenericExplicitConstructorT1AndT/**/(""); + +// Функция с дефолтными шаблонными параметрами +/** + * @kphp-generic T1, T2 = Vector + * @param T1 $a + * @return T2 + */ +function foo($a) { return $a; } + +$a = foo/**/(new Foo); +$a = foo/**/(new Foo); +$a = foo/*<>*/(new Foo); +$a = foo/**/(new Foo); + +/** + * @kphp-generic T1, T2 = Vector, T3 = int + * @param T1 $a + * @return T2 + */ +function foo1($a) { return $a; } + +$a = foo1/**/(new Foo); +$a = foo1/**/(new Foo); +$a = foo1/*<>*/(new Foo); +$a = foo1/**/(100); +$a = foo1/**/(new Foo); +$a = foo1/**/(new Foo); + + +/** + * @param GenericT $a + * @param GenericT $a1 + * @param GenericT $a2 + */ +function foo2($a, $a1, $a2) {} + + +/** + * @kphp-generic T1 = GenericT, T2 = GenericT + */ +function foo3() {} + + +/** + * @kphp-generic T1 = GenericT + */ +function foo4() {} diff --git a/src/test/fixtures/generics/inspections/no_enough_information.fixture.php b/src/test/fixtures/generics/inspections/no_enough_information.fixture.php new file mode 100644 index 0000000..23352d1 --- /dev/null +++ b/src/test/fixtures/generics/inspections/no_enough_information.fixture.php @@ -0,0 +1,84 @@ +new GenericT(); +new GenericT(100); // нет зависимости от типа аргумента + + +// Класс с одним шаблонным типом с нестандартным именем +/** @kphp-generic TKey */ +class GenericTKey {} + +new GenericTKey(); + + +// Класс с двумя шаблоннымм типом +/** @kphp-generic T1, T2 */ +class GenericT1T2 {} + +// Выводим ошибку только для первого шаблонного типа +new GenericT1T2(); + + +// Класс с одним шаблонным типом с конструктором с аргументом шаблонного типа Т +/** @kphp-generic T */ +class GenericExplicitConstructorT { + /** @param T $el */ + function __construct($el) {} +} + +new GenericExplicitConstructorT(); +new GenericExplicitConstructorT(100); // в отличии от класса GenericT тут зависимость есть + + +// Класс с одним шаблонным типом с конструктором с еще одним шаблонным типом +/** @kphp-generic T */ +class GenericExplicitConstructorT1AndT { + /** + * @kphp-generic T1 + * @param T1 $el + */ + function __construct($el) {} +} + +new GenericExplicitConstructorT1AndT(100); +new GenericExplicitConstructorT1AndT/**/(100); // ok + + +// Класс с одним шаблонным типом с конструктором с еще одним шаблонным типом оба используемые в аргументах +/** @kphp-generic T */ +class GenericExplicitConstructorT1AndExplicitT { + /** + * @kphp-generic T1 + * @param T $el + * @param T1 $el2 + */ + function __construct($el, $el2) {} +} + +new GenericExplicitConstructorT1AndExplicitT(100); +new GenericExplicitConstructorT1AndExplicitT(100, ""); // ok + + +// Класс с двумя шаблоннымм типом один из которых используется в конструкторе +/** @kphp-generic T1, T2 */ +class GenericExplicitConstructorT1AndImplicitT2 { + /** @param T1 $el */ + function __construct($el) {} +} + +// Выводим ошибку только для первого шаблонного типа +new GenericExplicitConstructorT1AndImplicitT2(); +new GenericExplicitConstructorT1AndImplicitT2(100); +new GenericExplicitConstructorT1AndImplicitT2/**/(100); diff --git a/src/test/fixtures/generics/inspections/several_reified_types.fixture.php b/src/test/fixtures/generics/inspections/several_reified_types.fixture.php new file mode 100644 index 0000000..9c9bf4b --- /dev/null +++ b/src/test/fixtures/generics/inspections/several_reified_types.fixture.php @@ -0,0 +1,13 @@ +: it's both string and int">takeSomething(100, ""); +takeSomething(new Foo, ""); +takeSomething/**/ (100, ""); diff --git a/src/test/fixtures/generics/kphp/016_kphp_param_depends_T.php b/src/test/fixtures/generics/kphp/016_kphp_param_depends_T.php new file mode 100644 index 0000000..f8510af --- /dev/null +++ b/src/test/fixtures/generics/kphp/016_kphp_param_depends_T.php @@ -0,0 +1,296 @@ + $a + * @return T + */ +function wait($a) { return $a; } + +/** + * @kphp-generic T + * @param T $a + * @return future + */ +function fork($a): future { return new future($a); } + +/** + * @kphp-generic TElem + * @param TElem[] $arr + */ +function acceptsArr($arr) { + foreach ($arr as $o) + if($o) $o->method(); +} + +/** + * @kphp-generic E1 + * @param E1 $obj + */ +function callArr1($obj) { + acceptsArr([$obj]); +} + +/** + * @kphp-generic E2 + * @param E2 $obj + */ +function callArr2($obj) { + acceptsArr/**/([$obj]); +} + +callArr1(new A); +callArr1(new B); +callArr2(new A); +callArr2/**/(null); +callArr2(new C); + + + +/** + * @kphp-generic T1, T2 + * @param T1[] $arr1 + * @param T2[] $arr2 + */ +function f1($arr1, $arr2) { + foreach ($arr1 as $a1) + if ($a1) $a1->method(); + foreach ($arr2 as $a2) + if ($a2) $a2->method(); +} + +f1/*<\KphpParamDepends\A, \KphpParamDepends\A>*/([null], [new A]); +f1/**/([new B], [new B]); +f1/**/([new A], [new B]); + + +/** + * @kphp-generic T1 + * @param T1 $first + * @param T1[] $all + */ +function f2($first, $all) { + if (count($all)) { + /** @var tuple(int, T1) $f */ + $f = tuple(1, [[$all]][0][0][0]); + if(0) $f[1]->method(); + } + foreach ($all as $a) + if($a) $a->method(); +} + +f2(new A, []); +f2(new A, [null]); + + +class F3c { + /** + * @kphp-generic T + * @param T $arg + * @param callable(T):T $cb + */ + function f3($arg, $cb) { + $m = $cb($arg); + $m->method(); + } +} + +$f3 = new F3c; +$f3->f3(new A, fn($a) => $a); +$f3->f3(new B, function($b) { $b->method(); return $b; }); + +/** + * @kphp-generic T : callable + * @param T $cb + */ +function f4($cb) { + echo $cb(), "\n"; +} + +f4(fn() => 4); +f4(fn() => 's'); + +/** + * @kphp-generic T1 : callable, T2 : callable + * @param T1 $cb1 + * @param T2 $cb2 + */ +function f5($cb1, $cb2) { + echo $cb1(), $cb2(), "\n"; +} + +f5(fn() => 5, fn() => 's5'); + + +/** + * @kphp-generic T2 : callable + * @param T2 $cb2 + */ +function f6(callable $cb1, $cb2) { + echo $cb1(), $cb2(), "\n"; +} + +f6(fn() => 6, fn() => 's6'); + + +/** + * @kphp-generic T + * @param tuple(int, T) $t + */ +function f7($t) { + echo $t[0], ' ', $t[1]->method(), "\n"; +} + +f7/**/(tuple(1, new A)); + + +/** + * @kphp-generic T + * @param T $first + * @param T ...$rest + */ +function f8($first, ...$rest) { + $first->method(); + foreach ($rest as $o) + $o->method(); +} + +f8(new A, new A, new A); +f8(new B, new B); +f8(new C); + + +class F9c { + /** + * @kphp-generic T + * @param T[] $all + */ + static function f9($all) { + foreach ($all as $o) + $o->method(); + } +} + +F9c::f9([new A, new A, new A]); +F9c::f9([new B]); +F9c::f9/**/([]); + + +class F10 { + /** + * @kphp-generic T + * @param T ...$all + */ + function f10(...$all) { + foreach ($all as $o) + $o->method(); + } +} + +$f10 = new F10; +(function() use($f10) { + $f10->f10(new A, new A, new A); + $f10->f10(new B); + $f10->f10/**/(); +})(); + + +/** + * @kphp-generic T, TCb : callable + * @param T $arg + * @param TCb $cb + */ +function strangeCall($arg, $cb) { + $cb($arg); +} + +strangeCall/**/(1, function($i) { echo $i, "\n"; }); +strangeCall/**/(new A, function($a) { $a->method(); return 's'; }); + + +class AVal { + public int $val = 0; + + /** + * @param string $val + */ + function __construct($val) { $this->val = $val; } + function f() { echo "AVal {$this->val}\n"; } +} + +function get_int(int $i): int { sched_yield(); return $i; } +function get_AVal(string $val): AVal { sched_yield(); return new AVal($val); } + +/** + * @return tuple(int, Aval) + */ +function get_tuple() { sched_yield(); return tuple(-1, new AVal("-1")); } + +/** @return future */ +function getFuture_int(int $i) { return fork(get_int($i)); } +/** @return future */ +function getFuture_AVal(string $val) { return fork(get_AVal($val)); } +/** @return future */ +function getFuture_tuple() { return fork(get_tuple()); } + +/** + * @kphp-generic T + * @param future ...$resumables + * @return (?T)[] + */ +function wait_all(...$resumables) { + $answers = []; + foreach ($resumables as $key => $resumable) + $answers[$key] = wait($resumable); + return $answers; +} + +function demoFutures() { + $int_arr = wait_all/**/(getFuture_int(10), getFuture_int(20)); + var_dump($int_arr); + + $fut_a_arr = [getFuture_AVal("10"), getFuture_AVal("20")]; + $a_arr = wait_all/**/(...$fut_a_arr); + foreach ($a_arr as $a) + $a->f(); + + $tup_arr = wait_all/**/(getFuture_tuple()); + foreach ($tup_arr as $tup) + $tup[1]->f(); +} + +demoFutures(); + + +/** + * @kphp-generic T + * @param T $o + */ +function fMagic($o) { + if ($o) $o->print_magic(); + else echo "o null\n"; +} + +fMagic/**/(null); +fMagic/**/(new UsedA); +fMagic/**/(null); +fMagic/*<\Classes\B>*/(null); +fMagic/*<\Classes\B>*/(new \Classes\B); diff --git a/src/test/fixtures/generics/kphp/017_templates_primitives.php b/src/test/fixtures/generics/kphp/017_templates_primitives.php new file mode 100644 index 0000000..342527e --- /dev/null +++ b/src/test/fixtures/generics/kphp/017_templates_primitives.php @@ -0,0 +1,142 @@ +*/([$first, $second]); +} + +g/**/(1, 2); +g/**/('a', 'b'); +g/**/(new S, new S); + + +class User { + public int $age; + + function __construct(int $age) { $this->age = $age; } + + function isOlder(User $r): bool { return $this->age > $r->age; } +} + + +/** + * @kphp-generic T + * @param callable(T,T): bool $gt + * @param T ...$arr + * @return T + */ +function maxBy($gt, ...$arr) { + $max = array_first_value($arr); + for ($i = 1; $i < count($arr); ++$i) { + if ($gt($arr[$i], $max)) + $max = $arr[$i]; + } + return $max; +} + +echo maxBy/**/(fn ($a, $b) => $a > $b, 1, 2, 9, 3), "\n"; +echo maxBy/**/(fn ($a, $b) => ord($a) > ord($b), 'a', 'z', 'd'), "\n"; +echo maxBy/**/(fn ($a, $b) => $a->isOlder($b), new User(8), new User(10), new User(7))->age, "\n"; + + +class Fields { + static public int $S_I = 0; + + public int $i = 0; + /** @var string[] */ + public array $s = ['s']; + public ?User $u = null; + + /** @return string[] */ + function getS(): array { + return $this->s; + } + + function getI(): int { + return $this->i; + } +} + +function justGetBool(): bool { + return true; +} +function getBoolNoHint(): bool { + return true; +} + + +/** + * @kphp-generic T + * @param T $some + */ +function myVarDump($some) { + var_dump($some); +} + +/** + * @kphp-generic T + * @param T $u + */ +function myCallUser($u) { + // Тут было без явного списка, но без него плагин не сможет вывести типы + myVarDump/**/($u->isOlder($u)); +} + +$f = new Fields; +$f->u = new User(10); + +myVarDump([1,2,3]); +myVarDump(Fields::$S_I); +myVarDump($f->i); +myVarDump($f->u->age); +myVarDump($f->s); +myVarDump($f->getI()); +myVarDump($f->getS()); +myVarDump($f->getS()[0]); +myVarDump(justGetBool()); +myVarDump(getBoolNoHint()); + +myCallUser($f->u); + +/** + * @kphp-generic T + * @param T[] $arr + */ +function testAutoReifyArr($arr) { + echo "arr count ", count($arr), "\n"; +} + +testAutoReifyArr([1,2,3]); +testAutoReifyArr(['s1', 's2']); +testAutoReifyArr/**/(['s1', 's2', null]); + +/** @var int[] */ +$global_ii = []; +testAutoReifyArr($global_ii); + +function fWithPrimitive(int $i) { + /** @var int $i */ + myVarDump($i); +} +fWithPrimitive(100); diff --git a/src/test/fixtures/generics/kphp/018_classof_keyword.php b/src/test/fixtures/generics/kphp/018_classof_keyword.php new file mode 100644 index 0000000..a40722e --- /dev/null +++ b/src/test/fixtures/generics/kphp/018_classof_keyword.php @@ -0,0 +1,132 @@ + + */ +function classof($obj) { + /** @var class-string $name */ + $name = get_class($obj); + return $name; +} + +/** + * @kphp-generic T1, T2 + * @param ?T1 $obj + * @param class-string $class_name + * @return ?T2 + */ +function instance_cast($obj, $class_name) { + if ($obj === null) + return null; + if (!($obj instanceof $class_name)) + return null; + return $obj; +} + +function array_reserve_from(array $arr, array $arr2) {} +#endif + +class B { + function method(): int { echo "B method\n"; return 1; } +} +class D1 extends B { + function dMethod() { echo "d1\n"; } +} +class D2 extends B { + function dMethod() { echo "d2\n"; } +} + +/** + * @kphp-generic T, DstClass + * @param T $obj + * @param class-string $to_classname + * @return DstClass + */ +function my_cast($obj, $to_classname) { + return instance_cast($obj, $to_classname); +} + +/** + * @kphp-generic T, DstClass + * @param T $obj + * @param class-string $if_classname + */ +function callDMethodIfNotNull($obj, $if_classname) { + /** @var DstClass */ + $casted = my_cast($obj, $if_classname); + if ($casted) + $casted->dMethod(); + else + echo "cast to $if_classname is null: obj is ", get_class($obj), "\n"; +} + +/** @var B */ +$b = new D1; +my_cast($b, D1::class)->dMethod(); + +callDMethodIfNotNull(new D1, D1::class); +callDMethodIfNotNull(new D1, D2::class); +callDMethodIfNotNull(new D2, D1::class); +callDMethodIfNotNull(new D2, D2::class); + +/** + * @kphp-generic TElem, ToName + * @param TElem[] $arr + * @param class-string $to + * @return ToName[] + */ +function my_array_cast($arr, $to) { + $out = []; + array_reserve_from($out, $arr); + foreach ($arr as $k => $v) { + $out[$k] = instance_cast($v, $to); + } + return $out; +} + +/** + * @param B[] $arr + */ +function demoCastAndPrintAllD1(array $arr) { + $casted_arr = my_array_cast($arr, D1::class); + foreach ($casted_arr as $obj) + if ($obj) $obj->dMethod(); +} + +demoCastAndPrintAllD1([new D1, new D2, new D1]); + +/** + * @kphp-generic T1, T2 + * @param T1 $o1 + * @param T2 $o2 + * @return ?T1 + */ +function castO2ToTypeofO1($o1, $o2) { + /** @var T1 */ + $casted = instance_cast($o2, classof($o1)); + echo get_class($casted), "\n"; + return $casted; +} + +/** @var B */ +$bb = new D1; +castO2ToTypeofO1($bb, new D1); + + +/** + * @kphp-generic T + * @param T $o1 + */ +function castToClassofLocal($o1) { + $d1 = new D1; + $casted = instance_cast($o1, classof($d1)); + echo $casted ? "can cast\n" : "can't cast\n"; +} + +castToClassofLocal(new D1); +castToClassofLocal(new D2); diff --git a/src/test/fixtures/generics/kphp/019_templates_and_lambdas.php b/src/test/fixtures/generics/kphp/019_templates_and_lambdas.php new file mode 100644 index 0000000..d28d66e --- /dev/null +++ b/src/test/fixtures/generics/kphp/019_templates_and_lambdas.php @@ -0,0 +1,163 @@ + + */ +function classof($obj) { + /** @var class-string $name */ + $name = get_class($obj); + return $name; +} +#endif + + +class BB { + function method(): int { echo "B method\n"; return 1; } +} +class D1 extends BB { + function dMethod() { echo "d1\n"; } +} + +class A { + function method(): int { echo "A method\n"; return 1; } +} +class B { + function method(): int { echo "B method\n"; return 2; } +} +class C { + function method(): int { echo "C method\n"; return 3; } +} + +/** + * @kphp-generic T + * @param T $o + */ +function tplFWithLambda1($o) { + $handler = function() use($o) { + (function() use($o) { + /** @var tuple(int, T) */ + $t = tuple(1, $o); + $t[1]->method(); + })(); + }; + $handler(); +} + +tplFWithLambda1(new A); +tplFWithLambda1(new B); + + + +/** + * @kphp-generic T1, T2 + * @param T1 $o1 + * @param T2 $o2 + * @return ?T1 + */ +function castO2ToTypeofO1($o1, $o2) { + $h = function() use($o1, $o2) { + /** @var T1 */ + $casted = instance_cast($o2, classof($o1)); + echo get_class($casted), "\n"; + return $casted; + }; + return $h(); +} + +/** @var BB */ +$bb = new D1; +castO2ToTypeofO1($bb, new D1); + + + + +interface ISignalParam { +} + +class JsonCommandParams implements ISignalParam { +} + +class ApiParams1 extends JsonCommandParams { + public int $p1 = 1; +} +class ApiParams2 extends JsonCommandParams { + public int $p2 = 2; +} + +interface ISignalResult { + function getResult(); +} +class RealSignalResult implements ISignalResult { + function getResult(): string { return "RealSignalResult"; } +} + +function getSignal(string $signalName): ?ISignalParam { + if ($signalName === '1') return new ApiParams1; + if ($signalName === '2') return new ApiParams2; + return null; +} + +/** + * @kphp-generic T + * @param string $signalName + * @param T $params + * @param callable(T):ISignalResult $handler + */ +function signalConnect(string $signalName, $params, callable $handler) { + /** @var ISignalResult $r */ + $r = (function(ISignalParam $signalParam) use($params, $handler) { + $param = instance_cast($signalParam, classof($params)); + return $handler($param); + })(getSignal($signalName)); + echo $r->getResult(), "\n"; +} + +signalConnect('1', new ApiParams1, function(ApiParams1 $p) { + echo $p->p1, "\n"; + return new RealSignalResult; +}); +signalConnect('2', new ApiParams2, function(ApiParams2 $p) { + echo $p->p2, "\n"; + return new RealSignalResult; +}); + +/** + * @kphp-generic TIn, TOut + * @param TIn $in + * @param callable(TIn):TOut $cb + * @return TOut + */ +function callAndGet($in, $cb) { + return $cb($in); +} + +callAndGet(new A, fn($a): B => new B)->method(); +callAndGet(new A, function($a): ?A { return new A; })->method(); + + +class WithData1 { public int $data = 0; function w1() {} } +class WithData2 { public ?WithData1 $data = null; } + +/** + * @kphp-generic T + * @param T $obj + * @return any + */ +function getData($obj) { return $obj->data; } + +function demoWithData() { + $ints = array_map(fn($obj) => getData($obj), [new WithData1]); + var_dump($ints); + /** @var (?WithData1)[] $w1 */ + $w1 = array_map(fn($obj) => getData($obj), [new WithData2]); + if ($w1[0] === null) echo "w1 null\n"; + else $w1[0]->w1(); +} + +demoWithData(); + diff --git a/src/test/fixtures/generics/kphp/020_extends_hint.php b/src/test/fixtures/generics/kphp/020_extends_hint.php new file mode 100644 index 0000000..c47266a --- /dev/null +++ b/src/test/fixtures/generics/kphp/020_extends_hint.php @@ -0,0 +1,93 @@ +f(); +} + +tplOne(new A); +tplOne(new B); +tplOne/**/(null); + + +/** + * @kphp-generic T: TemplateMagic + * @param T $o + */ +function tplTwo($o) { + if ($o !== null) + $o->print_magic(new \Classes\A); +} + +tplTwo/**/(null); +tplTwo(new TemplateMagic); + + +interface Stringable { + function __toString(): string; +} + +class S1 implements Stringable { + function __toString(): string { + return "S1\n"; + } +} + +/** + * @kphp-generic T: Stringable | string + * @param T $toStr + */ +function printToString($toStr) { + echo $toStr, "\n"; +} + +printToString("adsf"); +printToString(new S1); + + +/** + * @kphp-generic T: callable(int): void + * @param T $cb + */ +function typedCallableGen($cb) { + $cb(1); +} + +/** @kphp-required */ +function asCb(int $i): void { + echo $i * 3, "\n"; +} + +typedCallableGen(function($i) { echo $i, "\n"; }); +typedCallableGen(function($i) { echo $i*2, "\n"; }); +typedCallableGen("asCb"); + + +/** + * @kphp-generic T1: int|string|float|null, T2: SomeI|TemplateMagic + * @param T1 $t1 + * @param T2 $t2 + */ +function strange($t1, $t2) { + echo ($t1 === null ? 'null' : gettype($t1)), " ", ($t2 === null ? 'null' : get_class($t2)), "\n"; +} + +/** @var ?string $s1 */ +$s1 = null; +strange($s1, new A); +strange/**/(null, null); +$s1 = 's'; +strange($s1, new TemplateMagic); diff --git a/src/test/fixtures/generics/kphp/021_default_T.php b/src/test/fixtures/generics/kphp/021_default_T.php new file mode 100644 index 0000000..f24b93a --- /dev/null +++ b/src/test/fixtures/generics/kphp/021_default_T.php @@ -0,0 +1,169 @@ +f(); +} + +withDef1(); +withDef1/**/(); +withDef1(null); +withDef1/**/(null); +withDef1(new A); +withDef1(new B); +withDef1/**/(); +withDef1/**/(null); +withDef1/**/(new B); + + +/** + * @kphp-generic T = mixed, T2 = int[] + * @param T[] $arr + * @param T2 $arr2 + */ +function withDef2($arr, $arr2 = []) { + echo "count ", count($arr), " ", count($arr2), "\n"; +} + +withDef2([]); +withDef2/**/([]); +withDef2/**/([]); +withDef2/**/([], []); +withDef2/**/([], ['1']); +withDef2([new A], [new A]); + + +/** + * @kphp-generic T: I = I + * @param ?T $i + */ +function withDef3($i) { + if ($i === null) echo "null\n"; + else $i->f(); +} + +withDef3(null); +withDef3/**/(null); +withDef3(new A); +withDef3(new B); + + +/** + * @kphp-generic T1, T2 = mixed + * @param T1 $a + * @param T2 $b + */ +function withDef4($a, $b = null) { + if ($a === null) echo "null\n"; + else $a->f(); + var_dump($b); +} + +withDef4(new A); +withDef4(new B); +withDef4/**/(null); +withDef4/**/(null, 3); +withDef4/**/(null); +withDef4/**/(new A); + + +class CC { + /** + * @kphp-generic T: self = self + * @param T $cc + */ + function withDef6($cc) { + if ($cc === null) echo "cc null\n"; + else $cc->ccMethod(); + } + + function ccMethod() { + echo "cc\n"; + } +} + +class CC2 extends CC { + function ccMethod() { + echo "cc2\n"; + } +} + +$cc = new CC; +$cc->withDef6(null); +$cc->withDef6(new CC); +$cc->withDef6(new CC2); +$cc->withDef6/**/(null); + + +/** + * @kphp-generic T1, T2 = T1 + * @param T1 $t1 + * @param ?T2 $t2 + */ +function withDef7($t1, $t2) { + if ($t1 !== null) $t1->f(); + if ($t2 !== null) $t2->f(); +} + + +$a7 = new A; +withDef7($a7, null); +withDef7/**/($a7, null); +withDef7/**/($a7, null); +withDef7/**/(new B, null); +withDef7(new B, null); + + +/** + * @kphp-generic T1, T2 = T1, T3 = T1|T2, T4 = T1|T2|T3 + * @param T1[] $arr1 + * @param T2[] $arr2 + * @param T3[] $arr3 + * @param T4[] $arr4 + * @return (T1|T2|T3|T4)[] + */ +function pushInOne($arr1, $arr2 = [], $arr3 = [], $arr4 = []) { + $result = []; + foreach ($arr1 as $k => $v) + $result[] = $v; + foreach ($arr2 as $k => $v) + $result[] = $v; + foreach ($arr3 as $k => $v) + $result[] = $v; + foreach ($arr4 as $k => $v) + $result[] = $v; + return $result; +} + +/** @var int[] */ +$ints = pushInOne([1,2,3]); +$ints = pushInOne([1,2,3], []); +$ints = pushInOne([1,2,3], [4,5,6]); +$ints = pushInOne([1,2,3], [4,5,6], [7,8,9], [10,11,12]); +var_dump($ints); + +/** @var mixed[] */ +$mixeds = pushInOne([1,2,3], ['1','2','3']); +$mixeds = pushInOne([1,2,3], ['1','2','3'], [true], [null]); +var_dump($mixeds); + +$as = pushInOne([new A]); +$as = pushInOne([new A], [new A]); +$as[1]->f(); + +/** @var I[] */ +$is = pushInOne([new A]); +$is = pushInOne([new A], [new B]); +$is = pushInOne([new A], [new B], [], [new B, new B]); +$is[3]->f(); diff --git a/src/test/fixtures/generics/kphp/023_generic_method_in_interface.php b/src/test/fixtures/generics/kphp/023_generic_method_in_interface.php new file mode 100644 index 0000000..092caa4 --- /dev/null +++ b/src/test/fixtures/generics/kphp/023_generic_method_in_interface.php @@ -0,0 +1,91 @@ +getName(), " and ", stringify/**/($with), "\n"; + } + + function __toString(): string { + ob_start(); + $this->echoSelfWith("__toString"); + $s = ob_get_contents(); + ob_end_clean(); + return trim($s); + } +} + +class B1 extends BBase { + function getName(): string { return "B1"; } +} +class B2 extends BBase { + function getName(): string { return get_class($this); } +} +class B22 extends B2 {} + +function demo1() { + $a = new A; + $a->echoSelfWith(4); + $b = new B1; + $b->echoSelfWith("demo1"); + $b2 = new B2; + $b2->echoSelfWith("demo1"); +} + +function demo2() { + (new A)->echoSelfWith(new B1); +} + +/** + * @kphp-generic TWith + * @param TWith $with + */ +function echoI(StringableWith $obj, $with) { + $obj->echoSelfWith/**/($with); +} + +/** + * @kphp-generic TAny, TWith + * @param TAny $obj + * @param TWith $with + */ +function echoAny($obj, $with) { + $obj->echoSelfWith/**/($with); +} + +demo1(); +demo2(); +echoI(new B1, "blob"); +echoI(new B22, new B2); +echoAny(new B1, "hello"); +echoAny(new A, new B2); +echoAny(new A, new B22); diff --git a/src/test/fixtures/generics/kphp/Classes/A.php b/src/test/fixtures/generics/kphp/Classes/A.php new file mode 100644 index 0000000..5828224 --- /dev/null +++ b/src/test/fixtures/generics/kphp/Classes/A.php @@ -0,0 +1,19 @@ +magic = $magic; + } + + public function print_magic() { + var_dump("A: {$this->magic}"); + } +} diff --git a/src/test/fixtures/generics/kphp/Classes/B.php b/src/test/fixtures/generics/kphp/Classes/B.php new file mode 100644 index 0000000..9292298 --- /dev/null +++ b/src/test/fixtures/generics/kphp/Classes/B.php @@ -0,0 +1,19 @@ +magic = $magic; + } + + public function print_magic() { + var_dump("B: {$this->magic}"); + } +} diff --git a/src/test/fixtures/generics/kphp/Classes/TemplateMagic.php b/src/test/fixtures/generics/kphp/Classes/TemplateMagic.php new file mode 100644 index 0000000..0ab07bc --- /dev/null +++ b/src/test/fixtures/generics/kphp/Classes/TemplateMagic.php @@ -0,0 +1,17 @@ +magic}"); + var_dump("given magic: {$a->magic}"); + } +} diff --git a/src/test/fixtures/generics/kphp/Classes/TemplateMagicStatic.php b/src/test/fixtures/generics/kphp/Classes/TemplateMagicStatic.php new file mode 100644 index 0000000..f844dd0 --- /dev/null +++ b/src/test/fixtures/generics/kphp/Classes/TemplateMagicStatic.php @@ -0,0 +1,12 @@ +magic}"); + } +} diff --git a/src/test/fixtures/generics/simple_functions.fixture.php b/src/test/fixtures/generics/simple_functions.fixture.php new file mode 100644 index 0000000..61bfa95 --- /dev/null +++ b/src/test/fixtures/generics/simple_functions.fixture.php @@ -0,0 +1,166 @@ +*/ (new GlobalA()); + expr_type($a, "\GlobalA"); + } + + "FQN класса"; { + $a = mirror/*<\Classes\A>*/(new \Classes\A); + expr_type($a, "\Classes\A"); + } + + "Имя класса которое было импортировано"; { + $a = mirror/**/(new B); + expr_type($a, "\Classes\B"); + } + + "Имя класса которое было импортировано с алиасом"; { + $a = mirror/**/(new GlobalC()); + expr_type($a, "\Classes\C"); + } + + "Имя класса которое было импортировано с алиасом с слешем в начале"; { + $a = mirror/*<\GlobalC>*/(new GlobalC()); // Здесь будет ошибка, так как класс \GlobalC не будет найден + expr_type($a, "\GlobalC"); // однако нам нужно проверить, что в таком случае тип будет также \GlobalC. + } + } + + "Примитивные типы"; { + $a1 = mirror/**/("hello"); + expr_type($a1, "string"); + + $a2 = mirror/**/(1); + expr_type($a2, "int"); + + $a3 = mirror/**/(true); + expr_type($a3, "bool"); + } + + "Сложные типы"; { + "С примитивами"; { + "Массивы"; { + $a1 = mirror/**/(["hello"]); + expr_type($a1, "string[]"); + + $a2 = mirror/**/([1, 2, 3]); + expr_type($a2, "int[]"); + } + + "Nullable"; { + $a1 = mirror/**/("hello"); + expr_type($a1, "null|string"); + + $a2 = mirror/**/(1); + expr_type($a2, "int|null"); + } + + "Union"; { + $a1 = mirror/**/("hello"); + expr_type($a1, "int|string"); + + $a2 = mirror/**/(1.5); + expr_type($a2, "bool|float"); + + $a3 = mirror/**/(1.5); + expr_type($a3, "bool|float"); + } + + "tuple"; { + $a1 = mirror/**/(tuple(1, "hello")); + expr_type($a1, "tuple(int,string)"); + + $a2 = mirror/**/(tuple(1, "hello")); + expr_type($a2, "tuple(int,float|string)"); + } + + "shape"; { + $a1 = mirror/**/(shape(["key" => 1, "key2" => "hello"])); + expr_type($a1, "shape(key:int,key2:string)"); + + $a2 = mirror/**/(shape(["key" => 1, "key2" => "hello"])); + expr_type($a2, "shape(key:int,key2:float|string)"); + } + } + + "С классами"; { + "Массивы"; { + $a1 = mirror/*<\GlobalA[]>*/([new GlobalA()]); + expr_type($a1, "\GlobalA[]"); + + $a2 = mirror/*<\Classes\A[]>*/([new \Classes\A()]); + expr_type($a2, "\Classes\A[]"); + + $a3 = mirror/**/([new B()]); + expr_type($a3, "\Classes\B[]"); + + $a4 = mirror/**/([new GlobalC]); + expr_type($a4, "\Classes\C[]"); + } + + "Nullable"; { + $a1 = mirror/**/([new GlobalA()]); + expr_type($a1, "?\GlobalA"); + + $a2 = mirror/**/([new \Classes\A()]); + expr_type($a2, "?\Classes\A"); + + $a3 = mirror/**/([new B()]); + expr_type($a3, "?\Classes\B"); + + $a4 = mirror/**/([new GlobalC]); + expr_type($a4, "?\Classes\C"); + } + + "Union"; { + $a1 = mirror/*<\GlobalA|\Classes\A>*/("hello"); + expr_type($a1, "\Classes\A|\GlobalA"); + + $a2 = mirror/**/(1.5); + expr_type($a2, "\Classes\B|\Classes\C"); + + $a3 = mirror/*<\Classes\A|\GlobalC>*/(1.5); + expr_type($a3, "\Classes\A|\GlobalC"); + } + + "tuple"; { + $a1 = mirror/**/(tuple(1, "hello")); + expr_type($a1, "tuple(int,string)"); + + $a2 = mirror/**/(tuple(1, "hello")); + expr_type($a2, "tuple(int,float|string)"); + } + + "shape"; { + $a1 = mirror/**/(shape(["key" => 1, "key2" => "hello"])); + expr_type($a1, "shape(key:int,key2:string)"); + + $a2 = mirror/**/(shape(["key" => 1, "key2" => "hello"])); + expr_type($a2, "shape(key:int,key2:float|string)"); + } + } + } +} diff --git a/src/test/fixtures/generics/types/class-string/explicit.fixture.php b/src/test/fixtures/generics/types/class-string/explicit.fixture.php new file mode 100644 index 0000000..b127238 --- /dev/null +++ b/src/test/fixtures/generics/types/class-string/explicit.fixture.php @@ -0,0 +1,18 @@ +*/($base_array, Child1::class); +expr_type($children1_array, "\Classes\Child1[]"); diff --git a/src/test/fixtures/generics/types/class-string/implicit.fixture.php b/src/test/fixtures/generics/types/class-string/implicit.fixture.php new file mode 100644 index 0000000..4b0b747 --- /dev/null +++ b/src/test/fixtures/generics/types/class-string/implicit.fixture.php @@ -0,0 +1,16 @@ +*/ ([new GlobalA()]); +expr_type($a, "\GlobalA[]"); + +// Класс из пространства имен +$a = mirror/*<\Classes\A[]>*/ ([new \Classes\A()]); +expr_type($a, "\Classes\A[]"); + +// Импортированный класс из пространства имен +$a = mirror/**/ ([new B()]); +expr_type($a, "\Classes\B[]"); + +// Импортированный класс из пространства имен с алиасом +$a = mirror/**/ ([new GlobalC]); +expr_type($a, "\Classes\C[]"); + +// Импортированный класс из пространства имен с алиасом как у глобально класса +$a = mirror/**/ ([new GlobalD()]); +expr_type($a, "\Classes\D[]"); + +// Глобальный класс с именем как у локального алиаса для другого класса +$a = mirror/*<\GlobalD[]>*/ ([new \GlobalD()]); +expr_type($a, "\GlobalD[]"); diff --git a/src/test/fixtures/generics/types/classes-as-types/explicit/mixed.fixture.php b/src/test/fixtures/generics/types/classes-as-types/explicit/mixed.fixture.php new file mode 100644 index 0000000..7774b33 --- /dev/null +++ b/src/test/fixtures/generics/types/classes-as-types/explicit/mixed.fixture.php @@ -0,0 +1,9 @@ +*/(tuple([new GlobalA()], new \Classes\A())); +expr_type($a, "tuple(\GlobalA[],\Classes\A|\Classes\C)"); + +$a1 = mirror/**/(shape(["key1" => $a, "key2" => [new \GlobalD()]])); +expr_type($a1, "shape(key1:tuple(\GlobalA[],\Classes\A|\Classes\C),key2:?\GlobalD[])"); diff --git a/src/test/fixtures/generics/types/classes-as-types/explicit/nullable.fixture.php b/src/test/fixtures/generics/types/classes-as-types/explicit/nullable.fixture.php new file mode 100644 index 0000000..75fb979 --- /dev/null +++ b/src/test/fixtures/generics/types/classes-as-types/explicit/nullable.fixture.php @@ -0,0 +1,36 @@ +*/ (null); +$a->methodGlobalA(); + + +expr_type($a, "?\GlobalA"); + +// Класс из пространства имен +$a = mirror/**/ (new \Classes\A()); +expr_type($a, "?\Classes\A"); + +// Импортированный класс из пространства имен +$a = mirror/**/ (new B()); +expr_type($a, "?\Classes\B"); + +// Импортированный класс из пространства имен с алиасом +$a = mirror/**/ (new GlobalC); +expr_type($a, "?\Classes\C"); + +// Импортированный класс из пространства имен с алиасом как у глобально класса +$a = mirror/**/ (new GlobalD()); +expr_type($a, "?\Classes\D"); + +// Глобальный класс с именем как у локального алиаса для другого класса +$a = mirror/**/ (new \GlobalD()); +expr_type($a, "?\GlobalD"); diff --git a/src/test/fixtures/generics/types/classes-as-types/explicit/shape.fixture.php b/src/test/fixtures/generics/types/classes-as-types/explicit/shape.fixture.php new file mode 100644 index 0000000..afbd5fa --- /dev/null +++ b/src/test/fixtures/generics/types/classes-as-types/explicit/shape.fixture.php @@ -0,0 +1,21 @@ +*/ (shape(["key1" => new GlobalA, "key2" => new \Classes\A])); +expr_type($a, "shape(key1:\GlobalA,key2:\Classes\A)"); + +// Импортированный класс из пространства имен + Импортированный класс из пространства имен с алиасом +$a = mirror/**/ (shape(["key1" => new B(), "key2" => new GlobalC()])); +expr_type($a, "shape(key1:\Classes\B,key2:\Classes\C)"); + +// Импортированный класс из пространства имен с алиасом как у глобально класса + Глобальный класс с именем как у локального алиаса для другого класса +$a = mirror/**/ (shape(["key1" => new GlobalD(), "key2" => new \GlobalD()])); +expr_type($a, "shape(key1:\Classes\D,key2:\GlobalD)"); diff --git a/src/test/fixtures/generics/types/classes-as-types/explicit/standalone.fixture.php b/src/test/fixtures/generics/types/classes-as-types/explicit/standalone.fixture.php new file mode 100644 index 0000000..1aae9d1 --- /dev/null +++ b/src/test/fixtures/generics/types/classes-as-types/explicit/standalone.fixture.php @@ -0,0 +1,33 @@ +*/ (new GlobalA()); +expr_type($a, "\GlobalA"); + +// Класс из пространства имен +$a = mirror/*<\Classes\A>*/ (new \Classes\A()); +expr_type($a, "\Classes\A"); + +// Импортированный класс из пространства имен +$a = mirror/**/ (new B()); +expr_type($a, "\Classes\B"); + +// Импортированный класс из пространства имен с алиасом +$a = mirror/**/ (new GlobalC); +expr_type($a, "\Classes\C"); + +// Импортированный класс из пространства имен с алиасом как у глобально класса +$a = mirror/**/ (new GlobalD()); +expr_type($a, "\Classes\D"); + +// Глобальный класс с именем как у локального алиаса для другого класса +$a = mirror/*<\GlobalD>*/ (new \GlobalD()); +expr_type($a, "\GlobalD"); diff --git a/src/test/fixtures/generics/types/classes-as-types/explicit/tuple.fixture.php b/src/test/fixtures/generics/types/classes-as-types/explicit/tuple.fixture.php new file mode 100644 index 0000000..e462a0e --- /dev/null +++ b/src/test/fixtures/generics/types/classes-as-types/explicit/tuple.fixture.php @@ -0,0 +1,21 @@ +*/ (tuple(new GlobalA(), new \Classes\A())); +expr_type($a, "tuple(\GlobalA,\Classes\A)"); + +// Импортированный класс из пространства имен + Импортированный класс из пространства имен с алиасом +$a = mirror/**/ (tuple(new B(), new GlobalC())); +expr_type($a, "tuple(\Classes\B,\Classes\C)"); + +// Импортированный класс из пространства имен с алиасом как у глобально класса + Глобальный класс с именем как у локального алиаса для другого класса +$a = mirror/**/ (tuple(new GlobalD(), new \GlobalD())); +expr_type($a, "tuple(\Classes\D,\GlobalD)"); diff --git a/src/test/fixtures/generics/types/classes-as-types/explicit/union.fixture.php b/src/test/fixtures/generics/types/classes-as-types/explicit/union.fixture.php new file mode 100644 index 0000000..2242daa --- /dev/null +++ b/src/test/fixtures/generics/types/classes-as-types/explicit/union.fixture.php @@ -0,0 +1,21 @@ +*/ (new GlobalA()); +expr_type($a, "\Classes\A|\GlobalA"); + +// Импортированный класс из пространства имен + Импортированный класс из пространства имен с алиасом +$a = mirror/**/ (new B()); +expr_type($a, "\Classes\B|\Classes\C"); + +// Импортированный класс из пространства имен с алиасом как у глобально класса + Глобальный класс с именем как у локального алиаса для другого класса +$a = mirror/**/ (new GlobalD()); +expr_type($a, "\Classes\D|\GlobalD"); diff --git a/src/test/fixtures/generics/types/classes-as-types/implicit/array.fixture.php b/src/test/fixtures/generics/types/classes-as-types/implicit/array.fixture.php new file mode 100644 index 0000000..e1ed103 --- /dev/null +++ b/src/test/fixtures/generics/types/classes-as-types/implicit/array.fixture.php @@ -0,0 +1,33 @@ + new GlobalA, "key2" => new \Classes\A])); +expr_type($a, "shape(key1:\GlobalA,key2:\Classes\A)"); diff --git a/src/test/fixtures/generics/types/classes-as-types/implicit/standalone.fixture.php b/src/test/fixtures/generics/types/classes-as-types/implicit/standalone.fixture.php new file mode 100644 index 0000000..a1f640a --- /dev/null +++ b/src/test/fixtures/generics/types/classes-as-types/implicit/standalone.fixture.php @@ -0,0 +1,33 @@ +*/(); +expr_type($obj, "\WithoutConstructor(?\TestA)"); + + +/** @kphp-generic T */ +class OneArgumentConstructor { + /** @param T $a */ + public function __construct($a) {} +} + +$obj = new OneArgumentConstructor/**/(null); +expr_type($obj, "\OneArgumentConstructor(?\TestA)"); + +$obj = new OneArgumentConstructor(new TestA); +expr_type($obj, "\OneArgumentConstructor(\TestA)"); + + +/** @kphp-generic T1, T2 */ +class TwoArgumentConstructor { + /** + * @param T1 $a + * @param T2 $b + */ + public function __construct($a, $b) {} +} + +$obj1 = new TwoArgumentConstructor(new TestA, "Hello World"); +expr_type($obj1, "\TwoArgumentConstructor(\TestA,string)"); + + +/** @kphp-generic T1, T2 */ +class TwoGenericTypes {} + +$obj = new TwoGenericTypes/**/(); +expr_type($obj, "\TwoGenericTypes(?\TestA,string)"); +$obj = new TwoGenericTypes/**/(); +expr_type($obj, "\TwoGenericTypes(?\TestA,tuple(int,string))"); + + +/** @kphp-generic T1, T2, T3 */ +class ThreeGenericTypes {} + +$obj = new ThreeGenericTypes/**/(); +expr_type($obj, "\ThreeGenericTypes(?\TestA,string,int)"); diff --git a/src/test/fixtures/generics/types/functions/chain.fixture.php b/src/test/fixtures/generics/types/functions/chain.fixture.php new file mode 100644 index 0000000..8c29f0f --- /dev/null +++ b/src/test/fixtures/generics/types/functions/chain.fixture.php @@ -0,0 +1,46 @@ + $a + * @return Vector + */ +function takeVector($a) { + return $a; +} + +$vec = new Vector/**/ (); +$vec2 = takeVector/**/($vec)->get(0); +expr_type($vec2, "\Function\Chain\Foo"); diff --git a/src/test/fixtures/generics/types/methods/chain.fixture.php b/src/test/fixtures/generics/types/methods/chain.fixture.php new file mode 100644 index 0000000..52aa790 --- /dev/null +++ b/src/test/fixtures/generics/types/methods/chain.fixture.php @@ -0,0 +1,62 @@ +>> + */ +function returnVector() { + return new Vector; +} + +/** + * @kphp-generic T + * @param class-string $class + * @return T + */ +function templateFu($class) { + return new $class(); +} + +$a = templateFu(Foo::class); +expr_type($a, "\Methods\Chain\Foo"); + +$b = returnVector()->get(0); +expr_type($b, "\Vector|\Vector(\Vector|\Vector(string))"); + +$c = $b->get(0)->get(0); +expr_type($c, "string"); + + +/** + * @return Pair + */ +function returnPair(): Pair { + return new Pair(new Boo, new Goo); +} + +$x = returnPair()->first(); +$y = returnPair()->second(); +expr_type($x, "\Methods\Chain\Boo"); +expr_type($y, "\Methods\Chain\Goo"); + + +/** + * @return Vector> + */ +function returnVectorPair() { + return new Vector; +} + +$d = returnVectorPair()->get(0)->second(); +expr_type($d, "\Methods\Chain\Goo"); + +$e = returnVectorPair()->get(0)->first(); +expr_type($e, "\Methods\Chain\Boo"); diff --git a/src/test/fixtures/generics/types/methods/complex.fixture.php b/src/test/fixtures/generics/types/methods/complex.fixture.php new file mode 100644 index 0000000..567acca --- /dev/null +++ b/src/test/fixtures/generics/types/methods/complex.fixture.php @@ -0,0 +1,25 @@ +, Goo>>*/(); +$a = $vec->get(0)->first()->getArray()[0]; +expr_type($a, "\Methods\Complex\Goo"); + diff --git a/src/test/fixtures/generics/types/methods/static_and_non_generic.fixture.php b/src/test/fixtures/generics/types/methods/static_and_non_generic.fixture.php new file mode 100644 index 0000000..0ede09f --- /dev/null +++ b/src/test/fixtures/generics/types/methods/static_and_non_generic.fixture.php @@ -0,0 +1,42 @@ +*/(); +expr_type($a, "\Methods\Main\Foo"); + + +$c = new NonGenericClass(); +$d = $c->genericMethod/**/ (); +expr_type($d, "\Methods\Main\Foo"); + + +$e = NonGenericClass::staticGenericMethod/**/ (); +expr_type($e, "\Methods\Main\Foo"); diff --git a/src/test/fixtures/generics/types/primitives/explicit/simple.fixture.php b/src/test/fixtures/generics/types/primitives/explicit/simple.fixture.php new file mode 100644 index 0000000..5771af7 --- /dev/null +++ b/src/test/fixtures/generics/types/primitives/explicit/simple.fixture.php @@ -0,0 +1,10 @@ +*/(""); +expr_type($a, "string"); + +$a1 = mirror/**/(10); +expr_type($a1, "int"); + +$a2 = mirror/**/(true); +expr_type($a2, "bool"); diff --git a/src/test/fixtures/generics/types/primitives/explicit/union.fixture.php b/src/test/fixtures/generics/types/primitives/explicit/union.fixture.php new file mode 100644 index 0000000..6ee3d4d --- /dev/null +++ b/src/test/fixtures/generics/types/primitives/explicit/union.fixture.php @@ -0,0 +1,16 @@ +*/("", 1); +expr_type($a, "int|string"); + +$a1 = combine/**/("", true); +expr_type($a1, "bool|string"); + +$a2 = combine/**/("", false); +expr_type($a2, "false|string"); + +$a3 = combine/**/("", true); +expr_type($a3, "bool|string"); + +$a3 = combine/**/("", ""); +expr_type($a3, "string"); diff --git a/src/test/fixtures/generics/types/primitives/implicit/simple.fixture.php b/src/test/fixtures/generics/types/primitives/implicit/simple.fixture.php new file mode 100644 index 0000000..718e810 --- /dev/null +++ b/src/test/fixtures/generics/types/primitives/implicit/simple.fixture.php @@ -0,0 +1,10 @@ + $a + * @return T + */ +function takeClassString($a) { + return new $a; +} + +$c = takeClassString(Foo::class); +expr_type($c, "\Reify\Foo"); + + +/** + * @kphp-generic T + * @param class-string[] $a + * @return T + */ +function takeArray($a) { + return new $a; +} + +$c = takeArray([Foo::class]); +expr_type($c, "\Reify\Foo"); + + +/** + * @kphp-generic T1, T2, T3 + * @param tuple(T1, T2, T3) $a + * @return T1|T2|T3 + */ +function takeTuple($a) { + return new $a; +} + +$c = takeTuple(tuple(1, "", new Foo)); +expr_type($c, "\Reify\Foo|int|string"); diff --git a/src/test/fixtures/generics/types/reify/params.fixture.php b/src/test/fixtures/generics/types/reify/params.fixture.php new file mode 100644 index 0000000..81b6b34 --- /dev/null +++ b/src/test/fixtures/generics/types/reify/params.fixture.php @@ -0,0 +1,76 @@ +data, "\Serializable[]|any[]"); + expr_type($this->data1, "\Serializable|null"); + expr_type($this->data2, "\Serializable|int"); + } +} diff --git a/src/test/fixtures/strict_typing/props_assignments_10.fixture.php b/src/test/fixtures/strict_typing/props_assignments_10.fixture.php index d8b70ef..dfebc3d 100644 --- a/src/test/fixtures/strict_typing/props_assignments_10.fixture.php +++ b/src/test/fixtures/strict_typing/props_assignments_10.fixture.php @@ -89,7 +89,7 @@ function demo() { $a->s_arr[] = $s_arr[0]; $a->s_arr = str_replace('search', 'replace', getSArr()); - $a->s = str_replace('s', 'r', getTrash()); + $a->s = str_replace('s', 'r', getTrash()); } function demo2() { diff --git a/src/test/fixtures/strict_typing/props_assignments_12.fixture.php b/src/test/fixtures/strict_typing/props_assignments_12.fixture.php index fb15e56..8fff5c6 100644 --- a/src/test/fixtures/strict_typing/props_assignments_12.fixture.php +++ b/src/test/fixtures/strict_typing/props_assignments_12.fixture.php @@ -34,6 +34,6 @@ function demo1() { function demo2() { $a = new A; - $a->t_int_A = shape(['x'=>1]); + $a->t_int_A = shape(['x'=>1]); $a->s_int_a = tuple(1, new A); } diff --git a/src/test/fixtures/strict_typing/props_assignments_3.fixture.php b/src/test/fixtures/strict_typing/props_assignments_3.fixture.php index c5828b4..fec9a44 100644 --- a/src/test/fixtures/strict_typing/props_assignments_3.fixture.php +++ b/src/test/fixtures/strict_typing/props_assignments_3.fixture.php @@ -45,7 +45,7 @@ function demo2() { $holder = new Holder; $holder->i = $i; - $holder->a1 = $i; + $holder->a1 = $i; } function demo3(BaseObject $o) { diff --git a/src/test/fixtures/strict_typing/return_statement_2.fixture.php b/src/test/fixtures/strict_typing/return_statement_2.fixture.php index 3c27982..a330ea6 100644 --- a/src/test/fixtures/strict_typing/return_statement_2.fixture.php +++ b/src/test/fixtures/strict_typing/return_statement_2.fixture.php @@ -28,19 +28,19 @@ function getT3() { return null; } -/** @return tuple(int, int) */ +/** @return tuple(int, int) */ function getT4() { - return shape(['x'=>1]); + return shape(['x'=>1]); } function getT5() : int { return tuple(5); } -/** @return tuple(int, int) */ +/** @return tuple(int, int) */ function getT6() { // this message is slightly unexpected: it is because getT4() is corrupted by inferred shape - return getT4(); + return getT4(); } /** @return shape(x: string, y: int) */ diff --git a/src/test/kotlin/com/vk/kphpstorm/testing/infrastructure/GenericTestBase.kt b/src/test/kotlin/com/vk/kphpstorm/testing/infrastructure/GenericTestBase.kt new file mode 100644 index 0000000..3c58dff --- /dev/null +++ b/src/test/kotlin/com/vk/kphpstorm/testing/infrastructure/GenericTestBase.kt @@ -0,0 +1,45 @@ +package com.vk.kphpstorm.testing.infrastructure + +import com.jetbrains.php.lang.inspections.PhpUndefinedFieldInspection +import com.jetbrains.php.lang.inspections.PhpUndefinedMethodInspection +import com.vk.kphpstorm.configuration.KphpStormConfiguration +import com.vk.kphpstorm.inspections.KphpGenericsInspection +import com.vk.kphpstorm.inspections.KphpParameterTypeMismatchInspection +import com.vk.kphpstorm.inspections.KphpUndefinedClassInspection +import java.io.File + +abstract class GenericTestBase : TypeTestBase() { + + override fun getTestDataPath() = "src/test/fixtures" + + override fun setUp() { + super.setUp() + + myFixture.enableInspections(PhpUndefinedMethodInspection()) + myFixture.enableInspections(PhpUndefinedFieldInspection()) + myFixture.enableInspections(KphpUndefinedClassInspection()) + myFixture.enableInspections(KphpGenericsInspection()) + myFixture.enableInspections(KphpParameterTypeMismatchInspection()) + } + + /** + * Run inspection on file.fixture.php and check that all and match + * If file.qf.php exists, apply quickfixes and compare result to file.qf.php + */ + override fun runFixture(vararg fixtureFiles: String) { + // Highlighting test + KphpStormConfiguration.saveThatSetupForProjectDone(project) + myFixture.testHighlighting(true, false, true, *fixtureFiles) + + // Quick-fix test + fixtureFiles.forEach { fixtureFile -> + val qfFile = fixtureFile.replace(".fixture.php", ".qf.php") + if (qfFile != fixtureFile && File(myFixture.testDataPath + "/" + qfFile).exists()) { + myFixture.getAllQuickFixes().forEach { myFixture.launchAction(it) } + myFixture.checkResultByFile(qfFile) + } + } + + runTypeTest(fixtureFiles) + } +} diff --git a/src/test/kotlin/com/vk/kphpstorm/testing/infrastructure/InspectionTestBase.kt b/src/test/kotlin/com/vk/kphpstorm/testing/infrastructure/InspectionTestBase.kt index b4f4aab..981f2d5 100644 --- a/src/test/kotlin/com/vk/kphpstorm/testing/infrastructure/InspectionTestBase.kt +++ b/src/test/kotlin/com/vk/kphpstorm/testing/infrastructure/InspectionTestBase.kt @@ -22,17 +22,19 @@ abstract class InspectionTestBase( * Run inspection on file.fixture.php and check that all and match * If file.qf.php exists, apply quickfixes and compare result to file.qf.php */ - protected fun runFixture(fixtureFile: String) { + protected fun runFixture(vararg fixtureFiles: String) { // Highlighting test KphpStormConfiguration.saveThatSetupForProjectDone(project) - myFixture.configureByFile(fixtureFile) + myFixture.configureByFiles(*fixtureFiles) myFixture.testHighlighting(true, false, true) // Quick-fix test - val qfFile = fixtureFile.replace(".fixture.php", ".qf.php") - if (File(myFixture.testDataPath + "/" + qfFile).exists()) { - myFixture.getAllQuickFixes().forEach { myFixture.launchAction(it) } - myFixture.checkResultByFile(qfFile) + fixtureFiles.forEach { fixtureFile -> + val qfFile = fixtureFile.replace(".fixture.php", ".qf.php") + if (File(myFixture.testDataPath + "/" + qfFile).exists()) { + myFixture.getAllQuickFixes().forEach { myFixture.launchAction(it) } + myFixture.checkResultByFile(qfFile) + } } } } diff --git a/src/test/kotlin/com/vk/kphpstorm/testing/infrastructure/TypeTestBase.kt b/src/test/kotlin/com/vk/kphpstorm/testing/infrastructure/TypeTestBase.kt new file mode 100644 index 0000000..ead1fb9 --- /dev/null +++ b/src/test/kotlin/com/vk/kphpstorm/testing/infrastructure/TypeTestBase.kt @@ -0,0 +1,97 @@ +package com.vk.kphpstorm.testing.infrastructure + +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.jetbrains.php.lang.psi.elements.FunctionReference +import com.jetbrains.php.lang.psi.elements.PhpPsiElement +import com.jetbrains.php.lang.psi.elements.PhpTypedElement +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression +import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor +import com.vk.kphpstorm.configuration.KphpStormConfiguration +import com.vk.kphpstorm.exphptype.ExPhpTypePipe +import com.vk.kphpstorm.exphptype.PsiToExPhpType +import com.vk.kphpstorm.helpers.toExPhpType + +abstract class TypeTestBase : BasePlatformTestCase() { + override fun getTestDataPath() = "src/test/fixtures" + + protected open fun runFixture(vararg fixtureFiles: String) { + KphpStormConfiguration.saveThatSetupForProjectDone(project) + myFixture.configureByFiles(*fixtureFiles) + + runTypeTest(fixtureFiles) + } + + protected fun runTypeTest(fixtureFiles: Array) { + findExprTypeCalls(fixtureFiles).forEach { call -> + checkExprTypeCall(call) + } + } + + private inline fun PsiElement.findChildren(crossinline condition: (PsiElement) -> Boolean): List { + val result = mutableListOf() + accept(object : PhpElementVisitor() { + override fun visitElement(element: PsiElement) { + if (condition(element) && element is T) { + result.add(element) + } + + var child = element.firstChild + while (child != null) { + child.accept(this) + child = child.nextSibling + } + } + }) + return result + } + + private fun checkExprTypeCall(call: FunctionReference) { + val expr = call.parameters.first() as PhpTypedElement + val expectedTypePsi = call.parameters.last() as StringLiteralExpression + val expectedTypeString = expectedTypePsi.contents + val gotType = expr.type.global(myFixture.project).toExPhpType()?.let { PsiToExPhpType.dropGenerics(it) } + + val sortedGotType = if (gotType is ExPhpTypePipe) + ExPhpTypePipe(gotType.items.sortedBy { it.toString() }) + else gotType + + val gotTypeString = sortedGotType.toString().ifEmpty { "" } + + val file = call.containingFile + check(gotTypeString == expectedTypeString) { + """ + In file ${file.name}:${call.line()} + + Type mismatch. + Expected: $expectedTypeString + Found: $gotTypeString + + + """.trimIndent() + } + } + + private fun findExprTypeCalls(fixtureFiles: Array): List { + return fixtureFiles.map { + val file = myFixture.findFileInTempDir(it) ?: return@map emptyList() + myFixture.openFileInEditor(file) + + myFixture.file.findChildren { el -> + el is FunctionReference && el.name == "expr_type" && el.parameters.size == 2 && + el.parameters.last() is StringLiteralExpression && el.parameters.first() is PhpTypedElement + } + }.flatten() + } + + private fun PhpPsiElement.line(): Int { + val document = PsiDocumentManager.getInstance(project).getDocument(containingFile) + val lineNumber = if (document != null) { + document.getLineNumber(textRange.startOffset) + 1 + } else { + 0 + } + return lineNumber + } +} diff --git a/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/general/GenericsGeneralTest.kt b/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/general/GenericsGeneralTest.kt new file mode 100644 index 0000000..1ddbd60 --- /dev/null +++ b/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/general/GenericsGeneralTest.kt @@ -0,0 +1,132 @@ +package com.vk.kphpstorm.testing.tests.generics.general + +import com.vk.kphpstorm.testing.infrastructure.GenericTestBase + +class GenericsGeneralTest : GenericTestBase() { + fun testNewExpr() { + runFixture("generics/general/new_expr.fixture.php") + } + + fun testFields() { + runFixture("generics/general/fields/main.fixture.php") + runFixture("generics/general/fields/static.fixture.php") + } + + fun testCallback() { + runFixture("generics/general/callback/return.fixture.php") + runFixture("generics/general/callback/param.fixture.php") + } + + fun testVectorUse() { + runFixture( + "generics/general/vector_use.fixture.php", + "generics/Containers/Vector.php", + "generics/Containers/Pair.php", + ) + } + + fun testReifier() { + runFixture("generics/general/reifier/reify.fixture.php") + } + + fun testExtendsTypeReifier() { + runFixture("generics/general/reifier/extends/classes_union.fixture.php") + runFixture("generics/general/reifier/extends/primitives_union.fixture.php") + runFixture("generics/general/reifier/extends/callable.fixture.php") + runFixture("generics/general/reifier/extends/wrong.fixture.php") + } + + fun testDefaultTypeReifier() { + runFixture("generics/general/reifier/default/main.fixture.php") + runFixture( + "generics/general/reifier/default/wrong.fixture.php", + "generics/Containers/Pair.php", + ) + runFixture( + "generics/general/reifier/default/generic.fixture.php", + "generics/Containers/Vector.php", + "generics/Containers/Pair.php", + ) + } + + fun testReifyFromReturn() { + runFixture( + "generics/general/reifier/context/return.fixture.php", + "generics/general/reifier/context/classes.php", + ) + runFixture( + "generics/general/reifier/context/return_wrong.fixture.php", + "generics/general/reifier/context/classes.php", + ) + } + + fun testReifyFromParam() { + runFixture( + "generics/general/reifier/context/param.fixture.php", + "generics/general/reifier/context/classes.php" + ) + runFixture( + "generics/general/reifier/context/param_wrong.fixture.php", + "generics/general/reifier/context/classes.php" + ) + } + + fun testGenericInGeneric() { + runFixture("generics/general/generic_in_generic.fixture.php") + } + + fun testInheritTag() { + runFixture("generics/general/inherit/tag/main.fixture.php") + } + + fun testInherit() { + runFixture("generics/general/inherit/simple_class.fixture.php") + runFixture("generics/general/inherit/simple_class_with_defaults.fixture.php") + runFixture( + "generics/general/inherit/non_generic_child.fixture.php", + "generics/Containers/MutableVectorList.php", + "generics/Containers/VectorList.php", + ) + runFixture("generics/general/inherit/class_and_interface.fixture.php") + runFixture("generics/general/inherit/2_deep_extends.fixture.php") + runFixture("generics/general/inherit/2_deep_extends_with_override.fixture.php") + } + + /** + * Disabled. + * + * See https://youtrack.jetbrains.com/issue/WI-67021/During-a-test-primitive-type-hints-are-resolved-as-instances + */ + fun testFromKphp() { +// val classes = arrayOf( +// "generics/kphp/Classes/A.php", +// "generics/kphp/Classes/B.php", +// "generics/kphp/Classes/TemplateMagic.php", +// "generics/kphp/Classes/TemplateMagicStatic.php", +// ) +// runFixture( +// "generics/kphp/016_kphp_param_depends_T.php", +// *classes, +// ) +// +// runFixture( +// "generics/kphp/017_templates_primitives.php", +// *classes, +// ) +// +// runFixture( +// "generics/kphp/018_classof_keyword.php", +// *classes, +// ) +// +// runFixture( +// "generics/kphp/016_kphp_param_depends_T.php", +// *classes, +// ) +// +// runFixture( +// "generics/kphp/016_kphp_param_depends_T.php", +// *classes, +// ) + } +} diff --git a/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/inspections/KphpGenericsInspectionsTest.kt b/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/inspections/KphpGenericsInspectionsTest.kt new file mode 100644 index 0000000..1702b6b --- /dev/null +++ b/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/inspections/KphpGenericsInspectionsTest.kt @@ -0,0 +1,31 @@ +package com.vk.kphpstorm.testing.tests.generics.inspections + +import com.vk.kphpstorm.inspections.KphpGenericsInspection +import com.vk.kphpstorm.testing.infrastructure.InspectionTestBase + +class KphpGenericsInspectionsTest : InspectionTestBase(KphpGenericsInspection()) { + fun testBoundViolations() { + runFixture("generics/inspections/bound_violation.fixture.php") + } + + fun testNoEnoughInformation() { + runFixture("generics/inspections/no_enough_information.fixture.php") + } + + fun testInstantiationArgsMismatch() { + runFixture("generics/inspections/instantiation_args_mismatch.fixture.php") + } + + fun testSeveralReifiedTypes() { + runFixture("generics/inspections/several_reified_types.fixture.php") + } + + fun testDuplicateGenericParameters() { + runFixture("generics/inspections/duplicate_generic_params.fixture.php") + } + + fun testExtendsTypes() { + runFixture("generics/inspections/extends/ok.fixture.php") + runFixture("generics/inspections/extends/wrong.fixture.php") + } +} diff --git a/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/types/GenericsTypesTest.kt b/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/types/GenericsTypesTest.kt new file mode 100644 index 0000000..102bd04 --- /dev/null +++ b/src/test/kotlin/com/vk/kphpstorm/testing/tests/generics/types/GenericsTypesTest.kt @@ -0,0 +1,96 @@ +package com.vk.kphpstorm.testing.tests.generics.types + +import com.vk.kphpstorm.testing.infrastructure.TypeTestBase + +class GenericsTypesTest : TypeTestBase() { + fun testExplicitPrimitives() { + runFixture( + "generics/types/primitives/explicit/simple.fixture.php", + "generics/types/primitives/explicit/union.fixture.php", + "generics/.meta/functions.php" + ) + } + + fun testImplicitPrimitives() { + runFixture( + "generics/types/primitives/implicit/simple.fixture.php", + "generics/types/primitives/implicit/union.fixture.php", + "generics/.meta/functions.php" + ) + } + + fun testClassStrings() { + runFixture( + "generics/types/class-string/explicit.fixture.php", + "generics/types/class-string/implicit.fixture.php", + "generics/.meta/functions.php" + ) + } + + fun testImplicitClass() { + runFixture( + "generics/types/classes-as-types/implicit/standalone.fixture.php", + "generics/types/classes-as-types/implicit/array.fixture.php", + "generics/types/classes-as-types/implicit/nullable.fixture.php", + "generics/types/classes-as-types/implicit/union.fixture.php", + "generics/types/classes-as-types/implicit/tuple.fixture.php", + "generics/types/classes-as-types/implicit/shape.fixture.php", + "generics/types/classes-as-types/implicit/mixed.fixture.php", + "generics/.meta/functions.php", + ) + } + + fun testExplicitClass() { + runFixture( + "generics/types/classes-as-types/explicit/standalone.fixture.php", + "generics/types/classes-as-types/explicit/array.fixture.php", + "generics/types/classes-as-types/explicit/nullable.fixture.php", + "generics/types/classes-as-types/explicit/union.fixture.php", + "generics/types/classes-as-types/explicit/tuple.fixture.php", + "generics/types/classes-as-types/explicit/shape.fixture.php", + "generics/types/classes-as-types/explicit/mixed.fixture.php", + "generics/.meta/functions.php" + ) + } + + fun testNexExpr() { + runFixture( + "generics/types/classes/new_expr.fixture.php", + ) + } + + fun testFunctions() { + runFixture( + "generics/types/functions/chain.fixture.php", + "generics/Containers/Vector.php", + ) + } + + fun testMethods() { + runFixture( + "generics/types/methods/static_and_non_generic.fixture.php", + "generics/types/methods/chain.fixture.php", + "generics/types/methods/complex.fixture.php", + "generics/Containers/Vector.php", + "generics/Containers/Pair.php", + ) + } + + fun testReifier() { + runFixture( + "generics/types/reify/main.fixture.php", + "generics/types/reify/params.fixture.php", + ) + } + + // TODO: +// fun testSimpleFunctions() { +// runFixture("generics/simple_functions.fixture.php") +// } + + // Classes + // TODO +// fun testSimpleClasses() { +// runFixture("generics/classes/simple_classes.fixture.php", "generics/classes/Vector.fixture.php") +// } +}