diff --git a/src/Analyser/ExprHandler/BinaryOpHandler.php b/src/Analyser/ExprHandler/BinaryOpHandler.php index 4aa6fac7a8d..f6034c385f7 100644 --- a/src/Analyser/ExprHandler/BinaryOpHandler.php +++ b/src/Analyser/ExprHandler/BinaryOpHandler.php @@ -68,6 +68,8 @@ final class BinaryOpHandler implements ExprHandler { + private const COMPARISON_FORMS_ATTRIBUTE = 'phpstan_comparison_forms'; + public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private RicherScopeGetTypeHelper $richerScopeGetTypeHelper, @@ -737,12 +739,13 @@ private function resolveRememberedComparisonType(MutatingScope $scope, BinaryOp return null; } - foreach ($forms as [$form, $isComplement]) { - if (!$scope->hasExpressionType($form)->yes()) { + foreach ($forms as [, $isComplement, $formKey]) { + $holder = $scope->expressionTypes[$formKey] ?? null; + if ($holder === null || !$holder->getCertainty()->yes()) { continue; } - $formType = $scope->getTrackedExpressionType($form)->toBoolean(); + $formType = $holder->getType()->toBoolean(); if ($formType->isTrue()->yes()) { return new ConstantBooleanType(!$isComplement); } @@ -776,9 +779,12 @@ private function specifyRememberedComparisonForms(SpecifiedTypes $specifiedTypes $exprKey = $this->exprPrinter->printExpr($expr); $sureTypes = []; - foreach ($forms as [$form, $isComplement]) { - $formKey = $this->exprPrinter->printExpr($form); - if ($formKey === $exprKey || !$scope->hasExpressionType($form)->yes()) { + foreach ($forms as [$form, $isComplement, $formKey]) { + if ($formKey === $exprKey) { + continue; + } + $holder = $scope->expressionTypes[$formKey] ?? null; + if ($holder === null || !$holder->getCertainty()->yes()) { continue; } @@ -797,78 +803,121 @@ private function specifyRememberedComparisonForms(SpecifiedTypes $specifiedTypes * paired with whether it is the complement (always the opposite value). * * @param callable(Expr): Type $getType - * @return list|null + * @return list|null */ private function getEquivalentComparisonForms(BinaryOp $expr, callable $getType): ?array + { + if ( + !$expr instanceof BinaryOp\Identical + && !$expr instanceof BinaryOp\NotIdentical + && !$expr instanceof BinaryOp\Equal + && !$expr instanceof BinaryOp\NotEqual + && !$expr instanceof BinaryOp\Smaller + && !$expr instanceof BinaryOp\SmallerOrEqual + && !$expr instanceof BinaryOp\Greater + && !$expr instanceof BinaryOp\GreaterOrEqual + ) { + return null; + } + + $left = $expr->left; + $right = $expr->right; + + // The forms are built and printed once per comparison node and kept + // in its attributes: scopes are keyed by the printed expression, and + // fresh form nodes would be printed again on every lookup. Attributes + // travel to new nodes, which can be of another class or have other + // operands. + $cached = $expr->getAttribute(self::COMPARISON_FORMS_ATTRIBUTE); + if ($cached === null || $cached[0] !== get_class($expr) || $cached[1] !== $left || $cached[2] !== $right) { + $cached = [get_class($expr), $left, $right, ...$this->createComparisonForms($expr)]; + $expr->setAttribute(self::COMPARISON_FORMS_ATTRIBUTE, $cached); + } + + // `<` and `>=` only complement each other for comparable operands + // (NAN and uncomparable arrays/objects are neither smaller nor + // larger-or-equal) + if ($cached[4] === null || !$getType($left)->isInteger()->yes() || !$getType($right)->isInteger()->yes()) { + return $cached[3]; + } + + return $cached[4]; + } + + /** + * The equivalent forms, and for `<`, `<=`, `>` and `>=` also the + * equivalent forms followed by the complement forms that hold only for + * comparable operands - each form with its node key (a comparison's node + * key is its printed form). + * + * @return array{list, list|null} + */ + private function createComparisonForms(BinaryOp $expr): array { $left = $expr->left; $right = $expr->right; + $complementForms = []; if ($expr instanceof BinaryOp\Identical || $expr instanceof BinaryOp\NotIdentical) { $isNegated = $expr instanceof BinaryOp\NotIdentical; - - return [ + $forms = [ [new BinaryOp\Identical($left, $right), $isNegated], [new BinaryOp\Identical($right, $left), $isNegated], [new BinaryOp\NotIdentical($left, $right), !$isNegated], [new BinaryOp\NotIdentical($right, $left), !$isNegated], ]; - } - - if ($expr instanceof BinaryOp\Equal || $expr instanceof BinaryOp\NotEqual) { + } elseif ($expr instanceof BinaryOp\Equal || $expr instanceof BinaryOp\NotEqual) { $isNegated = $expr instanceof BinaryOp\NotEqual; - - return [ + $forms = [ [new BinaryOp\Equal($left, $right), $isNegated], [new BinaryOp\Equal($right, $left), $isNegated], [new BinaryOp\NotEqual($left, $right), !$isNegated], [new BinaryOp\NotEqual($right, $left), !$isNegated], ]; - } - - if ( - !$expr instanceof BinaryOp\Smaller - && !$expr instanceof BinaryOp\SmallerOrEqual - && !$expr instanceof BinaryOp\Greater - && !$expr instanceof BinaryOp\GreaterOrEqual - ) { - return null; - } - - // normalized to `$smaller < $larger` / `$smaller <= $larger` - if ($expr instanceof BinaryOp\Smaller || $expr instanceof BinaryOp\SmallerOrEqual) { - $smaller = $left; - $larger = $right; } else { - $smaller = $right; - $larger = $left; + // normalized to `$smaller < $larger` / `$smaller <= $larger` + if ($expr instanceof BinaryOp\Smaller || $expr instanceof BinaryOp\SmallerOrEqual) { + $smaller = $left; + $larger = $right; + } else { + $smaller = $right; + $larger = $left; + } + + if ($expr instanceof BinaryOp\SmallerOrEqual || $expr instanceof BinaryOp\GreaterOrEqual) { + $forms = [ + [new BinaryOp\SmallerOrEqual($smaller, $larger), false], + [new BinaryOp\GreaterOrEqual($larger, $smaller), false], + ]; + $complementForms = [ + [new BinaryOp\Smaller($larger, $smaller), true], + [new BinaryOp\Greater($smaller, $larger), true], + ]; + } else { + $forms = [ + [new BinaryOp\Smaller($smaller, $larger), false], + [new BinaryOp\Greater($larger, $smaller), false], + ]; + $complementForms = [ + [new BinaryOp\SmallerOrEqual($larger, $smaller), true], + [new BinaryOp\GreaterOrEqual($smaller, $larger), true], + ]; + } } - $orEqual = $expr instanceof BinaryOp\SmallerOrEqual || $expr instanceof BinaryOp\GreaterOrEqual; - $forms = $orEqual - ? [ - [new BinaryOp\SmallerOrEqual($smaller, $larger), false], - [new BinaryOp\GreaterOrEqual($larger, $smaller), false], - ] - : [ - [new BinaryOp\Smaller($smaller, $larger), false], - [new BinaryOp\Greater($larger, $smaller), false], - ]; - // `<` and `>=` only complement each other for comparable operands - // (NAN and uncomparable arrays/objects are neither smaller nor - // larger-or-equal) - if (!$getType($left)->isInteger()->yes() || !$getType($right)->isInteger()->yes()) { - return $forms; + $keyedForms = []; + foreach ($forms as [$form, $isComplement]) { + $keyedForms[] = [$form, $isComplement, $this->exprPrinter->printExpr($form)]; + } + if ($complementForms === []) { + return [$keyedForms, null]; } - if ($orEqual) { - $forms[] = [new BinaryOp\Smaller($larger, $smaller), true]; - $forms[] = [new BinaryOp\Greater($smaller, $larger), true]; - } else { - $forms[] = [new BinaryOp\SmallerOrEqual($larger, $smaller), true]; - $forms[] = [new BinaryOp\GreaterOrEqual($smaller, $larger), true]; + $keyedWithComplements = $keyedForms; + foreach ($complementForms as [$form, $isComplement]) { + $keyedWithComplements[] = [$form, $isComplement, $this->exprPrinter->printExpr($form)]; } - return $forms; + return [$keyedForms, $keyedWithComplements]; } /** diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index f42af28f3f2..cdbdefba2ad 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -128,6 +128,17 @@ private static function canRemoveUnionAtOnce(Type $fromType): bool public static function doRemove(Type $fromType, Type $typeToRemove): Type { if ($typeToRemove instanceof UnionType) { + // Plain mixed minus a union is mixed with that union subtracted - + // peeling the members would rebuild the subtracted union once per + // member and end with the same one. + if ( + get_class($fromType) === MixedType::class + && $fromType->getSubtractedType() === null + && get_class($typeToRemove) === UnionType::class + ) { + return $fromType->subtract($typeToRemove); + } + // Removing the members one at a time is what keeps the result precise: // removing one can narrow the type so that removing the next achieves // more. A sealed hierarchy is the exception - there the subtraction is diff --git a/turbo-ext/src/BinaryOpHandler.cpp b/turbo-ext/src/BinaryOpHandler.cpp index be34bec1c95..7bfaba7d0fe 100644 --- a/turbo-ext/src/BinaryOpHandler.cpp +++ b/turbo-ext/src/BinaryOpHandler.cpp @@ -38,6 +38,7 @@ #include "support.h" #include "generated/BinaryOpHandler.h" #include "generated/TypeResult.h" +#include "generated/MutatingScope.h" namespace slots = ptdecl::BinaryOpHandler::slot; namespace sigs = ptdecl::BinaryOpHandler::sig; @@ -342,6 +343,7 @@ int isScalarOrNegatedScalar(zval *node) /* the twin's literals, permanent interned strings (module startup) */ zend_string *pt_boh_division_by_zero_error = nullptr; zend_string *pt_boh_countable = nullptr; +zend_string *pt_boh_comparison_forms = nullptr; /* $result->type of a TypeResult (the slot of the native class, the property * otherwise); UNDEF = pending exception */ @@ -655,57 +657,151 @@ class BinaryOpHandler return typeResultType(typeResult.raw()); } - /* one entry of getEquivalentComparisonForms(): the form node and whether - * it is the complement */ + /* one entry of getEquivalentComparisonForms(): the form node, whether it + * is the complement and its node key - borrowed from the cache array the + * comparison node keeps in its attributes (the caller holds it) */ struct ComparisonForm { - zv::Val node; + zval *node = NULL; bool isComplement = false; + zend_string *key = NULL; }; static constexpr int COMPARISON_FORMS_LIMIT = 6; - /* Mirrors getEquivalentComparisonForms(); count = -1 for a non-comparison - * ($forms === null). false = pending exception */ - template - static bool equivalentComparisonForms(int kind, zval *left, zval *right, GetType &&getType, ComparisonForm (&forms)[COMPARISON_FORMS_LIMIT], int &count) + /* Mirrors the COMPARISON_FORMS_ATTRIBUTE cache of + * getEquivalentComparisonForms(): [class, left, right, forms, + * complementForms]. Built and stored on a miss; UNDEF = pending + * exception */ + zv::Val comparisonFormsCache(zval *expr, int kind, zval *left, zval *right) const { - count = 0; - auto add = [&](int classIdx, zval *formLeft, zval *formRight, bool isComplement) -> bool { + zval *cached = pt_node_attribute(Z_OBJ_P(expr), pt_boh_comparison_forms); + if (cached != NULL) { + ZVAL_DEREF(cached); + if (Z_TYPE_P(cached) == IS_ARRAY) { + zval *cachedClass = zend_hash_index_find(Z_ARRVAL_P(cached), 0); + zval *cachedLeft = zend_hash_index_find(Z_ARRVAL_P(cached), 1); + zval *cachedRight = zend_hash_index_find(Z_ARRVAL_P(cached), 2); + if ( + cachedClass != NULL && Z_TYPE_P(cachedClass) == IS_STRING && zend_string_equals(Z_STR_P(cachedClass), Z_OBJCE_P(expr)->name) + && cachedLeft != NULL && Z_TYPE_P(cachedLeft) == IS_OBJECT && Z_OBJ_P(cachedLeft) == Z_OBJ_P(left) + && cachedRight != NULL && Z_TYPE_P(cachedRight) == IS_OBJECT && Z_OBJ_P(cachedRight) == Z_OBJ_P(right) + ) { + return zv::Val::copyOf(zv::Ref(cached)); + } + } + } + + zv::Val created = createComparisonForms(expr, kind, left, right); + if (UNEXPECTED(created.isUndef())) return zv::Val(); + zv::Arr cache = zv::Arr::create(5); + cache.push(zv::Val::string(Z_OBJCE_P(expr)->name)); + cache.push(zv::Ref(left)); + cache.push(zv::Ref(right)); + cache.push(zv::Ref(zend_hash_index_find(Z_ARRVAL_P(created.raw()), 0))); + cache.push(zv::Ref(zend_hash_index_find(Z_ARRVAL_P(created.raw()), 1))); + zv::Val cacheHold = zv::Val(std::move(cache)); + pt_node_set_attribute(Z_OBJ_P(expr), pt_boh_comparison_forms, cacheHold.raw()); + return cacheHold; + } + + /* Mirrors createComparisonForms(): [forms, forms with the complement + * forms appended or null], each entry [node, isComplement, nodeKey]; + * UNDEF = pending exception */ + zv::Val createComparisonForms(zval *expr, int kind, zval *left, zval *right) const + { + zval *exprPrinter = OBJ_PROP_NUM(self, slots::exprPrinter); + zv::Arr forms = zv::Arr::create(4); + zv::Arr complementForms = zv::Arr::create(2); + auto add = [&](zv::Arr &list, int classIdx, zval *formLeft, zval *formRight, bool isComplement) -> bool { zv::Args argv{formLeft, formRight}; zv::Val node = pt_type_new(classIdx, 2, argv); if (UNEXPECTED(node.isUndef())) return false; - forms[count].node = std::move(node); - forms[count].isComplement = isComplement; - count++; + // a comparison's node key is its printed form + zend_string *key = pt_expr_printer_print(exprPrinter, Z_OBJ_P(node.raw())); + if (UNEXPECTED(key == NULL)) return false; + zv::Arr entry = zv::Arr::create(3); + entry.push(std::move(node)); + entry.push(zv::Val::boolean(isComplement)); + entry.push(zv::Val::adoptString(key)); + list.push(zv::Val(std::move(entry))); return true; }; + bool built; if (kind == KIND_IDENTICAL || kind == KIND_NOT_IDENTICAL || kind == KIND_EQUAL || kind == KIND_NOT_EQUAL) { bool identity = kind == KIND_IDENTICAL || kind == KIND_NOT_IDENTICAL; bool isNegated = kind == KIND_NOT_IDENTICAL || kind == KIND_NOT_EQUAL; int sameClass = identity ? PT_CLASS_IDENTICAL_EXPR : PT_CLASS_EQUAL_EXPR; int negatedClass = identity ? PT_CLASS_BINARY_OP_NOT_IDENTICAL : PT_CLASS_NOT_EQUAL_EXPR; - return add(sameClass, left, right, isNegated) - && add(sameClass, right, left, isNegated) - && add(negatedClass, left, right, !isNegated) - && add(negatedClass, right, left, !isNegated); + built = add(forms, sameClass, left, right, isNegated) + && add(forms, sameClass, right, left, isNegated) + && add(forms, negatedClass, left, right, !isNegated) + && add(forms, negatedClass, right, left, !isNegated); + } else { + // normalized to `$smaller < $larger` / `$smaller <= $larger` + bool smallerFirst = kind == KIND_SMALLER || kind == KIND_SMALLER_OR_EQUAL; + zval *smaller = smallerFirst ? left : right; + zval *larger = smallerFirst ? right : left; + if (kind == KIND_SMALLER_OR_EQUAL || kind == KIND_GREATER_OR_EQUAL) { + built = add(forms, PT_CLASS_SMALLER_OR_EQUAL_EXPR, smaller, larger, false) + && add(forms, PT_CLASS_GREATER_OR_EQUAL_EXPR, larger, smaller, false) + && add(complementForms, PT_CLASS_SMALLER_EXPR, larger, smaller, true) + && add(complementForms, PT_CLASS_GREATER_EXPR, smaller, larger, true); + } else { + built = add(forms, PT_CLASS_SMALLER_EXPR, smaller, larger, false) + && add(forms, PT_CLASS_GREATER_EXPR, larger, smaller, false) + && add(complementForms, PT_CLASS_SMALLER_OR_EQUAL_EXPR, larger, smaller, true) + && add(complementForms, PT_CLASS_GREATER_OR_EQUAL_EXPR, smaller, larger, true); + } } + if (UNEXPECTED(!built)) return zv::Val(); - if (kind != KIND_SMALLER && kind != KIND_SMALLER_OR_EQUAL && kind != KIND_GREATER && kind != KIND_GREATER_OR_EQUAL) { + zv::Val formsHold = zv::Val(std::move(forms)); + zv::Arr result = zv::Arr::create(2); + result.push(zv::Ref(formsHold.raw())); + if (zend_hash_num_elements(Z_ARRVAL_P(complementForms.raw())) == 0) { + result.push(zv::Val::null()); + } else { + zv::Arr withComplements = zv::Arr::create(6); + for (zv::ArrayEntry entry : zv::ArrRef(formsHold.raw())) withComplements.push(entry.value()); + for (zv::ArrayEntry entry : zv::ArrRef(complementForms.raw())) withComplements.push(entry.value()); + result.push(zv::Val(std::move(withComplements))); + } + return zv::Val(std::move(result)); + } + + /* Mirrors getEquivalentComparisonForms(); count = -1 for a non-comparison + * ($forms === null). The forms borrow from hold. false = pending + * exception */ + template + bool equivalentComparisonForms(zval *expr, int kind, zval *left, zval *right, GetType &&getType, zv::Val &hold, ComparisonForm (&forms)[COMPARISON_FORMS_LIMIT], int &count) const + { + count = 0; + if ( + kind != KIND_IDENTICAL && kind != KIND_NOT_IDENTICAL && kind != KIND_EQUAL && kind != KIND_NOT_EQUAL + && kind != KIND_SMALLER && kind != KIND_SMALLER_OR_EQUAL && kind != KIND_GREATER && kind != KIND_GREATER_OR_EQUAL + ) { count = -1; return true; } - // normalized to `$smaller < $larger` / `$smaller <= $larger` - bool smallerFirst = kind == KIND_SMALLER || kind == KIND_SMALLER_OR_EQUAL; - zval *smaller = smallerFirst ? left : right; - zval *larger = smallerFirst ? right : left; - bool orEqual = kind == KIND_SMALLER_OR_EQUAL || kind == KIND_GREATER_OR_EQUAL; - if (orEqual) { - if (!add(PT_CLASS_SMALLER_OR_EQUAL_EXPR, smaller, larger, false) || !add(PT_CLASS_GREATER_OR_EQUAL_EXPR, larger, smaller, false)) return false; - } else { - if (!add(PT_CLASS_SMALLER_EXPR, smaller, larger, false) || !add(PT_CLASS_GREATER_EXPR, larger, smaller, false)) return false; + hold = comparisonFormsCache(expr, kind, left, right); + if (UNEXPECTED(hold.isUndef())) return false; + auto read = [&](zval *list) { + for (zv::ArrayEntry entry : zv::ArrRef(list)) { + zval *form = entry.value().deref().raw(); + forms[count].node = zend_hash_index_find(Z_ARRVAL_P(form), 0); + forms[count].isComplement = Z_TYPE_P(zend_hash_index_find(Z_ARRVAL_P(form), 1)) == IS_TRUE; + forms[count].key = Z_STR_P(zend_hash_index_find(Z_ARRVAL_P(form), 2)); + count++; + } + }; + zval *plainForms = zend_hash_index_find(Z_ARRVAL_P(hold.raw()), 3); + zval *withComplements = zend_hash_index_find(Z_ARRVAL_P(hold.raw()), 4); + if (Z_TYPE_P(withComplements) == IS_NULL) { + read(plainForms); + return true; } // `<` and `>=` only complement each other for comparable operands @@ -713,37 +809,61 @@ class BinaryOpHandler if (UNEXPECTED(leftType.isUndef())) return false; int leftIsInteger = trinaryYes(leftType.raw(), PT_OP_IS_INTEGER); if (UNEXPECTED(leftIsInteger < 0)) return false; - if (!leftIsInteger) return true; - zv::Val rightType = getType(right); - if (UNEXPECTED(rightType.isUndef())) return false; - int rightIsInteger = trinaryYes(rightType.raw(), PT_OP_IS_INTEGER); - if (UNEXPECTED(rightIsInteger < 0)) return false; - if (!rightIsInteger) return true; - - if (orEqual) { - return add(PT_CLASS_SMALLER_EXPR, larger, smaller, true) && add(PT_CLASS_GREATER_EXPR, smaller, larger, true); + if (leftIsInteger) { + zv::Val rightType = getType(right); + if (UNEXPECTED(rightType.isUndef())) return false; + int rightIsInteger = trinaryYes(rightType.raw(), PT_OP_IS_INTEGER); + if (UNEXPECTED(rightIsInteger < 0)) return false; + if (rightIsInteger) { + read(withComplements); + return true; + } } - return add(PT_CLASS_SMALLER_OR_EQUAL_EXPR, larger, smaller, true) && add(PT_CLASS_GREATER_OR_EQUAL_EXPR, smaller, larger, true); + read(plainForms); + return true; + } + + /* $scope->expressionTypes[$formKey] ?? null when its certainty is yes; + * NULL otherwise, and false = pending exception */ + static bool certainHolder(zval *scope, zend_string *formKey, zend_object *&holder) + { + holder = NULL; + zval *table; + zval rv; + if (EXPECTED(instanceof_function(Z_OBJCE_P(scope), pt_ce_mutating_scope))) { + table = OBJ_PROP_NUM(Z_OBJ_P(scope), ptdecl::MutatingScope::slot::expressionTypes); + } else { + table = zend_read_property(Z_OBJCE_P(scope), Z_OBJ_P(scope), PT_LC("expressionTypes"), 0, &rv); + if (UNEXPECTED(EG(exception) != NULL)) return false; + } + ZVAL_DEREF(table); + if (UNEXPECTED(Z_TYPE_P(table) != IS_ARRAY)) return true; + zval *found = zend_symtable_find(Z_ARRVAL_P(table), formKey); + if (found == NULL) return true; + ZVAL_DEREF(found); + if (UNEXPECTED(!pt_check_holder(found))) return false; + if (pt_holder_certainty_value(Z_OBJ_P(found)) == PT_TRI_YES) holder = Z_OBJ_P(found); + return true; } /* Mirrors resolveRememberedComparisonType(); found = false for null */ template - zv::Val resolveRememberedComparisonType(zval *scope, int kind, zval *left, zval *right, GetType &&getType, bool &found) const + zv::Val resolveRememberedComparisonType(zval *scope, zval *expr, int kind, zval *left, zval *right, GetType &&getType, bool &found) const { found = false; + zv::Val formsHold; ComparisonForm forms[COMPARISON_FORMS_LIMIT]; int count; - if (UNEXPECTED(!equivalentComparisonForms(kind, left, right, getType, forms, count))) return zv::Val(); + if (UNEXPECTED(!equivalentComparisonForms(expr, kind, left, right, getType, formsHold, forms, count))) return zv::Val(); for (int i = 0; i < count; i++) { - zend_long has = pt_mutating_scope_has_expression_type(Z_OBJ_P(scope), forms[i].node.raw()); - if (UNEXPECTED(has < 0)) return zv::Val(); - if (has != PT_TRI_YES) continue; + zend_object *holder; + if (UNEXPECTED(!certainHolder(scope, forms[i].key, holder))) return zv::Val(); + if (holder == NULL) continue; - zv::Val formType = pt_mutating_scope_get_tracked_expression_type(Z_OBJ_P(scope), Z_OBJ_P(forms[i].node.raw())); - if (UNEXPECTED(formType.isUndef())) return zv::Val(); + zval *formType = OBJ_PROP_NUM(holder, PT_ETH_PROP_TYPE); ptoh::BooleanOf formBoolean; - if (UNEXPECTED(!formBoolean.init(formType.raw()))) return zv::Val(); + if (UNEXPECTED(!formBoolean.init(formType))) return zv::Val(); int isTrue = formBoolean.isTrue(); if (UNEXPECTED(isTrue < 0)) return zv::Val(); if (isTrue) { @@ -780,9 +900,10 @@ class BinaryOpHandler if (UNEXPECTED(!pt_mutating_scope_native_types_promoted(Z_OBJ_P(scope), scopeNativeTypesPromoted))) return zv::Val(); return pt_expression_result_get_type_on_scope(e == left ? leftResult : rightResult, scope, scopeNativeTypesPromoted); }; + zv::Val formsHold; ComparisonForm forms[COMPARISON_FORMS_LIMIT]; int count; - if (UNEXPECTED(!equivalentComparisonForms(kind, left, right, getType, forms, count))) return zv::Val(); + if (UNEXPECTED(!equivalentComparisonForms(expr, kind, left, right, getType, formsHold, forms, count))) return zv::Val(); if (count < 0) return specifiedTypes; zval *exprPrinter = OBJ_PROP_NUM(self, slots::exprPrinter); @@ -792,16 +913,14 @@ class BinaryOpHandler zv::Arr sureTypes = zv::Arr::create(COMPARISON_FORMS_LIMIT); bool hasSureTypes = false; for (int i = 0; i < count; i++) { - zend_string *formKey = pt_expr_printer_print(exprPrinter, Z_OBJ_P(forms[i].node.raw())); - if (UNEXPECTED(formKey == NULL)) return zv::Val(); - zv::Str formKeyHold = zv::Str::adopt(formKey); + zend_string *formKey = forms[i].key; if (zend_string_equals(formKey, exprKey)) continue; - zend_long has = pt_mutating_scope_has_expression_type(Z_OBJ_P(scope), forms[i].node.raw()); - if (UNEXPECTED(has < 0)) return zv::Val(); - if (has != PT_TRI_YES) continue; + zend_object *holder; + if (UNEXPECTED(!certainHolder(scope, formKey, holder))) return zv::Val(); + if (holder == NULL) continue; zv::Arr entry = zv::Arr::create(2); - entry.push(zv::Ref(forms[i].node.raw())); + entry.push(zv::Ref(forms[i].node)); entry.push(ptoh::constantBoolean(contextTrue != forms[i].isComplement)); sureTypes.set(formKey, zv::Val(std::move(entry))); hasSureTypes = true; @@ -1063,7 +1182,7 @@ class BinaryOpHandler if (UNEXPECTED(right == NULL)) return zv::Val(); { bool found; - zv::Val rememberedType = resolveRememberedComparisonType(scope, kind, left, right, getType, found); + zv::Val rememberedType = resolveRememberedComparisonType(scope, expr, kind, left, right, getType, found); if (UNEXPECTED(EG(exception) != NULL)) return zv::Val(); if (found) return rememberedType; } @@ -1893,10 +2012,12 @@ PT_MINIT_REGISTRATION(pt_register_binary_op_handler) { pt_boh_division_by_zero_error = zend_string_init_interned(PT_LC("DivisionByZeroError"), 1); pt_boh_countable = zend_string_init_interned(PT_LC("Countable"), 1); + pt_boh_comparison_forms = zend_string_init_interned(PT_LC("phpstan_comparison_forms"), 1); reg::Class cls("PHPStan\\Analyser\\ExprHandler\\BinaryOpHandler"); ptdecl::BinaryOpHandler::declareClass(cls); ptdecl::BinaryOpHandler::declareProperties(cls); + cls.privateClassConstantString("COMPARISON_FORMS_ATTRIBUTE", "phpstan_comparison_forms"); /* the real parameter class names: the DI container autowires the * service by reflecting the constructor */ @@ -1954,7 +2075,7 @@ PT_MINIT_REGISTRATION(pt_register_binary_op_handler) if (UNEXPECTED(right == NULL)) RETURN_THROWS(); auto getType = [&](zval *e) { return pt_type_call_callable(getTypeCallable, 1, e); }; bool found; - zv::Val type = BinaryOpHandler(Z_OBJ_P(ZEND_THIS)).resolveRememberedComparisonType(scope, kind, left, right, getType, found); + zv::Val type = BinaryOpHandler(Z_OBJ_P(ZEND_THIS)).resolveRememberedComparisonType(scope, expr, kind, left, right, getType, found); if (UNEXPECTED(EG(exception) != NULL)) RETURN_THROWS(); if (!found) RETURN_NULL(); PT_RETURN_VAL(std::move(type)); @@ -1979,20 +2100,34 @@ PT_MINIT_REGISTRATION(pt_register_binary_op_handler) zval *right = ptoh::binaryOpRight(expr); if (UNEXPECTED(right == NULL)) RETURN_THROWS(); auto getType = [&](zval *e) { return pt_type_call_callable(getTypeCallable, 1, e); }; + zv::Val formsHold; BinaryOpHandler::ComparisonForm forms[BinaryOpHandler::COMPARISON_FORMS_LIMIT]; int count; - if (UNEXPECTED(!BinaryOpHandler::equivalentComparisonForms(kind, left, right, getType, forms, count))) RETURN_THROWS(); + if (UNEXPECTED(!BinaryOpHandler(Z_OBJ_P(ZEND_THIS)).equivalentComparisonForms(expr, kind, left, right, getType, formsHold, forms, count))) RETURN_THROWS(); if (count < 0) RETURN_NULL(); zv::Arr list = zv::Arr::create((uint32_t) count); for (int i = 0; i < count; i++) { - zv::Arr pair = zv::Arr::create(2); - pair.push(std::move(forms[i].node)); - pair.push(zv::Val::boolean(forms[i].isComplement)); - list.push(zv::Val(std::move(pair))); + zv::Arr entry = zv::Arr::create(3); + entry.push(zv::Ref(forms[i].node)); + entry.push(zv::Val::boolean(forms[i].isComplement)); + entry.push(zv::Val::string(forms[i].key)); + list.push(zv::Val(std::move(entry))); } PT_RETURN_VAL(zv::Val(std::move(list))); }); + cls.method(sigs::createComparisonForms, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *expr; + if (!zp::parse(execute_data, expr)) RETURN_THROWS(); + int kind = kindOf(expr); + if (UNEXPECTED(kind < 0)) RETURN_THROWS(); + zval *left = ptoh::binaryOpLeft(expr); + if (UNEXPECTED(left == NULL)) RETURN_THROWS(); + zval *right = ptoh::binaryOpRight(expr); + if (UNEXPECTED(right == NULL)) RETURN_THROWS(); + PT_RETURN_VAL(BinaryOpHandler(Z_OBJ_P(ZEND_THIS)).createComparisonForms(expr, kind, left, right)); + }); + cls.method(sigs::resolveEqualType, [](INTERNAL_FUNCTION_PARAMETERS) { zval *scope, *expr, *leftResult, *rightResult; if (!zp::parse(execute_data, scope, expr, leftResult, rightResult)) RETURN_THROWS(); diff --git a/turbo-ext/src/TypeCombinator.cpp b/turbo-ext/src/TypeCombinator.cpp index 0f5f69d4ce0..0fe45058a14 100644 --- a/turbo-ext/src/TypeCombinator.cpp +++ b/turbo-ext/src/TypeCombinator.cpp @@ -632,6 +632,15 @@ class TypeCombinator static zv::Val doRemove(zval *fromType, zval *typeToRemove) { if (isInstance(typeToRemove, pt_ce_union_type)) { + // Plain mixed minus a union is mixed with that union subtracted - + // peeling the members would rebuild the subtracted union once per + // member and end with the same one. + if (Z_OBJCE_P(fromType) == pt_ce_mixed_type && Z_OBJCE_P(typeToRemove) == pt_ce_union_type) { + zv::Val subtractedType = call(fromType, PT_LC("getsubtractedtype")); + PT_FAIL_IF_UNDEF(subtractedType); + if (isNull(subtractedType)) return call(fromType, PT_LC("subtract"), 1, typeToRemove); + } + int atOnce = canRemoveUnionAtOnce(fromType); PT_FAIL_IF_NEG(atOnce); if (atOnce) { diff --git a/turbo-ext/src/generated/BinaryOpHandler.h b/turbo-ext/src/generated/BinaryOpHandler.h index fc6feb1a7dc..14eafa17c7a 100644 --- a/turbo-ext/src/generated/BinaryOpHandler.h +++ b/turbo-ext/src/generated/BinaryOpHandler.h @@ -91,11 +91,12 @@ inline constexpr char strings[] = "PHPStan\\Analyser\\TypeSpecifierContext\0" /* 1092 */ "specifyRememberedComparisonForms\0" /* 1130 */ "getEquivalentComparisonForms\0" /* 1163 */ - "PhpParser\\Node\\Expr\\BinaryOp\\Equal\0" /* 1192 */ - "resolveEqualType\0" /* 1227 */ - "rootExpr\0" /* 1244 */ - "type\0" /* 1253 */ - "createRangeTypes"; /* 1258 */ + "createComparisonForms\0" /* 1192 */ + "PhpParser\\Node\\Expr\\BinaryOp\\Equal\0" /* 1214 */ + "resolveEqualType\0" /* 1249 */ + "rootExpr\0" /* 1266 */ + "type\0" /* 1275 */ + "createRangeTypes"; /* 1280 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 28), /* __construct $initializerExprTypeResolver */ reg::packed(75, 0, 100), /* __construct $richerScopeGetTypeHelper */ @@ -130,14 +131,16 @@ inline constexpr reg::PackedArg args[] = { reg::packed(634, 0, 935), /* getEquivalentComparisonForms $expr */ reg::packed(964, MAY_BE_CALLABLE), /* getEquivalentComparisonForms $getType */ reg::packed(659, MAY_BE_NULL | MAY_BE_ARRAY), /* getEquivalentComparisonForms return */ + reg::packed(634, 0, 935), /* createComparisonForms $expr */ + reg::packed(659, MAY_BE_ARRAY), /* createComparisonForms return */ reg::packed(747, 0, 753), /* resolveEqualType $scope */ - reg::packed(634, 0, 1192), /* resolveEqualType $expr */ + reg::packed(634, 0, 1214), /* resolveEqualType $expr */ reg::packed(1069, 0, 889), /* resolveEqualType $leftResult */ reg::packed(1080, 0, 889), /* resolveEqualType $rightResult */ reg::packed(659, 0, 972), /* resolveEqualType return */ - reg::packed(1244, MAY_BE_NULL, 639), /* createRangeTypes $rootExpr */ + reg::packed(1266, MAY_BE_NULL, 639), /* createRangeTypes $rootExpr */ reg::packed(634, 0, 639), /* createRangeTypes $expr */ - reg::packed(1253, 0, 972), /* createRangeTypes $type */ + reg::packed(1275, 0, 972), /* createRangeTypes $type */ reg::packed(846, 0, 1092), /* createRangeTypes $context */ reg::packed(659, 0, 1037), /* createRangeTypes return */ }; @@ -152,8 +155,9 @@ inline constexpr sigtab::Sig processExpr = { { 923 /* processExpr */, 7, 11, 7, inline constexpr sigtab::Sig resolveRememberedComparisonType = { { 990 /* resolveRememberedComparisonType */, 3, 19, 3, 22, ZEND_ACC_PRIVATE } }; inline constexpr sigtab::Sig specifyRememberedComparisonForms = { { 1130 /* specifyRememberedComparisonForms */, 6, 23, 6, 29, ZEND_ACC_PRIVATE } }; inline constexpr sigtab::Sig getEquivalentComparisonForms = { { 1163 /* getEquivalentComparisonForms */, 2, 30, 2, 32, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig resolveEqualType = { { 1227 /* resolveEqualType */, 4, 33, 4, 37, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig createRangeTypes = { { 1258 /* createRangeTypes */, 4, 38, 4, 42, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig createComparisonForms = { { 1192 /* createComparisonForms */, 1, 33, 1, 34, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig resolveEqualType = { { 1249 /* resolveEqualType */, 4, 35, 4, 39, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig createRangeTypes = { { 1280 /* createRangeTypes */, 4, 40, 4, 44, ZEND_ACC_PRIVATE } }; } // namespace sig } // namespace ptdecl::BinaryOpHandler