Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
- fixed type compatibility for a callable and a typed callable
- fixed default types handling
- improved handling for union instantiations for methods and fields
- generic hint now contains only "<T>"
- fixed InstantiationParamsCount inspection
  • Loading branch information
i582 committed Jun 7, 2022
1 parent 57f898a commit 897f64d
Show file tree
Hide file tree
Showing 21 changed files with 262 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class ExPhpTypeCallable(val argTypes: List<ExPhpType>, val returnType: ExPhpType
is ExPhpTypeAny -> true
// TODO: добавить полноценную проверку?
is ExPhpTypeCallable -> true
is ExPhpTypePrimitive -> rhs == ExPhpType.CALLABLE
is ExPhpTypeNullable -> isAssignableFrom(rhs.inner, project)
else -> false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,16 @@ class GenericConstructorCall(private val call: NewExpression) : GenericCall(call
override fun isResolved() = method != null && klass != null

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

return mutableListOf<KphpDocGenericParameterDecl>()
.apply { addAll(methodsNames) }
.apply { addAll(classesNames) }
.apply { addAll(methodNames) }
.apply { addAll(classNames) }
.toList()
}

override fun ownGenericNames(): List<KphpDocGenericParameterDecl> {
return klass?.genericNames() ?: emptyList()
}
override fun ownGenericNames() = genericNames()

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.jetbrains.php.lang.psi.elements.PhpTypedElement
import com.jetbrains.php.lang.psi.resolve.types.PhpType
import com.vk.kphpstorm.exphptype.ExPhpType
import com.vk.kphpstorm.generics.GenericUtil.genericNames
import com.vk.kphpstorm.generics.GenericUtil.getInstantiation
import com.vk.kphpstorm.generics.GenericUtil.getInstantiations
import com.vk.kphpstorm.helpers.toExPhpType
import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl
import kotlin.math.min
Expand All @@ -27,7 +27,9 @@ class GenericMethodCall(private val call: MethodReference) : GenericCall(call.pr
val classType = PhpType().add(callType).global(project)
val parsed = classType.toExPhpType()

val instantiation = parsed?.getInstantiation()
val instantiation = parsed?.getInstantiations()?.firstOrNull {
it.classFqn == klass?.fqn
}

if (instantiation != null) {
val specialization = instantiation.specializationList
Expand Down
13 changes: 13 additions & 0 deletions src/main/kotlin/com/vk/kphpstorm/generics/GenericUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,19 @@ object GenericUtil {
} as? ExPhpTypeTplInstantiation
}

fun ExPhpType.getInstantiations(): List<ExPhpTypeTplInstantiation> {
if (this is ExPhpTypePipe) {
return items.filterIsInstance<ExPhpTypeTplInstantiation>()
}
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

Expand Down
28 changes: 14 additions & 14 deletions src/main/kotlin/com/vk/kphpstorm/generics/GenericsReifier.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,15 @@ class GenericsReifier(val project: Project) {
reifyArgumentGenericsT(argExType, paramExType)
}

var instantiation = contextType?.getInstantiation()
// TODO: А что если тут несколько инстанциаций в типе?
val instantiation = contextType?.getInstantiation()
if (instantiation != null) {
// TODO: сделать верный вывод типов тут
if (instantiation.classFqn == klass?.fqn) {
instantiation = instantiation.specializationList.firstOrNull() as? ExPhpTypeTplInstantiation
}

if (instantiation != null) {
val specList = instantiation.specializationList
for (i in 0 until min(specList.size, genericTs.size)) {
val type = specList[i]
val genericT = genericTs[i]
val specList = instantiation.specializationList
for (i in 0 until min(specList.size, genericTs.size)) {
val type = specList[i]
val genericT = genericTs[i]

implicitSpecializationNameMap[genericT.name] = type
}
implicitSpecializationNameMap[genericT.name] = type
}
}

Expand All @@ -73,7 +67,13 @@ class GenericsReifier(val project: Project) {
return@forEach
}

implicitSpecializationNameMap[it.name] = it.defaultType
val defaultType = if (it.defaultType.isGeneric()) {
it.defaultType.instantiateGeneric(implicitSpecializationNameMap)
} else {
it.defaultType
}

implicitSpecializationNameMap[it.name] = defaultType
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,20 @@ abstract class ResolvingGenericBase(val project: Project) {
return specializationNameMap
}

private fun specializationList() = explicitGenericsT.ifEmpty { reifier.implicitSpecs }
private fun explicitGenericTypes(): List<ExPhpType> {
if (explicitGenericsT.isEmpty()) return emptyList()

val specMap = mutableMapOf<String, ExPhpType>()

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 }

private fun unpack(incompleteType: String): Boolean {
val packedData = incompleteType.substring(2)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import com.jetbrains.php.lang.psi.resolve.types.PhpType
import com.vk.kphpstorm.exphptype.ExPhpTypeForcing
import com.vk.kphpstorm.exphptype.ExPhpTypeTplInstantiation
import com.vk.kphpstorm.generics.GenericUtil.genericNames
import com.vk.kphpstorm.generics.GenericUtil.getInstantiation
import com.vk.kphpstorm.generics.GenericUtil.getInstantiations
import com.vk.kphpstorm.helpers.toExPhpType
import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl
import com.vk.kphpstorm.typeProviders.GenericFieldsTypeProvider
Expand Down Expand Up @@ -60,20 +60,23 @@ class ResolvingGenericFieldFetch(project: Project) : ResolvingGenericBase(projec
val fqn = parts[0]

val dotIndex = fqn.lastIndexOf('.')
val className = fqn.substring(0, dotIndex)
val classRawName = fqn.substring(0, dotIndex)
val methodName = fqn.substring(dotIndex + 1)

val classType = PhpType().add(className).global(project)
val classType = PhpType().add(classRawName).global(project)
val parsed = classType.toExPhpType()
val instantiation = parsed?.getInstantiation()
?:
// Если не удалось найти класс, значит вывести
// тип поля мы не сможем, поэтому заканчиваем распаковку.
return false

classGenericType = instantiation
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(instantiation.classFqn).firstOrNull() ?: return false
klass = PhpIndex.getInstance(project).getClassesByFQN(foundInstantiation.classFqn).firstOrNull() ?: return false
field = klass?.findFieldByName(methodName, false) ?: return false

classGenericTs = klass!!.genericNames()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import com.jetbrains.php.lang.psi.resolve.types.PhpType
import com.vk.kphpstorm.exphptype.ExPhpTypeGenericsT
import com.vk.kphpstorm.exphptype.ExPhpTypeTplInstantiation
import com.vk.kphpstorm.generics.GenericUtil.genericNames
import com.vk.kphpstorm.generics.GenericUtil.getInstantiation
import com.vk.kphpstorm.generics.GenericUtil.getInstantiations
import com.vk.kphpstorm.generics.GenericUtil.isReturnGeneric
import com.vk.kphpstorm.helpers.toExPhpType
import com.vk.kphpstorm.kphptags.psi.KphpDocGenericParameterDecl
Expand Down Expand Up @@ -63,11 +63,18 @@ class ResolvingGenericMethodCall(project: Project) : ResolvingGenericBase(projec

val classType = PhpType().add(classRawName).global(project)
val parsed = classType.toExPhpType()
val instantiation = parsed?.getInstantiation()
val instantiations = parsed?.getInstantiations()

val className = if (instantiation != null && instantiation.specializationList.first() !is ExPhpTypeGenericsT) {
classGenericType = instantiation
instantiation.classFqn
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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ 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

@Suppress("UnstableApiUsage")
class InlayHintsCollector(
Expand Down Expand Up @@ -60,19 +59,13 @@ class InlayHintsCollector(
return
}

val genericNames = if (call is GenericConstructorCall) {
call.genericNames()
} else {
call.function()!!.genericNames()
}.joinToString(", ") {
it.toHumanReadable(place)
}
val genericNames = call.ownGenericNames()

if (genericNames.isEmpty()) {
return
}

val simplePresentation = myHintsFactory.inlayHint("<$genericNames>")
val simplePresentation = myHintsFactory.inlayHint("<T>")

sink.addInlineElement(place.endOffset, false, simplePresentation, false)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,24 @@ class KphpGenericsInspection : PhpInspection() {
}

override fun visitPhpDocTag(tag: PhpDocTag) {
checkGenericsTag(tag)
checkGenericTag(tag)
}

private fun checkGenericsTag(tag: PhpDocTag) {
private fun checkGenericTag(tag: PhpDocTag) {
if (tag !is KphpDocTagGenericPsiImpl) {
return
}

var wasNoDefault = false
var wasDefault = false
tag.getGenericArgumentsWithExtends().forEach {
if (it.defaultType == null) {
wasNoDefault = true
if (it.defaultType != null) {
wasDefault = true
}

if (it.defaultType != null && wasNoDefault) {
if (it.defaultType == null && wasDefault) {
holder.registerProblem(
tag,
"Generic parameters with a default type cannot come after parameters without a default type",
"Generic parameters with a default type cannot come before parameters without a default type",
ProblemHighlightType.GENERIC_ERROR
)
}
Expand All @@ -78,7 +78,7 @@ class KphpGenericsInspection : PhpInspection() {
val genericNames = call.genericNames()

checkGenericTypesBounds(call, genericNames)
checkInstantiationArgsCount(call)
checkInstantiationParamsCount(call)
checkReifiedGenericTypes(call, element, errorPsi)
checkReifiedSeveralGenericTypes(call, element, errorPsi)
}
Expand Down Expand Up @@ -115,22 +115,37 @@ class KphpGenericsInspection : PhpInspection() {
}
}

private fun checkInstantiationArgsCount(call: GenericCall) {
val countGenericNames = if (call is GenericMethodCall && call.isStatic()) {
call.ownGenericNames().size
} else {
call.genericNames().size - call.implicitClassSpecializationNameMap.size
}
private fun checkInstantiationParamsCount(call: GenericCall) {
val minCount = call.ownGenericNames().filter { it.defaultType == null }.size
val maxCount = call.ownGenericNames().size

val countExplicitSpecs = call.explicitSpecs.size
val explicitSpecsPsi = call.explicitSpecsPsi

if (countGenericNames != countExplicitSpecs && explicitSpecsPsi != null) {
if (minCount == maxCount && minCount != countExplicitSpecs && explicitSpecsPsi != null) {
holder.registerProblem(
explicitSpecsPsi,
"$countGenericNames type arguments expected for ${call.function()!!.fqn}",
"$minCount generic parameters expected for call, but $countExplicitSpecs passed",
ProblemHighlightType.GENERIC_ERROR
)
return
}

if (countExplicitSpecs < minCount && explicitSpecsPsi != null) {
holder.registerProblem(
explicitSpecsPsi,
"Not enough generic parameters for call, expected at least $minCount",
ProblemHighlightType.GENERIC_ERROR,
)
return
}

if (countExplicitSpecs > maxCount && explicitSpecsPsi != null) {
holder.registerProblem(
explicitSpecsPsi,
"Too many generic parameters for call, expected at most $maxCount",
ProblemHighlightType.GENERIC_ERROR,
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class GenericMethodsTypeProvider : PhpTypeProvider4 {
GenericFunctionsTypeProvider.KEY.signed(type) ||
GenericFieldsTypeProvider.KEY.signed(type) ||
KEY.signed(type) ||
!type.startsWith("#")
(!type.startsWith("#") && !type.startsWith("%"))
}

val resultType = PhpType()
Expand Down
19 changes: 19 additions & 0 deletions src/test/fixtures/generics/general/fields/main.fixture.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ class GenericClass {
public $class_string_field;
}

/**
* @kphp-generic T
*/
class GenericClass2 {
/** @var T */
public $class2_plain_field;
}

$a = new GenericClass/*<Foo>*/ ();

$a->plain_field->fooMethod();
Expand Down Expand Up @@ -68,3 +76,14 @@ function getGenericClass(): GenericClass { return new GenericClass(); }
expr_type(getGenericClass()->array_field[0]->fooMethod(), "?int");
expr_type(getGenericClass()->tuple_field[0]->fooMethod(), "?int");
expr_type(getGenericClass()->shape_field["key1"]->fooMethod(), "?int");

/**
* @return GenericClass<int>|GenericClass2<string>
*/
function union() {
return new GenericClass();
}

$b = union();
expr_type($b->class2_plain_field, "string");
expr_type($b->plain_field, "int");
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,11 @@ function returnClassOfStringViaGenericMethod() {
$a = new ClassWithGenericMethod();
return $a->genericMethod();
}

/**
* @return GenericClassWithSeveralTypes<int, string>|GenericClass<string>
*/
function returnClassOfStringViaGenericMethod1() {
$a = new ClassWithGenericMethod();
return $a->genericMethod();
}
Loading

0 comments on commit 897f64d

Please sign in to comment.