Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions src/Analyser/ExprHandler/IssetHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()));

Expand All @@ -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,
Expand All @@ -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()));
Expand All @@ -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
Expand Down
18 changes: 17 additions & 1 deletion src/Type/IntersectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions src/Type/ObjectType.php
Original file line number Diff line number Diff line change
Expand Up @@ -1523,9 +1523,12 @@

public function isOffsetAccessible(): TrinaryLogic
{
return $this->isInstanceOf(ArrayAccess::class)->or(
$this->isExtraOffsetAccessibleClass(),
);
$isArrayAccess = $this->isInstanceOf(ArrayAccess::class);
if ($isArrayAccess->yes()) {

Check warning on line 1527 in src/Type/ObjectType.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ public function isOffsetAccessible(): TrinaryLogic { $isArrayAccess = $this->isInstanceOf(ArrayAccess::class); - if ($isArrayAccess->yes()) { + if (!$isArrayAccess->no()) { return $isArrayAccess; }

Check warning on line 1527 in src/Type/ObjectType.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ public function isOffsetAccessible(): TrinaryLogic { $isArrayAccess = $this->isInstanceOf(ArrayAccess::class); - if ($isArrayAccess->yes()) { + if (!$isArrayAccess->no()) { return $isArrayAccess; }
return $isArrayAccess;
}

return $isArrayAccess->or($this->isExtraOffsetAccessibleClass());
}

public function isOffsetAccessLegal(): TrinaryLogic
Expand Down
34 changes: 33 additions & 1 deletion src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
26 changes: 22 additions & 4 deletions turbo-ext/src/IntersectionType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 5 additions & 4 deletions turbo-ext/src/IssetHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions turbo-ext/src/ObjectType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
42 changes: 40 additions & 2 deletions turbo-ext/src/TypeCombinator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
Expand Down
Loading