diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpType.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpType.kt index b3cf1ca..08b086c 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpType.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpType.kt @@ -51,6 +51,8 @@ interface ExPhpType { fun instantiateTemplate(nameMap: Map): ExPhpType fun isAssignableFrom(rhs: ExPhpType, project: Project): Boolean + fun dropForce(): ExPhpType? + companion object { val INT = ExPhpTypePrimitive(KphpPrimitiveTypes.INT) val FLOAT = ExPhpTypePrimitive(KphpPrimitiveTypes.FLOAT) diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeAny.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeAny.kt index 9b7f626..bf1e95d 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeAny.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeAny.kt @@ -39,4 +39,8 @@ class ExPhpTypeAny : ExPhpType { // anything can be assigned to any return true } + + override fun dropForce(): 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..ec91a77 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeArray.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeArray.kt @@ -35,4 +35,8 @@ class ExPhpTypeArray(val inner: ExPhpType) : ExPhpType { else -> false } } + + override fun dropForce(): ExPhpType? { + return ExPhpTypeArray(inner.dropForce() ?: return null) + } } diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeCallable.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeCallable.kt index 6760f01..b565208 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeCallable.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeCallable.kt @@ -31,4 +31,11 @@ class ExPhpTypeCallable(val argTypes: List, val returnType: ExPhpType is ExPhpTypeNullable -> isAssignableFrom(rhs.inner, project) else -> false } + + override fun dropForce(): ExPhpType { + val newArgTypes = argTypes.mapNotNull { it.dropForce() } + val newReturnType = returnType?.dropForce() + + return ExPhpTypeCallable(newArgTypes, newReturnType) + } } diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeForcing.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeForcing.kt index 4155f8f..16b3165 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeForcing.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeForcing.kt @@ -36,4 +36,8 @@ class ExPhpTypeForcing(val inner: ExPhpType) : ExPhpType { // that's why everything is assignable to forcing type, making stricy typing inspections assume all is ok return true } + + override fun dropForce(): ExPhpType? { + return inner.dropForce() + } } diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeInstance.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeInstance.kt index f5bf861..622ca23 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeInstance.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeInstance.kt @@ -52,4 +52,8 @@ class ExPhpTypeInstance(val fqn: String) : ExPhpType { else -> false } + + override fun dropForce(): ExPhpType { + return this + } } diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeNullable.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeNullable.kt index 2c281fd..a58e16f 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeNullable.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeNullable.kt @@ -31,4 +31,8 @@ class ExPhpTypeNullable(val inner: ExPhpType) : ExPhpType { is ExPhpTypePrimitive -> rhs === ExPhpType.NULL || inner.isAssignableFrom(rhs, project) else -> inner.isAssignableFrom(rhs, project) } + + override fun dropForce(): ExPhpType? { + return ExPhpTypeNullable(inner.dropForce() ?: return null) + } } diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePipe.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePipe.kt index a086cff..7fa0ace 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePipe.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePipe.kt @@ -98,6 +98,20 @@ class ExPhpTypePipe(val items: List) : ExPhpType { } } + override fun dropForce(): ExPhpType { + val typePipe = if (items.count { it is ExPhpTypeForcing } == 0) { + ExPhpTypePipe(items.mapNotNull { it.dropForce() }) + } else { + ExPhpTypePipe(items.filterIsInstance().mapNotNull { it.dropForce() }) + } + + if (typePipe.items.size == 1) { + return typePipe.items.first() + } + + return typePipe + } + /** * Heruistics: let this == "string|int", where can this be assigned to? * To "string" — no, "int" not compatible. To "string|float" — yes. To "string|int|A" — yes. diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePrimitive.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePrimitive.kt index 7f9c001..206af6b 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePrimitive.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypePrimitive.kt @@ -39,6 +39,10 @@ class ExPhpTypePrimitive(val typeStr: String) : ExPhpType { else -> false } + override fun dropForce(): ExPhpType { + return this + } + companion object { private fun canBeAssigned(l: ExPhpTypePrimitive, r: ExPhpTypePrimitive) = with(ExPhpType.Companion) { when { diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeShape.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeShape.kt index 2b6efcf..c1b57a6 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeShape.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeShape.kt @@ -39,4 +39,8 @@ class ExPhpTypeShape(val items: List) : ExPhpType { is ExPhpTypeShape -> true // any shape is compatible with any other, for simplification (tuples are not) else -> false } + + override fun dropForce(): ExPhpType { + return ExPhpTypeShape(items.mapNotNull { ShapeItem(it.keyName, it.nullable, it.type.dropForce() ?: return@mapNotNull null ) }) + } } diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTplInstantiation.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTplInstantiation.kt index e31667e..518188f 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTplInstantiation.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTplInstantiation.kt @@ -32,4 +32,8 @@ class ExPhpTypeTplInstantiation(val classFqn: String, val specializationList: Li is ExPhpTypeTplInstantiation -> classFqn == rhs.classFqn && specializationList.size == rhs.specializationList.size else -> false } + + override fun dropForce(): ExPhpType { + return this + } } diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTuple.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTuple.kt index 07ed945..edccef7 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTuple.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/ExPhpTypeTuple.kt @@ -35,4 +35,8 @@ class ExPhpTypeTuple(val items: List) : ExPhpType { is ExPhpTypeTuple -> items.size == rhs.items.size && items.indices.all { items[it].isAssignableFrom(rhs.items[it], project) } else -> false } + + override fun dropForce(): ExPhpType { + return ExPhpTypeTuple(items.mapNotNull { it.dropForce() }) + } } diff --git a/src/main/kotlin/com/vk/kphpstorm/exphptype/PhpTypeToExPhpTypeParsing.kt b/src/main/kotlin/com/vk/kphpstorm/exphptype/PhpTypeToExPhpTypeParsing.kt index 6261565..ee9cd4d 100644 --- a/src/main/kotlin/com/vk/kphpstorm/exphptype/PhpTypeToExPhpTypeParsing.kt +++ b/src/main/kotlin/com/vk/kphpstorm/exphptype/PhpTypeToExPhpTypeParsing.kt @@ -95,6 +95,7 @@ object PhpTypeToExPhpTypeParsing { // some "forced" that can occur often, @see [ForcingTypeProvider], \\ not needed "force(string)" to ExPhpTypeForcing(ExPhpType.STRING), + "force(float)" to ExPhpTypeForcing(ExPhpType.FLOAT), "force(int)" to ExPhpTypeForcing(ExPhpType.INT), "force(bool)" to ExPhpTypeForcing(ExPhpType.BOOL), "force(kmixed)" to ExPhpTypeForcing(ExPhpType.KMIXED), @@ -285,7 +286,7 @@ object PhpTypeToExPhpTypeParsing { 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 + return lhs val pipeItems = mutableListOf(lhs) while (builder.compareAndEat('|') || builder.compareAndEat('/')) { @@ -313,11 +314,6 @@ object PhpTypeToExPhpTypeParsing { if (size == 2 && pipeItems[1] === ExPhpType.NULL) return createNullableOrSimplified(pipeItems[0]) - // T1|T2|...|force(T) will be just T - for (item in pipeItems) - if (item is ExPhpTypeForcing) - return item.inner - return ExPhpTypePipe(pipeItems) } @@ -347,7 +343,7 @@ object PhpTypeToExPhpTypeParsing { else FQN_PREPARSED[str] ?: parseTypeExpression(ExPhpTypeBuilder(str)) } - createPipeOrSimplified(items) + createPipeOrSimplified(items)?.dropForce() } } } 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 39b7e67..44b9a36 100644 --- a/src/test/fixtures/strict_typing/props_assignments_10.fixture.php +++ b/src/test/fixtures/strict_typing/props_assignments_10.fixture.php @@ -72,7 +72,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() { @@ -80,8 +80,8 @@ function demo2() { $a->s = array_first_value($a->s_arr); $a->a = array_first_value($a->s_arr); $a->s_arr[] = array_last_value($a->s_arr); - $a->s = array_first_value($a->var); - $a->a = array_first_value($a->var); + $a->s = array_first_value($a->var); + $a->a = array_first_value($a->var); $a->var = array_first_value($a->var); $a->s_arr = array_filter_by_key($a->s_arr, function($k) { return true; }); $a->s_arr = array_filter_by_key($a->i_arr, function($k) { return true; }); diff --git a/src/test/kotlin/com/vk/kphpstorm/testing/tests/ExPhpTypeTest.kt b/src/test/kotlin/com/vk/kphpstorm/testing/tests/ExPhpTypeTest.kt index ccaeaf5..b79d765 100644 --- a/src/test/kotlin/com/vk/kphpstorm/testing/tests/ExPhpTypeTest.kt +++ b/src/test/kotlin/com/vk/kphpstorm/testing/tests/ExPhpTypeTest.kt @@ -48,9 +48,6 @@ class ExPhpTypeTest : TestCase() { } fun testParsingFromString() { - fun String.toExPhpType(): ExPhpType = - PhpTypeToExPhpTypeParsing.parseFromString(this) ?: throw RuntimeException("Couldnt parse $this") - "int".toExPhpType().apply { Assert.assertTrue(this is ExPhpTypePrimitive) Assert.assertSame(this, ExPhpType.INT) @@ -111,4 +108,25 @@ class ExPhpTypeTest : TestCase() { Assert.assertTrue(PhpType.STRING.toExPhpType()!!.isAssignableFrom(phpType.toExPhpType()!!, createMockProject())) } + fun testSupperForcing() { + checkDrop("int|string", "int|string") + checkDrop("force(force(int[])[])", "int[][]") + checkDrop("string|bool|force(int)", "int") + checkDrop("string|bool|force(int)|force(float)", "int|float") + checkDrop("force(force(int[])[])|force(null)", "int[][]|null") + checkDrop("tuple(force(int)|string, int|force(string), bool)", "tuple(int,string,bool)") + checkDrop("shape(key: int|force(string), key2:bool, key3: float|bool|force(int[][]))", "shape(key:string,key2:bool,key3:int[][])") + checkDrop("callable(string,force(int)|string):force(force(int[])[])|string", "callable(string,int):int[][]") + checkDrop("callable(int,force(?bool)|string)", "callable(int,?bool):void") + } + + private fun String.toExPhpType(): ExPhpType = + PhpTypeToExPhpTypeParsing.parseFromString(this) ?: throw RuntimeException("Couldnt parse $this") + + private fun checkDrop(forceType: String, expectedType: String) { + forceType.toExPhpType().apply { + Assert.assertEquals(expectedType, this.dropForce().toString()) + + } + } }