Skip to content
6 changes: 1 addition & 5 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
45 changes: 35 additions & 10 deletions src/Analyser/ScopeOps.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/Analyser/StmtHandler/DoWhileHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Analyser/StmtHandler/ForeachHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Analyser/StmtHandler/SwitchHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
2 changes: 1 addition & 1 deletion src/Analyser/StmtHandler/WhileHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Parser/RichParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down
32 changes: 14 additions & 18 deletions src/Turbo/TurboExtensionEnabler.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use function is_file;
use function json_decode;
use function phpversion;
use const DIRECTORY_SEPARATOR;

final class TurboExtensionEnabler
{
Expand All @@ -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;

Expand Down Expand Up @@ -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
Expand All @@ -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 . '/');
}

}
2 changes: 2 additions & 0 deletions src/Type/NeverType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,6 +24,7 @@
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;

/** @api */
#[ReferencedByTurboExtension(key: 'neverType')]
class NeverType implements CompoundType
{

Expand Down
42 changes: 17 additions & 25 deletions src/Type/Php/SubstrDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
24 changes: 24 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13833.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php declare(strict_types = 1);

namespace Bug13833;

use PHPStan\TrinaryLogic;
use function PHPStan\Testing\assertType;
use function PHPStan\Testing\assertVariableCertainty;

$a = (bool) rand(0,1);
$b = (bool) rand(0,1);

if ( $a || $b )
{
$msg = 'hello';
}

assertVariableCertainty(TrinaryLogic::createMaybe(), $msg);

if ( $a )
{
assertVariableCertainty(TrinaryLogic::createYes(), $msg);
assertType("'hello'", $msg);
echo $msg;
}
24 changes: 24 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14421.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php declare(strict_types = 1);

namespace Bug14421;

use function PHPStan\Testing\assertType;

/** @phpstan-impure */
function get_optional_int(): ?int {
return random_int(0, 1) ? 42 : null;
}

if (isset($_SESSION['a'])) {
$b = $_SESSION['a'];
}
else {
$b = get_optional_int();
}
if ($b !== null) {
assertType('array<mixed>', $_SESSION);
assertType('mixed~null', $b);
if (!isset($_SESSION['a'])) {
echo 'this is absolutely possible';
}
}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-5051.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public function testWithBooleans($data): void
assertType('false', $foo);
} else {
assertType('bool', $update);
assertType('bool', $foo);
assertType('true', $foo);
}

}
Expand Down
25 changes: 25 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-7706.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);

namespace Bug7706;

use PHPStan\TrinaryLogic;
use function PHPStan\Testing\assertVariableCertainty;

class HelloWorld
{
public function test(): void
{
$entity = null;
if (rand(0, 10) < 5) {
$entity = rand(0, 10) < 5 ? 1 : null;
$update = true;
}

if (!$entity) {
$update = false;
}

assertVariableCertainty(TrinaryLogic::createYes(), $update);
echo $update;
}
}
18 changes: 18 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-8360.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php declare(strict_types = 1);

namespace Bug8360Nsrt;

use PHPStan\TrinaryLogic;
use function PHPStan\Testing\assertVariableCertainty;

function logicalOr(bool $cond, bool $f): void
{
if ($cond || $f) {
$x = 1;
}

if ($cond && $f) {
assertVariableCertainty(TrinaryLogic::createYes(), $x);
echo $x;
}
}
Loading
Loading