Skip to content
Open
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
87 changes: 87 additions & 0 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\Php\VersionCompareHelper;
use PHPStan\Type\ResourceType;
use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
use PHPStan\Type\StaticType;
Expand All @@ -81,6 +82,7 @@
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\UnionType;
use function array_filter;
use function array_key_exists;
use function array_key_first;
use function array_keys;
Expand All @@ -89,6 +91,7 @@
use function array_merge;
use function array_reverse;
use function array_shift;
use function array_values;
use function count;
use function in_array;
use function is_string;
Expand Down Expand Up @@ -543,6 +546,17 @@ public function specifyTypesInCondition(
}
}

// version_compare(PHP_VERSION, 'x.y') [<|<=] N or N [<|<=] version_compare(PHP_VERSION, 'x.y')
$versionCompareResult = $this->specifyTypesForVersionCompare2Arg($expr->left, $expr->right, $orEqual, $scope, $context, $expr);
if ($versionCompareResult !== null) {
$result = $result->unionWith($versionCompareResult);
} else {
$versionCompareResult = $this->specifyTypesForVersionCompare2Arg($expr->right, $expr->left, $orEqual, $scope, $context, $expr, false);
if ($versionCompareResult !== null) {
$result = $result->unionWith($versionCompareResult);
}
}

return $result;

} elseif ($expr instanceof Node\Expr\BinaryOp\Greater) {
Expand Down Expand Up @@ -3091,6 +3105,29 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
)->setRootExpr($expr);
}

// version_compare(PHP_VERSION, 'x.y') === N
if (
!$context->null()
&& $unwrappedLeftExpr instanceof FuncCall
&& !$unwrappedLeftExpr->isFirstClassCallable()
&& count($unwrappedLeftExpr->getArgs()) === 2
&& $rightType instanceof ConstantIntegerType
&& in_array($rightType->getValue(), [-1, 0, 1], true)
) {
$parsed = VersionCompareHelper::parseVersionCompareFuncCall($unwrappedLeftExpr, $scope);
if ($parsed !== null) {
[$phpVersionArgIndex, $versionId] = $parsed;
$syntheticExpr = VersionCompareHelper::resultSetToPhpVersionIdComparison(
[$rightType->getValue()],
$phpVersionArgIndex,
$versionId,
);
if ($syntheticExpr !== null) {
return $this->specifyTypesInCondition($scope, $syntheticExpr, $context)->setRootExpr($expr);
}
}
}

// get_class($a) === 'Foo'
if (
$context->true()
Expand Down Expand Up @@ -3383,4 +3420,54 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
return (new SpecifiedTypes([], []))->setRootExpr($expr);
}

private function specifyTypesForVersionCompare2Arg(
Expr $funcCallSide,
Expr $constantSide,
bool $orEqual,
Scope $scope,
TypeSpecifierContext $context,
Expr $rootExpr,
bool $funcCallOnLeft = true,
): ?SpecifiedTypes
{
if (!$funcCallSide instanceof FuncCall || $funcCallSide->isFirstClassCallable() || count($funcCallSide->getArgs()) !== 2) {
return null;
}

$constantType = $scope->getType($constantSide);
if (!$constantType instanceof ConstantIntegerType) {
return null;
}

$parsed = VersionCompareHelper::parseVersionCompareFuncCall($funcCallSide, $scope);
if ($parsed === null) {
return null;
}

[$phpVersionArgIndex, $versionId] = $parsed;

$n = $constantType->getValue();
$possibleResults = [-1, 0, 1];

if ($funcCallOnLeft) {
// funcCall [<|<=] N
$resultSet = array_values(array_filter($possibleResults, static fn (int $r): bool => $orEqual ? $r <= $n : $r < $n));
} else {
// N [<|<=] funcCall
$resultSet = array_values(array_filter($possibleResults, static fn (int $r): bool => $orEqual ? $n <= $r : $n < $r));
}

$syntheticExpr = VersionCompareHelper::resultSetToPhpVersionIdComparison(
$resultSet,
$phpVersionArgIndex,
$versionId,
);

if ($syntheticExpr === null) {
return null;
}

return $this->specifyTypesInCondition($scope, $syntheticExpr, $context)->setRootExpr($rootExpr);
}

}
93 changes: 83 additions & 10 deletions src/Parser/RemoveUnusedCodeByPhpVersionIdVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
use PHPStan\Php\PhpVersion;
use PHPStan\Type\Php\VersionCompareHelper;
use function count;
use function in_array;
use function strtolower;
use function version_compare;

final class RemoveUnusedCodeByPhpVersionIdVisitor extends NodeVisitorAbstract
Expand All @@ -32,6 +35,31 @@ public function enterNode(Node $node): ?Node
}

$cond = $node->cond;

$result = $this->evaluateVersionCompareCall($cond);
if ($result === null) {
$result = $this->evaluatePhpVersionIdComparison($cond);
}
if ($result === null) {
return null;
}
if ($result) {
// remove else
$node->cond = new Node\Expr\ConstFetch(new Node\Name('true'));
$node->else = null;

return $node;
}

// remove if
$node->cond = new Node\Expr\ConstFetch(new Node\Name('false'));
$node->stmts = [];

return $node;
}

