From c0bfae6a2efe3daa7df1ab08802960f0fa305ce2 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 22 Nov 2024 19:09:12 +0100 Subject: [PATCH 01/76] Refactor TryRemove/Accepts for DateTime|DateTimeImmutable and Exception|Error --- phpstan-baseline.neon | 2 +- src/Type/ObjectType.php | 34 +++++++++++++--------------------- src/Type/UnionType.php | 28 ++++++++++++++++++++-------- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3b47d2e5fc..d3e9a54a9d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1422,7 +1422,7 @@ parameters: - message: '#^Doing instanceof PHPStan\\Type\\ObjectType is error\-prone and deprecated\. Use Type\:\:isObject\(\) or Type\:\:getObjectClassNames\(\) instead\.$#' identifier: phpstanApi.instanceofType - count: 6 + count: 3 path: src/Type/ObjectType.php - diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index a51539df05..3290fb97c7 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -5,11 +5,6 @@ use ArrayAccess; use Closure; use Countable; -use DateTime; -use DateTimeImmutable; -use DateTimeInterface; -use Error; -use Exception; use Iterator; use IteratorAggregate; use PHPStan\Analyser\OutOfClassScope; @@ -46,7 +41,6 @@ use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; -use Throwable; use Traversable; use function array_key_exists; use function array_map; @@ -1560,23 +1554,21 @@ private function getInterfaces(): array public function tryRemove(Type $typeToRemove): ?Type { - if ($this->getClassName() === DateTimeInterface::class) { - if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === DateTimeImmutable::class) { - return new ObjectType(DateTime::class); - } - - if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === DateTime::class) { - return new ObjectType(DateTimeImmutable::class); - } - } + if ($typeToRemove instanceof ObjectType) { + foreach (UnionType::EQUAL_UNION_CLASSES as $baseClass => $classes) { + if ($this->getClassName() !== $baseClass) { + continue; + } - if ($this->getClassName() === Throwable::class) { - if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === Error::class) { - return new ObjectType(Exception::class); // phpcs:ignore SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly.ReferencedGeneralException - } + foreach ($classes as $index => $class) { + if ($typeToRemove->getClassName() === $class) { + unset($classes[$index]); - if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === Exception::class) { // phpcs:ignore SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly.ReferencedGeneralException - return new ObjectType(Error::class); + return TypeCombinator::union( + ...array_map(static fn (string $objectClass): Type => new ObjectType($objectClass), $classes), + ); + } + } } } diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 1505fa1bb2..5d7627b306 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -5,6 +5,8 @@ use DateTime; use DateTimeImmutable; use DateTimeInterface; +use Error; +use Exception; use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; @@ -26,6 +28,7 @@ use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Generic\TemplateUnionType; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; +use Throwable; use function array_diff_assoc; use function array_fill_keys; use function array_map; @@ -45,6 +48,11 @@ class UnionType implements CompoundType use NonGeneralizableTypeTrait; + public const EQUAL_UNION_CLASSES = [ + DateTimeInterface::class => [DateTimeImmutable::class, DateTime::class], + Throwable::class => [Error::class, Exception::class], // phpcs:ignore SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly.ReferencedGeneralException + ]; + private bool $sortedTypes = false; /** @var array */ @@ -183,14 +191,18 @@ public function getConstantStrings(): array public function accepts(Type $type, bool $strictTypes): AcceptsResult { - if ( - $type->equals(new ObjectType(DateTimeInterface::class)) - && $this->accepts( - new UnionType([new ObjectType(DateTime::class), new ObjectType(DateTimeImmutable::class)]), - $strictTypes, - )->yes() - ) { - return AcceptsResult::createYes(); + foreach (self::EQUAL_UNION_CLASSES as $baseClass => $classes) { + if (!$type->equals(new ObjectType($baseClass))) { + continue; + } + + $union = TypeCombinator::union( + ...array_map(static fn (string $objectClass): Type => new ObjectType($objectClass), $classes), + ); + if ($this->accepts($union, $strictTypes)->yes()) { + return AcceptsResult::createYes(); + } + break; } $result = AcceptsResult::createNo(); From d9082ecf3c80d8ba169f0c2b15fed20613212bf0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 23 Nov 2024 11:23:12 +0100 Subject: [PATCH 02/76] Implement Scope->getPhpVersion() --- src/Analyser/MutatingScope.php | 11 +++ src/Analyser/Scope.php | 3 + src/Php/PhpVersions.php | 26 +++++++ src/Rules/Methods/FinalPrivateMethodRule.php | 9 +-- tests/PHPStan/Php/PhpVersionsTest.php | 68 +++++++++++++++++++ .../Methods/FinalPrivateMethodRuleTest.php | 36 ++++++++-- .../data/final-private-method-phpversions.php | 43 ++++++++++++ 7 files changed, 182 insertions(+), 14 deletions(-) create mode 100644 src/Php/PhpVersions.php create mode 100644 tests/PHPStan/Php/PhpVersionsTest.php create mode 100644 tests/PHPStan/Rules/Methods/data/final-private-method-phpversions.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index b8dcee4fd8..61f16a5119 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -47,6 +47,7 @@ use PHPStan\Parser\NewAssignedToPropertyVisitor; use PHPStan\Parser\Parser; use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\Callables\CallableParametersAcceptor; @@ -5721,4 +5722,14 @@ public function getIterableValueType(Type $iteratee): Type return $iteratee->getIterableValueType(); } + public function getPhpVersion(): PhpVersions + { + $versionExpr = new ConstFetch(new Name('PHP_VERSION_ID')); + if (!$this->hasExpressionType($versionExpr)->yes()) { + return new PhpVersions(new ConstantIntegerType($this->phpVersion->getVersionId())); + } + + return new PhpVersions($this->getType($versionExpr)); + } + } diff --git a/src/Analyser/Scope.php b/src/Analyser/Scope.php index a14c9d108d..bf89cbdf48 100644 --- a/src/Analyser/Scope.php +++ b/src/Analyser/Scope.php @@ -6,6 +6,7 @@ use PhpParser\Node\Expr; use PhpParser\Node\Name; use PhpParser\Node\Param; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; @@ -136,4 +137,6 @@ public function filterByFalseyValue(Expr $expr): self; public function isInFirstLevelStatement(): bool; + public function getPhpVersion(): PhpVersions; + } diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php new file mode 100644 index 0000000000..a85c6c4a42 --- /dev/null +++ b/src/Php/PhpVersions.php @@ -0,0 +1,26 @@ +isSuperTypeOf($this->phpVersions)->result; + } + +} diff --git a/src/Rules/Methods/FinalPrivateMethodRule.php b/src/Rules/Methods/FinalPrivateMethodRule.php index b205234dc2..7331e21bc1 100644 --- a/src/Rules/Methods/FinalPrivateMethodRule.php +++ b/src/Rules/Methods/FinalPrivateMethodRule.php @@ -5,7 +5,6 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\InClassMethodNode; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function sprintf; @@ -14,12 +13,6 @@ final class FinalPrivateMethodRule implements Rule { - public function __construct( - private PhpVersion $phpVersion, - ) - { - } - public function getNodeType(): string { return InClassMethodNode::class; @@ -28,7 +21,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $method = $node->getMethodReflection(); - if (!$this->phpVersion->producesWarningForFinalPrivateMethods()) { + if ($scope->getPhpVersion()->producesWarningForFinalPrivateMethods()->no()) { return []; } diff --git a/tests/PHPStan/Php/PhpVersionsTest.php b/tests/PHPStan/Php/PhpVersionsTest.php new file mode 100644 index 0000000000..b1563e2eb1 --- /dev/null +++ b/tests/PHPStan/Php/PhpVersionsTest.php @@ -0,0 +1,68 @@ +assertSame( + $expected->describe(), + $phpVersions->producesWarningForFinalPrivateMethods()->describe(), + ); + } + + public function dataProducesWarningForFinalPrivateMethods(): iterable + { + yield [ + TrinaryLogic::createNo(), + new ConstantIntegerType(70400), + ]; + + yield [ + TrinaryLogic::createYes(), + new ConstantIntegerType(80000), + ]; + + yield [ + TrinaryLogic::createYes(), + new ConstantIntegerType(80100), + ]; + + yield [ + TrinaryLogic::createYes(), + IntegerRangeType::fromInterval(80000, null), + ]; + + yield [ + TrinaryLogic::createMaybe(), + IntegerRangeType::fromInterval(null, 80000), + ]; + + yield [ + TrinaryLogic::createNo(), + IntegerRangeType::fromInterval(70200, 70400), + ]; + + yield [ + TrinaryLogic::createMaybe(), + new UnionType([ + IntegerRangeType::fromInterval(70200, 70400), + IntegerRangeType::fromInterval(80200, 80400), + ]), + ]; + } + +} diff --git a/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleTest.php b/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleTest.php index 64a4b89c0f..05be45a380 100644 --- a/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleTest.php +++ b/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleTest.php @@ -5,18 +5,15 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** @extends RuleTestCase */ class FinalPrivateMethodRuleTest extends RuleTestCase { - private int $phpVersionId; - protected function getRule(): Rule { - return new FinalPrivateMethodRule( - new PhpVersion($this->phpVersionId), - ); + return new FinalPrivateMethodRule(); } public function dataRule(): array @@ -44,8 +41,35 @@ public function dataRule(): array */ public function testRule(int $phpVersion, array $errors): void { - $this->phpVersionId = $phpVersion; + $testVersion = new PhpVersion($phpVersion); + $runtimeVersion = new PhpVersion(PHP_VERSION_ID); + + if ( + $testVersion->getMajorVersionId() !== $runtimeVersion->getMajorVersionId() + || $testVersion->getMinorVersionId() !== $runtimeVersion->getMinorVersionId() + ) { + $this->markTestSkipped('Test requires PHP version ' . $testVersion->getMajorVersionId() . '.' . $testVersion->getMinorVersionId() . '.*'); + } + $this->analyse([__DIR__ . '/data/final-private-method.php'], $errors); } + public function testRulePhpVersions(): void + { + $this->analyse([__DIR__ . '/data/final-private-method-phpversions.php'], [ + [ + 'Private method FinalPrivateMethodPhpVersions\FooBarPhp8orHigher::foo() cannot be final as it is never overridden by other classes.', + 9, + ], + [ + 'Private method FinalPrivateMethodPhpVersions\FooBarPhp74OrHigher::foo() cannot be final as it is never overridden by other classes.', + 29, + ], + [ + 'Private method FinalPrivateMethodPhpVersions\FooBarBaz::foo() cannot be final as it is never overridden by other classes.', + 39, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/final-private-method-phpversions.php b/tests/PHPStan/Rules/Methods/data/final-private-method-phpversions.php new file mode 100644 index 0000000000..c9424720d1 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/final-private-method-phpversions.php @@ -0,0 +1,43 @@ += 80000) { + class FooBarPhp8orHigher + { + + final private function foo(): void + { + } + } +} + +if (PHP_VERSION_ID < 80000) { + class FooBarPhp7 + { + + final private function foo(): void + { + } + } +} + +if (PHP_VERSION_ID > 70400) { + class FooBarPhp74OrHigher + { + + final private function foo(): void + { + } + } +} + +if (PHP_VERSION_ID < 70400 || PHP_VERSION_ID >= 80100) { + class FooBarBaz + { + + final private function foo(): void + { + } + } +} From adcabb3078603f46c811c7dc460174c9df551310 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Wed, 20 Nov 2024 15:48:39 +0100 Subject: [PATCH 03/76] TooWidePropertyTypeRule: dont skip even always-read properties --- src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php b/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php index 8d77cb01b4..68a513f295 100644 --- a/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php +++ b/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php @@ -61,9 +61,6 @@ public function processNode(Node $node, Scope $scope): array continue; } foreach ($this->extensionProvider->getExtensions() as $extension) { - if ($extension->isAlwaysRead($propertyReflection, $propertyName)) { - continue 2; - } if ($extension->isAlwaysWritten($propertyReflection, $propertyName)) { continue 2; } From 993db818fa59ae4f715a94a9e938aca69e7060a6 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 24 Nov 2024 09:17:40 +0100 Subject: [PATCH 04/76] Revert "TooWidePropertyTypeRule: dont skip even always-read properties" This reverts commit adcabb3078603f46c811c7dc460174c9df551310. --- src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php b/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php index 68a513f295..8d77cb01b4 100644 --- a/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php +++ b/src/Rules/TooWideTypehints/TooWidePropertyTypeRule.php @@ -61,6 +61,9 @@ public function processNode(Node $node, Scope $scope): array continue; } foreach ($this->extensionProvider->getExtensions() as $extension) { + if ($extension->isAlwaysRead($propertyReflection, $propertyName)) { + continue 2; + } if ($extension->isAlwaysWritten($propertyReflection, $propertyName)) { continue 2; } From e3867c05576f0b08656ce83f6ac2f6503ff22f94 Mon Sep 17 00:00:00 2001 From: schlndh Date: Sun, 24 Nov 2024 09:24:08 +0100 Subject: [PATCH 05/76] Bleeding edge - check that values passed to array_sum/product are castable to number --- conf/bleedingEdge.neon | 1 + conf/config.level5.neon | 6 + conf/config.neon | 1 + conf/parametersSchema.neon | 1 + .../ParameterCastableToNumberRule.php | 84 +++++++++ .../ParameterCastableToNumberRuleTest.php | 162 ++++++++++++++++++ .../Rules/Functions/data/bug-11883.php | 14 ++ ...aram-castable-to-number-functions-enum.php | 14 ++ ...astable-to-number-functions-named-args.php | 15 ++ .../param-castable-to-number-functions.php | 45 +++++ 10 files changed, 343 insertions(+) create mode 100644 src/Rules/Functions/ParameterCastableToNumberRule.php create mode 100644 tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php create mode 100644 tests/PHPStan/Rules/Functions/data/bug-11883.php create mode 100644 tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-enum.php create mode 100644 tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-named-args.php create mode 100644 tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions.php diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 8e06b22fda..7227e369b3 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -1,5 +1,6 @@ parameters: featureToggles: bleedingEdge: true + checkParameterCastableToNumberFunctions: true skipCheckGenericClasses!: [] stricterFunctionMap: true diff --git a/conf/config.level5.neon b/conf/config.level5.neon index b89a6dbddf..fd3835fbf1 100644 --- a/conf/config.level5.neon +++ b/conf/config.level5.neon @@ -5,6 +5,10 @@ parameters: checkFunctionArgumentTypes: true checkArgumentsPassedByReference: true +conditionalTags: + PHPStan\Rules\Functions\ParameterCastableToNumberRule: + phpstan.rules.rule: %featureToggles.checkParameterCastableToNumberFunctions% + rules: - PHPStan\Rules\DateTimeInstantiationRule - PHPStan\Rules\Functions\CallUserFuncRule @@ -36,3 +40,5 @@ services: treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule + - + class: PHPStan\Rules\Functions\ParameterCastableToNumberRule diff --git a/conf/config.neon b/conf/config.neon index 4679d5f31c..ba4bbb2f62 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -22,6 +22,7 @@ parameters: tooWideThrowType: true featureToggles: bleedingEdge: false + checkParameterCastableToNumberFunctions: false skipCheckGenericClasses: [] stricterFunctionMap: false fileExtensions: diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 6c7f9c40e6..f73011dcad 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -28,6 +28,7 @@ parametersSchema: ]) featureToggles: structure([ bleedingEdge: bool(), + checkParameterCastableToNumberFunctions: bool(), skipCheckGenericClasses: listOf(string()), stricterFunctionMap: bool() ]) diff --git a/src/Rules/Functions/ParameterCastableToNumberRule.php b/src/Rules/Functions/ParameterCastableToNumberRule.php new file mode 100644 index 0000000000..640c73a440 --- /dev/null +++ b/src/Rules/Functions/ParameterCastableToNumberRule.php @@ -0,0 +1,84 @@ + + */ +final class ParameterCastableToNumberRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private ParameterCastableToStringCheck $parameterCastableToStringCheck, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + $functionName = $functionReflection->getName(); + + if (!in_array($functionName, ['array_sum', 'array_product'], true)) { + return []; + } + + $origArgs = $node->getArgs(); + + if (count($origArgs) !== 1) { + return []; + } + + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $origArgs, + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $errorMessage = 'Parameter %s of function %s expects an array of values castable to number, %s given.'; + $functionParameters = $parametersAcceptor->getParameters(); + $error = $this->parameterCastableToStringCheck->checkParameter( + $origArgs[0], + $scope, + $errorMessage, + static fn (Type $t) => $t->toNumber(), + $functionName, + $this->parameterCastableToStringCheck->getParameterName( + $origArgs[0], + 0, + $functionParameters[0] ?? null, + ), + ); + + return $error !== null + ? [$error] + : []; + } + +} diff --git a/tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php b/tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php new file mode 100644 index 0000000000..77b64c3a30 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php @@ -0,0 +1,162 @@ + + */ +class ParameterCastableToNumberRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $broker = $this->createReflectionProvider(); + return new ParameterCastableToNumberRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, false, false, false))); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/param-castable-to-number-functions.php'], $this->hackPhp74ErrorMessages([ + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array> given.', + 20, + ], + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 21, + ], + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 22, + ], + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 23, + ], + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 24, + ], + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 25, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array> given.', + 27, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 28, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 29, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 30, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 31, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 32, + ], + ])); + } + + public function testNamedArguments(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/param-castable-to-number-functions-named-args.php'], [ + [ + 'Parameter $array of function array_sum expects an array of values castable to number, array> given.', + 7, + ], + [ + 'Parameter $array of function array_product expects an array of values castable to number, array> given.', + 8, + ], + ]); + } + + public function testEnum(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/param-castable-to-number-functions-enum.php'], [ + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 12, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 13, + ], + ]); + } + + public function testBug11883(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-11883.php'], [ + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 13, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 14, + ], + ]); + } + + /** + * @param list $errors + * @return list + */ + private function hackPhp74ErrorMessages(array $errors): array + { + if (PHP_VERSION_ID >= 80000) { + return $errors; + } + + return array_map(static function (array $error): array { + $error[0] = str_replace( + [ + '$array of function array_sum', + '$array of function array_product', + 'array', + ], + [ + '$input of function array_sum', + '$input of function array_product', + 'array', + ], + $error[0], + ); + + return $error; + }, $errors); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-11883.php b/tests/PHPStan/Rules/Functions/data/bug-11883.php new file mode 100644 index 0000000000..a14174777b --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11883.php @@ -0,0 +1,14 @@ += 8.1 + +namespace Bug11883; + +enum SomeEnum: int +{ + case A = 1; + case B = 2; +} + +$enums1 = [SomeEnum::A, SomeEnum::B]; + +var_dump(array_sum($enums1)); +var_dump(array_product($enums1)); diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-enum.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-enum.php new file mode 100644 index 0000000000..91e9f1f686 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-enum.php @@ -0,0 +1,14 @@ += 8.1 + +namespace ParamCastableToNumberFunctionsEnum; + +enum FooEnum +{ + case A; +} + +function invalidUsages() +{ + array_sum([FooEnum::A]); + array_product([FooEnum::A]); +} diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-named-args.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-named-args.php new file mode 100644 index 0000000000..4fdc546062 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions-named-args.php @@ -0,0 +1,15 @@ += 8.0 + +namespace ParamCastableToNumberFunctionsNamedArgs; + +function invalidUsages() +{ + var_dump(array_sum(array: [[0]])); + var_dump(array_product(array: [[0]])); +} + +function validUsages() +{ + var_dump(array_sum(array: [1])); + var_dump(array_product(array: [1])); +} diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions.php new file mode 100644 index 0000000000..9e7c5da4d2 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-number-functions.php @@ -0,0 +1,45 @@ +7.7'), 5, 5.5, null])); + var_dump(array_product(['5.5', false, true, new \SimpleXMLElement('7.7'), 5, 5.5, null])); +} From 2d637dab640f194f19b2c50a565b13e331c0227c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 24 Nov 2024 09:25:51 +0100 Subject: [PATCH 06/76] `Scope::getPhpVersion()` allows array via phpVersion min+max config --- src/Analyser/DirectInternalScopeFactory.php | 5 +++ src/Analyser/LazyInternalScopeFactory.php | 1 + src/Analyser/MutatingScope.php | 6 ++++ src/Testing/PHPStanTestCase.php | 1 + .../FinalPrivateMethodRuleConfigPhpTest.php | 34 +++++++++++++++++++ ...final-private-method-config-phpversion.php | 11 ++++++ .../data/final-private-php-version.neon | 4 +++ 7 files changed, 62 insertions(+) create mode 100644 tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleConfigPhpTest.php create mode 100644 tests/PHPStan/Rules/Methods/data/final-private-method-config-phpversion.php create mode 100644 tests/PHPStan/Rules/Methods/data/final-private-php-version.neon diff --git a/src/Analyser/DirectInternalScopeFactory.php b/src/Analyser/DirectInternalScopeFactory.php index 99f4287dfe..74b43d80c9 100644 --- a/src/Analyser/DirectInternalScopeFactory.php +++ b/src/Analyser/DirectInternalScopeFactory.php @@ -19,6 +19,9 @@ final class DirectInternalScopeFactory implements InternalScopeFactory { + /** + * @param int|array{min: int, max: int}|null $configPhpVersion + */ public function __construct( private ReflectionProvider $reflectionProvider, private InitializerExprTypeResolver $initializerExprTypeResolver, @@ -31,6 +34,7 @@ public function __construct( private NodeScopeResolver $nodeScopeResolver, private RicherScopeGetTypeHelper $richerScopeGetTypeHelper, private PhpVersion $phpVersion, + private int|array|null $configPhpVersion, private ConstantResolver $constantResolver, ) { @@ -78,6 +82,7 @@ public function create( $this->constantResolver, $context, $this->phpVersion, + $this->configPhpVersion, $declareStrictTypes, $function, $namespace, diff --git a/src/Analyser/LazyInternalScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php index 79d34fb54f..ac5b757991 100644 --- a/src/Analyser/LazyInternalScopeFactory.php +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -67,6 +67,7 @@ public function create( $this->container->getByType(ConstantResolver::class), $context, $this->container->getByType(PhpVersion::class), + $this->container->getParameter('phpVersion'), $declareStrictTypes, $function, $namespace, diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 61f16a5119..e7afd055e4 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -144,6 +144,7 @@ use function get_class; use function implode; use function in_array; +use function is_array; use function is_bool; use function is_numeric; use function is_string; @@ -184,6 +185,7 @@ final class MutatingScope implements Scope private static int $resolveClosureTypeDepth = 0; /** + * @param int|array{min: int, max: int}|null $configPhpVersion * @param array $expressionTypes * @param array $conditionalExpressions * @param list $inClosureBindScopeClasses @@ -207,6 +209,7 @@ public function __construct( private ConstantResolver $constantResolver, private ScopeContext $context, private PhpVersion $phpVersion, + private int|array|null $configPhpVersion, private bool $declareStrictTypes = false, private PhpFunctionFromParserNodeReflection|null $function = null, ?string $namespace = null, @@ -5726,6 +5729,9 @@ public function getPhpVersion(): PhpVersions { $versionExpr = new ConstFetch(new Name('PHP_VERSION_ID')); if (!$this->hasExpressionType($versionExpr)->yes()) { + if (is_array($this->configPhpVersion)) { + return new PhpVersions(IntegerRangeType::fromInterval($this->configPhpVersion['min'], $this->configPhpVersion['max'])); + } return new PhpVersions(new ConstantIntegerType($this->phpVersion->getVersionId())); } diff --git a/src/Testing/PHPStanTestCase.php b/src/Testing/PHPStanTestCase.php index 31cdfadb78..849fbfac11 100644 --- a/src/Testing/PHPStanTestCase.php +++ b/src/Testing/PHPStanTestCase.php @@ -163,6 +163,7 @@ public static function createScopeFactory(ReflectionProvider $reflectionProvider $container->getByType(NodeScopeResolver::class), new RicherScopeGetTypeHelper($initializerExprTypeResolver), $container->getByType(PhpVersion::class), + $container->getParameter('phpVersion'), $constantResolver, ), ); diff --git a/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleConfigPhpTest.php b/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleConfigPhpTest.php new file mode 100644 index 0000000000..dddd414b72 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/FinalPrivateMethodRuleConfigPhpTest.php @@ -0,0 +1,34 @@ + */ +class FinalPrivateMethodRuleConfigPhpTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new FinalPrivateMethodRule(); + } + + public function testRulePhpVersions(): void + { + $this->analyse([__DIR__ . '/data/final-private-method-config-phpversion.php'], [ + [ + 'Private method FinalPrivateMethodConfigPhpVersions\PhpVersionViaNEONConfg::foo() cannot be final as it is never overridden by other classes.', + 8, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/data/final-private-php-version.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/final-private-method-config-phpversion.php b/tests/PHPStan/Rules/Methods/data/final-private-method-config-phpversion.php new file mode 100644 index 0000000000..6880568c93 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/final-private-method-config-phpversion.php @@ -0,0 +1,11 @@ + Date: Tue, 26 Nov 2024 01:06:55 +0900 Subject: [PATCH 07/76] Last value was not recognized when passing an associative array as an argument --- src/Rules/FunctionCallParametersCheck.php | 17 +++++--- .../Rules/Classes/InstantiationRuleTest.php | 5 +++ .../PHPStan/Rules/Classes/data/bug-11815.php | 43 +++++++++++++++++++ 3 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 tests/PHPStan/Rules/Classes/data/bug-11815.php diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 130dd0bef8..d320c24d60 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -30,6 +30,7 @@ use function array_key_exists; use function count; use function implode; +use function in_array; use function is_int; use function is_string; use function max; @@ -128,30 +129,32 @@ public function check( if ($arg->unpack) { $arrays = $type->getConstantArrays(); if (count($arrays) > 0) { - $minKeys = null; + $maxKeys = null; foreach ($arrays as $array) { $countType = $array->getArraySize(); if ($countType instanceof ConstantIntegerType) { $keysCount = $countType->getValue(); } elseif ($countType instanceof IntegerRangeType) { - $keysCount = $countType->getMin(); + $keysCount = $countType->getMax(); if ($keysCount === null) { throw new ShouldNotHappenException(); } } else { throw new ShouldNotHappenException(); } - if ($minKeys !== null && $keysCount >= $minKeys) { + if ($maxKeys !== null && $keysCount >= $maxKeys) { continue; } - $minKeys = $keysCount; + $maxKeys = $keysCount; } - for ($j = 0; $j < $minKeys; $j++) { + for ($j = 0; $j < $maxKeys; $j++) { $types = []; $commonKey = null; + $isOptionalKey = false; foreach ($arrays as $constantArray) { + $isOptionalKey = in_array($j, $constantArray->getOptionalKeys(), true); $types[] = $constantArray->getValueTypes()[$j]; $keyType = $constantArray->getKeyTypes()[$j]; if ($commonKey === null) { @@ -165,6 +168,10 @@ public function check( $keyArgumentName = $commonKey; $hasNamedArguments = true; } + if ($isOptionalKey) { + continue; + } + $arguments[] = [ $arg->value, TypeCombinator::union(...$types), diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 657c47bd75..43b9091daf 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -499,4 +499,9 @@ public function testBug10248(): void $this->analyse([__DIR__ . '/data/bug-10248.php'], []); } + public function testBug11815(): void + { + $this->analyse([__DIR__ . '/data/bug-11815.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/bug-11815.php b/tests/PHPStan/Rules/Classes/data/bug-11815.php new file mode 100644 index 0000000000..c08dccb9ea --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-11815.php @@ -0,0 +1,43 @@ += 8.2 + +declare(strict_types = 1); + +class Dimensions +{ + public function __construct( + public int $width, + public int $height, + ) { + } +} + +class StoreProcessorResult +{ + public function __construct( + public string $path, + public string $mimetype, + public Dimensions $dimensions, + public int $filesize, + public true|null $identical = null, + ) { + } +} + +/** + * @return array{path: string, identical?: true} + */ +function getPath(): array +{ + $data = ['path' => 'some/path']; + if ((bool)rand(0, 1)) { + $data['identical'] = true; + } + return $data; +} + +$data = getPath(); +$data['dimensions'] = new Dimensions(100, 100); +$data['mimetype'] = 'image/png'; +$data['filesize'] = 123456; + +$dto = new StoreProcessorResult(...$data); From 970117e52512790507cd232d4146055fab1daccf Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 24 Nov 2024 21:51:21 +0100 Subject: [PATCH 08/76] Remove sha256 definition --- resources/functionMap.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index 9891593eb3..611ac517ec 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -10448,8 +10448,6 @@ 'settype' => ['bool', '&rw_var'=>'mixed', 'type'=>'string'], 'sha1' => ['non-falsy-string&lowercase-string', 'str'=>'string', 'raw_output='=>'bool'], 'sha1_file' => ['(non-falsy-string&lowercase-string)|false', 'filename'=>'string', 'raw_output='=>'bool'], -'sha256' => ['string', 'str'=>'string', 'raw_output='=>'bool'], -'sha256_file' => ['string', 'filename'=>'string', 'raw_output='=>'bool'], 'shapefileObj::__construct' => ['void', 'filename'=>'string', 'type'=>'int'], 'shapefileObj::addPoint' => ['int', 'point'=>'pointObj'], 'shapefileObj::addShape' => ['int', 'shape'=>'shapeObj'], From a0e007aee46a0a3ac56d74789f1107a87d0f5740 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 24 Nov 2024 11:26:38 +0100 Subject: [PATCH 09/76] non-capturing catch support-detection is scope php-version dependent --- src/Php/PhpVersions.php | 5 +++++ src/Rules/Exceptions/NoncapturingCatchRule.php | 7 +------ .../Rules/Exceptions/NoncapturingCatchRuleTest.php | 14 ++++++++++---- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index a85c6c4a42..7bdc70e3bf 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -18,6 +18,11 @@ public function __construct( { } + public function supportsNoncapturingCatches(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; + } + public function producesWarningForFinalPrivateMethods(): TrinaryLogic { return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; diff --git a/src/Rules/Exceptions/NoncapturingCatchRule.php b/src/Rules/Exceptions/NoncapturingCatchRule.php index 499b62e154..a4d91a9ba2 100644 --- a/src/Rules/Exceptions/NoncapturingCatchRule.php +++ b/src/Rules/Exceptions/NoncapturingCatchRule.php @@ -4,7 +4,6 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -14,10 +13,6 @@ final class NoncapturingCatchRule implements Rule { - public function __construct(private PhpVersion $phpVersion) - { - } - public function getNodeType(): string { return Node\Stmt\Catch_::class; @@ -28,7 +23,7 @@ public function getNodeType(): string */ public function processNode(Node $node, Scope $scope): array { - if ($this->phpVersion->supportsNoncapturingCatches()) { + if ($scope->getPhpVersion()->supportsNoncapturingCatches()->yes()) { return []; } diff --git a/tests/PHPStan/Rules/Exceptions/NoncapturingCatchRuleTest.php b/tests/PHPStan/Rules/Exceptions/NoncapturingCatchRuleTest.php index 9c8181f4a3..8139960e66 100644 --- a/tests/PHPStan/Rules/Exceptions/NoncapturingCatchRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/NoncapturingCatchRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -12,11 +13,9 @@ class NoncapturingCatchRuleTest extends RuleTestCase { - private PhpVersion $phpVersion; - protected function getRule(): Rule { - return new NoncapturingCatchRule($this->phpVersion); + return new NoncapturingCatchRule(); } public function dataRule(): array @@ -49,7 +48,14 @@ public function dataRule(): array */ public function testRule(int $phpVersion, array $expectedErrors): void { - $this->phpVersion = new PhpVersion($phpVersion); + $testVersion = new PhpVersion($phpVersion); + $runtimeVersion = new PhpVersion(PHP_VERSION_ID); + if ( + $testVersion->getMajorVersionId() !== $runtimeVersion->getMajorVersionId() + || $testVersion->getMinorVersionId() !== $runtimeVersion->getMinorVersionId() + ) { + $this->markTestSkipped('Test requires PHP version ' . $testVersion->getMajorVersionId() . '.' . $testVersion->getMinorVersionId() . '.*'); + } $this->analyse([ __DIR__ . '/data/noncapturing-catch.php', From d35a2f464d7756022b12acc83c994bcb39d07ac9 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Tue, 26 Nov 2024 00:03:38 +0000 Subject: [PATCH 10/76] Update PhpStorm stubs --- composer.json | 2 +- composer.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 73f1cc620f..71c48bfbc7 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", "hoa/file": "1.17.07.11", - "jetbrains/phpstorm-stubs": "dev-master#fda684a4826c1caf59efe1a1bf68d08c11aaddbf", + "jetbrains/phpstorm-stubs": "dev-master#9efcc4aa48b9c752c68de25f6240fcced0c95151", "nette/bootstrap": "^3.0", "nette/di": "^3.1.4", "nette/neon": "3.3.4", diff --git a/composer.lock b/composer.lock index d2adb5606c..6a7f1ff905 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a4991601b7590d9dc65443dfb5d34d16", + "content-hash": "274e72a3422f81d92070bbbedbb9f8f5", "packages": [ { "name": "clue/ndjson-react", @@ -1442,18 +1442,18 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "fda684a4826c1caf59efe1a1bf68d08c11aaddbf" + "reference": "9efcc4aa48b9c752c68de25f6240fcced0c95151" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/fda684a4826c1caf59efe1a1bf68d08c11aaddbf", - "reference": "fda684a4826c1caf59efe1a1bf68d08c11aaddbf", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/9efcc4aa48b9c752c68de25f6240fcced0c95151", + "reference": "9efcc4aa48b9c752c68de25f6240fcced0c95151", "shasum": "" }, "require-dev": { "friendsofphp/php-cs-fixer": "v3.64.0", "nikic/php-parser": "v5.3.1", - "phpdocumentor/reflection-docblock": "5.5.1", + "phpdocumentor/reflection-docblock": "5.6.0", "phpunit/phpunit": "11.4.3" }, "default-branch": true, @@ -1482,7 +1482,7 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2024-11-15T09:42:33+00:00" + "time": "2024-11-25T11:21:35+00:00" }, { "name": "nette/bootstrap", From 96f721d40a0492e2f33a2316efaa50da0d385b0f Mon Sep 17 00:00:00 2001 From: ondrejmirtes Date: Tue, 26 Nov 2024 20:00:44 +0000 Subject: [PATCH 11/76] Update BetterReflection --- composer.json | 2 +- composer.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 71c48bfbc7..8963aa75c3 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "nette/utils": "^3.2.5", "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.43.0.2", + "ondrejmirtes/better-reflection": "6.43.0.4", "phpstan/php-8-stubs": "0.4.6", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", diff --git a/composer.lock b/composer.lock index 6a7f1ff905..a1f8f8e83a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "274e72a3422f81d92070bbbedbb9f8f5", + "content-hash": "1866d60acbec835ca58159c3cb6c07a8", "packages": [ { "name": "clue/ndjson-react", @@ -2187,16 +2187,16 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.43.0.2", + "version": "6.43.0.4", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "c34ee726f9abc5a7057b0dacdf1c0991c9090584" + "reference": "6b869bb58972b7877509e8a24757b4108effcd79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/c34ee726f9abc5a7057b0dacdf1c0991c9090584", - "reference": "c34ee726f9abc5a7057b0dacdf1c0991c9090584", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/6b869bb58972b7877509e8a24757b4108effcd79", + "reference": "6b869bb58972b7877509e8a24757b4108effcd79", "shasum": "" }, "require": { @@ -2213,7 +2213,7 @@ "phpstan/phpstan": "^1.10.60", "phpstan/phpstan-phpunit": "^1.3.16", "phpunit/phpunit": "^11.4.3", - "rector/rector": "0.14.3" + "rector/rector": "1.2.10" }, "suggest": { "composer/composer": "Required to use the ComposerSourceLocator" @@ -2252,9 +2252,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.43.0.2" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.43.0.4" }, - "time": "2024-11-19T19:32:34+00:00" + "time": "2024-11-26T19:58:24+00:00" }, { "name": "phpstan/php-8-stubs", From 17da15181a75ec1bd184959185005e6e857e1220 Mon Sep 17 00:00:00 2001 From: ondrejmirtes Date: Tue, 26 Nov 2024 20:52:02 +0000 Subject: [PATCH 12/76] Update BetterReflection --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 8963aa75c3..988292f2d6 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "nette/utils": "^3.2.5", "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.43.0.4", + "ondrejmirtes/better-reflection": "6.44.0.2", "phpstan/php-8-stubs": "0.4.6", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", diff --git a/composer.lock b/composer.lock index a1f8f8e83a..d705cd728f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1866d60acbec835ca58159c3cb6c07a8", + "content-hash": "6c77a249a859b7eb74df36c091206c59", "packages": [ { "name": "clue/ndjson-react", @@ -2187,16 +2187,16 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.43.0.4", + "version": "6.44.0.2", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "6b869bb58972b7877509e8a24757b4108effcd79" + "reference": "50fe615bd6665bbc541b6ddb419bff11fc0718a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/6b869bb58972b7877509e8a24757b4108effcd79", - "reference": "6b869bb58972b7877509e8a24757b4108effcd79", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/50fe615bd6665bbc541b6ddb419bff11fc0718a1", + "reference": "50fe615bd6665bbc541b6ddb419bff11fc0718a1", "shasum": "" }, "require": { @@ -2252,9 +2252,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.43.0.4" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.44.0.2" }, - "time": "2024-11-26T19:58:24+00:00" + "time": "2024-11-26T20:50:48+00:00" }, { "name": "phpstan/php-8-stubs", From 1c4c1009a523a709d69cbe417580dd8aa44b4245 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Wed, 27 Nov 2024 00:21:18 +0000 Subject: [PATCH 13/76] Update PHP 8 stubs --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 988292f2d6..da38b8d286 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", "ondrejmirtes/better-reflection": "6.44.0.2", - "phpstan/php-8-stubs": "0.4.6", + "phpstan/php-8-stubs": "0.4.7", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", "react/async": "^3", diff --git a/composer.lock b/composer.lock index d705cd728f..569d899634 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6c77a249a859b7eb74df36c091206c59", + "content-hash": "d934490a94126dd32923c058834ef486", "packages": [ { "name": "clue/ndjson-react", @@ -2258,16 +2258,16 @@ }, { "name": "phpstan/php-8-stubs", - "version": "0.4.6", + "version": "0.4.7", "source": { "type": "git", "url": "https://github.com/phpstan/php-8-stubs.git", - "reference": "25ba0a11dc14a02c062392786486ada62d36b66d" + "reference": "794d410e0de8779afb4706d8667d2b3a11a3865a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/25ba0a11dc14a02c062392786486ada62d36b66d", - "reference": "25ba0a11dc14a02c062392786486ada62d36b66d", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/794d410e0de8779afb4706d8667d2b3a11a3865a", + "reference": "794d410e0de8779afb4706d8667d2b3a11a3865a", "shasum": "" }, "type": "library", @@ -2284,9 +2284,9 @@ "description": "PHP stubs extracted from php-src", "support": { "issues": "https://github.com/phpstan/php-8-stubs/issues", - "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.6" + "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.7" }, - "time": "2024-11-13T00:19:28+00:00" + "time": "2024-11-27T00:20:37+00:00" }, { "name": "phpstan/phpdoc-parser", From 5ec47623fa31f63912d904ba90693eac64b0de0c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 27 Nov 2024 10:00:39 +0100 Subject: [PATCH 14/76] PropertyHookType should not be prefixed in BetterReflection --- compiler/build/scoper.inc.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/compiler/build/scoper.inc.php b/compiler/build/scoper.inc.php index 957cfe721d..0d0008d341 100644 --- a/compiler/build/scoper.inc.php +++ b/compiler/build/scoper.inc.php @@ -226,6 +226,13 @@ function (string $filePath, string $prefix, string $content): string { sprintf('\\\\%s', $prefix), ], '', $content); }, + function (string $filePath, string $prefix, string $content): string { + if (!str_starts_with($filePath, 'vendor/ondrejmirtes/better-reflection')) { + return $content; + } + + return str_replace(sprintf('%s\\PropertyHookType', $prefix), 'PropertyHookType', $content); + }, function (string $filePath, string $prefix, string $content): string { if ( $filePath !== 'vendor/nette/utils/src/Utils/Strings.php' From 265a39bb014534095b2f1651de27f4c10e770c09 Mon Sep 17 00:00:00 2001 From: ondrejmirtes Date: Wed, 27 Nov 2024 10:50:13 +0000 Subject: [PATCH 15/76] Update BetterReflection --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index da38b8d286..593163649e 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "nette/utils": "^3.2.5", "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.44.0.2", + "ondrejmirtes/better-reflection": "6.44.0.3", "phpstan/php-8-stubs": "0.4.7", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", diff --git a/composer.lock b/composer.lock index 569d899634..a31b6c1e33 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d934490a94126dd32923c058834ef486", + "content-hash": "402089d9a3d76a9b791773a44ed74775", "packages": [ { "name": "clue/ndjson-react", @@ -2187,16 +2187,16 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.44.0.2", + "version": "6.44.0.3", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "50fe615bd6665bbc541b6ddb419bff11fc0718a1" + "reference": "c30607531b1ae173ea69a72c21ae598f15fef4af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/50fe615bd6665bbc541b6ddb419bff11fc0718a1", - "reference": "50fe615bd6665bbc541b6ddb419bff11fc0718a1", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/c30607531b1ae173ea69a72c21ae598f15fef4af", + "reference": "c30607531b1ae173ea69a72c21ae598f15fef4af", "shasum": "" }, "require": { @@ -2252,9 +2252,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.44.0.2" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.44.0.3" }, - "time": "2024-11-26T20:50:48+00:00" + "time": "2024-11-27T10:47:28+00:00" }, { "name": "phpstan/php-8-stubs", From 1abeeb77771fc3fece5c7196f75dfc1478840515 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 27 Nov 2024 14:01:24 +0100 Subject: [PATCH 16/76] Un-finalize Printer See https://github.com/rectorphp/rector-src/pull/6517#issuecomment-2503502706 --- build/PHPStan/Build/FinalClassRule.php | 2 ++ src/Node/Printer/Printer.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/build/PHPStan/Build/FinalClassRule.php b/build/PHPStan/Build/FinalClassRule.php index a4758648c6..10f30a8d48 100644 --- a/build/PHPStan/Build/FinalClassRule.php +++ b/build/PHPStan/Build/FinalClassRule.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\File\FileHelper; use PHPStan\Node\InClassNode; +use PHPStan\Node\Printer\Printer; use PHPStan\Reflection\FunctionVariant; use PHPStan\Reflection\ExtendedFunctionVariant; use PHPStan\Reflection\Php\DummyParameter; @@ -54,6 +55,7 @@ public function processNode(Node $node, Scope $scope): array ExtendedFunctionVariant::class, DummyParameter::class, PhpFunctionFromParserNodeReflection::class, + Printer::class, ], true)) { return []; } diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index 131376d66d..744c857f8f 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -22,7 +22,7 @@ /** * @api */ -final class Printer extends Standard +class Printer extends Standard { public function __construct() From 07d7c44d50677874086cb8796a97482d406d3442 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 27 Nov 2024 14:26:42 +0100 Subject: [PATCH 17/76] Revert "Un-finalize Printer" This reverts commit 1abeeb77771fc3fece5c7196f75dfc1478840515. --- build/PHPStan/Build/FinalClassRule.php | 2 -- src/Node/Printer/Printer.php | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/build/PHPStan/Build/FinalClassRule.php b/build/PHPStan/Build/FinalClassRule.php index 10f30a8d48..a4758648c6 100644 --- a/build/PHPStan/Build/FinalClassRule.php +++ b/build/PHPStan/Build/FinalClassRule.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\File\FileHelper; use PHPStan\Node\InClassNode; -use PHPStan\Node\Printer\Printer; use PHPStan\Reflection\FunctionVariant; use PHPStan\Reflection\ExtendedFunctionVariant; use PHPStan\Reflection\Php\DummyParameter; @@ -55,7 +54,6 @@ public function processNode(Node $node, Scope $scope): array ExtendedFunctionVariant::class, DummyParameter::class, PhpFunctionFromParserNodeReflection::class, - Printer::class, ], true)) { return []; } diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index 744c857f8f..131376d66d 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -22,7 +22,7 @@ /** * @api */ -class Printer extends Standard +final class Printer extends Standard { public function __construct() From fa9212fc7e68f9609dbfc5b1042ce83c5b34450d Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 27 Nov 2024 19:26:13 +0700 Subject: [PATCH 18/76] Remove shortArraySyntax definiton on Printer::construct() It seems alreayd short array syntax by default --- src/Node/Printer/Printer.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index 131376d66d..6f00bc8031 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -24,12 +24,6 @@ */ final class Printer extends Standard { - - public function __construct() - { - parent::__construct(['shortArraySyntax' => true]); - } - protected function pPHPStan_Node_TypeExpr(TypeExpr $expr): string // phpcs:ignore { return sprintf('__phpstanType(%s)', $expr->getExprType()->describe(VerbosityLevel::precise())); From 68803e997f4ab10c98a014e4a6d057b99e08b6b7 Mon Sep 17 00:00:00 2001 From: ondrejmirtes Date: Wed, 27 Nov 2024 13:47:17 +0000 Subject: [PATCH 19/76] Update BetterReflection --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 593163649e..684e819342 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "nette/utils": "^3.2.5", "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.44.0.3", + "ondrejmirtes/better-reflection": "6.44.0.4", "phpstan/php-8-stubs": "0.4.7", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", diff --git a/composer.lock b/composer.lock index a31b6c1e33..1f3a161391 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "402089d9a3d76a9b791773a44ed74775", + "content-hash": "ac31d4286789897fa7f8d303b33a72b0", "packages": [ { "name": "clue/ndjson-react", @@ -2187,16 +2187,16 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.44.0.3", + "version": "6.44.0.4", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "c30607531b1ae173ea69a72c21ae598f15fef4af" + "reference": "3865f1f8779d4e3562886ee261ccfdbf2da15650" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/c30607531b1ae173ea69a72c21ae598f15fef4af", - "reference": "c30607531b1ae173ea69a72c21ae598f15fef4af", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/3865f1f8779d4e3562886ee261ccfdbf2da15650", + "reference": "3865f1f8779d4e3562886ee261ccfdbf2da15650", "shasum": "" }, "require": { @@ -2252,9 +2252,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.44.0.3" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.44.0.4" }, - "time": "2024-11-27T10:47:28+00:00" + "time": "2024-11-27T13:45:40+00:00" }, { "name": "phpstan/php-8-stubs", From e1b1cad3c7da1ef175b039c4b1af37212d52a22f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 27 Nov 2024 15:04:32 +0100 Subject: [PATCH 20/76] Fix CS --- src/Node/Printer/Printer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index 6f00bc8031..6d9dab1062 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -24,6 +24,7 @@ */ final class Printer extends Standard { + protected function pPHPStan_Node_TypeExpr(TypeExpr $expr): string // phpcs:ignore { return sprintf('__phpstanType(%s)', $expr->getExprType()->describe(VerbosityLevel::precise())); From f9e84e05bf9b4e15a4b019d4e372b71ba98e42e8 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:03:11 +0000 Subject: [PATCH 21/76] Update PhpStorm stubs --- composer.json | 2 +- composer.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 684e819342..1505dfeb90 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", "hoa/file": "1.17.07.11", - "jetbrains/phpstorm-stubs": "dev-master#9efcc4aa48b9c752c68de25f6240fcced0c95151", + "jetbrains/phpstorm-stubs": "dev-master#7809499af1a5f3bdd20f1cab71717a2a9e1f8cf7", "nette/bootstrap": "^3.0", "nette/di": "^3.1.4", "nette/neon": "3.3.4", diff --git a/composer.lock b/composer.lock index 1f3a161391..b40729b153 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ac31d4286789897fa7f8d303b33a72b0", + "content-hash": "c1cf63bf474629a0d7b5fea00c2ec54c", "packages": [ { "name": "clue/ndjson-react", @@ -1442,12 +1442,12 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "9efcc4aa48b9c752c68de25f6240fcced0c95151" + "reference": "7809499af1a5f3bdd20f1cab71717a2a9e1f8cf7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/9efcc4aa48b9c752c68de25f6240fcced0c95151", - "reference": "9efcc4aa48b9c752c68de25f6240fcced0c95151", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/7809499af1a5f3bdd20f1cab71717a2a9e1f8cf7", + "reference": "7809499af1a5f3bdd20f1cab71717a2a9e1f8cf7", "shasum": "" }, "require-dev": { @@ -1482,7 +1482,7 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2024-11-25T11:21:35+00:00" + "time": "2024-11-27T14:37:09+00:00" }, { "name": "nette/bootstrap", From 0b925a9f5a3b664d1a02c9d6b9d1721263a34a06 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Wed, 27 Nov 2024 22:25:38 +0100 Subject: [PATCH 22/76] Retain list type when assigning to offset 1 of `non-empty-list` --- src/Type/IntersectionType.php | 9 +++++- tests/PHPStan/Analyser/nsrt/list-type.php | 25 +++++++++++++++ .../TypesAssignedToPropertiesRuleTest.php | 12 +++++++ .../Rules/Properties/data/bug-12131.php | 31 +++++++++++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100755 tests/PHPStan/Rules/Properties/data/bug-12131.php diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 72fecc7de6..d41b79e951 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -756,7 +756,14 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni ); }); } - return $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); + + $result = $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); + + if ($offsetType !== null && $this->isList()->yes() && $this->isIterableAtLeastOnce()->yes() && (new ConstantIntegerType(1))->isSuperTypeOf($offsetType)->yes()) { + $result = AccessoryArrayListType::intersectWith($result); + } + + return $result; } public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type diff --git a/tests/PHPStan/Analyser/nsrt/list-type.php b/tests/PHPStan/Analyser/nsrt/list-type.php index a80e8b066d..26640a5141 100644 --- a/tests/PHPStan/Analyser/nsrt/list-type.php +++ b/tests/PHPStan/Analyser/nsrt/list-type.php @@ -106,4 +106,29 @@ public function testUnset(array $list): void assertType('array|int<3, max>, int>', $list); } + /** @param list $list */ + public function testSetOffsetExplicitlyWithoutGap(array $list): void + { + assertType('list', $list); + $list[0] = 17; + assertType('non-empty-list&hasOffsetValue(0, 17)', $list); + $list[1] = 19; + assertType('non-empty-list&hasOffsetValue(0, 17)&hasOffsetValue(1, 19)', $list); + $list[0] = 21; + assertType('non-empty-list&hasOffsetValue(0, 21)&hasOffsetValue(1, 19)', $list); + + $list[2] = 23; + assertType('non-empty-array, int>&hasOffsetValue(0, 21)&hasOffsetValue(1, 19)&hasOffsetValue(2, 23)', $list); + } + + /** @param list $list */ + public function testSetOffsetExplicitlyWithGap(array $list): void + { + assertType('list', $list); + $list[0] = 17; + assertType('non-empty-list&hasOffsetValue(0, 17)', $list); + $list[2] = 21; + assertType('non-empty-array, int>&hasOffsetValue(0, 17)&hasOffsetValue(2, 21)', $list); + } + } diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index ff9f5be5ae..4c87845d24 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -692,4 +692,16 @@ public function testBug11617(): void ]); } + public function testBug12131(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12131.php'], [ + [ + 'Property Bug12131\Test::$array (non-empty-list) does not accept non-empty-array, int>.', + 29, + 'non-empty-array, int> might not be a list.', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-12131.php b/tests/PHPStan/Rules/Properties/data/bug-12131.php new file mode 100755 index 0000000000..6f7f8d83d8 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-12131.php @@ -0,0 +1,31 @@ += 7.4 + +namespace Bug12131; + +class Test +{ + /** + * @var non-empty-list + */ + public array $array; + + public function __construct() + { + $this->array = array_fill(0, 10, 1); + } + + public function setAtZero(): void + { + $this->array[0] = 1; + } + + public function setAtOne(): void + { + $this->array[1] = 1; + } + + public function setAtTwo(): void + { + $this->array[2] = 1; + } +} From 62c6a0a8b654224e4c6f0b5e111740b4a2d260e4 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Wed, 27 Nov 2024 22:37:57 +0100 Subject: [PATCH 23/76] Remove unnecessary test code --- tests/PHPStan/Analyser/nsrt/list-type.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/list-type.php b/tests/PHPStan/Analyser/nsrt/list-type.php index 26640a5141..40d94efff3 100644 --- a/tests/PHPStan/Analyser/nsrt/list-type.php +++ b/tests/PHPStan/Analyser/nsrt/list-type.php @@ -116,9 +116,6 @@ public function testSetOffsetExplicitlyWithoutGap(array $list): void assertType('non-empty-list&hasOffsetValue(0, 17)&hasOffsetValue(1, 19)', $list); $list[0] = 21; assertType('non-empty-list&hasOffsetValue(0, 21)&hasOffsetValue(1, 19)', $list); - - $list[2] = 23; - assertType('non-empty-array, int>&hasOffsetValue(0, 21)&hasOffsetValue(1, 19)&hasOffsetValue(2, 23)', $list); } /** @param list $list */ From ae2b6a709ddcd9a4260587565809bd7f1a50d2bd Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Thu, 28 Nov 2024 00:21:27 +0000 Subject: [PATCH 24/76] Update PHP 8 stubs --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 1505dfeb90..a9260b897c 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", "ondrejmirtes/better-reflection": "6.44.0.4", - "phpstan/php-8-stubs": "0.4.7", + "phpstan/php-8-stubs": "0.4.8", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", "react/async": "^3", diff --git a/composer.lock b/composer.lock index b40729b153..1135a6a574 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c1cf63bf474629a0d7b5fea00c2ec54c", + "content-hash": "d1310e0c489abe492a66dced167d3f1e", "packages": [ { "name": "clue/ndjson-react", @@ -2258,16 +2258,16 @@ }, { "name": "phpstan/php-8-stubs", - "version": "0.4.7", + "version": "0.4.8", "source": { "type": "git", "url": "https://github.com/phpstan/php-8-stubs.git", - "reference": "794d410e0de8779afb4706d8667d2b3a11a3865a" + "reference": "8a6278b2b9c9781cb969c4128da361b78eead604" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/794d410e0de8779afb4706d8667d2b3a11a3865a", - "reference": "794d410e0de8779afb4706d8667d2b3a11a3865a", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/8a6278b2b9c9781cb969c4128da361b78eead604", + "reference": "8a6278b2b9c9781cb969c4128da361b78eead604", "shasum": "" }, "type": "library", @@ -2284,9 +2284,9 @@ "description": "PHP stubs extracted from php-src", "support": { "issues": "https://github.com/phpstan/php-8-stubs/issues", - "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.7" + "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.8" }, - "time": "2024-11-27T00:20:37+00:00" + "time": "2024-11-28T00:20:48+00:00" }, { "name": "phpstan/phpdoc-parser", From c95367d52b04d524f1fb154e944ddb1ab8cab9a8 Mon Sep 17 00:00:00 2001 From: ondrejmirtes Date: Thu, 28 Nov 2024 20:40:22 +0000 Subject: [PATCH 25/76] Update BetterReflection --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index a9260b897c..912996ab0d 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "nette/utils": "^3.2.5", "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.44.0.4", + "ondrejmirtes/better-reflection": "6.44.0.5", "phpstan/php-8-stubs": "0.4.8", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", diff --git a/composer.lock b/composer.lock index 1135a6a574..7441a9d501 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d1310e0c489abe492a66dced167d3f1e", + "content-hash": "53bd280e3693edf29e8871fa22ad20f7", "packages": [ { "name": "clue/ndjson-react", @@ -2187,16 +2187,16 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.44.0.4", + "version": "6.44.0.5", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "3865f1f8779d4e3562886ee261ccfdbf2da15650" + "reference": "b6d0a9adbe61544f6e8992611590a4e61487a638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/3865f1f8779d4e3562886ee261ccfdbf2da15650", - "reference": "3865f1f8779d4e3562886ee261ccfdbf2da15650", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/b6d0a9adbe61544f6e8992611590a4e61487a638", + "reference": "b6d0a9adbe61544f6e8992611590a4e61487a638", "shasum": "" }, "require": { @@ -2252,9 +2252,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.44.0.4" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.44.0.5" }, - "time": "2024-11-27T13:45:40+00:00" + "time": "2024-11-28T20:34:45+00:00" }, { "name": "phpstan/php-8-stubs", From 86197c9987529b5545c0a09ac6ad92581087526f Mon Sep 17 00:00:00 2001 From: ondrejmirtes Date: Thu, 28 Nov 2024 21:07:43 +0000 Subject: [PATCH 26/76] Update BetterReflection --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 912996ab0d..0f28344835 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "nette/utils": "^3.2.5", "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.44.0.5", + "ondrejmirtes/better-reflection": "6.44.0.6", "phpstan/php-8-stubs": "0.4.8", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", diff --git a/composer.lock b/composer.lock index 7441a9d501..7cd32ee1ad 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "53bd280e3693edf29e8871fa22ad20f7", + "content-hash": "aa42dc6e1a7a8d5fc9844d231515e30f", "packages": [ { "name": "clue/ndjson-react", @@ -2187,16 +2187,16 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.44.0.5", + "version": "6.44.0.6", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "b6d0a9adbe61544f6e8992611590a4e61487a638" + "reference": "d942fd0af0214bb1250a55c2560f061b7b0c4bd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/b6d0a9adbe61544f6e8992611590a4e61487a638", - "reference": "b6d0a9adbe61544f6e8992611590a4e61487a638", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/d942fd0af0214bb1250a55c2560f061b7b0c4bd4", + "reference": "d942fd0af0214bb1250a55c2560f061b7b0c4bd4", "shasum": "" }, "require": { @@ -2252,9 +2252,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.44.0.5" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.44.0.6" }, - "time": "2024-11-28T20:34:45+00:00" + "time": "2024-11-28T21:05:45+00:00" }, { "name": "phpstan/php-8-stubs", From e6dc705b29b90dcc5f9773377c05aecbfe9fba3a Mon Sep 17 00:00:00 2001 From: Jacob Tobiasz <80641364+jakubtobiasz@users.noreply.github.com> Date: Thu, 28 Nov 2024 23:00:31 +0100 Subject: [PATCH 27/76] Sanity checks around hooked properties in interfaces and classes --- .github/workflows/lint.yml | 2 +- Makefile | 10 ++ build/collision-detector.json | 2 +- conf/config.level0.neon | 1 + src/Node/ClassPropertyNode.php | 18 +++ src/Php/PhpVersion.php | 5 + .../Properties/PropertiesInInterfaceRule.php | 59 ++++++- src/Rules/Properties/PropertyInClassRule.php | 113 +++++++++++++ .../PropertiesInInterfaceRuleTest.php | 96 ++++++++++- .../Properties/PropertyInClassRuleTest.php | 150 ++++++++++++++++++ .../abstract-hooked-properties-in-class.php | 10 ++ ...abstract-hooked-properties-with-bodies.php | 26 +++ ...on-hooked-properties-in-abstract-class.php | 10 ++ .../data/hooked-properties-in-class.php | 11 ++ ...ked-properties-without-bodies-in-class.php | 10 ++ ...ct-hooked-properties-in-abstract-class.php | 35 ++++ ...on-abstract-hooked-properties-in-class.php | 10 ++ .../data/properties-in-interface.php | 2 + .../property-hooks-bodies-in-interface.php | 20 +++ .../data/property-hooks-in-interface.php | 10 ++ ...property-hooks-visibility-in-interface.php | 12 ++ 21 files changed, 600 insertions(+), 12 deletions(-) create mode 100644 src/Rules/Properties/PropertyInClassRule.php create mode 100644 tests/PHPStan/Rules/Properties/PropertyInClassRuleTest.php create mode 100644 tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-in-class.php create mode 100644 tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-with-bodies.php create mode 100644 tests/PHPStan/Rules/Properties/data/abstract-non-hooked-properties-in-abstract-class.php create mode 100644 tests/PHPStan/Rules/Properties/data/hooked-properties-in-class.php create mode 100644 tests/PHPStan/Rules/Properties/data/hooked-properties-without-bodies-in-class.php create mode 100644 tests/PHPStan/Rules/Properties/data/non-abstract-hooked-properties-in-abstract-class.php create mode 100644 tests/PHPStan/Rules/Properties/data/non-abstract-hooked-properties-in-class.php create mode 100644 tests/PHPStan/Rules/Properties/data/property-hooks-bodies-in-interface.php create mode 100644 tests/PHPStan/Rules/Properties/data/property-hooks-in-interface.php create mode 100644 tests/PHPStan/Rules/Properties/data/property-hooks-visibility-in-interface.php diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4680ddb82d..86ee004f07 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -116,7 +116,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.3" + php-version: "8.4" - name: "Install dependencies" run: "composer install --no-interaction --no-progress" diff --git a/Makefile b/Makefile index 3282ee2c4e..be44969c3c 100644 --- a/Makefile +++ b/Makefile @@ -76,6 +76,16 @@ lint: --exclude tests/PHPStan/Rules/Classes/data/extends-readonly-class.php \ --exclude tests/PHPStan/Rules/Classes/data/instantiation-promoted-properties.php \ --exclude tests/PHPStan/Rules/Classes/data/bug-11592.php \ + --exclude tests/PHPStan/Rules/Properties/data/property-hooks-bodies-in-interface.php \ + --exclude tests/PHPStan/Rules/Properties/data/property-hooks-in-interface.php \ + --exclude tests/PHPStan/Rules/Properties/data/property-hooks-visibility-in-interface.php \ + --exclude tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-in-class.php \ + --exclude tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-with-bodies.php \ + --exclude tests/PHPStan/Rules/Properties/data/abstract-non-hooked-properties-in-abstract-class.php \ + --exclude tests/PHPStan/Rules/Properties/data/non-abstract-hooked-properties-in-abstract-class.php \ + --exclude tests/PHPStan/Rules/Properties/data/non-abstract-hooked-properties-in-class.php \ + --exclude tests/PHPStan/Rules/Properties/data/hooked-properties-in-class.php \ + --exclude tests/PHPStan/Rules/Properties/data/hooked-properties-without-bodies-in-class.php \ src tests cs: diff --git a/build/collision-detector.json b/build/collision-detector.json index 21228704f5..12de9af1d3 100644 --- a/build/collision-detector.json +++ b/build/collision-detector.json @@ -12,5 +12,5 @@ "../tests/notAutoloaded", "../tests/PHPStan/Rules/Functions/data/define-bug-3349.php", "../tests/PHPStan/Levels/data/stubs/function.php" - ] + ] } diff --git a/conf/config.level0.neon b/conf/config.level0.neon index d4927a56c4..8c2a23a9d4 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -96,6 +96,7 @@ rules: - PHPStan\Rules\Properties\MissingReadOnlyByPhpDocPropertyAssignRule - PHPStan\Rules\Properties\PropertiesInInterfaceRule - PHPStan\Rules\Properties\PropertyAttributesRule + - PHPStan\Rules\Properties\PropertyInClassRule - PHPStan\Rules\Properties\ReadOnlyPropertyRule - PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyRule - PHPStan\Rules\Regexp\RegularExpressionPatternRule diff --git a/src/Node/ClassPropertyNode.php b/src/Node/ClassPropertyNode.php index 3f500b62c1..c8602aef79 100644 --- a/src/Node/ClassPropertyNode.php +++ b/src/Node/ClassPropertyNode.php @@ -111,6 +111,11 @@ public function isAllowedPrivateMutation(): bool return $this->isAllowedPrivateMutation; } + public function isAbstract(): bool + { + return (bool) ($this->flags & Modifiers::ABSTRACT); + } + public function getNativeType(): ?Type { return $this->type; @@ -142,4 +147,17 @@ public function getSubNodeNames(): array return []; } + public function hasHooks(): bool + { + return $this->getHooks() !== []; + } + + /** + * @return Node\PropertyHook[] + */ + public function getHooks(): array + { + return $this->originalNode->hooks; + } + } diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index 8520f6488d..e636945cc2 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -347,6 +347,11 @@ public function supportsPregCaptureOnlyNamedGroups(): bool return $this->versionId >= 80200; } + public function supportsPropertyHooks(): bool + { + return $this->versionId >= 80400; + } + public function hasDateTimeExceptions(): bool { return $this->versionId >= 80300; diff --git a/src/Rules/Properties/PropertiesInInterfaceRule.php b/src/Rules/Properties/PropertiesInInterfaceRule.php index d9f62fa618..df5354adb3 100644 --- a/src/Rules/Properties/PropertiesInInterfaceRule.php +++ b/src/Rules/Properties/PropertiesInInterfaceRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\ClassPropertyNode; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -14,6 +15,10 @@ final class PropertiesInInterfaceRule implements Rule { + public function __construct(private PhpVersion $phpVersion) + { + } + public function getNodeType(): string { return ClassPropertyNode::class; @@ -25,12 +30,54 @@ public function processNode(Node $node, Scope $scope): array return []; } - return [ - RuleErrorBuilder::message('Interfaces may not include properties.') - ->nonIgnorable() - ->identifier('property.inInterface') - ->build(), - ]; + if (!$this->phpVersion->supportsPropertyHooks()) { + return [ + RuleErrorBuilder::message('Interfaces cannot include properties.') + ->nonIgnorable() + ->identifier('property.inInterface') + ->build(), + ]; + } + + if (!$node->hasHooks()) { + return [ + RuleErrorBuilder::message('Interfaces can only include hooked properties.') + ->nonIgnorable() + ->identifier('property.nonHookedInInterface') + ->build(), + ]; + } + + if (!$node->isPublic()) { + return [ + RuleErrorBuilder::message('Interfaces cannot include non-public properties.') + ->nonIgnorable() + ->identifier('property.nonPublicInInterface') + ->build(), + ]; + } + + if ($this->hasAnyHookBody($node)) { + return [ + RuleErrorBuilder::message('Interfaces cannot include property hooks with bodies.') + ->nonIgnorable() + ->identifier('property.hookBodyInInterface') + ->build(), + ]; + } + + return []; + } + + private function hasAnyHookBody(ClassPropertyNode $node): bool + { + foreach ($node->getHooks() as $hook) { + if ($hook->body !== null) { + return true; + } + } + + return false; } } diff --git a/src/Rules/Properties/PropertyInClassRule.php b/src/Rules/Properties/PropertyInClassRule.php new file mode 100644 index 0000000000..3500959541 --- /dev/null +++ b/src/Rules/Properties/PropertyInClassRule.php @@ -0,0 +1,113 @@ + + */ +final class PropertyInClassRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return ClassPropertyNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + + if (!$classReflection->isClass()) { + return []; + } + + if (!$this->phpVersion->supportsPropertyHooks()) { + if ($node->hasHooks()) { + return [ + RuleErrorBuilder::message('Property hooks are supported only on PHP 8.4 and later.') + ->nonIgnorable() + ->identifier('property.hooksNotSupported') + ->build(), + ]; + } + + return []; + } + + if ($node->isAbstract()) { + if (!$node->hasHooks()) { + return [ + RuleErrorBuilder::message('Only hooked properties can be declared abstract.') + ->nonIgnorable() + ->identifier('property.abstractNonHooked') + ->build(), + ]; + } + + if (!$this->isAtLeastOneHookBodyEmpty($node)) { + return [ + RuleErrorBuilder::message('Abstract properties must specify at least one abstract hook.') + ->nonIgnorable() + ->identifier('property.abstractWithoutAbstractHook') + ->build(), + ]; + } + + if (!$classReflection->isAbstract()) { + return [ + RuleErrorBuilder::message('Non-abstract classes cannot include abstract properties.') + ->nonIgnorable() + ->identifier('property.abstract') + ->build(), + ]; + } + + return []; + } + + if (!$this->doAllHooksHaveBody($node)) { + return [ + RuleErrorBuilder::message('Non-abstract properties cannot include hooks without bodies.') + ->nonIgnorable() + ->identifier('property.hookWithoutBody') + ->build(), + ]; + } + + return []; + } + + private function doAllHooksHaveBody(ClassPropertyNode $node): bool + { + foreach ($node->getHooks() as $hook) { + if ($hook->body === null) { + return false; + } + } + + return true; + } + + private function isAtLeastOneHookBodyEmpty(ClassPropertyNode $node): bool + { + foreach ($node->getHooks() as $hook) { + if ($hook->body === null) { + return true; + } + } + + return false; + } + +} diff --git a/tests/PHPStan/Rules/Properties/PropertiesInInterfaceRuleTest.php b/tests/PHPStan/Rules/Properties/PropertiesInInterfaceRuleTest.php index ecf4597d97..de425931da 100644 --- a/tests/PHPStan/Rules/Properties/PropertiesInInterfaceRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertiesInInterfaceRuleTest.php @@ -2,8 +2,10 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -13,21 +15,107 @@ class PropertiesInInterfaceRuleTest extends RuleTestCase protected function getRule(): Rule { - return new PropertiesInInterfaceRule(); + return new PropertiesInInterfaceRule(new PhpVersion(PHP_VERSION_ID)); } - public function testRule(): void + public function testPhp83AndPropertiesInInterface(): void { + if (PHP_VERSION_ID >= 80400) { + $this->markTestSkipped('Test requires PHP 8.3 or earlier.'); + } + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Property hooks cause syntax error on PHP 7.4'); + } + + $this->analyse([__DIR__ . '/data/properties-in-interface.php'], [ + [ + 'Interfaces cannot include properties.', + 7, + ], + [ + 'Interfaces cannot include properties.', + 9, + ], + [ + 'Interfaces cannot include properties.', + 11, + ], + ]); + } + + public function testPhp83AndPropertyHooksInInterface(): void + { + if (PHP_VERSION_ID >= 80400) { + $this->markTestSkipped('Test requires PHP 8.3 or earlier.'); + } + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Property hooks cause syntax error on PHP 7.4'); + } + + $this->analyse([__DIR__ . '/data/property-hooks-in-interface.php'], [ + [ + 'Interfaces cannot include properties.', + 7, + ], + [ + 'Interfaces cannot include properties.', + 9, + ], + ]); + } + + public function testPhp84AndPropertiesInInterface(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + $this->analyse([__DIR__ . '/data/properties-in-interface.php'], [ [ - 'Interfaces may not include properties.', + 'Interfaces can only include hooked properties.', + 9, + ], + [ + 'Interfaces can only include hooked properties.', + 11, + ], + ]); + } + + public function testPhp84AndNonPublicPropertyHooksInInterface(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/property-hooks-visibility-in-interface.php'], [ + [ + 'Interfaces cannot include non-public properties.', 7, ], [ - 'Interfaces may not include properties.', + 'Interfaces cannot include non-public properties.', 9, ], ]); } + public function testPhp84AndPropertyHooksWithBodiesInInterface(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/property-hooks-bodies-in-interface.php'], [ + [ + 'Interfaces cannot include property hooks with bodies.', + 7, + ], + [ + 'Interfaces cannot include property hooks with bodies.', + 13, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/PropertyInClassRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyInClassRuleTest.php new file mode 100644 index 0000000000..0b2ca5ba09 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/PropertyInClassRuleTest.php @@ -0,0 +1,150 @@ + + */ +class PropertyInClassRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PropertyInClassRule(new PhpVersion(PHP_VERSION_ID)); + } + + public function testPhpLessThan84AndHookedPropertiesInClass(): void + { + if (PHP_VERSION_ID >= 80400) { + $this->markTestSkipped('Test requires PHP 8.3 or earlier.'); + } + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Property hooks cause syntax error on PHP 7.4'); + } + + $this->analyse([__DIR__ . '/data/hooked-properties-in-class.php'], [ + [ + 'Property hooks are supported only on PHP 8.4 and later.', + 7, + ], + ]); + } + + public function testPhp84AndHookedPropertiesWithoutBodiesInClass(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/hooked-properties-without-bodies-in-class.php'], [ + [ + 'Non-abstract properties cannot include hooks without bodies.', + 7, + ], + [ + 'Non-abstract properties cannot include hooks without bodies.', + 9, + ], + ]); + } + + public function testPhp84AndNonAbstractHookedPropertiesInClass(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/non-abstract-hooked-properties-in-class.php'], [ + [ + 'Non-abstract properties cannot include hooks without bodies.', + 7, + ], + [ + 'Non-abstract properties cannot include hooks without bodies.', + 9, + ], + ]); + } + + public function testPhp84AndAbstractHookedPropertiesInClass(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/abstract-hooked-properties-in-class.php'], [ + [ + 'Non-abstract classes cannot include abstract properties.', + 7, + ], + [ + 'Non-abstract classes cannot include abstract properties.', + 9, + ], + ]); + } + + public function testPhp84AndNonAbstractHookedPropertiesInAbstractClass(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/non-abstract-hooked-properties-in-abstract-class.php'], [ + [ + 'Non-abstract properties cannot include hooks without bodies.', + 7, + ], + [ + 'Non-abstract properties cannot include hooks without bodies.', + 9, + ], + [ + 'Non-abstract properties cannot include hooks without bodies.', + 25, + ], + ]); + } + + public function testPhp84AndAbstractNonHookedPropertiesInAbstractClass(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/abstract-non-hooked-properties-in-abstract-class.php'], [ + [ + 'Only hooked properties can be declared abstract.', + 7, + ], + [ + 'Only hooked properties can be declared abstract.', + 9, + ], + ]); + } + + public function testPhp84AndAbstractHookedPropertiesWithBodies(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4 or later.'); + } + + $this->analyse([__DIR__ . '/data/abstract-hooked-properties-with-bodies.php'], [ + [ + 'Abstract properties must specify at least one abstract hook.', + 7, + ], + [ + 'Abstract properties must specify at least one abstract hook.', + 12, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-in-class.php b/tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-in-class.php new file mode 100644 index 0000000000..d035d36810 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/abstract-hooked-properties-in-class.php @@ -0,0 +1,10 @@ + $this->name; + set => $this->name = $value; + } + + public abstract string $lastName { + get => $this->lastName; + set => $this->lastName = $value; + } + + public abstract string $middleName { + get => $this->name; + set; + } + + public abstract string $familyName { + get; + set; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/abstract-non-hooked-properties-in-abstract-class.php b/tests/PHPStan/Rules/Properties/data/abstract-non-hooked-properties-in-abstract-class.php new file mode 100644 index 0000000000..b34e66a886 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/abstract-non-hooked-properties-in-abstract-class.php @@ -0,0 +1,10 @@ + $this->name; + set => $this->name = $value; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/hooked-properties-without-bodies-in-class.php b/tests/PHPStan/Rules/Properties/data/hooked-properties-without-bodies-in-class.php new file mode 100644 index 0000000000..dc839f0d2c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/hooked-properties-without-bodies-in-class.php @@ -0,0 +1,10 @@ + Date: Mon, 11 Nov 2024 17:15:04 +0100 Subject: [PATCH 28/76] get_defined_vars() return type contains known variables Co-Authored-By: Ruud Kamphuis --- conf/config.neon | 5 ++ ...DefinedVarsFunctionReturnTypeExtension.php | 46 +++++++++++++++++++ .../Analyser/nsrt/get-defined-vars.php | 46 +++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 src/Type/Php/GetDefinedVarsFunctionReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/nsrt/get-defined-vars.php diff --git a/conf/config.neon b/conf/config.neon index f8ad1af8b3..19b6388858 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1525,6 +1525,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\GetDefinedVarsFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\GetParentClassDynamicFunctionReturnTypeExtension tags: diff --git a/src/Type/Php/GetDefinedVarsFunctionReturnTypeExtension.php b/src/Type/Php/GetDefinedVarsFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..35999b424b --- /dev/null +++ b/src/Type/Php/GetDefinedVarsFunctionReturnTypeExtension.php @@ -0,0 +1,46 @@ +getName() === 'get_defined_vars'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + if ($scope->canAnyVariableExist()) { + return new ArrayType( + new StringType(), + new MixedType(), + ); + } + + $typeBuilder = ConstantArrayTypeBuilder::createEmpty(); + + foreach ($scope->getDefinedVariables() as $variable) { + $typeBuilder->setOffsetValueType(new ConstantStringType($variable), $scope->getVariableType($variable), false); + } + + foreach ($scope->getMaybeDefinedVariables() as $variable) { + $typeBuilder->setOffsetValueType(new ConstantStringType($variable), $scope->getVariableType($variable), true); + } + + return $typeBuilder->getArray(); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/get-defined-vars.php b/tests/PHPStan/Analyser/nsrt/get-defined-vars.php new file mode 100644 index 0000000000..345d54dbd3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/get-defined-vars.php @@ -0,0 +1,46 @@ +', get_defined_vars()); // any variable can exist + +function doFoo(int $param) { + $local = "foo"; + assertType('array{param: int, local: \'foo\'}', get_defined_vars()); + assertType('array{\'param\', \'local\'}', array_keys(get_defined_vars())); +} + +function doBar(int $param) { + global $global; + $local = "foo"; + assertType('array{param: int, global: mixed, local: \'foo\'}', get_defined_vars()); + assertType('array{\'param\', \'global\', \'local\'}', array_keys(get_defined_vars())); +} + +function doConditional(int $param) { + $local = "foo"; + if(true) { + $conditional = "bar"; + assertType('array{param: int, local: \'foo\', conditional: \'bar\'}', get_defined_vars()); + } else { + $other = "baz"; + assertType('array{param: int, local: \'foo\', other: \'baz\'}', get_defined_vars()); + } + assertType('array{param: int, local: \'foo\', conditional: \'bar\'}', get_defined_vars()); +} + +function doRandom(int $param) { + $local = "foo"; + if(rand(0, 1)) { + $random1 = "bar"; + assertType('array{param: int, local: \'foo\', random1: \'bar\'}', get_defined_vars()); + } else { + $random2 = "baz"; + assertType('array{param: int, local: \'foo\', random2: \'baz\'}', get_defined_vars()); + } + assertType('array{param: int, local: \'foo\', random2?: \'baz\', random1?: \'bar\'}', get_defined_vars()); +} From 5efffc694288b20e591709748a34aae7239e173e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 30 Nov 2024 07:55:27 +0100 Subject: [PATCH 29/76] Lazier return in `UnionType->isSuperTypeOfWithReason()` --- src/Type/UnionType.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 6f4cbf462b..14eb812267 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -245,10 +245,15 @@ public function isSuperTypeOfWithReason(Type $otherType): IsSuperTypeOfResult return $otherType->isSubTypeOfWithReason($this); } - $result = IsSuperTypeOfResult::createNo()->or(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOfWithReason($otherType), $this->types)); - if ($result->yes()) { - return $result; + $results = []; + foreach ($this->types as $innerType) { + $result = $innerType->isSuperTypeOfWithReason($otherType); + if ($result->yes()) { + return $result; + } + $results[] = $result; } + $result = IsSuperTypeOfResult::createNo()->or(...$results); if ($otherType instanceof TemplateUnionType) { return $result->or($otherType->isSubTypeOfWithReason($this)); From 25d712e9dd8ed236ff2b94a35e5add932b9a81a8 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Wed, 20 Nov 2024 20:48:04 +0800 Subject: [PATCH 30/76] Fix `iterator_to_array` return type with generators --- ...atorToArrayFunctionReturnTypeExtension.php | 9 +++++- .../Analyser/nsrt/iterator_to_array.php | 30 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php index 3eba789175..623e3c0bcd 100644 --- a/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php +++ b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php @@ -8,7 +8,9 @@ use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\ArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\ErrorType; use PHPStan\Type\IntegerType; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; use function strtolower; @@ -29,7 +31,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $traversableType = $scope->getType($arguments[0]->value); - $arrayKeyType = $traversableType->getIterableKeyType(); + $arrayKeyType = $traversableType->getIterableKeyType()->toArrayKey(); + + if ($arrayKeyType instanceof ErrorType) { + return new NeverType(true); + } + $isList = false; if (isset($arguments[1])) { diff --git a/tests/PHPStan/Analyser/nsrt/iterator_to_array.php b/tests/PHPStan/Analyser/nsrt/iterator_to_array.php index 64ecbdeb05..4c7ddbc2b0 100644 --- a/tests/PHPStan/Analyser/nsrt/iterator_to_array.php +++ b/tests/PHPStan/Analyser/nsrt/iterator_to_array.php @@ -2,6 +2,7 @@ namespace IteratorToArray; +use stdClass; use Traversable; use function iterator_to_array; use function PHPStan\Testing\assertType; @@ -31,4 +32,33 @@ public function testNotPreservingKeys(Traversable $foo) { assertType('list', iterator_to_array($foo, false)); } + + public function testBehaviorOnGenerators(): void + { + $generator1 = static function (): iterable { + yield 0 => 1; + yield true => 2; + yield 2 => 3; + yield null => 4; + }; + $generator2 = static function (): iterable { + yield 0 => 1; + yield 'a' => 2; + yield null => 3; + yield true => 4; + }; + + assertType('array<0|1|2|\'\', 1|2|3|4>', iterator_to_array($generator1())); + assertType('array<0|1|\'\'|\'a\', 1|2|3|4>', iterator_to_array($generator2())); + } + + public function testOnGeneratorsWithIllegalKeysForArray(): void + { + $illegalGenerator = static function (): iterable { + yield 'a' => 'b'; + yield new stdClass => 'c'; + }; + + assertType('*NEVER*', iterator_to_array($illegalGenerator())); + } } From bd4452864826dd690faac1ddc7f060c44fcea3da Mon Sep 17 00:00:00 2001 From: schlndh Date: Sat, 30 Nov 2024 09:01:04 +0100 Subject: [PATCH 31/76] skip param castable to X on non-arrays Fixes bug 12146 --- src/Rules/ParameterCastableToStringCheck.php | 8 +-- ...plodeParameterCastableToStringRuleTest.php | 12 ++++- .../ParameterCastableToNumberRuleTest.php | 16 +++++- .../ParameterCastableToStringRuleTest.php | 16 +++++- .../SortParameterCastableToStringRuleTest.php | 12 ++++- .../Rules/Functions/data/bug-12146.php | 49 +++++++++++++++++++ 6 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-12146.php diff --git a/src/Rules/ParameterCastableToStringCheck.php b/src/Rules/ParameterCastableToStringCheck.php index 2d4dea0dbd..9753863557 100644 --- a/src/Rules/ParameterCastableToStringCheck.php +++ b/src/Rules/ParameterCastableToStringCheck.php @@ -36,11 +36,13 @@ public function checkParameter( $scope, $parameter->value, '', - static fn (Type $type): bool => !$castFn($type->getIterableValueType()) instanceof ErrorType, + static fn (Type $type): bool => $type->isArray()->yes() && !$castFn($type->getIterableValueType()) instanceof ErrorType, ); - if ($typeResult->getType() instanceof ErrorType - || !$castFn($typeResult->getType()->getIterableValueType()) instanceof ErrorType) { + if ( + ! $typeResult->getType()->isArray()->yes() + || !$castFn($typeResult->getType()->getIterableValueType()) instanceof ErrorType + ) { return null; } diff --git a/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php b/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php index 8111e9a965..65e4714487 100644 --- a/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php @@ -17,7 +17,7 @@ class ImplodeParameterCastableToStringRuleTest extends RuleTestCase protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new ImplodeParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, false, false, false))); + return new ImplodeParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, true, true, false))); } public function testNamedArguments(): void @@ -100,4 +100,14 @@ public function testBug8467a(): void $this->analyse([__DIR__ . '/../Arrays/data/bug-8467a.php'], []); } + public function testBug12146(): void + { + $this->analyse([__DIR__ . '/data/bug-12146.php'], [ + [ + 'Parameter #2 $array of function implode expects array, array given.', + 28, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php b/tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php index 77b64c3a30..e4708c5f4c 100644 --- a/tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ParameterCastableToNumberRuleTest.php @@ -19,7 +19,7 @@ class ParameterCastableToNumberRuleTest extends RuleTestCase protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new ParameterCastableToNumberRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, false, false, false))); + return new ParameterCastableToNumberRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, true, true, false))); } public function testRule(): void @@ -130,6 +130,20 @@ public function testBug11883(): void ]); } + public function testBug12146(): void + { + $this->analyse([__DIR__ . '/data/bug-12146.php'], $this->hackPhp74ErrorMessages([ + [ + 'Parameter #1 $array of function array_sum expects an array of values castable to number, array given.', + 16, + ], + [ + 'Parameter #1 $array of function array_product expects an array of values castable to number, array given.', + 22, + ], + ])); + } + /** * @param list $errors * @return list diff --git a/tests/PHPStan/Rules/Functions/ParameterCastableToStringRuleTest.php b/tests/PHPStan/Rules/Functions/ParameterCastableToStringRuleTest.php index 83b0f40567..0ac6304231 100644 --- a/tests/PHPStan/Rules/Functions/ParameterCastableToStringRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ParameterCastableToStringRuleTest.php @@ -19,7 +19,7 @@ class ParameterCastableToStringRuleTest extends RuleTestCase protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new ParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, false, false, false))); + return new ParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, true, true, false))); } public function testRule(): void @@ -196,6 +196,20 @@ public function testBug11141(): void ]); } + public function testBug12146(): void + { + $this->analyse([__DIR__ . '/data/bug-12146.php'], $this->hackParameterNames([ + [ + 'Parameter #1 $array of function array_intersect expects an array of values castable to string, array given.', + 34, + ], + [ + 'Parameter #1 $keys of function array_fill_keys expects an array of values castable to string, array given.', + 40, + ], + ])); + } + /** * @param list $errors * @return list diff --git a/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php b/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php index 8c0105a424..2fc8264821 100644 --- a/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php +++ b/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php @@ -19,7 +19,7 @@ class SortParameterCastableToStringRuleTest extends RuleTestCase protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new SortParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, false, false, false))); + return new SortParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, true, true, false))); } public function testRule(): void @@ -145,6 +145,16 @@ public function testBug11167(): void $this->analyse([__DIR__ . '/data/bug-11167.php'], []); } + public function testBug12146(): void + { + $this->analyse([__DIR__ . '/data/bug-12146.php'], $this->hackParameterNames([ + [ + 'Parameter #1 $array of function array_unique expects an array of values castable to string, array given.', + 46, + ], + ])); + } + /** * @param list $errors * @return list diff --git a/tests/PHPStan/Rules/Functions/data/bug-12146.php b/tests/PHPStan/Rules/Functions/data/bug-12146.php new file mode 100644 index 0000000000..bd0bf858c3 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-12146.php @@ -0,0 +1,49 @@ +|array $validArrayUnion valid + * @param array|array<\stdClass> $invalidArrayUnion invalid, report + * @param ?array<\stdClass> $nullableInvalidArray invalid, but don't report because it's reported by CallToFunctionParametersRule + * @param array<\stdClass>|\SplFixedArray $arrayOrSplArray invalid, but don't report because it's reported by CallToFunctionParametersRule + * @return void + */ +function foo($mixed, $validArrayUnion, $invalidArrayUnion, $nullableInvalidArray, $arrayOrSplArray) { + var_dump(array_sum($mixed)); + var_dump(array_sum($validArrayUnion)); + var_dump(array_sum($invalidArrayUnion)); + var_dump(array_sum($nullableInvalidArray)); + var_dump(array_sum($arrayOrSplArray)); + + var_dump(array_product($mixed)); + var_dump(array_product($validArrayUnion)); + var_dump(array_product($invalidArrayUnion)); + var_dump(array_product($nullableInvalidArray)); + var_dump(array_product($arrayOrSplArray)); + + var_dump(implode(',', $mixed)); + var_dump(implode(',', $validArrayUnion)); + var_dump(implode(',', $invalidArrayUnion)); + var_dump(implode(',', $nullableInvalidArray)); + var_dump(implode(',', $arrayOrSplArray)); + + var_dump(array_intersect($mixed, [5])); + var_dump(array_intersect($validArrayUnion, [5])); + var_dump(array_intersect($invalidArrayUnion, [5])); + var_dump(array_intersect($nullableInvalidArray, [5])); + var_dump(array_intersect($arrayOrSplArray, [5])); + + var_dump(array_fill_keys($mixed, 1)); + var_dump(array_fill_keys($validArrayUnion, 1)); + var_dump(array_fill_keys($invalidArrayUnion, 1)); + var_dump(array_fill_keys($nullableInvalidArray, 1)); + var_dump(array_fill_keys($arrayOrSplArray, 1)); + + var_dump(array_unique($mixed)); + var_dump(array_unique($validArrayUnion)); + var_dump(array_unique($invalidArrayUnion)); + var_dump(array_unique($nullableInvalidArray)); + var_dump(array_unique($arrayOrSplArray)); +} From 48f899084c0786d84ae86463bfc25711e4aacd36 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 30 Nov 2024 14:49:14 +0100 Subject: [PATCH 32/76] 5x Faster `IntersectionType->getEnumCases()` --- src/Type/IntersectionType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index d41b79e951..519649a384 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -842,7 +842,7 @@ public function getEnumCases(): array foreach ($this->types as $type) { $oneType = []; foreach ($type->getEnumCases() as $enumCase) { - $oneType[md5($enumCase->describe(VerbosityLevel::typeOnly()))] = $enumCase; + $oneType[$enumCase->getClassName() . '::' . $enumCase->getEnumCaseName()] = $enumCase; } $compare[] = $oneType; } From c5860144ae7c00bc860ae8eb41d7343137829467 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 30 Nov 2024 16:09:56 +0100 Subject: [PATCH 33/76] `MixedType::toArrayKey()` returns BenevolentUnionType --- src/Type/MixedType.php | 2 +- .../Analyser/LegacyNodeScopeResolverTest.php | 4 ++-- tests/PHPStan/Analyser/nsrt/array-column-php82.php | 12 ++++++------ tests/PHPStan/Analyser/nsrt/array-column.php | 14 +++++++------- tests/PHPStan/Analyser/nsrt/array-flip-php7.php | 2 +- tests/PHPStan/Analyser/nsrt/array-flip-php8.php | 2 +- tests/PHPStan/Analyser/nsrt/array-flip.php | 2 +- tests/PHPStan/Analyser/nsrt/non-empty-array.php | 2 +- .../data/slevomat-foreach-array-key-exists-bug.php | 10 +++++----- 9 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 3ae9434a95..6ca4e4bad8 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -600,7 +600,7 @@ public function toArray(): Type public function toArrayKey(): Type { - return new UnionType([new IntegerType(), new StringType()]); + return new BenevolentUnionType([new IntegerType(), new StringType()]); } public function isIterable(): TrinaryLogic diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 5434782186..42b0d24959 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -3328,7 +3328,7 @@ public function dataLiteralArraysKeys(): array "'BooleansArray'", ], [ - 'int|string', + '(int|string)', "'UnknownConstantArray'", ], ]; @@ -9147,7 +9147,7 @@ public function dataGeneralizeScope(): array { return [ [ - 'array, removeCount: int<0, max>, loadCount: int<0, max>, hitCount: int<0, max>}>>', + 'array, removeCount: int<0, max>, loadCount: int<0, max>, hitCount: int<0, max>}>>', '$statistics', ], ]; diff --git a/tests/PHPStan/Analyser/nsrt/array-column-php82.php b/tests/PHPStan/Analyser/nsrt/array-column-php82.php index 62350f5992..d53ab61f00 100644 --- a/tests/PHPStan/Analyser/nsrt/array-column-php82.php +++ b/tests/PHPStan/Analyser/nsrt/array-column-php82.php @@ -177,8 +177,8 @@ public function testImprecise5(array $array): void assertType('array', array_column($array, null, 'tagName')); assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); - assertType('array', array_column($array, 'nodeName', 'foo')); - assertType('array', array_column($array, null, 'foo')); + assertType('array', array_column($array, 'nodeName', 'foo')); + assertType('array', array_column($array, null, 'foo')); } /** @param non-empty-array $array */ @@ -189,8 +189,8 @@ public function testObjects1(array $array): void assertType('non-empty-array', array_column($array, null, 'tagName')); assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); - assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); - assertType('non-empty-array', array_column($array, null, 'foo')); + assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); + assertType('non-empty-array', array_column($array, null, 'foo')); } /** @param array{DOMElement} $array */ @@ -201,8 +201,8 @@ public function testObjects2(array $array): void assertType('non-empty-array', array_column($array, null, 'tagName')); assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); - assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); - assertType('non-empty-array', array_column($array, null, 'foo')); + assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); + assertType('non-empty-array', array_column($array, null, 'foo')); } } diff --git a/tests/PHPStan/Analyser/nsrt/array-column.php b/tests/PHPStan/Analyser/nsrt/array-column.php index 2455d6ace9..3e3cedbbff 100644 --- a/tests/PHPStan/Analyser/nsrt/array-column.php +++ b/tests/PHPStan/Analyser/nsrt/array-column.php @@ -191,8 +191,8 @@ public function testImprecise5(array $array): void assertType('array', array_column($array, null, 'tagName')); assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); - assertType('array', array_column($array, 'nodeName', 'foo')); - assertType('array', array_column($array, null, 'foo')); + assertType('array', array_column($array, 'nodeName', 'foo')); + assertType('array', array_column($array, null, 'foo')); } /** @param non-empty-array $array */ @@ -203,8 +203,8 @@ public function testObjects1(array $array): void assertType('non-empty-array', array_column($array, null, 'tagName')); assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); - assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); - assertType('non-empty-array', array_column($array, null, 'foo')); + assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); + assertType('non-empty-array', array_column($array, null, 'foo')); } /** @param array{DOMElement} $array */ @@ -215,8 +215,8 @@ public function testObjects2(array $array): void assertType('non-empty-array', array_column($array, null, 'tagName')); assertType('list', array_column($array, 'foo')); assertType('array', array_column($array, 'foo', 'tagName')); - assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); - assertType('non-empty-array', array_column($array, null, 'foo')); + assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); + assertType('non-empty-array', array_column($array, null, 'foo')); } } @@ -228,7 +228,7 @@ final class Foo public function doFoo(array $a): void { assertType('list', array_column($a, 'nodeName')); - assertType('array', array_column($a, 'nodeName', 'tagName')); + assertType('array', array_column($a, 'nodeName', 'tagName')); } } diff --git a/tests/PHPStan/Analyser/nsrt/array-flip-php7.php b/tests/PHPStan/Analyser/nsrt/array-flip-php7.php index 0b7058de01..0f14074f5c 100644 --- a/tests/PHPStan/Analyser/nsrt/array-flip-php7.php +++ b/tests/PHPStan/Analyser/nsrt/array-flip-php7.php @@ -7,7 +7,7 @@ function mixedAndSubtractedArray($mixed) { if (is_array($mixed)) { - assertType('array', array_flip($mixed)); + assertType('array<(int|string)>', array_flip($mixed)); } else { assertType('mixed~array', $mixed); assertType('null', array_flip($mixed)); diff --git a/tests/PHPStan/Analyser/nsrt/array-flip-php8.php b/tests/PHPStan/Analyser/nsrt/array-flip-php8.php index b8f0e6793d..be439d0427 100644 --- a/tests/PHPStan/Analyser/nsrt/array-flip-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-flip-php8.php @@ -7,7 +7,7 @@ function mixedAndSubtractedArray($mixed) { if (is_array($mixed)) { - assertType('array', array_flip($mixed)); + assertType('array<(int|string)>', array_flip($mixed)); } else { assertType('mixed~array', $mixed); assertType('*NEVER*', array_flip($mixed)); diff --git a/tests/PHPStan/Analyser/nsrt/array-flip.php b/tests/PHPStan/Analyser/nsrt/array-flip.php index 2f02f1e733..b6a6eb6c66 100644 --- a/tests/PHPStan/Analyser/nsrt/array-flip.php +++ b/tests/PHPStan/Analyser/nsrt/array-flip.php @@ -20,7 +20,7 @@ function foo3($list) { $flip = array_flip($list); - assertType('array', $flip); + assertType('array<(int|string)>', $flip); } /** diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-array.php b/tests/PHPStan/Analyser/nsrt/non-empty-array.php index a7cdc6540a..9f95a09c0a 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-array.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-array.php @@ -54,7 +54,7 @@ public function arrayFunctions($array, $list, $stringArray): void assertType('non-empty-array', array_replace($array, [])); assertType('non-empty-array', array_replace($array, $array)); - assertType('non-empty-array', array_flip($array)); + assertType('non-empty-array<(int|string)>', array_flip($array)); assertType('non-empty-array', array_flip($stringArray)); } } diff --git a/tests/PHPStan/Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php b/tests/PHPStan/Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php index bddbb8f06d..0588be365b 100644 --- a/tests/PHPStan/Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php +++ b/tests/PHPStan/Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php @@ -15,26 +15,26 @@ public function doFoo(array $percentageIntervals, array $changes): void if ($percentageInterval->isInInterval((float) $changeInPercents)) { $key = $percentageInterval->getFormatted(); if (array_key_exists($key, $intervalResults)) { - assertType('array', $intervalResults); + assertType('array', $intervalResults); assertType('array{itemsCount: mixed, interval: mixed}', $intervalResults[$key]); $intervalResults[$key]['itemsCount'] += $itemsCount; - assertType('non-empty-array', $intervalResults); + assertType('non-empty-array', $intervalResults); assertType('array{itemsCount: (array|float|int), interval: mixed}', $intervalResults[$key]); } else { - assertType('array', $intervalResults); + assertType('array', $intervalResults); assertType('array{itemsCount: mixed, interval: mixed}', $intervalResults[$key]); $intervalResults[$key] = [ 'itemsCount' => $itemsCount, 'interval' => $percentageInterval, ]; - assertType('non-empty-array', $intervalResults); + assertType('non-empty-array', $intervalResults); assertType('array{itemsCount: mixed, interval: mixed}', $intervalResults[$key]); } } } } - assertType('array', $intervalResults); + assertType('array', $intervalResults); foreach ($intervalResults as $data) { echo $data['interval']; } From f8d27d5a803d7923a60e267cc8a78bfc2b8b9e10 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Mon, 2 Dec 2024 00:22:35 +0000 Subject: [PATCH 34/76] Update PHP 8 stubs --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 0f28344835..5e0a1f1e01 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", "ondrejmirtes/better-reflection": "6.44.0.6", - "phpstan/php-8-stubs": "0.4.8", + "phpstan/php-8-stubs": "0.4.9", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", "react/async": "^3", diff --git a/composer.lock b/composer.lock index 7cd32ee1ad..22c9a79b6e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "aa42dc6e1a7a8d5fc9844d231515e30f", + "content-hash": "db7c74816a1b1cd707c98ae62551ce35", "packages": [ { "name": "clue/ndjson-react", @@ -2258,16 +2258,16 @@ }, { "name": "phpstan/php-8-stubs", - "version": "0.4.8", + "version": "0.4.9", "source": { "type": "git", "url": "https://github.com/phpstan/php-8-stubs.git", - "reference": "8a6278b2b9c9781cb969c4128da361b78eead604" + "reference": "1857c330fea6e795af1f7435ed02a18652e7dd8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/8a6278b2b9c9781cb969c4128da361b78eead604", - "reference": "8a6278b2b9c9781cb969c4128da361b78eead604", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/1857c330fea6e795af1f7435ed02a18652e7dd8c", + "reference": "1857c330fea6e795af1f7435ed02a18652e7dd8c", "shasum": "" }, "type": "library", @@ -2284,9 +2284,9 @@ "description": "PHP stubs extracted from php-src", "support": { "issues": "https://github.com/phpstan/php-8-stubs/issues", - "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.8" + "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.9" }, - "time": "2024-11-28T00:20:48+00:00" + "time": "2024-12-02T00:21:59+00:00" }, { "name": "phpstan/phpdoc-parser", From 90933b318e332c88f675c8aa946cdfb5788a51fe Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Mon, 2 Dec 2024 22:54:08 +0200 Subject: [PATCH 35/76] Remove incorrect CURLOPT_ACCEPT_ENCODING alias --- src/Reflection/ParametersAcceptorSelector.php | 3 +-- .../CallToFunctionParametersRuleTest.php | 26 ++++++++++++------- .../Rules/Functions/data/curl_setopt.php | 4 +++ 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 82af7d4267..703d345806 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -919,7 +919,6 @@ private static function getCurlOptValueType(int $curlOpt): ?Type } $nullableStringConstants = [ - 'CURLOPT_ACCEPT_ENCODING', 'CURLOPT_CUSTOMREQUEST', 'CURLOPT_DNS_INTERFACE', 'CURLOPT_DNS_LOCAL_IP4', @@ -1032,7 +1031,7 @@ private static function getCurlOptValueType(int $curlOpt): ?Type $stringConstants = [ 'CURLOPT_COOKIEFILE', - 'CURLOPT_ENCODING', + 'CURLOPT_ENCODING', // Alias: CURLOPT_ACCEPT_ENCODING 'CURLOPT_PRE_PROXY', 'CURLOPT_PRIVATE', 'CURLOPT_PROXY', diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index af3d4af2c3..89017fe2cf 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1321,40 +1321,48 @@ public function testCurlSetOpt(): void 18, ], [ - 'Parameter #3 $value of function curl_setopt expects bool, int given.', + 'Parameter #3 $value of function curl_setopt expects string, int given.', + 19, + ], + [ + 'Parameter #3 $value of function curl_setopt expects string, int given.', 20, ], + [ + 'Parameter #3 $value of function curl_setopt expects bool, int given.', + 22, + ], [ 'Parameter #3 $value of function curl_setopt expects bool, string given.', - 21, + 23, ], [ 'Parameter #3 $value of function curl_setopt expects int, string given.', - 23, + 25, ], [ 'Parameter #3 $value of function curl_setopt expects array, string given.', - 25, + 27, ], [ 'Parameter #3 $value of function curl_setopt expects resource, string given.', - 27, + 29, ], [ 'Parameter #3 $value of function curl_setopt expects array|string, int given.', - 29, + 31, ], [ 'Parameter #3 $value of function curl_setopt expects non-empty-string, \'\' given.', - 31, + 33, ], [ 'Parameter #3 $value of function curl_setopt expects non-empty-string|null, \'\' given.', - 32, + 34, ], [ 'Parameter #3 $value of function curl_setopt expects array, array given.', - 73, + 77, ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/curl_setopt.php b/tests/PHPStan/Rules/Functions/data/curl_setopt.php index 24987ddbc9..bc5b8f6cea 100644 --- a/tests/PHPStan/Rules/Functions/data/curl_setopt.php +++ b/tests/PHPStan/Rules/Functions/data/curl_setopt.php @@ -16,6 +16,8 @@ public function errors(int $i, string $s) { curl_setopt($curl, CURLOPT_URL, $i); curl_setopt($curl, CURLOPT_HTTPHEADER, $i); curl_setopt($curl, CURLOPT_ABSTRACT_UNIX_SOCKET, null); + curl_setopt($curl, CURLOPT_ENCODING, $i); + curl_setopt($curl, CURLOPT_ACCEPT_ENCODING, $i); // expecting bool curl_setopt($curl, CURLOPT_AUTOREFERER, $i); curl_setopt($curl, CURLOPT_RETURNTRANSFER, $s); @@ -62,6 +64,8 @@ public function allGood(string $url, array $header) { curl_setopt($curl, CURLOPT_PRE_PROXY, ''); curl_setopt($curl, CURLOPT_PROXY, ''); curl_setopt($curl, CURLOPT_PRIVATE, ''); + curl_setopt($curl, CURLOPT_ENCODING, ''); + curl_setopt($curl, CURLOPT_ACCEPT_ENCODING, ''); } public function bug9263() { From 28dfac80bb4a05891ad4da675677c2077098d080 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Tue, 3 Dec 2024 00:03:57 +0000 Subject: [PATCH 36/76] Update PhpStorm stubs --- composer.json | 2 +- composer.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 5e0a1f1e01..c678b5998f 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", "hoa/file": "1.17.07.11", - "jetbrains/phpstorm-stubs": "dev-master#7809499af1a5f3bdd20f1cab71717a2a9e1f8cf7", + "jetbrains/phpstorm-stubs": "dev-master#bb981ec60b3838e56473a078edf7d0739ca20403", "nette/bootstrap": "^3.0", "nette/di": "^3.1.4", "nette/neon": "3.3.4", diff --git a/composer.lock b/composer.lock index 22c9a79b6e..8557d36dae 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "db7c74816a1b1cd707c98ae62551ce35", + "content-hash": "ee384a5c11fbc1dd087ab8dc956b8d73", "packages": [ { "name": "clue/ndjson-react", @@ -1442,12 +1442,12 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "7809499af1a5f3bdd20f1cab71717a2a9e1f8cf7" + "reference": "bb981ec60b3838e56473a078edf7d0739ca20403" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/7809499af1a5f3bdd20f1cab71717a2a9e1f8cf7", - "reference": "7809499af1a5f3bdd20f1cab71717a2a9e1f8cf7", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/bb981ec60b3838e56473a078edf7d0739ca20403", + "reference": "bb981ec60b3838e56473a078edf7d0739ca20403", "shasum": "" }, "require-dev": { @@ -1482,7 +1482,7 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2024-11-27T14:37:09+00:00" + "time": "2024-11-27T16:45:26+00:00" }, { "name": "nette/bootstrap", From fbcad414543b6f6fae4acc2801de21bfa1348887 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Thu, 5 Dec 2024 12:44:05 +0100 Subject: [PATCH 37/76] Fix `fgetcsv` return type; never returns null --- resources/functionMap_php80delta.php | 1 + tests/PHPStan/Analyser/nsrt/fgetcsv-php7.php | 12 ++++++++++++ tests/PHPStan/Analyser/nsrt/fgetcsv-php8.php | 12 ++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/fgetcsv-php7.php create mode 100644 tests/PHPStan/Analyser/nsrt/fgetcsv-php8.php diff --git a/resources/functionMap_php80delta.php b/resources/functionMap_php80delta.php index bb267a5e49..879dc310e4 100644 --- a/resources/functionMap_php80delta.php +++ b/resources/functionMap_php80delta.php @@ -46,6 +46,7 @@ 'error_log' => ['bool', 'message'=>'string', 'message_type='=>'0|1|3|4', 'destination='=>'string', 'extra_headers='=>'string'], 'explode' => ['list', 'separator'=>'non-empty-string', 'str'=>'string', 'limit='=>'int'], 'fdiv' => ['float', 'dividend'=>'float', 'divisor'=>'float'], + 'fgetcsv' => ['list|array{0: null}|false', 'fp'=>'resource', 'length='=>'0|positive-int|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'filter_input' => ['mixed', 'type'=>'INPUT_GET|INPUT_POST|INPUT_COOKIE|INPUT_SERVER|INPUT_ENV', 'variable_name'=>'string', 'filter='=>'int', 'options='=>'array|int'], 'filter_input_array' => ['array|false|null', 'type'=>'INPUT_GET|INPUT_POST|INPUT_COOKIE|INPUT_SERVER|INPUT_ENV', 'definition='=>'int|array', 'add_empty='=>'bool'], 'floor' => ['float', 'number'=>'float'], diff --git a/tests/PHPStan/Analyser/nsrt/fgetcsv-php7.php b/tests/PHPStan/Analyser/nsrt/fgetcsv-php7.php new file mode 100644 index 0000000000..8a38465040 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/fgetcsv-php7.php @@ -0,0 +1,12 @@ +|false|null', fgetcsv($resource)); // nullable when invalid argument is given (https://3v4l.org/4WmR5#v7.4.30) +} diff --git a/tests/PHPStan/Analyser/nsrt/fgetcsv-php8.php b/tests/PHPStan/Analyser/nsrt/fgetcsv-php8.php new file mode 100644 index 0000000000..fccf29931c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/fgetcsv-php8.php @@ -0,0 +1,12 @@ += 8.0 + +declare(strict_types = 1); + +namespace TestFGetCsvPhp8; + +use function PHPStan\Testing\assertType; + +function test($resource): void +{ + assertType('list|false', fgetcsv($resource)); +} From 7b4c9afd090d89d595eb113831bc4b79b45d22e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Steinbrink?= Date: Thu, 5 Dec 2024 12:15:02 +0100 Subject: [PATCH 38/76] Workaround bug in slevomat/coding-standard TypeNameMatchesFileName For each root namespace, the slevomat rule considers the left-most match of the given directory in the absolute path of the file. That is, for /home/user/src/phpstan-src/ the root namespace PHPStan is not assigned to /home/user/src/phpstan-src/src, but to /home/user/src, which is obviously wrong. The bug is known as slevomat/coding-standard#1249 for a long time, but yet to be fixed. To avoid issues for developers of PHPStan, we can set a basepath of "." in the PHP CodeSniffer config, which causes paths to be evaluated relative to the current directory, avoiding false-positives in the path leading up to the phpstan-src directory. --- phpcs.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/phpcs.xml b/phpcs.xml index 7ec0a21da2..fa9198745f 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,6 +1,7 @@ + From 4f142f59424b7d49856fdba9d70ac4d3ef184d36 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Fri, 6 Dec 2024 20:09:22 +0800 Subject: [PATCH 39/76] Fix `iterator_to_array` to early return when `$preserveKeys` is false --- ...atorToArrayFunctionReturnTypeExtension.php | 27 ++++++++----------- .../Analyser/nsrt/iterator_to_array.php | 3 +++ 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php index 623e3c0bcd..d1dc17f176 100644 --- a/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php +++ b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php @@ -31,33 +31,28 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $traversableType = $scope->getType($arguments[0]->value); - $arrayKeyType = $traversableType->getIterableKeyType()->toArrayKey(); - - if ($arrayKeyType instanceof ErrorType) { - return new NeverType(true); - } - - $isList = false; if (isset($arguments[1])) { $preserveKeysType = $scope->getType($arguments[1]->value); if ($preserveKeysType->isFalse()->yes()) { - $arrayKeyType = new IntegerType(); - $isList = true; + return AccessoryArrayListType::intersectWith(new ArrayType( + new IntegerType(), + $traversableType->getIterableValueType(), + )); } } - $arrayType = new ArrayType( - $arrayKeyType, - $traversableType->getIterableValueType(), - ); + $arrayKeyType = $traversableType->getIterableKeyType()->toArrayKey(); - if ($isList) { - $arrayType = AccessoryArrayListType::intersectWith($arrayType); + if ($arrayKeyType instanceof ErrorType) { + return new NeverType(true); } - return $arrayType; + return new ArrayType( + $arrayKeyType, + $traversableType->getIterableValueType(), + ); } } diff --git a/tests/PHPStan/Analyser/nsrt/iterator_to_array.php b/tests/PHPStan/Analyser/nsrt/iterator_to_array.php index 4c7ddbc2b0..038b36f7fd 100644 --- a/tests/PHPStan/Analyser/nsrt/iterator_to_array.php +++ b/tests/PHPStan/Analyser/nsrt/iterator_to_array.php @@ -49,7 +49,9 @@ public function testBehaviorOnGenerators(): void }; assertType('array<0|1|2|\'\', 1|2|3|4>', iterator_to_array($generator1())); + assertType('list<1|2|3|4>', iterator_to_array($generator1(), false)); assertType('array<0|1|\'\'|\'a\', 1|2|3|4>', iterator_to_array($generator2())); + assertType('list<1|2|3|4>', iterator_to_array($generator2(), false)); } public function testOnGeneratorsWithIllegalKeysForArray(): void @@ -60,5 +62,6 @@ public function testOnGeneratorsWithIllegalKeysForArray(): void }; assertType('*NEVER*', iterator_to_array($illegalGenerator())); + assertType('list<\'b\'|\'c\'>', iterator_to_array($illegalGenerator(), false)); } } From 9a718ee52c07c6b62c0eceac3bf170ac45740b6a Mon Sep 17 00:00:00 2001 From: Jack Worman Date: Wed, 4 Dec 2024 20:26:32 -0500 Subject: [PATCH 40/76] Fix-GH-12021 --- ...mptyStringFunctionsReturnTypeExtension.php | 30 +++++++++++++++++++ .../Analyser/nsrt/non-empty-string.php | 9 ++++++ .../Analyser/nsrt/non-falsy-string.php | 5 ++++ 3 files changed, 44 insertions(+) diff --git a/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php b/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php index 083914085a..893627aae2 100644 --- a/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php +++ b/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php @@ -2,17 +2,20 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use function count; use function in_array; +use const ENT_SUBSTITUTE; final class NonEmptyStringFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -45,6 +48,15 @@ public function getTypeFromFunctionCall( return null; } + if (in_array($functionReflection->getName(), [ + 'htmlspecialchars', + 'htmlentities', + ], true)) { + if (!$this->isSubstituteFlagSet($args, $scope)) { + return new StringType(); + } + } + $argType = $scope->getType($args[0]->value); if ($argType->isNonFalsyString()->yes()) { return new IntersectionType([ @@ -62,4 +74,22 @@ public function getTypeFromFunctionCall( return new StringType(); } + /** + * @param Arg[] $args + */ + private function isSubstituteFlagSet( + array $args, + Scope $scope, + ): bool + { + if (!isset($args[1])) { + return true; + } + $flagsType = $scope->getType($args[1]->value); + if (!$flagsType instanceof ConstantIntegerType) { + return false; + } + return (bool) ($flagsType->getValue() & ENT_SUBSTITUTE); + } + } diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string.php b/tests/PHPStan/Analyser/nsrt/non-empty-string.php index cd831db4d8..59d1af3931 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string.php @@ -8,6 +8,7 @@ use function strtolower; use function strtoupper; use function ucfirst; +use const ENT_SUBSTITUTE; class Foo { @@ -333,9 +334,17 @@ public function doFoo(string $s, string $nonEmpty, string $nonFalsy, int $i, boo assertType('string', ucwords($s)); assertType('non-empty-string', ucwords($nonEmpty)); assertType('string', htmlspecialchars($s)); + assertType('string', htmlspecialchars($s, ENT_SUBSTITUTE)); + assertType('string', htmlspecialchars($s, 0)); assertType('non-empty-string', htmlspecialchars($nonEmpty)); + assertType('non-empty-string', htmlspecialchars($nonEmpty, ENT_SUBSTITUTE)); + assertType('string', htmlspecialchars($nonEmpty, 0)); assertType('string', htmlentities($s)); + assertType('string', htmlentities($s, ENT_SUBSTITUTE)); + assertType('string', htmlentities($s, 0)); assertType('non-empty-string', htmlentities($nonEmpty)); + assertType('non-empty-string', htmlentities($nonEmpty, ENT_SUBSTITUTE)); + assertType('string', htmlentities($nonEmpty, 0)); assertType('string', urlencode($s)); assertType('non-empty-string', urlencode($nonEmpty)); diff --git a/tests/PHPStan/Analyser/nsrt/non-falsy-string.php b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php index c5fd9fc1d8..744bdaf3b5 100644 --- a/tests/PHPStan/Analyser/nsrt/non-falsy-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php @@ -3,6 +3,7 @@ namespace NonFalseyString; use function PHPStan\Testing\assertType; +use const ENT_SUBSTITUTE; class Foo { /** @@ -95,7 +96,11 @@ function stringFunctions(string $s, $nonFalsey, $arrayOfNonFalsey, $nonEmptyArra assertType('non-falsy-string', ucfirst($nonFalsey)); assertType('non-falsy-string', ucwords($nonFalsey)); assertType('non-falsy-string', htmlspecialchars($nonFalsey)); + assertType('non-falsy-string', htmlspecialchars($nonFalsey, ENT_SUBSTITUTE)); + assertType('string', htmlspecialchars($nonFalsey, 0)); assertType('non-falsy-string', htmlentities($nonFalsey)); + assertType('non-falsy-string', htmlentities($nonFalsey, ENT_SUBSTITUTE)); + assertType('string', htmlentities($nonFalsey, 0)); assertType('non-falsy-string', urlencode($nonFalsey)); assertType('non-falsy-string', urldecode($nonFalsey)); From acb109f0c4ad8871c11d6df962a78255b313f463 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 9 Dec 2024 09:56:05 +0100 Subject: [PATCH 41/76] Try reproduce a bug locally --- .../Rules/Keywords/RequireFileExistsRuleTest.php | 14 ++++++++++++++ tests/PHPStan/Rules/Keywords/data/bug-12203.php | 6 ++++++ 2 files changed, 20 insertions(+) create mode 100644 tests/PHPStan/Rules/Keywords/data/bug-12203.php diff --git a/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php index 6bd3e1dfd7..6bc5dc45c2 100644 --- a/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php +++ b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php @@ -121,4 +121,18 @@ public function testBug11738(): void $this->analyse([__DIR__ . '/data/bug-11738/bug-11738.php'], []); } + public function testBug12203(): void + { + $this->analyse([__DIR__ . '/data/bug-12203.php'], [ + [ + 'Path in require_once() "../bug-12203-sure-does-not-exist.php" is not a file or it does not exist.', + 5, + ], + [ + 'Path in require_once() "' . __DIR__ . '/data/../bug-12203-sure-does-not-exist.php" is not a file or it does not exist.', + 6, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Keywords/data/bug-12203.php b/tests/PHPStan/Rules/Keywords/data/bug-12203.php new file mode 100644 index 0000000000..d64dcc6ebe --- /dev/null +++ b/tests/PHPStan/Rules/Keywords/data/bug-12203.php @@ -0,0 +1,6 @@ + Date: Mon, 9 Dec 2024 10:06:07 +0100 Subject: [PATCH 42/76] Fix --- tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php index 6bc5dc45c2..732819b506 100644 --- a/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php +++ b/tests/PHPStan/Rules/Keywords/RequireFileExistsRuleTest.php @@ -8,6 +8,7 @@ use function implode; use function realpath; use function set_include_path; +use const DIRECTORY_SEPARATOR; use const PATH_SEPARATOR; /** @@ -129,7 +130,7 @@ public function testBug12203(): void 5, ], [ - 'Path in require_once() "' . __DIR__ . '/data/../bug-12203-sure-does-not-exist.php" is not a file or it does not exist.', + 'Path in require_once() "' . __DIR__ . DIRECTORY_SEPARATOR . 'data/../bug-12203-sure-does-not-exist.php" is not a file or it does not exist.', 6, ], ]); From 621e16829817e412f948420f24640971ee84e667 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 9 Dec 2024 10:10:24 +0100 Subject: [PATCH 43/76] Optimization - do not enter anonymous classes during loop analysis --- src/Analyser/NodeScopeResolver.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 5c4d86526e..5f518077a2 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -846,6 +846,9 @@ private function processStmtNode( } elseif ($stmt instanceof Node\Stmt\Trait_) { return new StatementResult($scope, false, false, [], [], []); } elseif ($stmt instanceof Node\Stmt\ClassLike) { + if (!$context->isTopLevel()) { + return new StatementResult($scope, false, false, [], [], []); + } $hasYield = false; $throwPoints = []; $impurePoints = []; From 09eab21c16ec92d238feb95520b5edf16a5a0d1d Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Mon, 9 Dec 2024 10:38:29 +0100 Subject: [PATCH 44/76] Add regression test for array self-append --- .../Analyser/AnalyserIntegrationTest.php | 6 ++ tests/PHPStan/Analyser/data/bug-6948.php | 64 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 tests/PHPStan/Analyser/data/bug-6948.php diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index ebdf3a03e4..a4ee70e41f 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -981,6 +981,12 @@ public function testArrayUnion(): void $this->assertNoErrors($errors); } + public function testBug6948(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6948.php'); + $this->assertNoErrors($errors); + } + public function testBug7963(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-7963.php'); diff --git a/tests/PHPStan/Analyser/data/bug-6948.php b/tests/PHPStan/Analyser/data/bug-6948.php new file mode 100644 index 0000000000..592f0af752 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6948.php @@ -0,0 +1,64 @@ + Date: Mon, 9 Dec 2024 21:56:00 +0100 Subject: [PATCH 45/76] Array map on multiple elements is a list --- .../Php/ArrayMapFunctionReturnTypeExtension.php | 10 ++++++---- .../PHPStan/Analyser/nsrt/array_map_multiple.php | 10 +++++----- .../PHPStan/Rules/Methods/ReturnTypeRuleTest.php | 5 +++++ tests/PHPStan/Rules/Methods/data/bug-12223.php | 15 +++++++++++++++ 4 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-12223.php diff --git a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php index ce29b37d2a..31c239af58 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -168,10 +168,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ); } } else { - $mappedArrayType = TypeCombinator::intersect(new ArrayType( - new IntegerType(), - $valueType, - ), ...TypeUtils::getAccessoryTypes($arrayType)); + $mappedArrayType = AccessoryArrayListType::intersectWith( + TypeCombinator::intersect(new ArrayType( + new IntegerType(), + $valueType, + ), ...TypeUtils::getAccessoryTypes($arrayType)), + ); } if ($arrayType->isIterableAtLeastOnce()->yes()) { diff --git a/tests/PHPStan/Analyser/nsrt/array_map_multiple.php b/tests/PHPStan/Analyser/nsrt/array_map_multiple.php index d986969c3e..2918ebeb89 100644 --- a/tests/PHPStan/Analyser/nsrt/array_map_multiple.php +++ b/tests/PHPStan/Analyser/nsrt/array_map_multiple.php @@ -15,7 +15,7 @@ public function doFoo(int $i, string $s): void return rand(0, 1) ? $a : $b; }, ['foo' => $i], ['bar' => $s]); - assertType('non-empty-array', $result); + assertType('non-empty-list', $result); } /** @@ -26,12 +26,12 @@ public function arrayMapNull(array $array, array $other): void { assertType('array{}', array_map(null, [])); assertType('array{foo: true}', array_map(null, ['foo' => true])); - assertType('non-empty-array', array_map(null, [1, 2, 3], [4, 5, 6])); + assertType('non-empty-list', array_map(null, [1, 2, 3], [4, 5, 6])); assertType('non-empty-array', array_map(null, $array)); - assertType('non-empty-array', array_map(null, $array, $array)); - assertType('non-empty-array', array_map(null, $array, $array, $array)); - assertType('non-empty-array', array_map(null, $array, $other)); + assertType('non-empty-list', array_map(null, $array, $array)); + assertType('non-empty-list', array_map(null, $array, $array, $array)); + assertType('non-empty-list', array_map(null, $array, $other)); assertType('array{1}|array{true}', array_map(null, rand() ? [1] : [true])); assertType('array{1}|array{true, false}', array_map(null, rand() ? [1] : [true, false])); diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index eaa0465a42..13082d79b0 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -1064,4 +1064,9 @@ public function testBug11857(): void $this->analyse([__DIR__ . '/data/bug-11857-builder.php'], []); } + public function testBug12223(): void + { + $this->analyse([__DIR__ . '/data/bug-12223.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-12223.php b/tests/PHPStan/Rules/Methods/data/bug-12223.php new file mode 100644 index 0000000000..29334bf835 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12223.php @@ -0,0 +1,15 @@ + + */ + public function sayHello(): array + { + $a = [1 => 'foo', 3 => 'bar', 5 => 'baz']; + return array_map(static fn(string $s, int $i): string => $s . $i, $a, array_keys($a)); + } +} From 71b25bc5c50b84433ec608284630ca8c25d97781 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 10 Dec 2024 11:47:07 +0100 Subject: [PATCH 46/76] Fix after merge --- src/Type/Php/ArrayMapFunctionReturnTypeExtension.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php index e79a889dd7..145756b971 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -168,12 +168,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ); } } else { - $mappedArrayType = AccessoryArrayListType::intersectWith( - TypeCombinator::intersect(new ArrayType( - new IntegerType(), - $valueType, - ), ...TypeUtils::getAccessoryTypes($arrayType)), - ); + $mappedArrayType = TypeCombinator::intersect(new ArrayType( + new IntegerType(), + $valueType, + ), new AccessoryArrayListType(), ...TypeUtils::getAccessoryTypes($arrayType)); } if ($arrayType->isIterableAtLeastOnce()->yes()) { From 9ca11225524a12176640d0c519cd835a3a6596af Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sun, 1 Dec 2024 02:45:14 +0800 Subject: [PATCH 47/76] Introduce `ClassAsClassConstantRule` --- Makefile | 1 + conf/config.level0.neon | 1 + .../Constants/ClassAsClassConstantRule.php | 40 +++++++++++++++++++ .../ClassAsClassConstantRuleTest.php | 33 +++++++++++++++ .../data/class-as-class-constant.php | 17 ++++++++ 5 files changed, 92 insertions(+) create mode 100644 src/Rules/Constants/ClassAsClassConstantRule.php create mode 100644 tests/PHPStan/Rules/Constants/ClassAsClassConstantRuleTest.php create mode 100644 tests/PHPStan/Rules/Constants/data/class-as-class-constant.php diff --git a/Makefile b/Makefile index 3282ee2c4e..11807088ca 100644 --- a/Makefile +++ b/Makefile @@ -54,6 +54,7 @@ lint: --exclude tests/PHPStan/Rules/Classes/data/first-class-instantiation-callable.php \ --exclude tests/PHPStan/Rules/Classes/data/instantiation-callable.php \ --exclude tests/PHPStan/Rules/Classes/data/bug-9402.php \ + --exclude tests/PHPStan/Rules/Constants/data/class-as-class-constant.php \ --exclude tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant-native-type.php \ --exclude tests/PHPStan/Rules/Constants/data/overriding-constant-native-types.php \ --exclude tests/PHPStan/Rules/Methods/data/bug-10043.php \ diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 1382d99ee1..15f1048b86 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -68,6 +68,7 @@ rules: - PHPStan\Rules\Classes\NonClassAttributeClassRule - PHPStan\Rules\Classes\ReadOnlyClassRule - PHPStan\Rules\Classes\TraitAttributeClassRule + - PHPStan\Rules\Constants\ClassAsClassConstantRule - PHPStan\Rules\Constants\DynamicClassConstantFetchRule - PHPStan\Rules\Constants\FinalConstantRule - PHPStan\Rules\Constants\NativeTypedClassConstantRule diff --git a/src/Rules/Constants/ClassAsClassConstantRule.php b/src/Rules/Constants/ClassAsClassConstantRule.php new file mode 100644 index 0000000000..b10d2d0080 --- /dev/null +++ b/src/Rules/Constants/ClassAsClassConstantRule.php @@ -0,0 +1,40 @@ + + */ +final class ClassAsClassConstantRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $errors = []; + + foreach ($node->consts as $const) { + if ($const->name->toLowerString() !== 'class') { + continue; + } + + $errors[] = RuleErrorBuilder::message('A class constant must not be called \'class\'; it is reserved for class name fetching.') + ->line($const->getStartLine()) + ->identifier('classConstant.class') + ->nonIgnorable() + ->build(); + } + + return $errors; + } + +} diff --git a/tests/PHPStan/Rules/Constants/ClassAsClassConstantRuleTest.php b/tests/PHPStan/Rules/Constants/ClassAsClassConstantRuleTest.php new file mode 100644 index 0000000000..1a8fda4bf6 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/ClassAsClassConstantRuleTest.php @@ -0,0 +1,33 @@ + + */ +class ClassAsClassConstantRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ClassAsClassConstantRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/class-as-class-constant.php'], [ + [ + 'A class constant must not be called \'class\'; it is reserved for class name fetching.', + 9, + ], + [ + 'A class constant must not be called \'class\'; it is reserved for class name fetching.', + 16, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Constants/data/class-as-class-constant.php b/tests/PHPStan/Rules/Constants/data/class-as-class-constant.php new file mode 100644 index 0000000000..fdf86f77f1 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/class-as-class-constant.php @@ -0,0 +1,17 @@ + Date: Sun, 1 Dec 2024 02:58:42 +0800 Subject: [PATCH 48/76] Use native PHPDocs for `Rule` and `RuleTestCase` --- src/Rules/Rule.php | 6 +++--- src/Testing/RuleTestCase.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Rules/Rule.php b/src/Rules/Rule.php index e145e4b530..f0ebf3be85 100644 --- a/src/Rules/Rule.php +++ b/src/Rules/Rule.php @@ -20,18 +20,18 @@ * Learn more: https://phpstan.org/developing-extensions/rules * * @api - * @phpstan-template TNodeType of Node + * @template TNodeType of Node */ interface Rule { /** - * @phpstan-return class-string + * @return class-string */ public function getNodeType(): string; /** - * @phpstan-param TNodeType $node + * @param TNodeType $node * @return (string|RuleError)[] errors */ public function processNode(Node $node, Scope $scope): array; diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index b507b05ae6..b3ff058c11 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -45,7 +45,7 @@ abstract class RuleTestCase extends PHPStanTestCase private ?Analyser $analyser = null; /** - * @phpstan-return TRule + * @return TRule */ abstract protected function getRule(): Rule; From 4d3a9abb165484e18d0d9d787842d1328843b684 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Tue, 10 Dec 2024 21:56:01 +0800 Subject: [PATCH 49/76] Use native PHPDocs wherever possible --- src/Collectors/Collector.php | 8 ++++---- src/Collectors/Registry.php | 9 +++------ src/DependencyInjection/Container.php | 7 +++---- src/DependencyInjection/Nette/NetteContainer.php | 7 +++---- src/Rules/DirectRegistry.php | 9 +++------ src/Rules/LazyRegistry.php | 9 +++------ src/Rules/Registry.php | 6 ++---- stubs/bleedingEdge/Rule.stub | 6 +++--- 8 files changed, 24 insertions(+), 37 deletions(-) diff --git a/src/Collectors/Collector.php b/src/Collectors/Collector.php index e046f89750..d7c87c8ecc 100644 --- a/src/Collectors/Collector.php +++ b/src/Collectors/Collector.php @@ -20,19 +20,19 @@ * Learn more: https://phpstan.org/developing-extensions/collectors * * @api - * @phpstan-template-covariant TNodeType of Node - * @phpstan-template-covariant TValue + * @template-covariant TNodeType of Node + * @template-covariant TValue */ interface Collector { /** - * @phpstan-return class-string + * @return class-string */ public function getNodeType(): string; /** - * @phpstan-param TNodeType $node + * @param TNodeType $node * @return TValue|null Collected data */ public function processNode(Node $node, Scope $scope); diff --git a/src/Collectors/Registry.php b/src/Collectors/Registry.php index 11586e3097..cc0ae09a97 100644 --- a/src/Collectors/Registry.php +++ b/src/Collectors/Registry.php @@ -27,10 +27,8 @@ public function __construct(array $collectors) /** * @template TNodeType of Node - * @phpstan-param class-string $nodeType - * @param Node $nodeType - * @phpstan-return array> - * @return Collector[] + * @param class-string $nodeType + * @return array> */ public function getCollectors(string $nodeType): array { @@ -48,8 +46,7 @@ public function getCollectors(string $nodeType): array } /** - * @phpstan-var array> $selectedCollectors - * @var Collector[] $selectedCollectors + * @var array> $selectedCollectors */ $selectedCollectors = $this->cache[$nodeType]; diff --git a/src/DependencyInjection/Container.php b/src/DependencyInjection/Container.php index 07a7a574e1..cd785677b1 100644 --- a/src/DependencyInjection/Container.php +++ b/src/DependencyInjection/Container.php @@ -14,10 +14,9 @@ public function hasService(string $serviceName): bool; public function getService(string $serviceName); /** - * @phpstan-template T of object - * @phpstan-param class-string $className - * @phpstan-return T - * @return mixed + * @template T of object + * @param class-string $className + * @return T */ public function getByType(string $className); diff --git a/src/DependencyInjection/Nette/NetteContainer.php b/src/DependencyInjection/Nette/NetteContainer.php index 7546993297..914d0a43f1 100644 --- a/src/DependencyInjection/Nette/NetteContainer.php +++ b/src/DependencyInjection/Nette/NetteContainer.php @@ -32,10 +32,9 @@ public function getService(string $serviceName) } /** - * @phpstan-template T of object - * @phpstan-param class-string $className - * @phpstan-return T - * @return mixed + * @template T of object + * @param class-string $className + * @return T */ public function getByType(string $className) { diff --git a/src/Rules/DirectRegistry.php b/src/Rules/DirectRegistry.php index 0dfb5d71ec..13c8bfe3a2 100644 --- a/src/Rules/DirectRegistry.php +++ b/src/Rules/DirectRegistry.php @@ -30,10 +30,8 @@ public function __construct(array $rules) /** * @template TNodeType of Node - * @phpstan-param class-string $nodeType - * @param Node $nodeType - * @phpstan-return array> - * @return Rule[] + * @param class-string $nodeType + * @return array> */ public function getRules(string $nodeType): array { @@ -51,8 +49,7 @@ public function getRules(string $nodeType): array } /** - * @phpstan-var array> $selectedRules - * @var Rule[] $selectedRules + * @var array> $selectedRules */ $selectedRules = $this->cache[$nodeType]; diff --git a/src/Rules/LazyRegistry.php b/src/Rules/LazyRegistry.php index f1b9181923..ec5b1dc13b 100644 --- a/src/Rules/LazyRegistry.php +++ b/src/Rules/LazyRegistry.php @@ -24,10 +24,8 @@ public function __construct(private Container $container) /** * @template TNodeType of Node - * @phpstan-param class-string $nodeType - * @param Node $nodeType - * @phpstan-return array> - * @return Rule[] + * @param class-string $nodeType + * @return array> */ public function getRules(string $nodeType): array { @@ -46,8 +44,7 @@ public function getRules(string $nodeType): array } /** - * @phpstan-var array> $selectedRules - * @var Rule[] $selectedRules + * @var array> $selectedRules */ $selectedRules = $this->cache[$nodeType]; diff --git a/src/Rules/Registry.php b/src/Rules/Registry.php index 36c609811b..792f4428f0 100644 --- a/src/Rules/Registry.php +++ b/src/Rules/Registry.php @@ -9,10 +9,8 @@ interface Registry /** * @template TNodeType of Node - * @phpstan-param class-string $nodeType - * @param Node $nodeType - * @phpstan-return array> - * @return Rule[] + * @param class-string $nodeType + * @return array> */ public function getRules(string $nodeType): array; diff --git a/stubs/bleedingEdge/Rule.stub b/stubs/bleedingEdge/Rule.stub index 0a86ea9d2c..0e721b0497 100644 --- a/stubs/bleedingEdge/Rule.stub +++ b/stubs/bleedingEdge/Rule.stub @@ -7,18 +7,18 @@ use PHPStan\Analyser\Scope; /** * @api - * @phpstan-template TNodeType of Node + * @template TNodeType of Node */ interface Rule { /** - * @phpstan-return class-string + * @return class-string */ public function getNodeType(): string; /** - * @phpstan-param TNodeType $node + * @param TNodeType $node * @return list */ public function processNode(Node $node, Scope $scope): array; From e9e419c12a1f0ec973ce05e1bfec08d4f7a2b957 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 11 Dec 2024 09:45:38 +0100 Subject: [PATCH 50/76] RegexArrayShapeMatcher: fix regex wildcard omitted from type --- src/Type/Regex/RegexGroupParser.php | 16 +++++++++++----- tests/PHPStan/Analyser/nsrt/bug-12211.php | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12211.php diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index dba7fcfe4e..946e492aa7 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -535,13 +535,19 @@ private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals, bool $ap if ( $appendLiterals && $onlyLiterals !== null - && (!in_array($value, ['.'], true) || $isEscaped || $inCharacterClass) ) { - if ($onlyLiterals === []) { - $onlyLiterals = [$value]; + if ( + in_array($value, ['.'], true) + && !($isEscaped || $inCharacterClass) + ) { + $onlyLiterals = null; } else { - foreach ($onlyLiterals as &$literal) { - $literal .= $value; + if ($onlyLiterals === []) { + $onlyLiterals = [$value]; + } else { + foreach ($onlyLiterals as &$literal) { + $literal .= $value; + } } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12211.php b/tests/PHPStan/Analyser/nsrt/bug-12211.php new file mode 100644 index 0000000000..72a268c506 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12211.php @@ -0,0 +1,16 @@ += 7.4 + +declare(strict_types = 1); + +namespace Bug12211; + +use function PHPStan\Testing\assertType; + +const REGEX = '((m.x))'; + +function foo(string $text): void { + assert(preg_match(REGEX, $text, $match) === 1); + assertType('array{string, non-falsy-string}', $match); +} + + From f390b54abf7a2c423ddfe292542e822072d8039e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 11 Dec 2024 10:47:06 +0100 Subject: [PATCH 51/76] RegexArrayShapeMatcher: fix regex alternatives in capture group are concatenated --- src/Type/Regex/RegexGroupParser.php | 25 ++++++++++++++++++++- tests/PHPStan/Analyser/nsrt/bug-12210.php | 27 +++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12210.php diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 946e492aa7..75db1668c7 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -446,7 +446,30 @@ private function walkGroupAst( } if ($ast->getId() === '#alternation') { - $inAlternation = true; + $newLiterals = []; + foreach ($children as $child) { + $walkResult = $this->walkGroupAst( + $child, + true, + $inClass, + $patternModifiers, + $walkResult->onlyLiterals([]), + ); + + if ($newLiterals === null) { + continue; + } + + if (count($walkResult->getOnlyLiterals() ?? []) > 0) { + foreach ($walkResult->getOnlyLiterals() as $alternationLiterals) { + $newLiterals[] = $alternationLiterals; + } + } else { + $newLiterals = null; + } + } + + return $walkResult->onlyLiterals($newLiterals); } // [^0-9] should not parse as numeric-string, and [^list-everything-but-numbers] is technically diff --git a/tests/PHPStan/Analyser/nsrt/bug-12210.php b/tests/PHPStan/Analyser/nsrt/bug-12210.php new file mode 100644 index 0000000000..165b61b63e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12210.php @@ -0,0 +1,27 @@ += 7.4 + +declare(strict_types = 1); + +namespace Bug12210; + +use function PHPStan\Testing\assertType; + +function bug12210a(string $text): void { + assert(preg_match('(((sum|min|max)))', $text, $match) === 1); + assertType("array{string, 'max'|'min'|'sum', 'max'|'min'|'sum'}", $match); +} + +function bug12210b(string $text): void { + assert(preg_match('(((sum|min|ma.)))', $text, $match) === 1); + assertType("array{string, non-empty-string, non-falsy-string}", $match); +} + +function bug12210c(string $text): void { + assert(preg_match('(((su.|min|max)))', $text, $match) === 1); + assertType("array{string, non-empty-string, non-falsy-string}", $match); +} + +function bug12210d(string $text): void { + assert(preg_match('(((sum|mi.|max)))', $text, $match) === 1); + assertType("array{string, non-empty-string, non-falsy-string}", $match); +} From 9a6e12cf9d000420842922194384f5b7d6342370 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 11 Dec 2024 11:01:03 +0100 Subject: [PATCH 52/76] Added regression test --- tests/PHPStan/Analyser/nsrt/bug-12173.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12173.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-12173.php b/tests/PHPStan/Analyser/nsrt/bug-12173.php new file mode 100644 index 0000000000..e92ce7da4e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12173.php @@ -0,0 +1,19 @@ += 7.4 + +namespace Bug12173; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + public function parse(string $string): void + { + $regex = '#.*(?(apple|orange)).*#'; + + if (preg_match($regex, $string, $matches) !== 1) { + throw new \Exception('Invalid input'); + } + + assertType("'apple'|'orange'", $matches['fruit']);; + } +} From ecdd6c6d2734c34893e5875f9e77b99f6d72fe63 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 11 Dec 2024 11:32:03 +0100 Subject: [PATCH 53/76] RegexArrayShapeMatcher: Don't narrow 'J' modifier --- src/Type/Regex/RegexGroupParser.php | 12 +++++++- tests/PHPStan/Analyser/nsrt/bug-12126.php | 36 +++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12126.php diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 75db1668c7..c818426111 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -34,6 +34,10 @@ final class RegexGroupParser { + private const NOT_SUPPORTED_MODIFIERS = [ + 'J', // rare modifier too complicated to support + ]; + private static ?Parser $parser = null; public function __construct( @@ -67,8 +71,14 @@ public function parseGroups(string $regex): ?array return null; } - $captureOnlyNamed = false; $modifiers = $this->regexExpressionHelper->getPatternModifiers($regex) ?? ''; + foreach (self::NOT_SUPPORTED_MODIFIERS as $notSupportedModifier) { + if (str_contains($modifiers, $notSupportedModifier)) { + return null; + } + } + + $captureOnlyNamed = false; if ($this->phpVersion->supportsPregCaptureOnlyNamedGroups()) { $captureOnlyNamed = str_contains($modifiers, 'n'); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12126.php b/tests/PHPStan/Analyser/nsrt/bug-12126.php new file mode 100644 index 0000000000..c494d8d60d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12126.php @@ -0,0 +1,36 @@ += 7.4 + +namespace Bug12126; + +use function PHPStan\Testing\assertType; + + +class HelloWorld +{ + public function sayHello(): void + { + $options = ['footest', 'testfoo']; + $key = array_rand($options, 1); + + $regex = '/foo(?Ptest)|test(?Pfoo)/J'; + if (!preg_match_all($regex, $options[$key], $matches, PREG_SET_ORDER)) { + return; + } + + assertType('list>', $matches); + // could be assertType("list", $matches); + if (!preg_match_all($regex, $options[$key], $matches, PREG_PATTERN_ORDER)) { + return; + } + + assertType('array>', $matches); + // could be assertType("array{0: list, test: list<'foo'|'test'>, 1: list<'test'|''>, 2: list<''|'foo'>}", $matches); + + if (!preg_match($regex, $options[$key], $matches)) { + return; + } + + assertType('array', $matches); + // could be assertType("array{0: list, test: 'foo', 1: '', 2: 'foo'}|array{0: list, test: 'test', 1: 'test', 2: ''}", $matches); + } +} From e98335bd09659c4567b1259f524f6ff7b785c6e7 Mon Sep 17 00:00:00 2001 From: vindic Date: Thu, 12 Dec 2024 10:19:28 +0200 Subject: [PATCH 54/76] Fix apcu_cache_info and apcu_sma_info signatures --- resources/functionMap.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index e2fcdaf7e6..9543cf7cd3 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -209,7 +209,7 @@ 'APCIterator::valid' => ['bool'], 'apcu_add' => ['bool', 'key'=>'string', 'var'=>'', 'ttl='=>'int'], 'apcu_add\'1' => ['array', 'values'=>'array', 'unused='=>'', 'ttl='=>'int'], -'apcu_cache_info' => ['array', 'limited='=>'bool'], +'apcu_cache_info' => ['array|false', 'limited='=>'bool'], 'apcu_cas' => ['bool', 'key'=>'string', 'old'=>'int', 'new'=>'int'], 'apcu_clear_cache' => ['bool'], 'apcu_dec' => ['int', 'key'=>'string', 'step='=>'int', '&w_success='=>'bool', 'ttl='=>'int'], @@ -220,7 +220,7 @@ 'apcu_exists\'1' => ['array', 'keys'=>'string[]'], 'apcu_fetch' => ['mixed', 'key'=>'string|string[]', '&w_success='=>'bool'], 'apcu_inc' => ['int', 'key'=>'string', 'step='=>'int', '&w_success='=>'bool', 'ttl='=>'int'], -'apcu_sma_info' => ['array', 'limited='=>'bool'], +'apcu_sma_info' => ['array|false', 'limited='=>'bool'], 'apcu_store' => ['bool', 'key'=>'string', 'var='=>'', 'ttl='=>'int'], 'apcu_store\'1' => ['array', 'values'=>'array', 'unused='=>'', 'ttl='=>'int'], 'APCuIterator::__construct' => ['void', 'search='=>'string|string[]|null', 'format='=>'int', 'chunk_size='=>'int', 'list='=>'int'], From e7e80934023abc94a4f4bb9066ba6d6db26f6cde Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 12 Dec 2024 09:53:14 +0100 Subject: [PATCH 55/76] Make these benevolent --- resources/functionMap.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index 9543cf7cd3..b49b97178c 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -209,7 +209,7 @@ 'APCIterator::valid' => ['bool'], 'apcu_add' => ['bool', 'key'=>'string', 'var'=>'', 'ttl='=>'int'], 'apcu_add\'1' => ['array', 'values'=>'array', 'unused='=>'', 'ttl='=>'int'], -'apcu_cache_info' => ['array|false', 'limited='=>'bool'], +'apcu_cache_info' => ['__benevolent|false>', 'limited='=>'bool'], 'apcu_cas' => ['bool', 'key'=>'string', 'old'=>'int', 'new'=>'int'], 'apcu_clear_cache' => ['bool'], 'apcu_dec' => ['int', 'key'=>'string', 'step='=>'int', '&w_success='=>'bool', 'ttl='=>'int'], @@ -220,7 +220,7 @@ 'apcu_exists\'1' => ['array', 'keys'=>'string[]'], 'apcu_fetch' => ['mixed', 'key'=>'string|string[]', '&w_success='=>'bool'], 'apcu_inc' => ['int', 'key'=>'string', 'step='=>'int', '&w_success='=>'bool', 'ttl='=>'int'], -'apcu_sma_info' => ['array|false', 'limited='=>'bool'], +'apcu_sma_info' => ['__benevolent', 'limited='=>'bool'], 'apcu_store' => ['bool', 'key'=>'string', 'var='=>'', 'ttl='=>'int'], 'apcu_store\'1' => ['array', 'values'=>'array', 'unused='=>'', 'ttl='=>'int'], 'APCuIterator::__construct' => ['void', 'search='=>'string|string[]|null', 'format='=>'int', 'chunk_size='=>'int', 'list='=>'int'], From 627ad2a75c564ce81793f1a25056d73421a11351 Mon Sep 17 00:00:00 2001 From: ondrejmirtes Date: Thu, 12 Dec 2024 12:44:39 +0000 Subject: [PATCH 56/76] Update BetterReflection --- composer.json | 2 +- composer.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index c678b5998f..166ac44388 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "nette/utils": "^3.2.5", "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.44.0.6", + "ondrejmirtes/better-reflection": "6.46.0.0", "phpstan/php-8-stubs": "0.4.9", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", diff --git a/composer.lock b/composer.lock index 8557d36dae..f619dbd9c9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ee384a5c11fbc1dd087ab8dc956b8d73", + "content-hash": "9dad77eea28263a8f7daa01a1b3dd47a", "packages": [ { "name": "clue/ndjson-react", @@ -2187,16 +2187,16 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.44.0.6", + "version": "6.46.0.0", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "d942fd0af0214bb1250a55c2560f061b7b0c4bd4" + "reference": "242b3e1cdb59a81585be5722b6c44ae44c74c671" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/d942fd0af0214bb1250a55c2560f061b7b0c4bd4", - "reference": "d942fd0af0214bb1250a55c2560f061b7b0c4bd4", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/242b3e1cdb59a81585be5722b6c44ae44c74c671", + "reference": "242b3e1cdb59a81585be5722b6c44ae44c74c671", "shasum": "" }, "require": { @@ -2212,7 +2212,7 @@ "doctrine/coding-standard": "^12.0.0", "phpstan/phpstan": "^1.10.60", "phpstan/phpstan-phpunit": "^1.3.16", - "phpunit/phpunit": "^11.4.3", + "phpunit/phpunit": "^11.5.1", "rector/rector": "1.2.10" }, "suggest": { @@ -2252,9 +2252,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.44.0.6" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.46.0.0" }, - "time": "2024-11-28T21:05:45+00:00" + "time": "2024-12-12T12:40:29+00:00" }, { "name": "phpstan/php-8-stubs", From 0edf18d140c5b640a63317c3017088322a566180 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 12 Dec 2024 20:06:47 +0100 Subject: [PATCH 57/76] Added `strictRulesInstalled` parameter --- conf/config.neon | 1 + conf/parametersSchema.neon | 1 + 2 files changed, 2 insertions(+) diff --git a/conf/config.neon b/conf/config.neon index 9b382375c3..40e57d400d 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -51,6 +51,7 @@ parameters: checkTooWideReturnTypesInProtectedAndPublicMethods: false checkUninitializedProperties: false checkDynamicProperties: false + strictRulesInstalled: false deprecationRulesInstalled: false inferPrivatePropertyTypeFromConstructor: false reportMaybes: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index f73011dcad..3dbe4e87ec 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -57,6 +57,7 @@ parametersSchema: checkTooWideReturnTypesInProtectedAndPublicMethods: bool() checkUninitializedProperties: bool() checkDynamicProperties: bool() + strictRulesInstalled: bool() deprecationRulesInstalled: bool() inferPrivatePropertyTypeFromConstructor: bool() From edc13ad57c10c8ba6ad590fa1b8b7be621a7ba95 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 15 Dec 2024 13:54:08 +0100 Subject: [PATCH 58/76] More precise reflection-classes return types --- resources/functionMap.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index 611ac517ec..d21c346e0e 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -9934,10 +9934,10 @@ 'ReflectionClass::getConstructor' => ['ReflectionMethod|null'], 'ReflectionClass::getDefaultProperties' => ['array'], 'ReflectionClass::getDocComment' => ['string|false'], -'ReflectionClass::getEndLine' => ['int|false'], +'ReflectionClass::getEndLine' => ['positive-int|false'], 'ReflectionClass::getExtension' => ['ReflectionExtension|null'], 'ReflectionClass::getExtensionName' => ['string|false'], -'ReflectionClass::getFileName' => ['string|false'], +'ReflectionClass::getFileName' => ['non-empty-string|false'], 'ReflectionClass::getInterfaceNames' => ['list'], 'ReflectionClass::getInterfaces' => ['array'], 'ReflectionClass::getMethod' => ['ReflectionMethod', 'name'=>'string'], @@ -9951,7 +9951,7 @@ 'ReflectionClass::getReflectionConstant' => ['ReflectionClassConstant|false', 'name'=>'string'], 'ReflectionClass::getReflectionConstants' => ['list'], 'ReflectionClass::getShortName' => ['string'], -'ReflectionClass::getStartLine' => ['int|false'], +'ReflectionClass::getStartLine' => ['positive-int|false'], 'ReflectionClass::getStaticProperties' => ['array'], 'ReflectionClass::getStaticPropertyValue' => ['mixed', 'name'=>'string', 'default='=>'mixed'], 'ReflectionClass::getTraitAliases' => ['array'], @@ -10012,10 +10012,10 @@ 'ReflectionFunction::getClosureScopeClass' => ['ReflectionClass'], 'ReflectionFunction::getClosureThis' => ['bool'], 'ReflectionFunction::getDocComment' => ['string|false'], -'ReflectionFunction::getEndLine' => ['int|false'], +'ReflectionFunction::getEndLine' => ['positive-int|false'], 'ReflectionFunction::getExtension' => ['ReflectionExtension|null'], 'ReflectionFunction::getExtensionName' => ['string|false'], -'ReflectionFunction::getFileName' => ['string|false'], +'ReflectionFunction::getFileName' => ['non-empty-string|false'], 'ReflectionFunction::getName' => ['non-empty-string'], 'ReflectionFunction::getNamespaceName' => ['string'], 'ReflectionFunction::getNumberOfParameters' => ['int'], @@ -10023,7 +10023,7 @@ 'ReflectionFunction::getParameters' => ['list'], 'ReflectionFunction::getReturnType' => ['?ReflectionType'], 'ReflectionFunction::getShortName' => ['string'], -'ReflectionFunction::getStartLine' => ['int|false'], +'ReflectionFunction::getStartLine' => ['positive-int|false'], 'ReflectionFunction::getStaticVariables' => ['array'], 'ReflectionFunction::inNamespace' => ['bool'], 'ReflectionFunction::invoke' => ['mixed', '...args='=>'mixed'], @@ -10041,10 +10041,10 @@ 'ReflectionFunctionAbstract::getClosureScopeClass' => ['ReflectionClass|null'], 'ReflectionFunctionAbstract::getClosureThis' => ['object|null'], 'ReflectionFunctionAbstract::getDocComment' => ['string|false'], -'ReflectionFunctionAbstract::getEndLine' => ['int|false'], +'ReflectionFunctionAbstract::getEndLine' => ['positive-int|false'], 'ReflectionFunctionAbstract::getExtension' => ['ReflectionExtension|null'], 'ReflectionFunctionAbstract::getExtensionName' => ['string|false'], -'ReflectionFunctionAbstract::getFileName' => ['string|false'], +'ReflectionFunctionAbstract::getFileName' => ['non-empty-string|false'], 'ReflectionFunctionAbstract::getName' => ['non-empty-string'], 'ReflectionFunctionAbstract::getNamespaceName' => ['string'], 'ReflectionFunctionAbstract::getNumberOfParameters' => ['int'], @@ -10052,7 +10052,7 @@ 'ReflectionFunctionAbstract::getParameters' => ['list'], 'ReflectionFunctionAbstract::getReturnType' => ['?ReflectionType'], 'ReflectionFunctionAbstract::getShortName' => ['string'], -'ReflectionFunctionAbstract::getStartLine' => ['int|false'], +'ReflectionFunctionAbstract::getStartLine' => ['positive-int|false'], 'ReflectionFunctionAbstract::getStaticVariables' => ['array'], 'ReflectionFunctionAbstract::hasReturnType' => ['bool'], 'ReflectionFunctionAbstract::inNamespace' => ['bool'], From 5d07949d88c8554bad583f8e5584fdb7534d737c Mon Sep 17 00:00:00 2001 From: Anders Jenbo Date: Sun, 15 Dec 2024 13:32:32 +0100 Subject: [PATCH 59/76] Remove incorrect doc leftover from 1.x --- src/Type/Type.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Type/Type.php b/src/Type/Type.php index 8021baae62..0badf2930c 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -64,13 +64,6 @@ public function getConstantArrays(): array; /** @return list */ public function getConstantStrings(): array; - /** - * This is like accepts() but gives reasons - * why the type was not/might not be accepted in some non-intuitive scenarios. - * - * In PHPStan 2.0 this method will be removed and the return type of accepts() - * will change to AcceptsResult. - */ public function accepts(Type $type, bool $strictTypes): AcceptsResult; public function isSuperTypeOf(Type $type): IsSuperTypeOfResult; From de0553c5f03685f235f484433e8d439b3a2b2cb0 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Sun, 15 Dec 2024 12:56:23 +0000 Subject: [PATCH 60/76] Update PhpStorm stubs --- composer.json | 2 +- composer.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 166ac44388..8670cde3ee 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", "hoa/file": "1.17.07.11", - "jetbrains/phpstorm-stubs": "dev-master#bb981ec60b3838e56473a078edf7d0739ca20403", + "jetbrains/phpstorm-stubs": "dev-master#0e82bdfe850c71857ee4ee3501ed82a9fc5d043c", "nette/bootstrap": "^3.0", "nette/di": "^3.1.4", "nette/neon": "3.3.4", diff --git a/composer.lock b/composer.lock index f619dbd9c9..265045a14f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9dad77eea28263a8f7daa01a1b3dd47a", + "content-hash": "3f90069e33e3f9bd4610862a5a5aed2e", "packages": [ { "name": "clue/ndjson-react", @@ -1442,12 +1442,12 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "bb981ec60b3838e56473a078edf7d0739ca20403" + "reference": "0e82bdfe850c71857ee4ee3501ed82a9fc5d043c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/bb981ec60b3838e56473a078edf7d0739ca20403", - "reference": "bb981ec60b3838e56473a078edf7d0739ca20403", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/0e82bdfe850c71857ee4ee3501ed82a9fc5d043c", + "reference": "0e82bdfe850c71857ee4ee3501ed82a9fc5d043c", "shasum": "" }, "require-dev": { @@ -1482,7 +1482,7 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2024-11-27T16:45:26+00:00" + "time": "2024-12-14T08:03:12+00:00" }, { "name": "nette/bootstrap", From 2d6468685d134686d0987b7bd0cf2788bf4ad3b3 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 16 Dec 2024 14:02:26 +0100 Subject: [PATCH 61/76] 10% faster FunctionCallParametersCheck --- src/Rules/FunctionCallParametersCheck.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 623b593f5f..69c8467515 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -88,8 +88,7 @@ public function check( $hasNamedArguments = false; $hasUnpackedArgument = false; $errors = []; - foreach ($args as $i => $arg) { - $type = $scope->getType($arg->value); + foreach ($args as $arg) { if ($hasNamedArguments && $arg->unpack) { $errors[] = RuleErrorBuilder::message('Named argument cannot be followed by an unpacked (...) argument.') ->identifier('argument.unpackAfterNamed') @@ -113,6 +112,7 @@ public function check( $argumentName = $arg->name->toString(); } if ($arg->unpack) { + $type = $scope->getType($arg->value); $arrays = $type->getConstantArrays(); if (count($arrays) > 0) { $minKeys = null; @@ -191,7 +191,7 @@ public function check( if (!$hasNamedArguments) { $invokedParametersCount = count($arguments); - foreach ($arguments as $i => [$argumentValue, $argumentValueType, $unpack, $argumentName]) { + foreach ($arguments as [$argumentValue, $argumentValueType, $unpack, $argumentName]) { if ($unpack) { $invokedParametersCount = max($functionParametersMinCount, $functionParametersMaxCount); break; From b66058fe93b5187d0b6755b7c7111133c5722e10 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Tue, 17 Dec 2024 08:50:44 +0000 Subject: [PATCH 62/76] Add support for internal classes that overload offset access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Mirtes --- src/Type/ObjectType.php | 14 ++- ...nexistentOffsetInArrayDimFetchRuleTest.php | 26 ++++++ ...s-overload-offset-access-invalid-php84.php | 91 +++++++++++++++++++ ...classes-overload-offset-access-invalid.php | 52 +++++++++++ ...l-classes-overload-offset-access-php84.php | 74 +++++++++++++++ ...nternal-classes-overload-offset-access.php | 37 ++++++++ 6 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Arrays/data/internal-classes-overload-offset-access-invalid-php84.php create mode 100644 tests/PHPStan/Rules/Arrays/data/internal-classes-overload-offset-access-invalid.php create mode 100644 tests/PHPStan/Rules/Arrays/data/internal-classes-overload-offset-access-php84.php create mode 100644 tests/PHPStan/Rules/Arrays/data/internal-classes-overload-offset-access.php diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 9c633d62da..fab7c056b0 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -67,7 +67,19 @@ class ObjectType implements TypeWithClassName, SubtractableType use UndecidedComparisonTypeTrait; use NonGeneralizableTypeTrait; - private const EXTRA_OFFSET_CLASSES = ['SimpleXMLElement', 'DOMNodeList', 'Threaded']; + private const EXTRA_OFFSET_CLASSES = [ + 'DOMNamedNodeMap', // Only read and existence + 'Dom\NamedNodeMap', // Only read and existence + 'DOMNodeList', // Only read and existence + 'Dom\NodeList', // Only read and existence + 'Dom\HTMLCollection', // Only read and existence + 'Dom\DtdNamedNodeMap', // Only read and existence + 'PDORow', // Only read and existence + 'ResourceBundle', // Only read + 'FFI\CData', // Very funky and weird + 'SimpleXMLElement', + 'Threaded', + ]; private ?Type $subtractedType; diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 2838f2cbd9..6699adb5f7 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -898,4 +898,30 @@ public function testBug2634(): void $this->analyse([__DIR__ . '/data/bug-2634.php'], []); } + public function testInternalClassesWithOverloadedOffsetAccess(): void + { + $this->analyse([__DIR__ . '/data/internal-classes-overload-offset-access.php'], []); + } + + public function testInternalClassesWithOverloadedOffsetAccess84(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + $this->analyse([__DIR__ . '/data/internal-classes-overload-offset-access-php84.php'], []); + } + + public function testInternalClassesWithOverloadedOffsetAccessInvalid(): void + { + $this->analyse([__DIR__ . '/data/internal-classes-overload-offset-access-invalid.php'], []); + } + + public function testInternalClassesWithOverloadedOffsetAccessInvalid84(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + $this->analyse([__DIR__ . '/data/internal-classes-overload-offset-access-invalid-php84.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/internal-classes-overload-offset-access-invalid-php84.php b/tests/PHPStan/Rules/Arrays/data/internal-classes-overload-offset-access-invalid-php84.php new file mode 100644 index 0000000000..f57bd14122 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/internal-classes-overload-offset-access-invalid-php84.php @@ -0,0 +1,91 @@ + Date: Tue, 17 Dec 2024 09:34:14 +0100 Subject: [PATCH 63/76] Faster `MutatingScope->getNodeKey()` --- src/Analyser/MutatingScope.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 5659a107bc..52ae676397 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -710,15 +710,16 @@ private function getNodeKey(Expr $node): string { $key = $this->exprPrinter->printExpr($node); + $attributes = $node->getAttributes(); if ( $node instanceof Node\FunctionLike - && $node->hasAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME) - && $node->hasAttribute('startFilePos') + && (($attributes[ArrayMapArgVisitor::ATTRIBUTE_NAME] ?? null) !== null) + && (($attributes['startFilePos'] ?? null) !== null) ) { - $key .= '/*' . $node->getAttribute('startFilePos') . '*/'; + $key .= '/*' . $attributes['startFilePos'] . '*/'; } - if ($node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME) === true) { + if (($attributes[self::KEEP_VOID_ATTRIBUTE_NAME] ?? null) === true) { $key .= '/*' . self::KEEP_VOID_ATTRIBUTE_NAME . '*/'; } From 73d0f13cdfb39503044c8b1f404941935809146b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 17 Dec 2024 11:32:44 +0100 Subject: [PATCH 64/76] Fix `DOMDocument::create*()` return types --- resources/functionMap.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index d21c346e0e..1b8e4c28a4 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -1898,15 +1898,15 @@ 'DOMCharacterData::substringData' => ['string', 'offset'=>'int', 'count'=>'int'], 'DOMComment::__construct' => ['void', 'value='=>'string'], 'DOMDocument::__construct' => ['void', 'version='=>'string', 'encoding='=>'string'], -'DOMDocument::createAttribute' => ['DOMAttr', 'name'=>'string'], -'DOMDocument::createAttributeNS' => ['DOMAttr', 'namespaceuri'=>'string', 'qualifiedname'=>'string'], -'DOMDocument::createCDATASection' => ['DOMCDATASection', 'data'=>'string'], +'DOMDocument::createAttribute' => ['__benevolent', 'name'=>'string'], +'DOMDocument::createAttributeNS' => ['__benevolent', 'namespaceuri'=>'string', 'qualifiedname'=>'string'], +'DOMDocument::createCDATASection' => ['__benevolent', 'data'=>'string'], 'DOMDocument::createComment' => ['DOMComment', 'data'=>'string'], 'DOMDocument::createDocumentFragment' => ['DOMDocumentFragment'], -'DOMDocument::createElement' => ['DOMElement', 'name'=>'string', 'value='=>'string'], -'DOMDocument::createElementNS' => ['DOMElement', 'namespaceuri'=>'string', 'qualifiedname'=>'string', 'value='=>'string'], -'DOMDocument::createEntityReference' => ['DOMEntityReference', 'name'=>'string'], -'DOMDocument::createProcessingInstruction' => ['DOMProcessingInstruction', 'target'=>'string', 'data='=>'string'], +'DOMDocument::createElement' => ['__benevolent', 'name'=>'string', 'value='=>'string'], +'DOMDocument::createElementNS' => ['__benevolent', 'namespaceuri'=>'string', 'qualifiedname'=>'string', 'value='=>'string'], +'DOMDocument::createEntityReference' => ['__benevolent', 'name'=>'string'], +'DOMDocument::createProcessingInstruction' => ['__benevolent', 'target'=>'string', 'data='=>'string'], 'DOMDocument::createTextNode' => ['DOMText', 'content'=>'string'], 'DOMDocument::getElementById' => ['DOMElement|null', 'elementid'=>'string'], 'DOMDocument::getElementsByTagName' => ['DOMNodeList', 'name'=>'string'], From c053dbc01983f6dd78a78b8154a00afb64088b33 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 17 Dec 2024 11:35:44 +0100 Subject: [PATCH 65/76] Support `#` comments in regex with `x` modifier --- src/Type/Regex/RegexGroupParser.php | 20 +++++++++----- tests/PHPStan/Analyser/nsrt/bug-12242.php | 32 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12242.php diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index c818426111..a98fb20b42 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -23,6 +23,7 @@ use function count; use function in_array; use function is_int; +use function preg_replace; use function rtrim; use function sscanf; use function str_contains; @@ -64,13 +65,6 @@ public function parseGroups(string $regex): ?array return null; } - $rawRegex = $this->regexExpressionHelper->removeDelimitersAndModifiers($regex); - try { - $ast = self::$parser->parse($rawRegex); - } catch (Exception) { - return null; - } - $modifiers = $this->regexExpressionHelper->getPatternModifiers($regex) ?? ''; foreach (self::NOT_SUPPORTED_MODIFIERS as $notSupportedModifier) { if (str_contains($modifiers, $notSupportedModifier)) { @@ -78,6 +72,18 @@ public function parseGroups(string $regex): ?array } } + if (str_contains($modifiers, 'x')) { + // in freespacing mode the # character starts a comment and runs until the end of the line + $regex = preg_replace('/[^?]#.*/', '', $regex) ?? ''; + } + + $rawRegex = $this->regexExpressionHelper->removeDelimitersAndModifiers($regex); + try { + $ast = self::$parser->parse($rawRegex); + } catch (Exception) { + return null; + } + $captureOnlyNamed = false; if ($this->phpVersion->supportsPregCaptureOnlyNamedGroups()) { $captureOnlyNamed = str_contains($modifiers, 'n'); diff --git a/tests/PHPStan/Analyser/nsrt/bug-12242.php b/tests/PHPStan/Analyser/nsrt/bug-12242.php new file mode 100644 index 0000000000..cb6d424567 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12242.php @@ -0,0 +1,32 @@ += 7.4 + +namespace Bug12242; + +use function PHPStan\Testing\assertType; + +function foo(string $str): void +{ + $regexp = '/ + # ( + ([\d,]*) + # ) + /x'; + if (preg_match($regexp, $str, $match)) { + assertType('array{string, string}', $match); + } +} + +function bar(string $str): void +{ + $regexp = '/^ + (\w+) # column type [1] + [\(] # ( + ?([\d,]*) # size or size, precision [2] + [\)] # ) + ?\s* # whitespace + (\w*) # extra description (UNSIGNED, CHARACTER SET, ...) [3] + $/x'; + if (preg_match($regexp, $str, $matches)) { + assertType('array{string, non-empty-string, string, string}', $matches); + } +} From 7dc4cc62e885d3895af515129b19dc50e75c1d01 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 17 Dec 2024 11:52:29 +0100 Subject: [PATCH 66/76] Fix regex comment support eating too much chars --- src/Type/Regex/RegexGroupParser.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-12242.php | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index a98fb20b42..beea95bce0 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -74,7 +74,7 @@ public function parseGroups(string $regex): ?array if (str_contains($modifiers, 'x')) { // in freespacing mode the # character starts a comment and runs until the end of the line - $regex = preg_replace('/[^?]#.*/', '', $regex) ?? ''; + $regex = preg_replace('/(?regexExpressionHelper->removeDelimitersAndModifiers($regex); diff --git a/tests/PHPStan/Analyser/nsrt/bug-12242.php b/tests/PHPStan/Analyser/nsrt/bug-12242.php index cb6d424567..4d065367a2 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12242.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12242.php @@ -30,3 +30,14 @@ function bar(string $str): void assertType('array{string, non-empty-string, string, string}', $matches); } } + +function foobar(string $str): void +{ + $regexp = '/ + # ( + ([\d,]*)# a comment immediately behind with a closing parenthesis ) + /x'; + if (preg_match($regexp, $str, $match)) { + assertType('array{string, string}', $match); + } +} From 5dfc5830f3b37b8b23e73514c0d6a43c65c57152 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 19 Dec 2024 16:52:07 +0100 Subject: [PATCH 67/76] Fix typo --- src/Rules/AttributesCheck.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index e04381033e..6976c7d049 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -151,7 +151,7 @@ public function check( 'Unknown parameter $%s in call to ' . $attributeClassName . ' constructor.', 'Return type of call to ' . $attributeClassName . ' constructor contains unresolvable type.', 'Parameter %s of attribute class ' . $attributeClassName . ' constructor contains unresolvable type.', - 'Attribute class ' . $attributeClassName . ' constructorinvoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %s, but it\'s not allowed because of @no-named-arguments.', ], 'attribute', $attributeConstructor->acceptsNamedArguments(), From 202dd81d113dd8262977fc32c895386e38bda166 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Dec 2024 10:00:02 +0100 Subject: [PATCH 68/76] Readonly classes cannot be combined with `#[AllowDynamicProperties]` + check trait attributes --- Makefile | 2 + conf/config.level0.neon | 1 + src/Rules/Classes/ClassAttributesRule.php | 32 ++++++- src/Rules/Traits/TraitAttributesRule.php | 51 ++++++++++ .../Rules/Classes/ClassAttributesRuleTest.php | 24 +++++ .../PHPStan/Rules/Classes/data/bug-12281.php | 16 ++++ .../Rules/Traits/TraitAttributesRuleTest.php | 96 +++++++++++++++++++ tests/PHPStan/Rules/Traits/data/bug-12011.php | 26 +++++ tests/PHPStan/Rules/Traits/data/bug-12281.php | 19 ++++ .../Rules/Traits/data/trait-attributes.php | 30 ++++++ 10 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 src/Rules/Traits/TraitAttributesRule.php create mode 100644 tests/PHPStan/Rules/Classes/data/bug-12281.php create mode 100644 tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php create mode 100644 tests/PHPStan/Rules/Traits/data/bug-12011.php create mode 100644 tests/PHPStan/Rules/Traits/data/bug-12281.php create mode 100644 tests/PHPStan/Rules/Traits/data/trait-attributes.php diff --git a/Makefile b/Makefile index ad6075e3bf..d8566bfdb0 100644 --- a/Makefile +++ b/Makefile @@ -87,6 +87,8 @@ lint: --exclude tests/PHPStan/Rules/Properties/data/non-abstract-hooked-properties-in-class.php \ --exclude tests/PHPStan/Rules/Properties/data/hooked-properties-in-class.php \ --exclude tests/PHPStan/Rules/Properties/data/hooked-properties-without-bodies-in-class.php \ + --exclude tests/PHPStan/Rules/Classes/data/bug-12281.php \ + --exclude tests/PHPStan/Rules/Traits/data/bug-12281.php \ src tests cs: diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 3964727b8d..c84cf8f5f7 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -103,6 +103,7 @@ rules: - PHPStan\Rules\Regexp\RegularExpressionPatternRule - PHPStan\Rules\Traits\ConflictingTraitConstantsRule - PHPStan\Rules\Traits\ConstantsInTraitsRule + - PHPStan\Rules\Traits\TraitAttributesRule - PHPStan\Rules\Types\InvalidTypesInUnionRule - PHPStan\Rules\Variables\UnsetRule - PHPStan\Rules\Whitespace\FileWhitespaceRule diff --git a/src/Rules/Classes/ClassAttributesRule.php b/src/Rules/Classes/ClassAttributesRule.php index afe9786db0..190be4331b 100644 --- a/src/Rules/Classes/ClassAttributesRule.php +++ b/src/Rules/Classes/ClassAttributesRule.php @@ -8,6 +8,9 @@ use PHPStan\Node\InClassNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +use function count; +use function sprintf; /** * @implements Rule @@ -28,12 +31,39 @@ public function processNode(Node $node, Scope $scope): array { $classLikeNode = $node->getOriginalNode(); - return $this->attributesCheck->check( + $errors = $this->attributesCheck->check( $scope, $classLikeNode->attrGroups, Attribute::TARGET_CLASS, 'class', ); + + $classReflection = $node->getClassReflection(); + if ( + $classReflection->isReadOnly() + || $classReflection->isEnum() + || $classReflection->isInterface() + ) { + $typeName = 'readonly class'; + $identifier = 'class.allowDynamicPropertiesReadonly'; + if ($classReflection->isEnum()) { + $typeName = 'enum'; + $identifier = 'enum.allowDynamicProperties'; + } + if ($classReflection->isInterface()) { + $typeName = 'interface'; + $identifier = 'interface.allowDynamicProperties'; + } + + if (count($classReflection->getNativeReflection()->getAttributes('AllowDynamicProperties')) > 0) { + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class AllowDynamicProperties cannot be used with %s.', $typeName)) + ->identifier($identifier) + ->nonIgnorable() + ->build(); + } + } + + return $errors; } } diff --git a/src/Rules/Traits/TraitAttributesRule.php b/src/Rules/Traits/TraitAttributesRule.php new file mode 100644 index 0000000000..2b9fa768d0 --- /dev/null +++ b/src/Rules/Traits/TraitAttributesRule.php @@ -0,0 +1,51 @@ + + */ +final class TraitAttributesRule implements Rule +{ + + public function __construct( + private AttributesCheck $attributesCheck, + ) + { + } + + public function getNodeType(): string + { + return InTraitNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $originalNode = $node->getOriginalNode(); + $errors = $this->attributesCheck->check( + $scope, + $originalNode->attrGroups, + Attribute::TARGET_CLASS, + 'class', + ); + + if (count($node->getTraitReflection()->getNativeReflection()->getAttributes('AllowDynamicProperties')) > 0) { + $errors[] = RuleErrorBuilder::message('Attribute class AllowDynamicProperties cannot be used with trait.') + ->identifier('trait.allowDynamicProperties') + ->nonIgnorable() + ->build(); + } + + return $errors; + } + +} diff --git a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php index 1d72b629a8..18e7c7a1fd 100644 --- a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php @@ -167,4 +167,28 @@ public function testBug12011(): void ]); } + public function testBug12281(): void + { + if (PHP_VERSION_ID < 80200) { + $this->markTestSkipped('Test requires PHP 8.2.'); + } + + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12281.php'], [ + [ + 'Attribute class AllowDynamicProperties cannot be used with readonly class.', + 05, + ], + [ + 'Attribute class AllowDynamicProperties cannot be used with enum.', + 12, + ], + [ + 'Attribute class AllowDynamicProperties cannot be used with interface.', + 15, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/bug-12281.php b/tests/PHPStan/Rules/Classes/data/bug-12281.php new file mode 100644 index 0000000000..293d9e5e41 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-12281.php @@ -0,0 +1,16 @@ += 8.2 + +namespace Bug12281; + +#[\AllowDynamicProperties] +readonly class BlogData { /* … */ } + +/** @readonly */ +#[\AllowDynamicProperties] +class BlogDataPhpdoc { /* … */ } + +#[\AllowDynamicProperties] +enum BlogDataEnum { /* … */ } + +#[\AllowDynamicProperties] +interface BlogDataInterface { /* … */ } diff --git a/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php b/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php new file mode 100644 index 0000000000..a3be5ead04 --- /dev/null +++ b/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php @@ -0,0 +1,96 @@ + + */ +class TraitAttributesRuleTest extends RuleTestCase +{ + + private bool $checkExplicitMixed = false; + + private bool $checkImplicitMixed = false; + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new TraitAttributesRule( + new AttributesCheck( + $reflectionProvider, + new FunctionCallParametersCheck( + new RuleLevelHelper($reflectionProvider, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false), + new NullsafeCheck(), + new PhpVersion(80000), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), + true, + true, + true, + true, + ), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + ), + true, + ), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/trait-attributes.php'], [ + [ + 'Attribute class TraitAttributes\AbstractAttribute is abstract.', + 8, + ], + [ + 'Attribute class TraitAttributes\MyTargettedAttribute does not have the class target.', + 20, + ], + ]); + } + + public function testBug12011(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-12011.php'], [ + [ + 'Parameter #1 $name of attribute class Bug12011Trait\Table constructor expects string|null, int given.', + 8, + ], + ]); + } + + public function testBug12281(): void + { + $this->analyse([__DIR__ . '/data/bug-12281.php'], [ + [ + 'Attribute class AllowDynamicProperties cannot be used with trait.', + 11, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Traits/data/bug-12011.php b/tests/PHPStan/Rules/Traits/data/bug-12011.php new file mode 100644 index 0000000000..32b09d38d3 --- /dev/null +++ b/tests/PHPStan/Rules/Traits/data/bug-12011.php @@ -0,0 +1,26 @@ += 8.3 + +namespace Bug12011Trait; + +use Attribute; + + +#[Table(self::TABLE_NAME)] +trait MyTrait +{ + private const int TABLE_NAME = 1; +} + +class X { + use MyTrait; +} + +#[Attribute(Attribute::TARGET_CLASS)] +final class Table +{ + public function __construct( + public readonly string|null $name = null, + public readonly string|null $schema = null, + ) { + } +} diff --git a/tests/PHPStan/Rules/Traits/data/bug-12281.php b/tests/PHPStan/Rules/Traits/data/bug-12281.php new file mode 100644 index 0000000000..da7d088f1a --- /dev/null +++ b/tests/PHPStan/Rules/Traits/data/bug-12281.php @@ -0,0 +1,19 @@ += 8.2 + +namespace Bug12281Traits; + +#[\AllowDynamicProperties] +enum BlogDataEnum { /* … */ } // reported by ClassAttributesRule + +#[\AllowDynamicProperties] +interface BlogDataInterface { /* … */ } // reported by ClassAttributesRule + +#[\AllowDynamicProperties] +trait BlogDataTrait { /* … */ } + +class Uses +{ + + use BlogDataTrait; + +} diff --git a/tests/PHPStan/Rules/Traits/data/trait-attributes.php b/tests/PHPStan/Rules/Traits/data/trait-attributes.php new file mode 100644 index 0000000000..67906a2dfe --- /dev/null +++ b/tests/PHPStan/Rules/Traits/data/trait-attributes.php @@ -0,0 +1,30 @@ + Date: Fri, 20 Dec 2024 10:04:20 +0100 Subject: [PATCH 69/76] Named argument detection is scope-PHP version dependent --- src/Analyser/MutatingScope.php | 6 ++-- src/Php/PhpVersions.php | 5 +++ src/Rules/FunctionCallParametersCheck.php | 4 +-- .../Analyser/Bug9307CallMethodsRuleTest.php | 4 +-- .../nsrt/bug-2600-php-version-scope.php | 26 ++++++++++++++ .../Rules/Classes/ClassAttributesRuleTest.php | 2 -- .../ClassConstantAttributesRuleTest.php | 2 -- .../ForbiddenNameCheckExtensionRuleTest.php | 3 +- .../Rules/Classes/InstantiationRuleTest.php | 11 ++++-- .../EnumCases/EnumCaseAttributesRuleTest.php | 2 -- .../ArrowFunctionAttributesRuleTest.php | 2 -- .../Rules/Functions/CallCallablesRuleTest.php | 6 ++-- .../CallToFunctionParametersRuleTest.php | 13 +++---- .../Rules/Functions/CallUserFuncRuleTest.php | 3 +- .../Functions/ClosureAttributesRuleTest.php | 2 -- .../Functions/FunctionAttributesRuleTest.php | 2 -- .../Functions/ParamAttributesRuleTest.php | 2 -- .../Rules/Methods/CallMethodsRuleTest.php | 30 ++++++++++++---- .../Methods/CallStaticMethodsRuleTest.php | 6 ++-- .../Methods/MethodAttributesRuleTest.php | 7 ---- ...llow-named-arguments-php-version-scope.php | 35 +++++++++++++++++++ .../Properties/PropertyAttributesRuleTest.php | 2 -- 22 files changed, 119 insertions(+), 56 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-2600-php-version-scope.php create mode 100644 tests/PHPStan/Rules/Methods/data/disallow-named-arguments-php-version-scope.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index aa143cc3af..832c73b1c0 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3139,7 +3139,7 @@ private function enterFunctionLike( $paramExprString = '$' . $parameter->getName(); if ($parameter->isVariadic()) { - if ($this->phpVersion->supportsNamedArguments() && $functionReflection->acceptsNamedArguments()->yes()) { + if (!$this->getPhpVersion()->supportsNamedArguments()->no() && $functionReflection->acceptsNamedArguments()->yes()) { $parameterType = new ArrayType(new UnionType([new IntegerType(), new StringType()]), $parameterType); } else { $parameterType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $parameterType), new AccessoryArrayListType()); @@ -3154,7 +3154,7 @@ private function enterFunctionLike( $nativeParameterType = $parameter->getNativeType(); if ($parameter->isVariadic()) { - if ($this->phpVersion->supportsNamedArguments() && $functionReflection->acceptsNamedArguments()->yes()) { + if (!$this->getPhpVersion()->supportsNamedArguments()->no() && $functionReflection->acceptsNamedArguments()->yes()) { $nativeParameterType = new ArrayType(new UnionType([new IntegerType(), new StringType()]), $nativeParameterType); } else { $nativeParameterType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $nativeParameterType), new AccessoryArrayListType()); @@ -3629,7 +3629,7 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type ); } if ($isVariadic) { - if ($this->phpVersion->supportsNamedArguments()) { + if (!$this->getPhpVersion()->supportsNamedArguments()->no()) { return new ArrayType(new UnionType([new IntegerType(), new StringType()]), $this->getFunctionType( $type, false, diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index 7bdc70e3bf..74474b28b0 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -28,4 +28,9 @@ public function producesWarningForFinalPrivateMethods(): TrinaryLogic return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; } + public function supportsNamedArguments(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; + } + } diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index c39da64ce1..50371ab23c 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; @@ -42,7 +41,6 @@ final class FunctionCallParametersCheck public function __construct( private RuleLevelHelper $ruleLevelHelper, private NullsafeCheck $nullsafeCheck, - private PhpVersion $phpVersion, private UnresolvableTypeHelper $unresolvableTypeHelper, private PropertyReflectionFinder $propertyReflectionFinder, private bool $checkArgumentTypes, @@ -201,7 +199,7 @@ public function check( ]; } - if ($hasNamedArguments && !$this->phpVersion->supportsNamedArguments() && !(bool) $funcCall->getAttribute('isAttribute', false)) { + if ($hasNamedArguments && !$scope->getPhpVersion()->supportsNamedArguments()->yes() && !(bool) $funcCall->getAttribute('isAttribute', false)) { $errors[] = RuleErrorBuilder::message('Named arguments are supported only on PHP 8.0 and later.') ->identifier('argument.namedNotSupported') ->line($funcCall->getStartLine()) diff --git a/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php index a49a650286..ff1be83109 100644 --- a/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php +++ b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Analyser; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\Methods\CallMethodsRule; use PHPStan\Rules\Methods\MethodCallCheck; @@ -12,7 +11,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -26,7 +24,7 @@ protected function getRule(): Rule $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, true, false, false); return new CallMethodsRule( new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), - new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new PhpVersion(PHP_VERSION_ID), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), ); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-2600-php-version-scope.php b/tests/PHPStan/Analyser/nsrt/bug-2600-php-version-scope.php new file mode 100644 index 0000000000..bf13358857 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2600-php-version-scope.php @@ -0,0 +1,26 @@ + 7.4 + +namespace Bug2600PhpVersionScope; + +use function PHPStan\Testing\assertType; + +if (PHP_VERSION_ID >= 80000) { + class Foo8 { + /** + * @param mixed $x + */ + public function doBaz(...$x) { + assertType('array', $x); + } + } +} else { + class Foo9 { + /** + * @param mixed $x + */ + public function doBaz(...$x) { + assertType('list', $x); + } + } + +} diff --git a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php index 18e7c7a1fd..d4a9dc96d5 100644 --- a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Classes; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -35,7 +34,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( new RuleLevelHelper($reflectionProvider, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, diff --git a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php index d5787f8344..64b9783877 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Classes; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -30,7 +29,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, diff --git a/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php index 4907d1d7ec..5286e079fc 100644 --- a/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Classes; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; @@ -26,7 +25,7 @@ protected function getRule(): Rule $reflectionProvider = $this->createReflectionProvider(); return new InstantiationRule( $reflectionProvider, - new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 43b9091daf..e4155ce55b 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Classes; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; @@ -26,7 +25,7 @@ protected function getRule(): Rule $reflectionProvider = $this->createReflectionProvider(); return new InstantiationRule( $reflectionProvider, - new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), @@ -290,6 +289,10 @@ public function testBug4056(): void public function testNamedArguments(): void { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + $this->analyse([__DIR__ . '/data/instantiation-named-arguments.php'], [ [ 'Missing parameter $j (int) in call to InstantiationNamedArguments\Foo constructor.', @@ -501,6 +504,10 @@ public function testBug10248(): void public function testBug11815(): void { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + $this->analyse([__DIR__ . '/data/bug-11815.php'], []); } diff --git a/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php index 52428ced2b..d61265e3e7 100644 --- a/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php +++ b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\EnumCases; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -30,7 +29,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), - new PhpVersion(80100), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php index af9266b7e5..1be1cfae69 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -30,7 +29,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index 31a92a4f92..a8ffcaf800 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -27,7 +26,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( $ruleLevelHelper, new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, @@ -158,6 +156,10 @@ public function testRule(): void public function testNamedArguments(): void { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + $this->analyse([__DIR__ . '/data/callables-named-arguments.php'], [ [ 'Missing parameter $j (int) in call to closure.', diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 89017fe2cf..a6513f03cb 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -28,7 +27,7 @@ protected function getRule(): Rule $broker = $this->createReflectionProvider(); return new CallToFunctionParametersRule( $broker, - new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), ); } @@ -477,6 +476,10 @@ public function testGenericFunction(): void public function testNamedArguments(): void { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + $errors = [ [ 'Missing parameter $j (int) in call to function FunctionNamedArguments\foo.', @@ -491,12 +494,6 @@ public function testNamedArguments(): void 14, ], ]; - if (PHP_VERSION_ID < 80000) { - $errors[] = [ - 'Missing parameter $arr1 (array) in call to function array_merge.', - 14, - ]; - } require_once __DIR__ . '/data/named-arguments-define.php'; $this->analyse([__DIR__ . '/data/named-arguments.php'], $errors); diff --git a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php index f4eb4c2c6e..ccff7cb19d 100644 --- a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -21,7 +20,7 @@ class CallUserFuncRuleTest extends RuleTestCase protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); - return new CallUserFuncRule($reflectionProvider, new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, true, false, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true)); + return new CallUserFuncRule($reflectionProvider, new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, true, false, false), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true)); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php index dbab699a2b..41cc608457 100644 --- a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -30,7 +29,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, diff --git a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php index 6d028b1b96..1961e8e81c 100644 --- a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -30,7 +29,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, diff --git a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php index 842d0513a1..678738ac50 100644 --- a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -30,7 +29,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index bd20b469b8..0da6911a43 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Methods; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -28,15 +27,13 @@ class CallMethodsRuleTest extends RuleTestCase private bool $checkImplicitMixed = false; - private int $phpVersion = PHP_VERSION_ID; - protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, $this->checkNullables, $this->checkThisOnly, $this->checkUnionTypes, $this->checkExplicitMixed, $this->checkImplicitMixed, false); return new CallMethodsRule( new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), - new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new PhpVersion($this->phpVersion), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), ); } @@ -1822,12 +1819,29 @@ public function testDisallowNamedArguments(): void ]); } + public function testDisallowNamedArgumentsInPhpVersionScope(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + + $this->analyse([__DIR__ . '/data/disallow-named-arguments-php-version-scope.php'], [ + [ + 'Named arguments are supported only on PHP 8.0 and later.', + 26, + ], + ]); + } + public function testNamedArguments(): void { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->phpVersion = 80000; $this->analyse([__DIR__ . '/data/named-arguments.php'], [ [ @@ -2104,7 +2118,11 @@ public function testBug4800(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->phpVersion = 80000; + + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + $this->analyse([__DIR__ . '/data/bug-4800.php'], [ [ 'Missing parameter $bar (string) in call to method Bug4800\HelloWorld2::a().', diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index 80d61e299e..51210125cf 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Methods; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; @@ -47,7 +46,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( $ruleLevelHelper, new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, @@ -414,6 +412,10 @@ public function testNamedArguments(): void { $this->checkThisOnly = false; + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + $this->analyse([__DIR__ . '/data/static-method-named-arguments.php'], [ [ 'Missing parameter $j (int) in call to static method StaticMethodNamedArguments\Foo::doFoo().', diff --git a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php index ef5ca25aef..c87aabde90 100644 --- a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Methods; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -21,8 +20,6 @@ class MethodAttributesRuleTest extends RuleTestCase { - private int $phpVersion; - protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); @@ -32,7 +29,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), - new PhpVersion($this->phpVersion), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, @@ -51,8 +47,6 @@ protected function getRule(): Rule public function testRule(): void { - $this->phpVersion = 80000; - $this->analyse([__DIR__ . '/data/method-attributes.php'], [ [ 'Attribute class MethodAttributes\Foo does not have the method target.', @@ -63,7 +57,6 @@ public function testRule(): void public function testBug5898(): void { - $this->phpVersion = 70400; $this->analyse([__DIR__ . '/data/bug-5898.php'], []); } diff --git a/tests/PHPStan/Rules/Methods/data/disallow-named-arguments-php-version-scope.php b/tests/PHPStan/Rules/Methods/data/disallow-named-arguments-php-version-scope.php new file mode 100644 index 0000000000..174fef244d --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/disallow-named-arguments-php-version-scope.php @@ -0,0 +1,35 @@ += 80000) { + class Foo + { + + public function doFoo(): void + { + $this->doBar(i: 1); + } + + public function doBar(int $i): void + { + + } + + } +} else { + class FooBar + { + + public function doFoo(): void + { + $this->doBar(i: 1); + } + + public function doBar(int $i): void + { + + } + + } +} diff --git a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php index 02b75d1007..6f4a6121ec 100644 --- a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Properties; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -29,7 +28,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, From defd40604f38b0f62b25cfeee99c21d90dc960ff Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 19 Dec 2024 20:31:20 +0100 Subject: [PATCH 70/76] Fix preg_match() group containing start/end meta characters --- src/Type/Regex/RegexGroupParser.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-12297.php | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12297.php diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index beea95bce0..a8a9755e8a 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -527,7 +527,7 @@ private function isMaybeEmptyNode(TreeNode $node, string $patternModifiers, bool if ($literal !== '' && $literal !== '0') { $isNonFalsy = true; } - return false; + return $literal === ''; } foreach ($node->getChildren() as $child) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-12297.php b/tests/PHPStan/Analyser/nsrt/bug-12297.php new file mode 100644 index 0000000000..4a956e42a4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12297.php @@ -0,0 +1,19 @@ + Date: Fri, 20 Dec 2024 10:12:43 +0100 Subject: [PATCH 71/76] Fix test --- tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php b/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php index a3be5ead04..fb35b438a0 100644 --- a/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Traits; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -35,7 +34,6 @@ protected function getRule(): Rule new FunctionCallParametersCheck( new RuleLevelHelper($reflectionProvider, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false), new NullsafeCheck(), - new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, @@ -54,6 +52,10 @@ protected function getRule(): Rule public function testRule(): void { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $this->analyse([__DIR__ . '/data/trait-attributes.php'], [ [ 'Attribute class TraitAttributes\AbstractAttribute is abstract.', @@ -85,6 +87,10 @@ public function testBug12011(): void public function testBug12281(): void { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $this->analyse([__DIR__ . '/data/bug-12281.php'], [ [ 'Attribute class AllowDynamicProperties cannot be used with trait.', From 90e48fa876696f221874a2766c2bf3fc1bea0ec0 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 20 Dec 2024 10:16:44 +0100 Subject: [PATCH 72/76] Attributes rules use `In*Node` virtual nodes for more precise Scope --- src/Rules/Functions/ArrowFunctionAttributesRule.php | 7 ++++--- src/Rules/Functions/ClosureAttributesRule.php | 7 ++++--- src/Rules/Functions/FunctionAttributesRule.php | 7 ++++--- src/Rules/Methods/MethodAttributesRule.php | 7 ++++--- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/Rules/Functions/ArrowFunctionAttributesRule.php b/src/Rules/Functions/ArrowFunctionAttributesRule.php index b849f968aa..67af9eb5e0 100644 --- a/src/Rules/Functions/ArrowFunctionAttributesRule.php +++ b/src/Rules/Functions/ArrowFunctionAttributesRule.php @@ -5,11 +5,12 @@ use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\InArrowFunctionNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** - * @implements Rule + * @implements Rule */ final class ArrowFunctionAttributesRule implements Rule { @@ -20,14 +21,14 @@ public function __construct(private AttributesCheck $attributesCheck) public function getNodeType(): string { - return Node\Expr\ArrowFunction::class; + return InArrowFunctionNode::class; } public function processNode(Node $node, Scope $scope): array { return $this->attributesCheck->check( $scope, - $node->attrGroups, + $node->getOriginalNode()->attrGroups, Attribute::TARGET_FUNCTION, 'function', ); diff --git a/src/Rules/Functions/ClosureAttributesRule.php b/src/Rules/Functions/ClosureAttributesRule.php index 841ae2f46c..fc206e19d4 100644 --- a/src/Rules/Functions/ClosureAttributesRule.php +++ b/src/Rules/Functions/ClosureAttributesRule.php @@ -5,11 +5,12 @@ use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\InClosureNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** - * @implements Rule + * @implements Rule */ final class ClosureAttributesRule implements Rule { @@ -20,14 +21,14 @@ public function __construct(private AttributesCheck $attributesCheck) public function getNodeType(): string { - return Node\Expr\Closure::class; + return InClosureNode::class; } public function processNode(Node $node, Scope $scope): array { return $this->attributesCheck->check( $scope, - $node->attrGroups, + $node->getOriginalNode()->attrGroups, Attribute::TARGET_FUNCTION, 'function', ); diff --git a/src/Rules/Functions/FunctionAttributesRule.php b/src/Rules/Functions/FunctionAttributesRule.php index 153c222091..9c5ad24d73 100644 --- a/src/Rules/Functions/FunctionAttributesRule.php +++ b/src/Rules/Functions/FunctionAttributesRule.php @@ -5,11 +5,12 @@ use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\InFunctionNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** - * @implements Rule + * @implements Rule */ final class FunctionAttributesRule implements Rule { @@ -20,14 +21,14 @@ public function __construct(private AttributesCheck $attributesCheck) public function getNodeType(): string { - return Node\Stmt\Function_::class; + return InFunctionNode::class; } public function processNode(Node $node, Scope $scope): array { return $this->attributesCheck->check( $scope, - $node->attrGroups, + $node->getOriginalNode()->attrGroups, Attribute::TARGET_FUNCTION, 'function', ); diff --git a/src/Rules/Methods/MethodAttributesRule.php b/src/Rules/Methods/MethodAttributesRule.php index fefc7bac89..433931baf3 100644 --- a/src/Rules/Methods/MethodAttributesRule.php +++ b/src/Rules/Methods/MethodAttributesRule.php @@ -5,11 +5,12 @@ use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\InClassMethodNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; /** - * @implements Rule + * @implements Rule */ final class MethodAttributesRule implements Rule { @@ -20,14 +21,14 @@ public function __construct(private AttributesCheck $attributesCheck) public function getNodeType(): string { - return Node\Stmt\ClassMethod::class; + return InClassMethodNode::class; } public function processNode(Node $node, Scope $scope): array { return $this->attributesCheck->check( $scope, - $node->attrGroups, + $node->getOriginalNode()->attrGroups, Attribute::TARGET_METHOD, 'method', ); From f6c556f625078b39caff3d67fafd6ae49957333e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Dec 2024 10:24:48 +0100 Subject: [PATCH 73/76] Scope: use `scope->getConstant` instead --- src/Analyser/MutatingScope.php | 40 +++++++++++------ src/Php/PhpVersions.php | 5 +++ .../PHPStan/Analyser/ScopePhpVersionTest.php | 43 +++++++++++++++++++ .../Analyser/data/scope-constants-global.php | 9 ++++ .../data/scope-constants-namespace.php | 9 ++++ 5 files changed, 93 insertions(+), 13 deletions(-) create mode 100644 tests/PHPStan/Analyser/ScopePhpVersionTest.php create mode 100644 tests/PHPStan/Analyser/data/scope-constants-global.php create mode 100644 tests/PHPStan/Analyser/data/scope-constants-namespace.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 832c73b1c0..f883c95578 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -610,12 +610,7 @@ public function hasConstant(Name $name): bool return $this->fileHasCompilerHaltStatementCalls(); } - if (!$name->isFullyQualified() && $this->getNamespace() !== null) { - if ($this->hasExpressionType(new ConstFetch(new FullyQualified([$this->getNamespace(), $name->toString()])))->yes()) { - return true; - } - } - if ($this->hasExpressionType(new ConstFetch(new FullyQualified($name->toString())))->yes()) { + if ($this->getGlobalConstantType($name) !== null) { return true; } @@ -5686,6 +5681,25 @@ private function getConstantTypes(): array return $constantTypes; } + private function getGlobalConstantType(Name $name): ?Type + { + $fetches = []; + if (!$name->isFullyQualified() && $this->getNamespace() !== null) { + $fetches[] = new ConstFetch(new FullyQualified([$this->getNamespace(), $name->toString()])); + } + + $fetches[] = new ConstFetch(new FullyQualified($name->toString())); + $fetches[] = new ConstFetch($name); + + foreach ($fetches as $constFetch) { + if ($this->hasExpressionType($constFetch)->yes()) { + return $this->getType($constFetch); + } + } + + return null; + } + /** * @return array */ @@ -5728,15 +5742,15 @@ public function getIterableValueType(Type $iteratee): Type public function getPhpVersion(): PhpVersions { - $versionExpr = new ConstFetch(new Name('PHP_VERSION_ID')); - if (!$this->hasExpressionType($versionExpr)->yes()) { - if (is_array($this->configPhpVersion)) { - return new PhpVersions(IntegerRangeType::fromInterval($this->configPhpVersion['min'], $this->configPhpVersion['max'])); - } - return new PhpVersions(new ConstantIntegerType($this->phpVersion->getVersionId())); + $constType = $this->getGlobalConstantType(new Name('PHP_VERSION_ID')); + if ($constType !== null) { + return new PhpVersions($constType); } - return new PhpVersions($this->getType($versionExpr)); + if (is_array($this->configPhpVersion)) { + return new PhpVersions(IntegerRangeType::fromInterval($this->configPhpVersion['min'], $this->configPhpVersion['max'])); + } + return new PhpVersions(new ConstantIntegerType($this->phpVersion->getVersionId())); } } diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index 74474b28b0..229dccb72d 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -18,6 +18,11 @@ public function __construct( { } + public function getType(): Type + { + return $this->phpVersions; + } + public function supportsNoncapturingCatches(): TrinaryLogic { return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; diff --git a/tests/PHPStan/Analyser/ScopePhpVersionTest.php b/tests/PHPStan/Analyser/ScopePhpVersionTest.php new file mode 100644 index 0000000000..fac3b8a066 --- /dev/null +++ b/tests/PHPStan/Analyser/ScopePhpVersionTest.php @@ -0,0 +1,43 @@ +', + __DIR__ . '/data/scope-constants-global.php', + ], + [ + 'int<80000, 80499>', + __DIR__ . '/data/scope-constants-namespace.php', + ], + ]; + } + + /** + * @dataProvider dataTestPhpVersion + */ + public function testPhpVersion(string $expected, string $file): void + { + self::processFile($file, function (Node $node, Scope $scope) use ($expected): void { + if (!($node instanceof Exit_)) { + return; + } + $this->assertSame( + $expected, + $scope->getPhpVersion()->getType()->describe(VerbosityLevel::precise()), + ); + }); + } + +} diff --git a/tests/PHPStan/Analyser/data/scope-constants-global.php b/tests/PHPStan/Analyser/data/scope-constants-global.php new file mode 100644 index 0000000000..56eba41c9a --- /dev/null +++ b/tests/PHPStan/Analyser/data/scope-constants-global.php @@ -0,0 +1,9 @@ + Date: Fri, 20 Dec 2024 14:52:28 +0100 Subject: [PATCH 74/76] Bleeding edge - UnusedFunctionParametersCheck: report precise line --- conf/bleedingEdge.neon | 1 + conf/config.neon | 3 ++ conf/parametersSchema.neon | 1 + .../UnusedConstructorParametersRule.php | 7 ++-- src/Rules/Functions/UnusedClosureUsesRule.php | 9 +---- src/Rules/UnusedFunctionParametersCheck.php | 35 +++++++++++++------ .../UnusedConstructorParametersRuleTest.php | 20 ++++++++++- .../Functions/UnusedClosureUsesRuleTest.php | 6 ++-- 8 files changed, 55 insertions(+), 27 deletions(-) diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 7227e369b3..76dd0d8904 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -4,3 +4,4 @@ parameters: checkParameterCastableToNumberFunctions: true skipCheckGenericClasses!: [] stricterFunctionMap: true + reportPreciseLineForUnusedFunctionParameter: true diff --git a/conf/config.neon b/conf/config.neon index 40e57d400d..d77e889531 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -25,6 +25,7 @@ parameters: checkParameterCastableToNumberFunctions: false skipCheckGenericClasses: [] stricterFunctionMap: false + reportPreciseLineForUnusedFunctionParameter: false fileExtensions: - php checkAdvancedIsset: false @@ -1043,6 +1044,8 @@ services: - class: PHPStan\Rules\UnusedFunctionParametersCheck + arguments: + reportExactLine: %featureToggles.reportPreciseLineForUnusedFunctionParameter% - class: PHPStan\Rules\TooWideTypehints\TooWideParameterOutTypeCheck diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 3dbe4e87ec..f7328f7863 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -31,6 +31,7 @@ parametersSchema: checkParameterCastableToNumberFunctions: bool(), skipCheckGenericClasses: listOf(string()), stricterFunctionMap: bool() + reportPreciseLineForUnusedFunctionParameter: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/src/Rules/Classes/UnusedConstructorParametersRule.php b/src/Rules/Classes/UnusedConstructorParametersRule.php index e42a60d5f7..8b38392470 100644 --- a/src/Rules/Classes/UnusedConstructorParametersRule.php +++ b/src/Rules/Classes/UnusedConstructorParametersRule.php @@ -15,7 +15,6 @@ use function array_map; use function array_values; use function count; -use function is_string; use function sprintf; use function strtolower; @@ -56,11 +55,11 @@ public function processNode(Node $node, Scope $scope): array return $this->check->getUnusedParameters( $scope, - array_map(static function (Param $parameter): string { - if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { + array_map(static function (Param $parameter): Variable { + if (!$parameter->var instanceof Variable) { throw new ShouldNotHappenException(); } - return $parameter->var->name; + return $parameter->var; }, array_values(array_filter($originalNode->params, static fn (Param $parameter): bool => $parameter->flags === 0))), $originalNode->stmts, $message, diff --git a/src/Rules/Functions/UnusedClosureUsesRule.php b/src/Rules/Functions/UnusedClosureUsesRule.php index c019d69240..ed9639e41f 100644 --- a/src/Rules/Functions/UnusedClosureUsesRule.php +++ b/src/Rules/Functions/UnusedClosureUsesRule.php @@ -6,10 +6,8 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\UnusedFunctionParametersCheck; -use PHPStan\ShouldNotHappenException; use function array_map; use function count; -use function is_string; /** * @implements Rule @@ -34,12 +32,7 @@ public function processNode(Node $node, Scope $scope): array return $this->check->getUnusedParameters( $scope, - array_map(static function (Node\ClosureUse $use): string { - if (!is_string($use->var->name)) { - throw new ShouldNotHappenException(); - } - return $use->var->name; - }, $node->uses), + array_map(static fn (Node\ClosureUse $use): Node\Expr\Variable => $use->var, $node->uses), $node->stmts, 'Anonymous function has an unused use $%s.', 'closure.unusedUse', diff --git a/src/Rules/UnusedFunctionParametersCheck.php b/src/Rules/UnusedFunctionParametersCheck.php index 4fbe76d20d..628041a032 100644 --- a/src/Rules/UnusedFunctionParametersCheck.php +++ b/src/Rules/UnusedFunctionParametersCheck.php @@ -3,11 +3,13 @@ namespace PHPStan\Rules; use PhpParser\Node; +use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantStringType; -use function array_fill_keys; -use function array_keys; +use function array_combine; +use function array_map; use function array_merge; use function is_array; use function is_string; @@ -16,25 +18,34 @@ final class UnusedFunctionParametersCheck { - public function __construct(private ReflectionProvider $reflectionProvider) + public function __construct( + private ReflectionProvider $reflectionProvider, + private bool $reportExactLine, + ) { } /** - * @param string[] $parameterNames + * @param Variable[] $parameterVars * @param Node[] $statements * @param 'constructor.unusedParameter'|'closure.unusedUse' $identifier * @return list */ public function getUnusedParameters( Scope $scope, - array $parameterNames, + array $parameterVars, array $statements, string $unusedParameterMessage, string $identifier, ): array { - $unusedParameters = array_fill_keys($parameterNames, true); + $parameterNames = array_map(static function (Variable $variable): string { + if (!is_string($variable->name)) { + throw new ShouldNotHappenException(); + } + return $variable->name; + }, $parameterVars); + $unusedParameters = array_combine($parameterNames, $parameterVars); foreach ($this->getUsedVariables($scope, $statements) as $variableName) { if (!isset($unusedParameters[$variableName])) { continue; @@ -43,10 +54,12 @@ public function getUnusedParameters( unset($unusedParameters[$variableName]); } $errors = []; - foreach (array_keys($unusedParameters) as $name) { - $errors[] = RuleErrorBuilder::message( - sprintf($unusedParameterMessage, $name), - )->identifier($identifier)->build(); + foreach ($unusedParameters as $name => $variable) { + $errorBuilder = RuleErrorBuilder::message(sprintf($unusedParameterMessage, $name))->identifier($identifier); + if ($this->reportExactLine) { + $errorBuilder->line($variable->getStartLine()); + } + $errors[] = $errorBuilder->build(); } return $errors; @@ -66,7 +79,7 @@ private function getUsedVariables(Scope $scope, $node): array return $scope->getDefinedVariables(); } } - if ($node instanceof Node\Expr\Variable && is_string($node->name) && $node->name !== 'this') { + if ($node instanceof Variable && is_string($node->name) && $node->name !== 'this') { return [$node->name]; } if ($node instanceof Node\ClosureUse && is_string($node->var->name)) { diff --git a/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php b/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php index b6530920df..beb402c267 100644 --- a/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php +++ b/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php @@ -12,15 +12,19 @@ class UnusedConstructorParametersRuleTest extends RuleTestCase { + private bool $reportExactLine = true; + protected function getRule(): Rule { return new UnusedConstructorParametersRule(new UnusedFunctionParametersCheck( $this->createReflectionProvider(), + $this->reportExactLine, )); } - public function testUnusedConstructorParameters(): void + public function testUnusedConstructorParametersNoExactLine(): void { + $this->reportExactLine = false; $this->analyse([__DIR__ . '/data/unused-constructor-parameters.php'], [ [ 'Constructor of class UnusedConstructorParameters\Foo has an unused parameter $unusedParameter.', @@ -33,6 +37,20 @@ public function testUnusedConstructorParameters(): void ]); } + public function testUnusedConstructorParameters(): void + { + $this->analyse([__DIR__ . '/data/unused-constructor-parameters.php'], [ + [ + 'Constructor of class UnusedConstructorParameters\Foo has an unused parameter $unusedParameter.', + 19, + ], + [ + 'Constructor of class UnusedConstructorParameters\Foo has an unused parameter $anotherUnusedParameter.', + 20, + ], + ]); + } + public function testPromotedProperties(): void { $this->analyse([__DIR__ . '/data/unused-constructor-parameters-promoted-properties.php'], []); diff --git a/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php b/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php index 0d033268d8..38a555afda 100644 --- a/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php @@ -14,7 +14,7 @@ class UnusedClosureUsesRuleTest extends RuleTestCase protected function getRule(): Rule { - return new UnusedClosureUsesRule(new UnusedFunctionParametersCheck($this->createReflectionProvider())); + return new UnusedClosureUsesRule(new UnusedFunctionParametersCheck($this->createReflectionProvider(), true)); } public function testUnusedClosureUses(): void @@ -22,11 +22,11 @@ public function testUnusedClosureUses(): void $this->analyse([__DIR__ . '/data/unused-closure-uses.php'], [ [ 'Anonymous function has an unused use $unused.', - 3, + 6, ], [ 'Anonymous function has an unused use $anotherUnused.', - 3, + 7, ], [ 'Anonymous function has an unused use $usedInClosureUse.', From 72b31c081c52682736318d62a09b60f740d9ce41 Mon Sep 17 00:00:00 2001 From: ondrejmirtes Date: Fri, 20 Dec 2024 19:31:57 +0000 Subject: [PATCH 75/76] Update BetterReflection --- composer.json | 2 +- composer.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 8670cde3ee..30dfc158c3 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "nette/utils": "^3.2.5", "nikic/php-parser": "^5.3.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.46.0.0", + "ondrejmirtes/better-reflection": "6.49.0.0", "phpstan/php-8-stubs": "0.4.9", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", diff --git a/composer.lock b/composer.lock index 265045a14f..731974732a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3f90069e33e3f9bd4610862a5a5aed2e", + "content-hash": "7887860dff8af8b2ff60352d573b1aba", "packages": [ { "name": "clue/ndjson-react", @@ -2187,21 +2187,21 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.46.0.0", + "version": "6.49.0.0", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "242b3e1cdb59a81585be5722b6c44ae44c74c671" + "reference": "11abb6b4c9c8b29ee2730a3307ebae77b17fa94d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/242b3e1cdb59a81585be5722b6c44ae44c74c671", - "reference": "242b3e1cdb59a81585be5722b6c44ae44c74c671", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/11abb6b4c9c8b29ee2730a3307ebae77b17fa94d", + "reference": "11abb6b4c9c8b29ee2730a3307ebae77b17fa94d", "shasum": "" }, "require": { "ext-json": "*", - "jetbrains/phpstorm-stubs": "dev-master#217ed9356d07ef89109d3cd7d8c5df10aab4b0d4", + "jetbrains/phpstorm-stubs": "dev-master#b61d4a5f40c3940be440d85355fef4e2416b8527", "nikic/php-parser": "^5.3.1", "php": "^7.4 || ^8.0" }, @@ -2252,9 +2252,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.46.0.0" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.49.0.0" }, - "time": "2024-12-12T12:40:29+00:00" + "time": "2024-12-20T19:27:15+00:00" }, { "name": "phpstan/php-8-stubs", From 46b98196c0643eeb3b0eb3c5e91e10099e00f5c1 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 21 Dec 2024 15:55:41 +0100 Subject: [PATCH 76/76] Prepare for 2.1.x-dev --- .github/workflows/apiref.yml | 4 ++-- .github/workflows/issue-bot.yml | 4 ++-- .github/workflows/pr-base-on-previous-branch.yml | 4 ++-- .github/workflows/update-phpstorm-stubs.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/apiref.yml b/.github/workflows/apiref.yml index 7700ceb911..24ba44284b 100644 --- a/.github/workflows/apiref.yml +++ b/.github/workflows/apiref.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: push: branches: - - "2.0.x" + - "2.1.x" paths: - 'src/**' - 'composer.lock' @@ -14,7 +14,7 @@ on: - '.github/workflows/apiref.yml' env: - COMPOSER_ROOT_VERSION: "2.0.x-dev" + COMPOSER_ROOT_VERSION: "2.1.x-dev" concurrency: group: apigen-${{ github.ref }} # will be canceled on subsequent pushes in branch diff --git a/.github/workflows/issue-bot.yml b/.github/workflows/issue-bot.yml index c3d3488fea..3165350af2 100644 --- a/.github/workflows/issue-bot.yml +++ b/.github/workflows/issue-bot.yml @@ -11,7 +11,7 @@ on: - 'changelog-generator/**' push: branches: - - "2.0.x" + - "2.1.x" paths-ignore: - 'compiler/**' - 'apigen/**' @@ -164,7 +164,7 @@ jobs: - name: "Evaluate results - push" working-directory: "issue-bot" - if: "github.repository_owner == 'phpstan' && github.ref == 'refs/heads/2.0.x'" + if: "github.repository_owner == 'phpstan' && github.ref == 'refs/heads/2.1.x'" env: GITHUB_PAT: ${{ secrets.PHPSTAN_BOT_TOKEN }} PHPSTAN_SRC_COMMIT_BEFORE: ${{ github.event.before }} diff --git a/.github/workflows/pr-base-on-previous-branch.yml b/.github/workflows/pr-base-on-previous-branch.yml index 7c2f57188d..34ef71bb83 100644 --- a/.github/workflows/pr-base-on-previous-branch.yml +++ b/.github/workflows/pr-base-on-previous-branch.yml @@ -7,7 +7,7 @@ on: types: - opened branches: - - '2.1.x' + - '2.2.x' jobs: @@ -19,6 +19,6 @@ jobs: - name: Comment PR uses: peter-evans/create-or-update-comment@v4 with: - body: "You've opened the pull request against the latest branch 2.1.x. PHPStan 2.1 is not going to be released for months. If your code is relevant on 2.0.x and you want it to be released sooner, please rebase your pull request and change its target to 2.0.x." + body: "You've opened the pull request against the latest branch 2.2.x. PHPStan 2.2 is not going to be released for months. If your code is relevant on 2.1.x and you want it to be released sooner, please rebase your pull request and change its target to 2.1.x." token: ${{ secrets.PHPSTAN_BOT_TOKEN }} issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/update-phpstorm-stubs.yml b/.github/workflows/update-phpstorm-stubs.yml index 320b4eba1b..396be1c0be 100644 --- a/.github/workflows/update-phpstorm-stubs.yml +++ b/.github/workflows/update-phpstorm-stubs.yml @@ -16,7 +16,7 @@ jobs: - name: "Checkout" uses: actions/checkout@v4 with: - ref: 2.0.x + ref: 2.1.x fetch-depth: '0' token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - name: "Install PHP"