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
38 changes: 13 additions & 25 deletions src/Analyser/ExprHandler/FuncCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\PropertyFetch;
Expand All @@ -19,6 +18,7 @@
use PHPStan\Analyser\ExpressionResult;
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\ExprHandler;
use PHPStan\Analyser\ExprHandler\Helper\OutputBufferHelper;
use PHPStan\Analyser\ExprHandler\Helper\VoidToNullTypeTransformer;
use PHPStan\Analyser\ImpurePoint;
use PHPStan\Analyser\InternalThrowPoint;
Expand Down Expand Up @@ -53,7 +53,6 @@
use PHPStan\Type\ClosureType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\ErrorType;
use PHPStan\Type\GeneralizePrecision;
use PHPStan\Type\Generic\TemplateTypeHelper;
Expand Down Expand Up @@ -572,29 +571,18 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$scope = $scope->afterOpenSslCall($functionReflection->getName());
}

if ($functionReflection !== null) {
$functionName = $functionReflection->getName();
if ($functionName === 'ob_start') {
$outputBufferDelta = 1;
} elseif (in_array($functionName, ['ob_get_clean', 'ob_get_flush', 'ob_end_clean', 'ob_end_flush'], true)) {
$outputBufferDelta = -1;
} else {
$outputBufferDelta = 0;
}
if ($outputBufferDelta !== 0) {
$obGetLevelCall = new FuncCall(new Name('ob_get_level'), []);
$scope = $scope->assignExpression(
$obGetLevelCall,
$scope->getType(new BinaryOp\Plus(
new TypeExpr($scope->getType($obGetLevelCall)),
new TypeExpr(new ConstantIntegerType($outputBufferDelta)),
)),
$scope->getType(new BinaryOp\Plus(
new TypeExpr($scope->getNativeType($obGetLevelCall)),
new TypeExpr(new ConstantIntegerType($outputBufferDelta)),
)),
);
}
$outputBufferDelta = $functionReflection !== null ? OutputBufferHelper::getLevelDelta($functionReflection->getName()) : 0;
if ($outputBufferDelta !== 0) {
$scope = OutputBufferHelper::applyLevelDelta($scope, $outputBufferDelta);
}

$pureCallable = $parametersAcceptor instanceof CallableParametersAcceptor
&& count($parametersAcceptor->getImpurePoints()) === 0;
if (
($functionReflection !== null && !$functionReflection->isBuiltin() && !$functionReflection->hasSideEffects()->no())
|| ($functionReflection === null && !$pureCallable)
) {
$scope = $scope->invalidateVolatileExpressions();
}

return new ExpressionResult(
Expand Down
54 changes: 54 additions & 0 deletions src/Analyser/ExprHandler/Helper/OutputBufferHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser\ExprHandler\Helper;

use PhpParser\Node\Expr\BinaryOp;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Node\Expr\TypeExpr;
use PHPStan\Type\Constant\ConstantIntegerType;
use function in_array;

final class OutputBufferHelper
{

private const LEVEL_INCREMENTING_FUNCTIONS = ['ob_start'];

private const LEVEL_DECREMENTING_FUNCTIONS = ['ob_get_clean', 'ob_get_flush', 'ob_end_clean', 'ob_end_flush'];

public static function getLevelDelta(string $functionName): int
{
if (in_array($functionName, self::LEVEL_INCREMENTING_FUNCTIONS, true)) {
return 1;
}

if (in_array($functionName, self::LEVEL_DECREMENTING_FUNCTIONS, true)) {
return -1;
}

return 0;
}

public static function applyLevelDelta(MutatingScope $scope, int $delta): MutatingScope
{
foreach ([new Name('ob_get_level'), new Name\FullyQualified('ob_get_level')] as $name) {
$obGetLevelCall = new FuncCall($name, []);

$scope = $scope->assignExpression(
$obGetLevelCall,
$scope->getType(new BinaryOp\Plus(
new TypeExpr($scope->getType($obGetLevelCall)),
new TypeExpr(new ConstantIntegerType($delta)),
)),
$scope->getType(new BinaryOp\Plus(
new TypeExpr($scope->getNativeType($obGetLevelCall)),
new TypeExpr(new ConstantIntegerType($delta)),
)),
);
}

return $scope;
}

}
7 changes: 7 additions & 0 deletions src/Analyser/ExprHandler/MethodCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,13 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$scope = $scope->invalidateExpression($normalizedExpr->var, true);
$throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr);
}
if (
$methodReflection === null
|| (!$methodReflection->getDeclaringClass()->isBuiltin() && !$methodReflection->hasSideEffects()->no())
) {
$scope = $scope->invalidateVolatileExpressions();
}

