diff --git a/src/Analyser/ExprHandler/IssetHandler.php b/src/Analyser/ExprHandler/IssetHandler.php index 122fffaa642..0ad3a20cb62 100644 --- a/src/Analyser/ExprHandler/IssetHandler.php +++ b/src/Analyser/ExprHandler/IssetHandler.php @@ -218,7 +218,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } $accExpr = new Isset_([$expr->vars[0]], $subjectAttributes); - $accTypes = $makeSubjectTypes($expr->vars[0], $varResults[0]); + $accTypes = self::memoizeSubjectTypes($makeSubjectTypes($expr->vars[0], $varResults[0])); $accTruthyScope = $afterScope->applySpecifiedTypes($accTypes($afterScope, TypeSpecifierContext::createTruthy())); $accFalseyScope = $afterScope->applySpecifiedTypes($accTypes($afterScope, TypeSpecifierContext::createFalsey())); @@ -228,10 +228,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $rightFalseyScope = $accTruthyScope->applySpecifiedTypes($rightTypes($accTruthyScope, TypeSpecifierContext::createFalsey())); $leftExprNode = $accExpr; - $leftTypes = self::memoizeSubjectTypes($accTypes); + $leftTypes = $accTypes; $leftTruthyScope = $accTruthyScope; $leftFalseyScope = $accFalseyScope; - $accTypes = fn (MutatingScope $scope, TypeSpecifierContext $ctx): SpecifiedTypes => $this->booleanNarrowingHelper->specifyConjunction( + $accTypes = self::memoizeSubjectTypes(fn (MutatingScope $scope, TypeSpecifierContext $ctx): SpecifiedTypes => $this->booleanNarrowingHelper->specifyConjunction( $nodeScopeResolver, $scope, $ctx, @@ -243,7 +243,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $rightExprNode, $rightTypes, static fn (): MutatingScope => $rightFalseyScope, - ); + )); $accExpr = new BooleanAnd($leftExprNode, $rightExprNode); $accTruthyScope = $accTruthyScope->applySpecifiedTypes($rightTypes($accTruthyScope, TypeSpecifierContext::createTruthy())); $accFalseyScope = $afterScope->applySpecifiedTypes($accTypes($afterScope, TypeSpecifierContext::createFalsey())); @@ -267,10 +267,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } /** - * The narrowing of the conjunction folded so far: every later conjunction - * asks it again as its left operand, with the scopes and contexts it was - * already asked with - answered once per pair instead of re-folding every - * subject before it on each ask. + * The narrowing of the conjunction folded so far: the fold asks it for its + * falsey scope and the next conjunction asks it again as its left operand, + * with the scopes and contexts it was already asked with - answered once + * per pair instead of re-folding the subjects before it on each ask. * * @param Closure(MutatingScope, TypeSpecifierContext): SpecifiedTypes $types * @return Closure(MutatingScope, TypeSpecifierContext): SpecifiedTypes diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 72340073543..3aab9fa85ba 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -1771,7 +1771,23 @@ public function traverseSimultaneously(Type $right, callable $cb): Type public function tryRemove(Type $typeToRemove): ?Type { - return $this->intersectTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove)); + $removedTypes = []; + $changed = false; + foreach ($this->types as $type) { + $removed = TypeCombinator::remove($type, $typeToRemove); + if (!$removed->equals($type)) { + $changed = true; + } + $removedTypes[] = $removed; + } + + // no member changed, so neither does the intersection - intersecting + // the same members again would only re-reduce every pair of them + if (!$changed) { + return $this; + } + + return self::intersectOperands($removedTypes); } public function arithmeticPower(Type $exponent, PhpVersion $phpVersion): Type diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 90cbb62f36b..bab94e27b97 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -1523,9 +1523,12 @@ private function isExtraOffsetAccessibleClass(): TrinaryLogic public function isOffsetAccessible(): TrinaryLogic { - return $this->isInstanceOf(ArrayAccess::class)->or( - $this->isExtraOffsetAccessibleClass(), - ); + $isArrayAccess = $this->isInstanceOf(ArrayAccess::class); + if ($isArrayAccess->yes()) { + return $isArrayAccess; + } + + return $isArrayAccess->or($this->isExtraOffsetAccessibleClass()); } public function isOffsetAccessLegal(): TrinaryLogic diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index f42af28f3f2..c1bab12d116 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -140,6 +140,34 @@ public static function doRemove(Type $fromType, Type $typeToRemove): Type if ($removed !== null) { return $removed; } + + // a union removed the whole union from each of its members and none + // of them changed - removing the members of it one at a time cannot + // change one either, peeling would only repeat that work + if ($fromType instanceof UnionType) { + return $fromType; + } + } + + // mixed is the top type, so removing a union from it is exactly + // subtracting the members it does not subtract already, all of them at + // once. Peeling them one at a time would ask each member about the mixed + // the earlier members left, which walks everything they subtracted. + if ($fromType instanceof MixedType && !$fromType instanceof TemplateType) { + $subtractedType = $fromType->getSubtractedType(); + $remainingTypes = []; + foreach ($typeToRemove->getTypes() as $unionTypeToRemove) { + if ($subtractedType !== null && $subtractedType->isSuperTypeOf($unionTypeToRemove)->yes()) { + continue; + } + $remainingTypes[] = $unionTypeToRemove; + } + + if ($remainingTypes === []) { + return $fromType; + } + + return $fromType->subtract(count($remainingTypes) === 1 ? $remainingTypes[0] : new UnionType($remainingTypes)); } foreach ($typeToRemove->getTypes() as $unionTypeToRemove) { @@ -1155,6 +1183,9 @@ private static function processArrayAccessoryTypes(array $arrayTypes): array $isIterableAtLeastOnce[] = $arrayType->isIterableAtLeastOnce(); if ($arrayType instanceof IntersectionType) { + // asked once: the same for every hasOffset() member, and an + // intersection computes it from all of its members + $iterableValueType = null; foreach ($arrayType->getTypes() as $innerType) { if ($innerType instanceof TemplateType) { break; @@ -1163,7 +1194,8 @@ private static function processArrayAccessoryTypes(array $arrayTypes): array continue; } if ($innerType instanceof HasOffsetType) { - $innerType = new HasOffsetValueType($innerType->getOffsetType(), $arrayType->getIterableValueType()); + $iterableValueType ??= $arrayType->getIterableValueType(); + $innerType = new HasOffsetValueType($innerType->getOffsetType(), $iterableValueType); } if ($innerType instanceof HasOffsetValueType) { $accessoryTypes[sprintf('hasOffsetValue(%s)', $innerType->getOffsetType()->describe(VerbosityLevel::cache()))][$i] = $innerType; diff --git a/turbo-ext/src/IntersectionType.cpp b/turbo-ext/src/IntersectionType.cpp index 1a0eafae53a..4dea955f32f 100644 --- a/turbo-ext/src/IntersectionType.cpp +++ b/turbo-ext/src/IntersectionType.cpp @@ -2131,12 +2131,30 @@ class IntersectionType traversed.intoReturnValue(return_value); } - /* TypeCombinator::remove($type, $typeToRemove) per member, intersected */ + /* TypeCombinator::remove($type, $typeToRemove) per member, intersected + * by intersectOperands() - $this when no member changed */ zv::Val tryRemove(zval *typeToRemove) const { - return intersectTypes([&](zval *type) -> zv::Val { - return combinator2(PT_LC("remove"), type, typeToRemove); - }); + zval *types = this->types(); + if (UNEXPECTED(types == NULL)) return zv::Val(); + zv::Val typesCopy = zv::Val::copyOf(zv::Ref(types)); + zv::Arr removedTypes = zv::Arr::create(zv::ArrRef(typesCopy.raw()).size()); + bool changed = false; + for (zv::ArrayEntry entry : zv::ArrRef(typesCopy.raw())) { + zval *type = entry.value().deref().raw(); + zv::Val removed = combinator2(PT_LC("remove"), type, typeToRemove); + if (UNEXPECTED(removed.isUndef())) return zv::Val(); + if (UNEXPECTED(!zv::Ref(removed.raw()).isObject())) { + zend_type_error("phpstan_turbo: TypeCombinator::remove() must return %s", ptcls::type); + return zv::Val(); + } + int equal = pt_type_call_is_true(Z_OBJ_P(removed.raw()), PT_LC("equals"), 1, type); + if (UNEXPECTED(equal < 0)) return zv::Val(); + if (equal == 0) changed = true; + removedTypes.push(std::move(removed)); + } + if (!changed) return thisValue(); + return intersectOperands(removedTypes.arrRef()); } /* every member's power intersected, unless a member's is an ErrorType, diff --git a/turbo-ext/src/IssetHandler.cpp b/turbo-ext/src/IssetHandler.cpp index 5085a8854fa..5083acee440 100644 --- a/turbo-ext/src/IssetHandler.cpp +++ b/turbo-ext/src/IssetHandler.cpp @@ -556,7 +556,8 @@ class IssetHandler if (UNEXPECTED(attributes.isUndef())) return zv::Val(); zv::Val accExpr = newIsset(firstVar, attributes.raw()); if (UNEXPECTED(accExpr.isUndef())) return zv::Val(); - zv::Val accTypes = pt_native_closure(&subjectTypesBody, handler, chainResults, expr, firstVar, firstResult); + zv::Val firstTypes = pt_native_closure(&subjectTypesBody, handler, chainResults, expr, firstVar, firstResult); + zv::Val accTypes = memoizeSubjectTypes(firstTypes.raw()); zv::Val accTruthyScope = applySpecifiedTypes(afterScope, callNarrowing(accTypes.raw(), afterScope, pt_type_specifier_context_create_truthy())); if (UNEXPECTED(accTruthyScope.isUndef())) return zv::Val(); zv::Val accFalseyScope = applySpecifiedTypes(afterScope, callNarrowing(accTypes.raw(), afterScope, pt_type_specifier_context_create_falsey())); @@ -584,14 +585,14 @@ class IssetHandler ZVAL_COPY_VALUE(&conjunctionCaptures[1], nodeScopeResolver); ZVAL_COPY_VALUE(&conjunctionCaptures[2], expr); ZVAL_COPY_VALUE(&conjunctionCaptures[3], leftExprNode.raw()); - zv::Val leftTypes = memoizeSubjectTypes(accTypes.raw()); - ZVAL_COPY_VALUE(&conjunctionCaptures[4], leftTypes.raw()); + ZVAL_COPY_VALUE(&conjunctionCaptures[4], accTypes.raw()); ZVAL_COPY_VALUE(&conjunctionCaptures[5], accTruthyScope.raw()); ZVAL_COPY_VALUE(&conjunctionCaptures[6], accFalseyScope.raw()); ZVAL_COPY_VALUE(&conjunctionCaptures[7], rightExprNode.raw()); ZVAL_COPY_VALUE(&conjunctionCaptures[8], rightTypes.raw()); ZVAL_COPY_VALUE(&conjunctionCaptures[9], rightFalseyScope.raw()); - accTypes = pt_native_closure_new(&conjunctionBody, 10, conjunctionCaptures); + zv::Val conjunctionTypes = pt_native_closure_new(&conjunctionBody, 10, conjunctionCaptures); + accTypes = memoizeSubjectTypes(conjunctionTypes.raw()); zv::Args andArgv{leftExprNode.raw(), rightExprNode.raw()}; accExpr = pt_type_new(PT_CLASS_BOOLEAN_AND_EXPR, 2, andArgv); diff --git a/turbo-ext/src/ObjectType.cpp b/turbo-ext/src/ObjectType.cpp index 00372680ea3..1e81b93bad3 100644 --- a/turbo-ext/src/ObjectType.cpp +++ b/turbo-ext/src/ObjectType.cpp @@ -1910,6 +1910,7 @@ class ObjectType { zend_long arrayAccess = thisIsInstanceOfLiteral(PT_LC("ArrayAccess")); if (UNEXPECTED(arrayAccess < 0)) return -1; + if (arrayAccess == PT_TRI_YES) return arrayAccess; zend_long extra = isExtraOffsetAccessibleClass(); if (UNEXPECTED(extra < 0)) return -1; return arrayAccess > extra ? arrayAccess : extra; diff --git a/turbo-ext/src/TypeCombinator.cpp b/turbo-ext/src/TypeCombinator.cpp index 0f5f69d4ce0..8de47bc74b1 100644 --- a/turbo-ext/src/TypeCombinator.cpp +++ b/turbo-ext/src/TypeCombinator.cpp @@ -638,10 +638,44 @@ class TypeCombinator zv::Val removed = call(fromType, PT_LC("tryremove"), 1, typeToRemove); PT_FAIL_IF_UNDEF(removed); if (!isNull(removed)) return removed; + /* no member of the union changed - peeling cannot change one */ + if (isInstance(fromType, pt_ce_union_type)) return copy(fromType); } zv::Val types = getTypes(typeToRemove); PT_FAIL_IF_UNDEF(types); + + /* mixed is the top type: the members it does not subtract + * already are subtracted all at once */ + if (isInstance(fromType, pt_ce_mixed_type)) { + int isTemplate = isInstanceMap(fromType, PT_CLASS_TEMPLATE_TYPE); + PT_FAIL_IF_NEG(isTemplate); + if (!isTemplate) { + zv::Val subtractedType = call(fromType, PT_LC("getsubtractedtype")); + PT_FAIL_IF_UNDEF(subtractedType); + zv::Arr remainingTypes = zv::Arr::create(0); + for (zv::ArrayEntry entry : zv::ArrRef(types.raw())) { + zval *member = entry.value().deref().raw(); + if (!isNull(subtractedType)) { + zend_long covered = callResultTrinary(subtractedType.raw(), PT_LC("issupertypeof"), 1, member); + PT_FAIL_IF_NEG(covered); + if (covered == PT_TRI_YES) continue; + } + remainingTypes.push(zv::Val::copyOf(zv::Ref(member))); + } + uint32_t remainingCount = zv::ArrRef(remainingTypes.raw()).size(); + if (remainingCount == 0) return copy(fromType); + zv::Val subtract; + if (remainingCount == 1) { + subtract = copy(indexOf(remainingTypes.raw(), 0)); + } else { + subtract = unionType(std::move(remainingTypes), false); + PT_FAIL_IF_UNDEF(subtract); + } + return call(fromType, PT_LC("subtract"), 1, subtract.raw()); + } + } + zv::Val current = copy(fromType); for (zv::ArrayEntry entry : zv::ArrRef(types.raw())) { zv::Val next = remove(current.raw(), entry.value().raw()); @@ -1944,6 +1978,8 @@ class TypeCombinator if (isInstance(arrayType, pt_ce_intersection_type)) { zv::Val innerTypes = getTypes(arrayType); PT_FAIL_IF_UNDEF(innerTypes); + /* the same for every hasOffset() member, asked once */ + zv::Val valueType; for (zv::ArrayEntry entry : zv::ArrRef(innerTypes.raw())) { zv::Val innerType = copy(entry.value().raw()); int isTemplate = isInstanceMap(innerType.raw(), PT_CLASS_TEMPLATE_TYPE); @@ -1955,8 +1991,10 @@ class TypeCombinator if (isInstance(innerType.raw(), pt_ce_has_offset_type)) { zv::Val offsetType = pt_has_offset_type_get_offset_type(Z_OBJ_P(innerType.raw())); PT_FAIL_IF_UNDEF(offsetType); - zv::Val valueType = call(arrayType, PT_LC("getiterablevaluetype")); - PT_FAIL_IF_UNDEF(valueType); + if (valueType.isUndef()) { + valueType = call(arrayType, PT_LC("getiterablevaluetype")); + PT_FAIL_IF_UNDEF(valueType); + } innerType = hasOffsetValueType(offsetType.raw(), valueType.raw()); PT_FAIL_IF_UNDEF(innerType); }