From 0227006b0d58407078f1bf59d0947444e8b06e75 Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:28:28 +0000 Subject: [PATCH 1/2] Narrow PHP_VERSION_ID scope from version_compare() calls When version_compare(PHP_VERSION, '8.4', '<') or similar calls are used in conditions, PHPStan now narrows the PHP_VERSION_ID type in the corresponding scope branches, matching the existing behavior of direct PHP_VERSION_ID comparisons. Supports all three-argument operators (<, <=, >, >=, lt, le, gt, ge, ==, =, eq, !=, <>, ne), the two-argument form compared with ===, !==, <, <=, >=, >, and PHP_VERSION in either argument position. Closes https://github.com/phpstan/phpstan/issues/13904 --- CLAUDE.md | 10 + src/Analyser/TypeSpecifier.php | 260 ++++++++++++++++++ ...CompareFunctionTypeSpecifyingExtension.php | 160 +++++++++++ .../nsrt/version-compare-scope-narrowing.php | 122 ++++++++ 4 files changed, 552 insertions(+) create mode 100644 src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php create mode 100644 tests/PHPStan/Analyser/nsrt/version-compare-scope-narrowing.php diff --git a/CLAUDE.md b/CLAUDE.md index 993b10f193..d1beb5ec76 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -415,3 +415,13 @@ When adding or editing PHPDoc comments in this codebase, follow these guidelines ### Ternary expression type narrowing in TypeSpecifier `TypeSpecifier::specifyTypesInCondition()` handles ternary expressions (`$cond ? $a : $b`) for type narrowing. In a truthy context (e.g., inside `assert()`), the ternary is semantically equivalent to `($cond && $a) || (!$cond && $b)` — meaning if the condition is true, the "if" branch must be truthy, and if false, the "else" branch must be truthy. The fix converts ternary expressions to this `BooleanOr(BooleanAnd(...), BooleanAnd(...))` form so the existing OR/AND narrowing logic handles both branches correctly. This enables `assert($cond ? $x instanceof A : $x instanceof B)` to narrow `$x` to `A|B`. The `AssertFunctionTypeSpecifyingExtension` calls `specifyTypesInCondition` with `TypeSpecifierContext::createTruthy()` context for the assert argument. + +### Synthetic ConstFetch expression key matching + +When creating synthetic `ConstFetch` AST nodes (e.g., for `PHP_VERSION_ID`) to use with `TypeSpecifier::create()`, the `Name` node type matters for expression key matching. `MutatingScope` tracks expression types keyed by `ExprPrinter::printExpr()` output. A `ConstFetch(new Name('PHP_VERSION_ID'))` prints as `PHP_VERSION_ID`, while `ConstFetch(new Name\FullyQualified('PHP_VERSION_ID'))` prints as `\PHP_VERSION_ID`. These are different keys, so using `FullyQualified` in a synthetic node will fail to match the scope's existing entry, causing the narrowing to have no effect. Always use `new Name('PHP_VERSION_ID')` (unqualified) to match how the original AST represents predefined constants. + +### version_compare scope narrowing via FunctionTypeSpecifyingExtension + +`version_compare(PHP_VERSION, '8.4', '<')` (three-argument form) narrows `PHP_VERSION_ID` scope via `VersionCompareFunctionTypeSpecifyingExtension`, which implements `FunctionTypeSpecifyingExtension`. It converts the version string to a `PHP_VERSION_ID` equivalent and builds a synthetic comparison expression (`Smaller`, `SmallerOrEqual`, `Identical`, `NotIdentical`) that is then processed by `TypeSpecifier::specifyTypesInCondition()`. + +The two-argument form (`version_compare(PHP_VERSION, '8.0') === 1`) is handled directly in `TypeSpecifier::resolveNormalizedIdentical()` and the `Smaller/SmallerOrEqual` section. It maps the comparison result (-1, 0, 1) to a set of version_compare outcomes, then converts those to `PHP_VERSION_ID` type constraints using `getSmallerType()`/`getGreaterType()`/etc. Context handling (true vs false) is done by computing the result set for the truthy case and the complement for the falsey case, then always using `TypeSpecifierContext::createTruthy()` in the `create()` call — matching the pattern used by the existing `Smaller/SmallerOrEqual` handler for direct comparisons. diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 3a1f8bd98e..2cb56d8e6a 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -70,6 +70,7 @@ use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\Php\VersionCompareFunctionTypeSpecifyingExtension; use PHPStan\Type\ResourceType; use PHPStan\Type\StaticMethodTypeSpecifyingExtension; use PHPStan\Type\StaticType; @@ -87,6 +88,7 @@ use function array_shift; use function count; use function in_array; +use function is_int; use function is_string; use function strtolower; use function substr; @@ -261,6 +263,34 @@ public function specifyTypesInCondition( )->setRootExpr($expr); } + // version_compare(PHP_VERSION, '8.0') < 0 or 0 < version_compare(PHP_VERSION, '8.0') + if (!$context->null()) { + $orEqual = $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual; + + if ( + $expr->left instanceof FuncCall + && $expr->left->name instanceof Name + && $expr->left->name->toLowerString() === 'version_compare' + && count($expr->left->getArgs()) === 2 + ) { + $result = $this->specifyTypesForVersionCompareSmallerResult($expr->left, $scope, $scope->getType($expr->right), $orEqual, false, $context, $expr); + if ($result !== null) { + return $result; + } + } elseif ( + $expr->right instanceof FuncCall + && $expr->right->name instanceof Name + && $expr->right->name->toLowerString() === 'version_compare' + && count($expr->right->getArgs()) === 2 + ) { + // value < version_compare(...) → version_compare(...) > value + $result = $this->specifyTypesForVersionCompareSmallerResult($expr->right, $scope, $scope->getType($expr->left), $orEqual, true, $context, $expr); + if ($result !== null) { + return $result; + } + } + } + $orEqual = $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual; $offset = $orEqual ? 0 : 1; $leftType = $scope->getType($expr->left); @@ -2486,6 +2516,21 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope )->setRootExpr($expr); } + // version_compare(PHP_VERSION, '8.0') === 1 + if ( + !$context->null() + && $unwrappedLeftExpr instanceof FuncCall + && $unwrappedLeftExpr->name instanceof Name + && $unwrappedLeftExpr->name->toLowerString() === 'version_compare' + && count($unwrappedLeftExpr->getArgs()) === 2 + && $rightType->isInteger()->yes() + ) { + $result = $this->specifyTypesForVersionCompareResult($unwrappedLeftExpr, $scope, $rightType, $context, $expr); + if ($result !== null) { + return $result; + } + } + // get_class($a) === 'Foo' if ( $context->true() @@ -2776,4 +2821,219 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope return (new SpecifiedTypes([], []))->setRootExpr($expr); } + /** + * Parses a version_compare FuncCall to extract the PHP version argument index and the version ID. + * + * @return array{int, int}|null [phpVersionArgIndex, versionId] or null if not applicable + */ + private function parseVersionCompareFuncCall(FuncCall $funcCall, Scope $scope): ?array + { + $args = $funcCall->getArgs(); + if (count($args) !== 2) { + return null; + } + + $phpVersionArgIndex = null; + if ($args[0]->value instanceof ConstFetch && $args[0]->value->name->toString() === 'PHP_VERSION') { + $phpVersionArgIndex = 0; + } elseif ($args[1]->value instanceof ConstFetch && $args[1]->value->name->toString() === 'PHP_VERSION') { + $phpVersionArgIndex = 1; + } + + if ($phpVersionArgIndex === null) { + return null; + } + + $otherArgIndex = $phpVersionArgIndex === 0 ? 1 : 0; + $versionStrings = $scope->getType($args[$otherArgIndex]->value)->getConstantStrings(); + if (count($versionStrings) !== 1) { + return null; + } + + $versionId = VersionCompareFunctionTypeSpecifyingExtension::parseVersionStringToId($versionStrings[0]->getValue()); + if ($versionId === null) { + return null; + } + + return [$phpVersionArgIndex, $versionId]; + } + + /** + * Handles version_compare(PHP_VERSION, 'ver') === value in resolveNormalizedIdentical. + */ + private function specifyTypesForVersionCompareResult( + FuncCall $funcCall, + Scope $scope, + Type $comparisonValue, + TypeSpecifierContext $context, + Expr $rootExpr, + ): ?SpecifiedTypes + { + $parsed = $this->parseVersionCompareFuncCall($funcCall, $scope); + if ($parsed === null) { + return null; + } + + [$phpVersionArgIndex, $versionId] = $parsed; + $phpVersionSwapped = $phpVersionArgIndex === 1; + + $constantValues = $comparisonValue->getConstantScalarValues(); + if (count($constantValues) !== 1 || !is_int($constantValues[0])) { + return null; + } + + $value = $constantValues[0]; + if (!in_array($value, [-1, 0, 1], true)) { + return null; + } + + // For true/truthy context (===), the result set is the matched value + // For false/falsey context (!==), the result set is the complement + if ($context->true() || $context->truthy()) { + $resultSet = [$value]; + } else { + $all = [-1, 0, 1]; + $resultSet = []; + foreach ($all as $r) { + if ($r === $value) { + continue; + } + + $resultSet[] = $r; + } + } + + $phpVersionIdExpr = new ConstFetch(new Name('PHP_VERSION_ID')); + $versionIdType = new ConstantIntegerType($versionId); + + return $this->specifyTypesFromVersionCompareResultSet($resultSet, $phpVersionIdExpr, $versionIdType, $phpVersionSwapped, $scope, $rootExpr); + } + + /** + * Handles version_compare(PHP_VERSION, 'ver') < value or value < version_compare(PHP_VERSION, 'ver') + * in the Smaller/SmallerOrEqual section. + */ + private function specifyTypesForVersionCompareSmallerResult( + FuncCall $funcCall, + Scope $scope, + Type $comparisonValue, + bool $orEqual, + bool $exprSwapped, + TypeSpecifierContext $context, + Expr $rootExpr, + ): ?SpecifiedTypes + { + $parsed = $this->parseVersionCompareFuncCall($funcCall, $scope); + if ($parsed === null) { + return null; + } + + [$phpVersionArgIndex, $versionId] = $parsed; + $phpVersionSwapped = $phpVersionArgIndex === 1; + + $constantValues = $comparisonValue->getConstantScalarValues(); + if (count($constantValues) !== 1 || !is_int($constantValues[0])) { + return null; + } + + $value = $constantValues[0]; + + // Determine what comparison operation version_compare_result OP value implies + // First, compute the effective operator on version_compare_result + $op = $orEqual ? '<=' : '<'; + if ($exprSwapped) { + // value < version_compare(...) → version_compare(...) > value + // value <= version_compare(...) → version_compare(...) >= value + $op = $orEqual ? '>=' : '>'; + } + + // For false context, flip the operator (like the standard Smaller handler does) + if ($context->false()) { + $op = match ($op) { + '<' => '>=', + '<=' => '>', + '>' => '<=', + '>=' => '<', + }; + } + + // Determine which version_compare results satisfy: result OP value + $resultSet = match ($op) { + '<' => $value > -1 ? ($value > 0 ? ($value > 1 ? [] : [-1, 0]) : [-1]) : [], + '<=' => $value >= -1 ? ($value >= 0 ? ($value >= 1 ? [-1, 0, 1] : [-1, 0]) : [-1]) : [], + '>' => $value < 1 ? ($value < 0 ? ($value < -1 ? [] : [0, 1]) : [1]) : [], + default => $value <= 1 ? ($value <= 0 ? ($value <= -1 ? [-1, 0, 1] : [0, 1]) : [1]) : [], + }; + + if ($resultSet === [] || count($resultSet) === 3) { + return null; + } + + $phpVersionIdExpr = new ConstFetch(new Name('PHP_VERSION_ID')); + $versionIdType = new ConstantIntegerType($versionId); + + // Always use truthy context since we've already computed the correct type for the branch + return $this->specifyTypesFromVersionCompareResultSet($resultSet, $phpVersionIdExpr, $versionIdType, $phpVersionSwapped, $scope, $rootExpr); + } + + /** + * Creates SpecifiedTypes for PHP_VERSION_ID based on a set of version_compare results. + * + * @param list $resultSet Subset of [-1, 0, 1] + */ + private function specifyTypesFromVersionCompareResultSet( + array $resultSet, + Expr $phpVersionIdExpr, + ConstantIntegerType $versionIdType, + bool $phpVersionSwapped, + Scope $scope, + Expr $rootExpr, + ): ?SpecifiedTypes + { + // Map result set to PHP_VERSION_ID type, accounting for swapped argument order + if (count($resultSet) === 1) { + $r = $resultSet[0]; + $effectiveR = $phpVersionSwapped ? -$r : $r; + + $type = match ($effectiveR) { + 1 => $versionIdType->getGreaterType($this->phpVersion), + -1 => $versionIdType->getSmallerType($this->phpVersion), + 0 => $versionIdType, + default => null, + }; + + if ($type === null) { + return null; + } + + return $this->create($phpVersionIdExpr, $type, TypeSpecifierContext::createTruthy(), $scope)->setRootExpr($rootExpr); + } + + // Two results: [-1, 0] or [0, 1] or [-1, 1] + if ($resultSet === [-1, 0]) { + $effectiveOp = $phpVersionSwapped ? '>' : '<='; + } elseif ($resultSet === [0, 1]) { + $effectiveOp = $phpVersionSwapped ? '<' : '>='; + } elseif ($resultSet === [-1, 1]) { + // != case: PHP_VERSION_ID < versionId OR PHP_VERSION_ID > versionId + $type = TypeCombinator::union( + $versionIdType->getSmallerType($this->phpVersion), + $versionIdType->getGreaterType($this->phpVersion), + ); + + return $this->create($phpVersionIdExpr, $type, TypeSpecifierContext::createTruthy(), $scope)->setRootExpr($rootExpr); + } else { + return null; + } + + $type = match ($effectiveOp) { + '<' => $versionIdType->getSmallerType($this->phpVersion), + '<=' => $versionIdType->getSmallerOrEqualType($this->phpVersion), + '>' => $versionIdType->getGreaterType($this->phpVersion), + default => $versionIdType->getGreaterOrEqualType($this->phpVersion), + }; + + return $this->create($phpVersionIdExpr, $type, TypeSpecifierContext::createTruthy(), $scope)->setRootExpr($rootExpr); + } + } diff --git a/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php b/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php new file mode 100644 index 0000000000..65f46f44d3 --- /dev/null +++ b/src/Type/Php/VersionCompareFunctionTypeSpecifyingExtension.php @@ -0,0 +1,160 @@ +typeSpecifier = $typeSpecifier; + } + + public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool + { + return $functionReflection->getName() === 'version_compare' + && !$context->null() + && count($node->getArgs()) === 3; + } + + public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + $args = $node->getArgs(); + + $operatorStrings = $scope->getType($args[2]->value)->getConstantStrings(); + if (count($operatorStrings) !== 1) { + return new SpecifiedTypes([], []); + } + + $operator = $operatorStrings[0]->getValue(); + if (!in_array($operator, VersionCompareFunctionDynamicReturnTypeExtension::VALID_OPERATORS, true)) { + return new SpecifiedTypes([], []); + } + + $phpVersionArgIndex = $this->getPhpVersionArgIndex($args[0]->value, $args[1]->value); + if ($phpVersionArgIndex === null) { + return new SpecifiedTypes([], []); + } + + $otherArgIndex = $phpVersionArgIndex === 0 ? 1 : 0; + $versionStrings = $scope->getType($args[$otherArgIndex]->value)->getConstantStrings(); + if (count($versionStrings) !== 1) { + return new SpecifiedTypes([], []); + } + + $versionId = self::parseVersionStringToId($versionStrings[0]->getValue()); + if ($versionId === null) { + return new SpecifiedTypes([], []); + } + + // When PHP_VERSION is the second argument, the comparison direction is swapped + $swapped = $phpVersionArgIndex === 1; + + $phpVersionIdExpr = new ConstFetch(new Name('PHP_VERSION_ID')); + $versionIdExpr = new Int_($versionId); + + $comparisonExpr = $this->buildComparisonExpr($phpVersionIdExpr, $versionIdExpr, $operator, $swapped); + if ($comparisonExpr === null) { + return new SpecifiedTypes([], []); + } + + return $this->typeSpecifier->specifyTypesInCondition($scope, $comparisonExpr, $context); + } + + private function getPhpVersionArgIndex(Expr $arg0, Expr $arg1): ?int + { + if ($arg0 instanceof ConstFetch && $arg0->name->toString() === 'PHP_VERSION') { + return 0; + } + if ($arg1 instanceof ConstFetch && $arg1->name->toString() === 'PHP_VERSION') { + return 1; + } + + return null; + } + + /** + * @return int|null The PHP_VERSION_ID equivalent of the version string + */ + public static function parseVersionStringToId(string $version): ?int + { + $parts = explode('.', $version); + if (count($parts) > 3) { + return null; + } + + $major = $parts[0]; + $minor = $parts[1] ?? '0'; + $patch = $parts[2] ?? '0'; + + if (!is_numeric($major) || !is_numeric($minor) || !is_numeric($patch)) { + return null; + } + + return (int) $major * 10000 + (int) $minor * 100 + (int) $patch; + } + + private function buildComparisonExpr(Expr $phpVersionIdExpr, Expr $versionIdExpr, string $operator, bool $swapped): ?Expr + { + // Normalize operator aliases + $normalizedOp = match ($operator) { + '<', 'lt' => '<', + '<=', 'le' => '<=', + '>', 'gt' => '>', + '>=', 'ge' => '>=', + '==', '=', 'eq' => '==', + '!=', '<>', 'ne' => '!=', + default => null, + }; + + if ($normalizedOp === null) { + return null; + } + + // When swapped (PHP_VERSION is second arg), reverse the comparison direction + if ($swapped) { + $normalizedOp = match ($normalizedOp) { + '<' => '>', + '<=' => '>=', + '>' => '<', + '>=' => '<=', + default => $normalizedOp, // == and != are symmetric + }; + } + + return match ($normalizedOp) { + '<' => new Smaller($phpVersionIdExpr, $versionIdExpr), + '<=' => new SmallerOrEqual($phpVersionIdExpr, $versionIdExpr), + '>' => new Smaller($versionIdExpr, $phpVersionIdExpr), + '>=' => new SmallerOrEqual($versionIdExpr, $phpVersionIdExpr), + '==' => new Identical($phpVersionIdExpr, $versionIdExpr), + default => new NotIdentical($phpVersionIdExpr, $versionIdExpr), + }; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/version-compare-scope-narrowing.php b/tests/PHPStan/Analyser/nsrt/version-compare-scope-narrowing.php new file mode 100644 index 0000000000..a68700b37c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/version-compare-scope-narrowing.php @@ -0,0 +1,122 @@ +', PHP_VERSION_ID); + } else { + assertType('int<80000, 80599>', PHP_VERSION_ID); + } + + if (version_compare(PHP_VERSION, '8.0', '>=')) { + assertType('int<80000, 80599>', PHP_VERSION_ID); + } else { + assertType('int<50207, 79999>', PHP_VERSION_ID); + } + + if (version_compare(PHP_VERSION, '8.0', '>')) { + assertType('int<80001, 80599>', PHP_VERSION_ID); + } + + if (version_compare(PHP_VERSION, '8.0', '<=')) { + assertType('int<50207, 80000>', PHP_VERSION_ID); + } + + if (version_compare(PHP_VERSION, '8.0', 'lt')) { + assertType('int<50207, 79999>', PHP_VERSION_ID); + } + + if (version_compare(PHP_VERSION, '8.0', 'ge')) { + assertType('int<80000, 80599>', PHP_VERSION_ID); + } + + if (version_compare(PHP_VERSION, '8.4.1', '<')) { + assertType('int<50207, 80400>', PHP_VERSION_ID); + } + + if (version_compare(PHP_VERSION, '8.4.1', '>=')) { + assertType('int<80401, 80599>', PHP_VERSION_ID); + } +} + +// version_compare with PHP_VERSION as second argument +function secondArgPhpVersion(): void +{ + if (version_compare('8.0', PHP_VERSION, '<')) { + assertType('int<80001, 80599>', PHP_VERSION_ID); + } + + if (version_compare('8.0', PHP_VERSION, '>=')) { + assertType('int<50207, 80000>', PHP_VERSION_ID); + } +} + +// Two-argument form: version_compare(PHP_VERSION, '8.4') === 1 +function twoArgFormIdentical(): void +{ + if (version_compare(PHP_VERSION, '8.0') === 1) { + assertType('int<80001, 80599>', PHP_VERSION_ID); + } + + if (version_compare(PHP_VERSION, '8.0') === -1) { + assertType('int<50207, 79999>', PHP_VERSION_ID); + } + + if (version_compare(PHP_VERSION, '8.0') >= 0) { + assertType('int<80000, 80599>', PHP_VERSION_ID); + } + + if (version_compare(PHP_VERSION, '8.0') === 0) { + assertType('80000', PHP_VERSION_ID); + } + + if (version_compare(PHP_VERSION, '8.0') !== -1) { + assertType('int<80000, 80599>', PHP_VERSION_ID); + } + + if (version_compare(PHP_VERSION, '8.0') !== 1) { + assertType('int<50207, 80000>', PHP_VERSION_ID); + } +} + +// eq and ne operators (three-argument form) +function eqNeOperators(): void +{ + if (version_compare(PHP_VERSION, '8.0', '==')) { + assertType('80000', PHP_VERSION_ID); + } + + if (version_compare(PHP_VERSION, '8.0', 'eq')) { + assertType('80000', PHP_VERSION_ID); + } + + if (version_compare(PHP_VERSION, '8.0', '!=')) { + assertType('int<50207, 79999>|int<80001, 80599>', PHP_VERSION_ID); + } + + if (version_compare(PHP_VERSION, '8.0', 'ne')) { + assertType('int<50207, 79999>|int<80001, 80599>', PHP_VERSION_ID); + } +} + +// Two-argument form with comparison operators +function twoArgFormComparison(): void +{ + if (version_compare(PHP_VERSION, '8.0') < 0) { + assertType('int<50207, 79999>', PHP_VERSION_ID); + } + + if (version_compare(PHP_VERSION, '8.0') > 0) { + assertType('int<80001, 80599>', PHP_VERSION_ID); + } + + if (version_compare(PHP_VERSION, '8.0') <= 0) { + assertType('int<50207, 80000>', PHP_VERSION_ID); + } +} From 3d69a115a4b729b736095ec9a11dc81ee8eff0f1 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 16 Feb 2026 09:35:41 +0000 Subject: [PATCH 2/2] Add regression test for #13904 Closes https://github.com/phpstan/phpstan/issues/13904 Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-13904.php | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13904.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-13904.php b/tests/PHPStan/Analyser/nsrt/bug-13904.php new file mode 100644 index 0000000000..dcade02008 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13904.php @@ -0,0 +1,25 @@ + 7.4 + +namespace Bug13904; + +use function PHPStan\Testing\assertType; + +if (version_compare( PHP_VERSION, '8.0', '>=' )) { + class Foo8 { + /** + * @param mixed $x + */ + public function doBaz(...$x): void { + assertType('array', $x); + } + } +} else { + class Foo9 { + /** + * @param mixed $x + */ + public function doBaz(...$x): void { + assertType('list', $x); + } + } +}