private function evaluatePhpVersionIdComparison(Node\Expr $cond): ?bool
{
if (
!$cond instanceof Node\Expr\BinaryOp\Smaller
&& !$cond instanceof Node\Expr\BinaryOp\SmallerOrEqual
Expand All @@ -57,20 +85,65 @@ public function enterNode(Node $node): ?Node
return null;
}

$result = version_compare($operands[0], $operands[1], $operator);
if ($result) {
// remove else
$node->cond = new Node\Expr\ConstFetch(new Node\Name('true'));
$node->else = null;
return version_compare($operands[0], $operands[1], $operator);
}

return $node;
private function evaluateVersionCompareCall(Node\Expr $cond): ?bool
{
if (!$cond instanceof Node\Expr\FuncCall) {
return null;
}

// remove if
$node->cond = new Node\Expr\ConstFetch(new Node\Name('false'));
$node->stmts = [];
if (!$cond->name instanceof Node\Name) {
return null;
}

return $node;
if (strtolower((string) $cond->name) !== 'version_compare') {
return null;
}

$args = $cond->getArgs();
if (count($args) !== 3) {
return null;
}

$phpVersionArgIndex = null;
if (
$args[0]->value instanceof Node\Expr\ConstFetch
&& $args[0]->value->name->toString() === 'PHP_VERSION'
) {
$phpVersionArgIndex = 0;
} elseif (
$args[1]->value instanceof Node\Expr\ConstFetch
&& $args[1]->value->name->toString() === 'PHP_VERSION'
) {
$phpVersionArgIndex = 1;
}

if ($phpVersionArgIndex === null) {
return null;
}

$otherArgIndex = $phpVersionArgIndex === 0 ? 1 : 0;
if (!$args[$otherArgIndex]->value instanceof Node\Scalar\String_) {
return null;
}
$versionString = $args[$otherArgIndex]->value->value;

if (!$args[2]->value instanceof Node\Scalar\String_) {
return null;
}
$operator = $args[2]->value->value;

if (!in_array($operator, VersionCompareHelper::VALID_OPERATORS, true)) {
return null;
}

if ($phpVersionArgIndex === 0) {
return version_compare($this->phpVersionString, $versionString, $operator);
}

return version_compare($versionString, $this->phpVersionString, $operator);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,6 @@
final class VersionCompareFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

public const VALID_OPERATORS = [
'<',
'lt',
'<=',
'le',
'>',
'gt',
'>=',
'ge',
'==',
'=',
'eq',
'!=',
'<>',
'ne',
];

/**
* @param int|array{min: int, max: int}|null $configPhpVersion
*/
Expand Down Expand Up @@ -111,7 +94,7 @@ public function getTypeFromFunctionCall(
if (isset($operatorStrings)) {
foreach ($operatorStrings as $operatorString) {
$operatorValue = $operatorString->getValue();
if (!in_array($operatorValue, self::VALID_OPERATORS, true)) {
if (!in_array($operatorValue, VersionCompareHelper::VALID_OPERATORS, true)) {
if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) {
$canBeNull = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public function getThrowTypeFromFunctionCall(

foreach ($operatorStrings as $operatorString) {
$operatorValue = $operatorString->getValue();
if (!in_array($operatorValue, VersionCompareFunctionDynamicReturnTypeExtension::VALID_OPERATORS, true)) {
if (!in_array($operatorValue, VersionCompareHelper::VALID_OPERATORS, true)) {
return $functionReflection->getThrowType();
}
}
Expand Down
78 changes: 78 additions & 0 deletions src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\Int_;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Analyser\TypeSpecifierAwareExtension;
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\FunctionTypeSpecifyingExtension;
use function count;

#[AutowiredService]
final class VersionCompareFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
{

private TypeSpecifier $typeSpecifier;

public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
{
$this->typeSpecifier = $typeSpecifier;
}

public function isFunctionSupported(
FunctionReflection $functionReflection,
FuncCall $node,
TypeSpecifierContext $context,
): bool
{
return !$context->null()
&& $functionReflection->getName() === 'version_compare'
&& count($node->getArgs()) === 3;
}

public function specifyTypes(
FunctionReflection $functionReflection,
FuncCall $node,
Scope $scope,
TypeSpecifierContext $context,
): SpecifiedTypes
{
$parsed = VersionCompareHelper::parseVersionCompareFuncCall($node, $scope);
if ($parsed === null) {
return new SpecifiedTypes([], []);
}

[$phpVersionArgIndex, $versionId] = $parsed;

$args = $node->getArgs();
$operatorStrings = $scope->getType($args[2]->value)->getConstantStrings();
if (count($operatorStrings) !== 1) {
return new SpecifiedTypes([], []);
}

$comparisonClass = VersionCompareHelper::operatorToComparisonClass($operatorStrings[0]->getValue());
if ($comparisonClass === null) {
return new SpecifiedTypes([], []);
}

$phpVersionIdExpr = new ConstFetch(new Name('PHP_VERSION_ID'));
$versionIdExpr = new Int_($versionId);

if ($phpVersionArgIndex === 0) {
$syntheticExpr = new $comparisonClass($phpVersionIdExpr, $versionIdExpr);
} else {
$syntheticExpr = new $comparisonClass($versionIdExpr, $phpVersionIdExpr);
}

return $this->typeSpecifier->specifyTypesInCondition($scope, $syntheticExpr, $context);
}

}
Loading
Loading