Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,10 @@ jobs:
cd e2e/composer-version-config
composer install
../../bin/phpstan analyze test.php --level=0
- script: |
cd e2e/version-compare-phpversionid
composer install
../../bin/phpstan analyze narrow-phpversionid-on-version-compare.php --level=0
- script: |
cd e2e/bug13425
timeout 15 ../bashunit -a exit_code "1" "../../bin/phpstan analyze src/ plugins/"
Expand Down
2 changes: 2 additions & 0 deletions e2e/version-compare-phpversionid/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/vendor/
composer.lock
5 changes: 5 additions & 0 deletions e2e/version-compare-phpversionid/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"require": {
"php": "^8"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

// intentional without namespace

use function PHPStan\Testing\assertType;

function narrowPhpVersionIdViaVersionComapre(): void {
if (PHP_VERSION_ID < 80000) {
return;
}
$x = PHP_VERSION_ID;
assertType('int<80000, max>', $x);

if (
version_compare( PHP_VERSION, '8.4', '<' )
) {
$x = PHP_VERSION_ID;
assertType('int<80000, 80399>', $x);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php

namespace Bug162;

use function PHPStan\Testing\assertType;
use const PHP_VERSION;
use const PHP_VERSION_ID;

function lower(): void
{
// add a upper bound, so we don't need to adjust
// the test when PHPStan adds support for PHP8.6+
if (PHP_VERSION_ID > 80599) {
return;
}

// lower limit inferred from composer.json
$x = PHP_VERSION_ID;
assertType('int<80000, 80599>', $x);

if (
version_compare( PHP_VERSION, '8.4', '<' )
) {
$x = PHP_VERSION_ID;
assertType('int<80000, 80399>', $x);
}

if (
version_compare( PHP_VERSION, '8.4', 'lt' )
) {
$x = PHP_VERSION_ID;
assertType('int<80000, 80399>', $x);
}

if (
version_compare( PHP_VERSION, '8.4', '<=' )
) {
$x = PHP_VERSION_ID;
assertType('int<80000, 80400>', $x);
}

if (
version_compare( PHP_VERSION, '8.4', 'le' )
) {
$x = PHP_VERSION_ID;
assertType('int<80000, 80400>', $x);
}
}

function greater(): void
{
if (
version_compare( PHP_VERSION, '8.4', '>' )
) {
$x = PHP_VERSION_ID;
assertType('int<80401, max>', $x);
}

if (
version_compare( PHP_VERSION, '8.4', 'gt' )
) {
$x = PHP_VERSION_ID;
assertType('int<80401, max>', $x);
}

if (
version_compare( PHP_VERSION, '8.4', '>=' )
) {
$x = PHP_VERSION_ID;
assertType('int<80400, max>', $x);
}
if (
version_compare( PHP_VERSION, '8.4', 'ge' )
) {
$x = PHP_VERSION_ID;
assertType('int<80400, max>', $x);
}
}

function equal(): void
{
if (
version_compare( PHP_VERSION, '8.4', '=' )
) {
$x = PHP_VERSION_ID;
assertType('80400', $x);
}

if (
version_compare( PHP_VERSION, '8.4', '==' )
) {
$x = PHP_VERSION_ID;
assertType('80400', $x);
}

if (
version_compare( PHP_VERSION, '8.4', 'eq' )
) {
$x = PHP_VERSION_ID;
assertType('80400', $x);
}
}


function not(): void
{
if (
version_compare( PHP_VERSION, '8.4', '!=' )
) {
$x = PHP_VERSION_ID;
assertType('int<min, 80399>|int<80401, max>', $x);
}

if (
version_compare( PHP_VERSION, '8.4', '<>' )
) {
$x = PHP_VERSION_ID;
assertType('int<min, 80399>|int<80401, max>', $x);
}

if (
version_compare( PHP_VERSION, '8.4', 'ne' )
) {
$x = PHP_VERSION_ID;
assertType('int<min, 80399>|int<80401, max>', $x);
}
}

function inverseOperandLower(): void
{
if (
version_compare( '8.3.12', PHP_VERSION, '<' )
) {
$x = PHP_VERSION_ID;
assertType('int<min, 80311>', $x);
}

if (
version_compare( '8.3.12', PHP_VERSION, '>=' )
) {
$x = PHP_VERSION_ID;
assertType('int<80312, max>', $x);
}

if (
version_compare( '8.3.12', PHP_VERSION, '>' )
) {
$x = PHP_VERSION_ID;
assertType('int<80313, max>', $x);
}
}

function narrow(): void {
if (PHP_VERSION_ID < 80000) {
return;
}

if (
version_compare( PHP_VERSION, '8.4', '<' )
) {
$x = PHP_VERSION_ID;
assertType('int<80000, 80399>', $x);
}
}
26 changes: 26 additions & 0 deletions src/Php/SimplePhpVersionParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php declare(strict_types = 1);

namespace PHPStan\Php;

use Nette\Utils\Strings;
use function sprintf;

final class SimplePhpVersionParser
{

public static function parseVersion(string $version): ?PhpVersion
{
$matches = Strings::match($version, '#^(\d+)\.(\d+)(?:\.(\d+))?#');
if ($matches === null) {
return null;
}

$major = $matches[1];
$minor = $matches[2] ?? 0;
$patch = $matches[3] ?? 0;
$versionId = (int) sprintf('%d%02d%02d', $major, $minor, $patch);

return new PhpVersion($versionId);
}

}
141 changes: 141 additions & 0 deletions src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
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\Php\SimplePhpVersionParser;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\FunctionTypeSpecifyingExtension;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
use function in_array;
use function strtolower;

#[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 strtolower($functionReflection->getName()) === 'version_compare' && $context->true();
}

public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
{
if (!$node->name instanceof Name) {
return new SpecifiedTypes();
}

$args = $node->getArgs();
if (count($args) < 2) {
return new SpecifiedTypes();
}

$version1 = $args[0]->value;
$version2 = $args[1]->value;

if (
$version1 instanceof ConstFetch
&& $version1->name->name === 'PHP_VERSION'
&& $version2 instanceof String_
) {
$integerVersionRange = $this->getVersionCompareType($version2->value, isset($args[2]) ? $args[2]->value : null, $scope);

if ($integerVersionRange !== null) {
$narrowedVersion = TypeCombinator::intersect($scope->getPhpVersion()->getType(), $integerVersionRange);
return $this->typeSpecifier->create(
new ConstFetch(new Name('\\PHP_VERSION_ID')),
$narrowedVersion,
$context,
$scope,
);
}
}

if (
$version2 instanceof ConstFetch
&& $version2->name->name === 'PHP_VERSION'
&& $version1 instanceof String_
) {
$integerVersionRange = $this->getVersionCompareType($version1->value, isset($args[2]) ? $args[2]->value : null, $scope);
if ($integerVersionRange !== null) {
$narrowedVersion = TypeCombinator::intersect($scope->getPhpVersion()->getType(), $integerVersionRange);
return $this->typeSpecifier->create(
new ConstFetch(new Name('\\PHP_VERSION_ID')),
$narrowedVersion,
$context,
$scope,
);
}
}

return new SpecifiedTypes();
}

private function getVersionCompareType(string $value, ?Expr $operator, Scope $scope): ?Type
{
$parsedVersion = SimplePhpVersionParser::parseVersion($value);
if ($parsedVersion === null) {
return null;
}

if ($operator !== null) {
$operators = $scope->getType($operator)->getConstantStrings();
if (count($operators) !== 1) {
return null;
}

$operatorString = $operators[0]->getValue();
} else {
$operatorString = '<';
}

if (!in_array($operatorString, VersionCompareFunctionDynamicReturnTypeExtension::VALID_OPERATORS, true)) {
return null;
}

if (in_array($operatorString, ['<', 'lt'], true)) {
return IntegerRangeType::fromInterval(null, $parsedVersion->getVersionId() - 1);
}
if (in_array($operatorString, ['<=', 'le'], true)) {
return IntegerRangeType::fromInterval(null, $parsedVersion->getVersionId());
}

if (in_array($operatorString, ['>', 'gt'], true)) {
return IntegerRangeType::fromInterval($parsedVersion->getVersionId() + 1, null);
}
if (in_array($operatorString, ['>=', 'ge'], true)) {
return IntegerRangeType::fromInterval($parsedVersion->getVersionId(), null);
}

if (
in_array($operatorString, ['==', '=', 'eq'], true)
) {
return new ConstantIntegerType($parsedVersion->getVersionId());
}

return TypeCombinator::remove(new IntegerType(), new ConstantIntegerType($parsedVersion->getVersionId()));
}

}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/data/bug-5639.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Bug5639;

if (\version_compare(\PHP_VERSION, '7.0', '<'))
if (\version_compare(\PHP_VERSION, '7.0', '<')) // @phpstan-ignore function.impossibleType
{
class Foo extends \Exception {
function __toString(): string {
Expand Down
Loading