diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index e86cdf715f4..10f6012453c 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3175,7 +3175,7 @@ public function processArgs( $gatheredArgTypeByIndex[$i] = $exprResult->getType(); $this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArg, $i, $gatheredArgTypeByIndex[$i]); $templateArgumentFrame = $this->observingTemplateArgumentFrame($scope); - if ($templateArgumentFrame !== null && $parameter !== null && $argMetadataAcceptor !== null) { + if ($templateArgumentFrame !== null && $parameter !== null) { // the metadata acceptor is resolved against the arguments gathered // before this one, so a template this argument itself decides is // still its bound there - observe the declared parameter type, @@ -3262,10 +3262,6 @@ public function processArgs( } if ($assignByReference) { - if ($currentParameter === null) { - throw new ShouldNotHappenException(); - } - $argValue = $arg->value; if (!$argValue instanceof Variable || $argValue->name !== 'this') { $paramOutType = $this->getParameterOutExtensionsType($callLike, $calleeReflection, $currentParameter, $scope); diff --git a/src/Analyser/ScopeOps.php b/src/Analyser/ScopeOps.php index d38e0e5cf74..ed77b3baa95 100644 --- a/src/Analyser/ScopeOps.php +++ b/src/Analyser/ScopeOps.php @@ -22,7 +22,9 @@ use PHPStan\TrinaryLogic; use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\ErrorType; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function array_filter; use function array_key_exists; use function array_key_first; @@ -377,10 +379,10 @@ public static function createConditionalExpressions( $newVariableTypes = $ourExpressionTypes; // When our-branch type is a subtype of their-branch type, the union - // absorbs it (merged === their). Such a variable is a poor *guard* — - // asserting its our-branch type later wouldn't reliably select this - // branch — but it remains a valid conditional *target*, so only exclude - // it from guard selection instead of dropping it entirely. + // absorbs it (merged === their). Such a variable cannot be a *guard* — + // its branch set difference below comes out empty — but it remains a + // valid conditional *target*; the flag also lets the target loop skip + // pairing these absorbed targets with constant-array guards. $guardsToExclude = []; foreach (array_keys($differingKeys) as $exprString) { if (!array_key_exists($exprString, $theirExpressionTypes)) { @@ -425,18 +427,41 @@ public static function createConditionalExpressions( continue; } - if ( - array_key_exists($exprString, $theirExpressionTypes) - && !$theirExpressionTypes[$exprString]->getCertainty()->yes() - ) { + if (!array_key_exists($exprString, $theirExpressionTypes)) { + // with no their-branch entry the merged holder keeps our type with + // lowered certainty, so no later type assertion can tell the + // branches apart + continue; + } + $theirHolder = $theirExpressionTypes[$exprString]; + if (!$theirHolder->getCertainty()->yes()) { + continue; + } + if ($holder->equalTypes($theirHolder)) { continue; } - if ($mergedExpressionTypes[$exprString]->equalTypes($holder)) { + // The set difference between the branch types is the part of our type + // the other branch cannot produce: observing it later proves this + // branch was taken, even when the full branch types overlap - so a + // representable remainder makes a sound guard where the full type + // would not (the full our-branch type may even equal the merged + // type). When the subtraction is not representable, remove() keeps + // our full type and the merged-type comparison below restores the + // long-standing behavior for such guards. + $remainder = TypeCombinator::remove($holder->getType(), $theirHolder->getType()); + if ($remainder instanceof NeverType) { + continue; + } + if ($mergedExpressionTypes[$exprString]->getType()->equals($remainder)) { + // matching this guard later would not discriminate the branches - + // the merged scope already guarantees it continue; } - $typeGuards[$exprString] = $holder; + $typeGuards[$exprString] = $remainder === $holder->getType() + ? $holder + : ExpressionTypeHolder::createYes($holder->getExpr(), $remainder); } if (count($typeGuards) === 0) { diff --git a/src/Analyser/StmtHandler/DoWhileHandler.php b/src/Analyser/StmtHandler/DoWhileHandler.php index 82a10c887eb..b70ec50cc6d 100644 --- a/src/Analyser/StmtHandler/DoWhileHandler.php +++ b/src/Analyser/StmtHandler/DoWhileHandler.php @@ -108,7 +108,7 @@ public function processStmt( $storage = $originalStorage; if ( $replayBodyRecording !== null && $replayPassStorage !== null && $replayPassResult !== null - && $prevEntryScope !== null && $bodyScope->equals($prevEntryScope) + && $bodyScope->equals($prevEntryScope) ) { // the final body walk would repeat the recorded fixpoint pass exactly // (same entry scope, deterministic walk) - adopt the pass's results diff --git a/src/Analyser/StmtHandler/ForeachHandler.php b/src/Analyser/StmtHandler/ForeachHandler.php index 071b11a1aa5..44b7efd0597 100644 --- a/src/Analyser/StmtHandler/ForeachHandler.php +++ b/src/Analyser/StmtHandler/ForeachHandler.php @@ -266,7 +266,7 @@ static function () use ($condResult, $emptyArrayType): Type { if ( $replayBodyRecording !== null && $replayPassStorage !== null && $replayPassResult !== null && $replayEntryScope !== null - && $unrolledTotalKeys === null && $finalEntryScope->equals($replayEntryScope) + && $finalEntryScope->equals($replayEntryScope) ) { // the final walk would repeat the recorded fixpoint pass exactly // (same entry scope, deterministic walk) - adopt the pass's results diff --git a/src/Analyser/StmtHandler/SwitchHandler.php b/src/Analyser/StmtHandler/SwitchHandler.php index 4f94cce08cc..0ed2e117143 100644 --- a/src/Analyser/StmtHandler/SwitchHandler.php +++ b/src/Analyser/StmtHandler/SwitchHandler.php @@ -164,7 +164,7 @@ public function processStmt( $alwaysTerminating = false; } - if ($prevScope !== null && isset($branchFinalScopeResult)) { + if ($prevScope !== null) { $finalScope = $prevScope->mergeWith($finalScope); $alwaysTerminating = $alwaysTerminating && $branchFinalScopeResult->isAlwaysTerminating(); } diff --git a/src/Analyser/StmtHandler/WhileHandler.php b/src/Analyser/StmtHandler/WhileHandler.php index ec0eeeedd51..0dd656935e1 100644 --- a/src/Analyser/StmtHandler/WhileHandler.php +++ b/src/Analyser/StmtHandler/WhileHandler.php @@ -134,7 +134,7 @@ public function processStmt( $replayCondRecording !== null && $replayBodyRecording !== null && $replayPassStorage !== null && $replayPassResult !== null && $replayCondResult !== null - && $prevEntryScope !== null && $bodyScope->equals($prevEntryScope) + && $bodyScope->equals($prevEntryScope) ) { // the final walk would repeat the recorded fixpoint pass exactly // (same entry scope, deterministic walk) - adopt the pass's results diff --git a/src/Parser/RichParser.php b/src/Parser/RichParser.php index 75adc400405..89a4e91f871 100644 --- a/src/Parser/RichParser.php +++ b/src/Parser/RichParser.php @@ -360,7 +360,7 @@ private function parseIdentifiers(string $text, int $ignorePos): array } if ($openParenthesisCount > 0) { - throw new IgnoreParseException('Unexpected end, unclosed opening parenthesis', $tokenLine ?? 1); + throw new IgnoreParseException('Unexpected end, unclosed opening parenthesis', $tokenLine); } if (count($identifiers) === 0) { diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php index 82b887361ec..3dda469450c 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php @@ -195,7 +195,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): return null; } - [$reflectionCacheKey, $variableCacheKey] = $this->getCacheKeys($file, $identifier); // @phpstan-ignore variable.undefined + [$reflectionCacheKey, $variableCacheKey] = $this->getCacheKeys($file, $identifier); $functionReflection = $this->nodeToReflection($reflector, $fetchedFunctionNode); $this->cache->save($reflectionCacheKey, $variableCacheKey, $functionReflection->exportToCache()); diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index 2c6ffe08164..5b4c21119a1 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -12,7 +12,6 @@ use function is_file; use function json_decode; use function phpversion; -use const DIRECTORY_SEPARATOR; final class TurboExtensionEnabler { @@ -23,7 +22,7 @@ final class TurboExtensionEnabler * version is the short SHA of the last commit touching turbo-ext/src/, * enforced by the phar.yml turbo-version job. */ - public const EXPECTED_EXTENSION_VERSION = 'b1c223b'; + public const EXPECTED_EXTENSION_VERSION = 'f010107'; private static bool $typeCombinatorCacheEnabled = false; @@ -178,13 +177,12 @@ public static function isTrustingOwnTypes(): bool * the engine's run-time checks of its parameter and return types re-check * what analysis already proved — at about 8% of the analysis CPU: a * class-typed parameter costs a class lookup and an instanceof on every - * call, a typed return the same on the way out. With the extension - * active, its optimizer pass (TrustedTypes.cpp) drops those checks from - * the code compiled out of the running phar — or out of the source - * checkout bin/phpstan runs from. Nothing else is touched: extensions, - * bootstrap files and the analysed project keep their checks, including - * on what they receive from PHPStan and return to it — a check sits in - * the callee. + * call, a typed return the same on the way out. With the extension active + * and PHPStan running from a phar, its optimizer pass (TrustedTypes.cpp) + * drops those checks from the code compiled out of the phar. Nothing else + * is touched: extensions, bootstrap files and the analysed project keep + * their checks, including on what they receive from PHPStan and return to + * it — a check sits in the callee. * * What is lost is the TypeError at the boundary when such code passes a * wrong value into PHPStan: it surfaces later, deeper. That is why --debug @@ -206,17 +204,15 @@ public static function trustOwnTypesIfSuitable(array $argv): void if (in_array('--debug', $argv, true)) { return; } - $pharPath = class_exists('Phar', false) ? Phar::running(false) : ''; - if ($pharPath !== '') { - $prefix = 'phar://' . $pharPath . '/'; - } else { - // bin/phpstan of a source checkout: src/, vendor/ and build/ of the - // checkout, the same code the phar would hold (compiled filenames - // are resolved paths, as __DIR__ is) - $prefix = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR; + if (!class_exists('Phar', false)) { + return; + } + $pharPath = Phar::running(false); + if ($pharPath === '') { + return; } - self::$trustingOwnTypes = Runtime::trustTypesUnder($prefix); + self::$trustingOwnTypes = Runtime::trustTypesUnder('phar://' . $pharPath . '/'); } } diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index 37f5dfc4b61..fc1e8a0caf7 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -14,6 +14,7 @@ use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; @@ -23,6 +24,7 @@ use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; /** @api */ +#[ReferencedByTurboExtension(key: 'neverType')] class NeverType implements CompoundType { diff --git a/src/Type/Php/SubstrDynamicReturnTypeExtension.php b/src/Type/Php/SubstrDynamicReturnTypeExtension.php index 0c4f8587ab2..be4ec43a315 100644 --- a/src/Type/Php/SubstrDynamicReturnTypeExtension.php +++ b/src/Type/Php/SubstrDynamicReturnTypeExtension.php @@ -23,7 +23,6 @@ use PHPStan\Type\UnionType; use function count; use function in_array; -use function is_bool; use function mb_substr; use function strlen; use function substr; @@ -75,32 +74,25 @@ public function getTypeFromFunctionCall( ) { $results = []; foreach ($constantStrings as $constantString) { - if ($length !== null) { - if ($functionReflection->getName() === 'mb_substr') { - $substr = mb_substr($constantString->getValue(), $offset->getValue(), $length->getValue()); - } elseif ($this->phpVersion->substrReturnFalseInsteadOfEmptyString()) { - $substr = $this->substrOrFalse($constantString->getValue(), $offset->getValue(), $length->getValue()); - } else { - $substr = substr($constantString->getValue(), $offset->getValue(), $length->getValue()); - } - } else { - if ($functionReflection->getName() === 'mb_substr') { - $substr = mb_substr($constantString->getValue(), $offset->getValue()); - } elseif ($this->phpVersion->substrReturnFalseInsteadOfEmptyString()) { - // Simulate substr call on an older PHP version if the runtime one is too new. - $substr = $this->substrOrFalse($constantString->getValue(), $offset->getValue()); - } else { - $substr = substr($constantString->getValue(), $offset->getValue()); - } + if ($functionReflection->getName() === 'mb_substr') { + $substr = $length !== null + ? mb_substr($constantString->getValue(), $offset->getValue(), $length->getValue()) + : mb_substr($constantString->getValue(), $offset->getValue()); + $results[] = new ConstantStringType($substr); + continue; } - if (is_bool($substr)) { - if ($this->phpVersion->substrReturnFalseInsteadOfEmptyString()) { - $results[] = new ConstantBooleanType($substr); - } else { - // Simulate substr call on a recent PHP version if the runtime one is too old. - $results[] = new ConstantStringType(''); - } + // substrOrFalse() detects an out-of-range offset with its own length + // check, so the result does not depend on the runtime PHP version's + // substr() semantics. false is then mapped to the analysed version's + // result: false on PHP < 8, an empty string on PHP >= 8. + $substr = $length !== null + ? $this->substrOrFalse($constantString->getValue(), $offset->getValue(), $length->getValue()) + : $this->substrOrFalse($constantString->getValue(), $offset->getValue()); + if ($substr === false) { + $results[] = $this->phpVersion->substrReturnFalseInsteadOfEmptyString() + ? new ConstantBooleanType(false) + : new ConstantStringType(''); } else { $results[] = new ConstantStringType($substr); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-13833.php b/tests/PHPStan/Analyser/nsrt/bug-13833.php new file mode 100644 index 00000000000..ab2a27c72dd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13833.php @@ -0,0 +1,24 @@ +', $_SESSION); + assertType('mixed~null', $b); + if (!isset($_SESSION['a'])) { + echo 'this is absolutely possible'; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-5051.php b/tests/PHPStan/Analyser/nsrt/bug-5051.php index a91a87716bd..3e1c4f0b2f8 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-5051.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5051.php @@ -92,7 +92,7 @@ public function testWithBooleans($data): void assertType('false', $foo); } else { assertType('bool', $update); - assertType('bool', $foo); + assertType('true', $foo); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-7706.php b/tests/PHPStan/Analyser/nsrt/bug-7706.php new file mode 100644 index 00000000000..a051831393d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7706.php @@ -0,0 +1,25 @@ +format('Y'); + } + + return 0; + } + + protected function test(string $filter): int + { + $all = false; + if ($filter === 'all') { + $date = new \DateTime(); + $all = true; + + if (mt_rand() === 0) { // all other code expect this condition is the same as in self::working() + $all = false; + } + } + + if ($all) { + assertVariableCertainty(TrinaryLogic::createYes(), $date); + return (int) $date->format('Y'); + } + + return 0; + } +} diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 5b5345235f0..8a874eb39a6 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -174,6 +174,10 @@ public function testTypesAssignedToPropertiesExpressionNames(): void 'Property PropertiesFromArrayIntoObject\Foo::$lall (int) does not accept string.', 69, ], + [ + 'Property PropertiesFromArrayIntoObject\Foo::$foo (string) does not accept float.', + 83, + ], [ 'Property PropertiesFromArrayIntoObject\Foo::$foo (string) does not accept float|int|string.', 97, diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 397dabed083..3f79e8c5dd1 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1723,4 +1723,40 @@ public function testBug2032(): void ]); } + public function testBug13833(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-13833.php'], []); + } + + public function testBug9685(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-9685.php'], []); + } + + public function testBug7706(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-7706.php'], []); + } + + public function testBug8360(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-8360.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index 61a9e57ed0f..836549ee6b4 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -671,4 +671,11 @@ public function testBug14416(): void $this->analyse([__DIR__ . '/data/bug-14416.php'], []); } + public function testBug14421(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-14421.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-13833.php b/tests/PHPStan/Rules/Variables/data/bug-13833.php new file mode 100644 index 00000000000..199dd53d22c --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-13833.php @@ -0,0 +1,16 @@ +format('Y'); + } + + return 0; + } + + protected function test(string $filter): int + { + $all = false; + if ($filter === 'all') { + $date = new \DateTime(); + $all = true; + + if (mt_rand() === 0) { // all other code expect this condition is the same as in self::working() + $all = false; + } + } + + if ($all) { + return (int) $date->format('Y'); + } + + return 0; + } +} diff --git a/turbo-ext/src/ScopeOps.cpp b/turbo-ext/src/ScopeOps.cpp index 23f8c77c01a..8f45bb469dd 100644 --- a/turbo-ext/src/ScopeOps.cpp +++ b/turbo-ext/src/ScopeOps.cpp @@ -471,17 +471,22 @@ class ScopeOps static zv::Val createConditionalExpressions(zv::TableRef conditional, zv::TableRef ours, zv::TableRef theirs, zv::TableRef merged, zv::TableRef differingKeys) { zend_class_entry *virtualNodeCe = pt_class(PT_CLASS_VIRTUAL_NODE); - if (UNEXPECTED(virtualNodeCe == NULL)) { + zend_class_entry *neverTypeCe = pt_class(PT_CLASS_NEVER_TYPE); + if (UNEXPECTED(virtualNodeCe == NULL || neverTypeCe == NULL)) { return zv::Val(); } zv::ScratchTable guardsToExclude(8); zv::ScratchTable typeGuards(8); - - /* guardsToExclude: subtype-absorbed their-branch variables are poor - * guards but stay valid conditional targets. Only the merge's differing - * keys can qualify — iterate those (in their insertion order, like the - * twin) instead of the whole holder maps. */ + /* owns the remainder-typed holders created below; the scratch table + * only borrows them */ + zv::Arr createdGuardHolders; + + /* guardsToExclude: subtype-absorbed their-branch variables cannot be + * guards (their branch set difference is empty) but stay valid + * conditional targets. Only the merge's differing keys can qualify — + * iterate those (in their insertion order, like the twin) instead of + * the whole holder maps. */ for (auto diffEntry : differingKeys) { zend_string *key = diffEntry.stringKeyOrNull(); zend_ulong idx = diffEntry.indexKey(); @@ -561,33 +566,68 @@ class ScopeOps continue; } zval *theirSlot = pt_ht_find(theirs.table(), key, idx); - if (theirSlot != NULL) { - zv::Ref theirHolder = zv::Ref(theirSlot).deref(); - if (UNEXPECTED(!pt_check_holder(theirHolder.raw()))) { - return zv::Val(); - } - if (pt_holder_certainty_value(theirHolder.asObject()) != PT_TRI_YES) { - continue; - } + if (theirSlot == NULL) { + /* with no their-branch entry the merged holder keeps our type + * with lowered certainty, so no later type assertion can tell + * the branches apart */ + continue; + } + zv::Ref theirHolder = zv::Ref(theirSlot).deref(); + if (UNEXPECTED(!pt_check_holder(theirHolder.raw()))) { + return zv::Val(); + } + if (pt_holder_certainty_value(theirHolder.asObject()) != PT_TRI_YES) { + continue; } bool equalTypes; + if (UNEXPECTED(!pt_holder_equal_types(holder.raw(), theirHolder.raw(), &equalTypes))) { + return zv::Val(); + } + if (equalTypes) { + continue; + } + + /* the branch set difference — see the twin for why an unchanged + * remainder falls back to the merged-type comparison */ + zv::Val remainder = typeCombinatorRemove(holderType(holder), holderType(theirHolder)); + if (UNEXPECTED(remainder.isUndef())) { + return zv::Val(); + } + if (remainder.ref().instanceOf(neverTypeCe)) { + continue; + } { zv::Ref mergedHolder = zv::Ref(mergedSlot).deref(); if (UNEXPECTED(!pt_check_holder(mergedHolder.raw()))) { return zv::Val(); } - if (UNEXPECTED(!pt_holder_equal_types(mergedHolder.raw(), holder.raw(), &equalTypes))) { + bool mergedEqualsRemainder = pt_types_identical_or_equal(holderType(mergedHolder), remainder.raw()); + if (UNEXPECTED(EG(exception))) { return zv::Val(); } - } - if (equalTypes) { - continue; + if (mergedEqualsRemainder) { + continue; + } } - /* borrowed entry — the scratch table has no destructor */ - zval borrowed; - ZVAL_COPY_VALUE(&borrowed, holder.raw()); - pt_ht_update(typeGuards.table(), key, idx, &borrowed); + if (Z_OBJ_P(remainder.raw()) == Z_OBJ_P(holderType(holder))) { + /* borrowed entry — the scratch table has no destructor */ + zval borrowed; + ZVAL_COPY_VALUE(&borrowed, holder.raw()); + pt_ht_update(typeGuards.table(), key, idx, &borrowed); + } else { + /* ExpressionTypeHolder::createYes($holder->expr, $remainder) — + * owned by createdGuardHolders, borrowed by the scratch table */ + zval created; + pt_holder_create(&created, zv::ObjRef(holder.asObject()).propAt(PT_ETH_PROP_EXPR).raw(), remainder.raw(), PT_TRI_YES); + zval borrowed; + ZVAL_COPY_VALUE(&borrowed, &created); + if (createdGuardHolders.isUndef()) { + createdGuardHolders = zv::Arr::create(4); + } + createdGuardHolders.push(zv::Val::adopt(created)); + pt_ht_update(typeGuards.table(), key, idx, &borrowed); + } } if (typeGuards.size() == 0) { @@ -1481,6 +1521,21 @@ class ScopeOps return !EG(exception); } + /* TypeCombinator::remove($fromType, $typeToRemove) */ + static zv::Val typeCombinatorRemove(zval *fromType, zval *typeToRemove) + { + zval retval; + if (UNEXPECTED(!pt_type_combinator_binary("remove", sizeof("remove") - 1, fromType, typeToRemove, &retval))) { + return zv::Val(); + } + zv::Val result = zv::Val::adopt(retval); + if (UNEXPECTED(!result.ref().isObject())) { + zend_throw_error(NULL, "phpstan_turbo: TypeCombinator::remove did not return an object"); + return zv::Val(); + } + return result; + } + /* $type->isSuperTypeOf($otherType)->result->value */ static bool isSuperTypeOfValue(zval *type, zval *otherType, zend_long *out) { diff --git a/turbo-ext/src/support.cpp b/turbo-ext/src/support.cpp index 069102d2bf0..bba42e212a9 100644 --- a/turbo-ext/src/support.cpp +++ b/turbo-ext/src/support.cpp @@ -51,6 +51,7 @@ static const pt_class_template pt_class_templates[PT_CLASS_COUNT] = { /* PT_CLASS_ARROW_FUNCTION */ {"arrowFunction", "PhpParser\\Node\\Expr\\ArrowFunction"}, /* PT_CLASS_TYPE */ {"type", "PHPStan\\Type\\Type"}, /* PT_CLASS_RECURSION_GUARD */ {"recursionGuard", "PHPStan\\Type\\RecursionGuard"}, + /* PT_CLASS_NEVER_TYPE */ {"neverType", "PHPStan\\Type\\NeverType"}, /* PT_CLASS_TRINARY */ {"trinaryLogic", NULL}, /* PT_CLASS_ETH */ {"expressionTypeHolder", NULL}, /* PT_CLASS_CEH */ {"conditionalExpressionHolder", NULL}, diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index ae0c1295cee..4b7c4b406fd 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -83,6 +83,7 @@ enum { PT_CLASS_ARROW_FUNCTION, PT_CLASS_TYPE, PT_CLASS_RECURSION_GUARD, + PT_CLASS_NEVER_TYPE, /* classes the extension instantiates (their PHP twins are themselves * shadowed, hence no default name): configured to the stub subclasses * so created objects satisfy the original PHPStan type hints */