$hasYield = $hasYield || $argsResult->hasYield();
$throwPoints = array_merge($throwPoints, $argsResult->getThrowPoints());
$impurePoints = array_merge($impurePoints, $argsResult->getImpurePoints());
Expand Down
8 changes: 8 additions & 0 deletions src/Analyser/ExprHandler/NewHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,14 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr);
}

$calleeKnown = $classReflection !== null && !($isDynamic && !$classReflection->isFinal());
if (
($constructorReflection !== null && !$constructorReflection->getDeclaringClass()->isBuiltin() && !$constructorReflection->hasSideEffects()->no())
|| ($constructorReflection === null && !$calleeKnown)
) {
$scope = $scope->invalidateVolatileExpressions();
}

return new ExpressionResult(
$scope,
hasYield: $hasYield,
Expand Down
7 changes: 7 additions & 0 deletions src/Analyser/ExprHandler/StaticCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,13 @@
}
}

if (
$methodReflection === null
|| (!$methodReflection->getDeclaringClass()->isBuiltin() && !$methodReflection->hasSideEffects()->no())

Check warning on line 279 in src/Analyser/ExprHandler/StaticCallHandler.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ( $methodReflection === null - || (!$methodReflection->getDeclaringClass()->isBuiltin() && !$methodReflection->hasSideEffects()->no()) + || (!$methodReflection->getDeclaringClass()->isBuiltin() && $methodReflection->hasSideEffects()->yes()) ) { $scope = $scope->invalidateVolatileExpressions(); }

Check warning on line 279 in src/Analyser/ExprHandler/StaticCallHandler.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ( $methodReflection === null - || (!$methodReflection->getDeclaringClass()->isBuiltin() && !$methodReflection->hasSideEffects()->no()) + || (!$methodReflection->getDeclaringClass()->isBuiltin() && $methodReflection->hasSideEffects()->yes()) ) { $scope = $scope->invalidateVolatileExpressions(); }
) {
$scope = $scope->invalidateVolatileExpressions();
}

$hasYield = $hasYield || $argsResult->hasYield();
$throwPoints = array_merge($throwPoints, $argsResult->getThrowPoints());
$impurePoints = array_merge($impurePoints, $argsResult->getImpurePoints());
Expand Down
54 changes: 54 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,60 @@ public function afterOpenSslCall(string $openSslFunctionName): self
);
}

/**
* Forgets every tracked volatile global-state expression: argument-less
* function-call expressions whose value reflects mutable global/output-buffer
* state rather than just their arguments (ob_get_level(), openssl_error_string()).
* Any call to code PHPStan cannot inspect may change that state transitively, so
* these narrowings must be forgotten afterwards. These functions take no
* arguments, so the exact key lookups keep this O(1) in the common case where
* nothing is tracked.
*/
public function invalidateVolatileExpressions(): self
{
$expressionTypes = $this->expressionTypes;
$nativeExpressionTypes = $this->nativeExpressionTypes;

$changed = false;
foreach (['ob_get_level', 'openssl_error_string'] as $functionName) {
foreach ([$functionName . '()', '\\' . $functionName . '()'] as $exprString) {
if (
!array_key_exists($exprString, $expressionTypes)
&& !array_key_exists($exprString, $nativeExpressionTypes)
) {
continue;
}

unset($expressionTypes[$exprString]);
unset($nativeExpressionTypes[$exprString]);
$changed = true;
}
}

if (!$changed) {
return $this;
}

return $this->scopeFactory->create(
$this->context,
$this->isDeclareStrictTypes(),
$this->getFunction(),
$this->getNamespace(),
$expressionTypes,
$nativeExpressionTypes,
$this->conditionalExpressions,
$this->inClosureBindScopeClasses,
$this->anonymousFunctionReflection,
$this->isInFirstLevelStatement(),
$this->currentlyAssignedExpressions,
$this->currentlyAllowedUndefinedExpressions,
$this->inFunctionCallsStack,
$this->afterExtractCall,
$this->parentScope,
$this->nativeTypesPromoted,
);
}

/** @api */
public function hasVariableType(string $variableName): TrinaryLogic
{
Expand Down
15 changes: 15 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-7106.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,19 @@ public function openSslError(string $signature): string
assertNativeType('string|false', openssl_error_string());
}
}

public function impureCallForgetsOpenSslError(string $signature): void
{
if (false === \openssl_error_string()) {
assertType('false', openssl_error_string());
// the impure method may call openssl_*() transitively
$this->doImpure($signature);
assertType('string|false', openssl_error_string());
}
}

public function doImpure(string $signature): void
{
openssl_sign('1', $signature, '');
}
}
Loading
Loading