From 707f0a49b2210b0696bb309b33739dd569d84fcb Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Sep 2024 15:54:56 +0200 Subject: [PATCH 01/18] Open 2.0.x --- .github/workflows/build.yml | 2 +- composer.json | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index efc1cce..0dc529d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "1.2.x" + - "2.0.x" jobs: lint: diff --git a/composer.json b/composer.json index 38ce21f..2d84af6 100644 --- a/composer.json +++ b/composer.json @@ -6,16 +6,15 @@ "MIT" ], "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.12" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" }, "require-dev": { - "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-deprecation-rules": "^1.2", - "phpstan/phpstan-phpunit": "^1.4", - "phpstan/phpstan-strict-rules": "^1.6", - "phpunit/phpunit": "^9.5", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", "webmozart/assert": "^1.11.0" }, "config": { From c10d8afebf4b0520e15044c1fa4c8a0d6a7f5429 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Sep 2024 15:55:26 +0200 Subject: [PATCH 02/18] Stop testing PHP 7.2 and 7.3 --- .github/workflows/build.yml | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0dc529d..8d659b1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,8 +16,6 @@ jobs: strategy: matrix: php-version: - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" @@ -41,10 +39,6 @@ jobs: - name: "Install dependencies" run: "composer install --no-interaction --no-progress" - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.2' || matrix.php-version == '7.3' - run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" - - name: "Lint" run: "make lint" @@ -94,8 +88,6 @@ jobs: fail-fast: false matrix: php-version: - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" @@ -124,10 +116,6 @@ jobs: if: ${{ matrix.dependencies == 'highest' }} run: "composer update --no-interaction --no-progress" - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.2' || matrix.php-version == '7.3' - run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" - - name: "Tests" run: "make tests" @@ -139,8 +127,6 @@ jobs: fail-fast: false matrix: php-version: - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" @@ -171,9 +157,5 @@ jobs: if: ${{ matrix.dependencies == 'highest' }} run: "composer update --no-interaction --no-progress" - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.2' || matrix.php-version == '7.3' - run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" - - name: "PHPStan" run: "make phpstan" From 3990758b51bbeb344e77413f37a631e11412305b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Sep 2024 15:56:00 +0200 Subject: [PATCH 03/18] Update build-cs --- .github/workflows/build.yml | 2 +- Makefile | 2 +- .../AssertTypeSpecifyingExtension.php | 848 ++++++++---------- 3 files changed, 359 insertions(+), 493 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8d659b1..88543fb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,7 +56,7 @@ jobs: with: repository: "phpstan/build-cs" path: "build-cs" - ref: "1.x" + ref: "2.x" - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/Makefile b/Makefile index 35e7804..90ca1f2 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ lint: .PHONY: cs-install cs-install: git clone https://github.com/phpstan/build-cs.git || true - git -C build-cs fetch origin && git -C build-cs reset --hard origin/1.x + git -C build-cs fetch origin && git -C build-cs reset --hard origin/2.x composer install --working-dir build-cs .PHONY: cs diff --git a/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php b/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php index c3afdef..8d82409 100644 --- a/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php +++ b/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php @@ -61,13 +61,11 @@ class AssertTypeSpecifyingExtension implements StaticMethodTypeSpecifyingExtensi { /** @var Closure[] */ - private $resolvers; + private array $resolvers; - /** @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; - /** @var TypeSpecifier */ - private $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function __construct(ReflectionProvider $reflectionProvider) { @@ -138,9 +136,7 @@ public function specifyTypes( $staticMethodReflection->getName(), $node, $scope, - static function (Type $type) { - return TypeCombinator::addNull($type); - } + static fn (Type $type) => TypeCombinator::addNull($type), ); } @@ -148,7 +144,7 @@ static function (Type $type) { return $this->handleAllNot( $staticMethodReflection->getName(), $node, - $scope + $scope, ); } @@ -156,7 +152,7 @@ static function (Type $type) { return $this->handleAll( $staticMethodReflection->getName(), $node, - $scope + $scope, ); } @@ -169,7 +165,7 @@ static function (Type $type) { $scope, $expr, TypeSpecifierContext::createTruthy(), - $rootExpr + $rootExpr, ); return $this->specifyRootExprIfSet($rootExpr, $specifiedTypes); @@ -206,8 +202,8 @@ private function createExpression( $expr, new Identical( $args[0]->value, - new ConstFetch(new Name('null')) - ) + new ConstFetch(new Name('null')), + ), ); } @@ -221,217 +217,169 @@ private function getExpressionResolvers(): array { if ($this->resolvers === null) { $this->resolvers = [ - 'integer' => static function (Scope $scope, Arg $value): Expr { - return new FuncCall( + 'integer' => static fn (Scope $scope, Arg $value): Expr => new FuncCall( + new Name('is_int'), + [$value], + ), + 'positiveInteger' => static fn (Scope $scope, Arg $value): Expr => new BooleanAnd( + new FuncCall( new Name('is_int'), - [$value] - ); - }, - 'positiveInteger' => static function (Scope $scope, Arg $value): Expr { - return new BooleanAnd( - new FuncCall( - new Name('is_int'), - [$value] - ), - new Greater( - $value->value, - new LNumber(0) - ) - ); - }, - 'string' => static function (Scope $scope, Arg $value): Expr { - return new FuncCall( + [$value], + ), + new Greater( + $value->value, + new LNumber(0), + ), + ), + 'string' => static fn (Scope $scope, Arg $value): Expr => new FuncCall( + new Name('is_string'), + [$value], + ), + 'stringNotEmpty' => static fn (Scope $scope, Arg $value): Expr => new BooleanAnd( + new FuncCall( new Name('is_string'), - [$value] - ); - }, - 'stringNotEmpty' => static function (Scope $scope, Arg $value): Expr { - return new BooleanAnd( - new FuncCall( - new Name('is_string'), - [$value] - ), - new NotIdentical( - $value->value, - new String_('') - ) - ); - }, - 'float' => static function (Scope $scope, Arg $value): Expr { - return new FuncCall( - new Name('is_float'), - [$value] - ); - }, - 'integerish' => static function (Scope $scope, Arg $value): Expr { - return new BooleanAnd( - new FuncCall( - new Name('is_numeric'), - [$value] - ), - new Equal( - $value->value, - new Int_( - $value->value - ) - ) - ); - }, - 'numeric' => static function (Scope $scope, Arg $value): Expr { - return new FuncCall( + [$value], + ), + new NotIdentical( + $value->value, + new String_(''), + ), + ), + 'float' => static fn (Scope $scope, Arg $value): Expr => new FuncCall( + new Name('is_float'), + [$value], + ), + 'integerish' => static fn (Scope $scope, Arg $value): Expr => new BooleanAnd( + new FuncCall( new Name('is_numeric'), - [$value] - ); - }, - 'natural' => static function (Scope $scope, Arg $value): Expr { - return new BooleanAnd( - new FuncCall( - new Name('is_int'), - [$value] - ), - new GreaterOrEqual( + [$value], + ), + new Equal( + $value->value, + new Int_( $value->value, - new LNumber(0) - ) - ); - }, - 'boolean' => static function (Scope $scope, Arg $value): Expr { - return new FuncCall( - new Name('is_bool'), - [$value] - ); - }, - 'scalar' => static function (Scope $scope, Arg $value): Expr { - return new FuncCall( - new Name('is_scalar'), - [$value] - ); - }, - 'object' => static function (Scope $scope, Arg $value): Expr { - return new FuncCall( - new Name('is_object'), - [$value] - ); - }, - 'resource' => static function (Scope $scope, Arg $value): Expr { - return new FuncCall( - new Name('is_resource'), - [$value] - ); - }, - 'isCallable' => static function (Scope $scope, Arg $value): Expr { - return new FuncCall( - new Name('is_callable'), - [$value] - ); - }, - 'isArray' => static function (Scope $scope, Arg $value): Expr { - return new FuncCall( - new Name('is_array'), - [$value] - ); - }, - 'isTraversable' => function (Scope $scope, Arg $value): Expr { - return $this->resolvers['isIterable']($scope, $value); - }, - 'isIterable' => static function (Scope $scope, Arg $expr): Expr { - return new BooleanOr( - new FuncCall( - new Name('is_array'), - [$expr] - ), - new Instanceof_( - $expr->value, - new Name(Traversable::class) - ) - ); - }, - 'isList' => static function (Scope $scope, Arg $expr): Expr { - return new BooleanAnd( - new FuncCall( - new Name('is_array'), - [$expr] ), - new Identical( - $expr->value, - new FuncCall( - new Name('array_values'), - [$expr] - ) - ) - ); - }, - 'isNonEmptyList' => function (Scope $scope, Arg $expr): Expr { - return new BooleanAnd( - $this->resolvers['isList']($scope, $expr), - new NotIdentical( - $expr->value, - new Array_() - ) - ); - }, - 'isMap' => static function (Scope $scope, Arg $expr): Expr { - return new BooleanAnd( + ), + ), + 'numeric' => static fn (Scope $scope, Arg $value): Expr => new FuncCall( + new Name('is_numeric'), + [$value], + ), + 'natural' => static fn (Scope $scope, Arg $value): Expr => new BooleanAnd( + new FuncCall( + new Name('is_int'), + [$value], + ), + new GreaterOrEqual( + $value->value, + new LNumber(0), + ), + ), + 'boolean' => static fn (Scope $scope, Arg $value): Expr => new FuncCall( + new Name('is_bool'), + [$value], + ), + 'scalar' => static fn (Scope $scope, Arg $value): Expr => new FuncCall( + new Name('is_scalar'), + [$value], + ), + 'object' => static fn (Scope $scope, Arg $value): Expr => new FuncCall( + new Name('is_object'), + [$value], + ), + 'resource' => static fn (Scope $scope, Arg $value): Expr => new FuncCall( + new Name('is_resource'), + [$value], + ), + 'isCallable' => static fn (Scope $scope, Arg $value): Expr => new FuncCall( + new Name('is_callable'), + [$value], + ), + 'isArray' => static fn (Scope $scope, Arg $value): Expr => new FuncCall( + new Name('is_array'), + [$value], + ), + 'isTraversable' => fn (Scope $scope, Arg $value): Expr => $this->resolvers['isIterable']($scope, $value), + 'isIterable' => static fn (Scope $scope, Arg $expr): Expr => new BooleanOr( + new FuncCall( + new Name('is_array'), + [$expr], + ), + new Instanceof_( + $expr->value, + new Name(Traversable::class), + ), + ), + 'isList' => static fn (Scope $scope, Arg $expr): Expr => new BooleanAnd( + new FuncCall( + new Name('is_array'), + [$expr], + ), + new Identical( + $expr->value, new FuncCall( - new Name('is_array'), - [$expr] + new Name('array_values'), + [$expr], ), - new Identical( - new FuncCall( - new Name('array_filter'), - [$expr, new Arg(new String_('is_string')), new Arg(new ConstFetch(new Name('ARRAY_FILTER_USE_KEY')))] - ), - $expr->value - ) - ); - }, - 'isNonEmptyMap' => function (Scope $scope, Arg $expr): Expr { - return new BooleanAnd( - $this->resolvers['isMap']($scope, $expr), - new NotIdentical( - $expr->value, - new Array_() - ) - ); - }, - 'isCountable' => static function (Scope $scope, Arg $expr): Expr { - return new BooleanOr( + ), + ), + 'isNonEmptyList' => fn (Scope $scope, Arg $expr): Expr => new BooleanAnd( + $this->resolvers['isList']($scope, $expr), + new NotIdentical( + $expr->value, + new Array_(), + ), + ), + 'isMap' => static fn (Scope $scope, Arg $expr): Expr => new BooleanAnd( + new FuncCall( + new Name('is_array'), + [$expr], + ), + new Identical( new FuncCall( - new Name('is_array'), - [$expr] + new Name('array_filter'), + [$expr, new Arg(new String_('is_string')), new Arg(new ConstFetch(new Name('ARRAY_FILTER_USE_KEY')))], ), - new Instanceof_( - $expr->value, - new Name(Countable::class) - ) - ); - }, + $expr->value, + ), + ), + 'isNonEmptyMap' => fn (Scope $scope, Arg $expr): Expr => new BooleanAnd( + $this->resolvers['isMap']($scope, $expr), + new NotIdentical( + $expr->value, + new Array_(), + ), + ), + 'isCountable' => static fn (Scope $scope, Arg $expr): Expr => new BooleanOr( + new FuncCall( + new Name('is_array'), + [$expr], + ), + new Instanceof_( + $expr->value, + new Name(Countable::class), + ), + ), 'isInstanceOf' => static function (Scope $scope, Arg $expr, Arg $class): ?Expr { $classType = $scope->getType($class->value); $classNames = $classType->getObjectTypeOrClassStringObjectType()->getObjectClassNames(); if (count($classNames) !== 0) { - return self::implodeExpr(array_map(static function (string $className) use ($expr): Expr { - return new Instanceof_($expr->value, new Name($className)); - }, $classNames), BooleanOr::class); + return self::implodeExpr(array_map(static fn (string $className): Expr => new Instanceof_($expr->value, new Name($className)), $classNames), BooleanOr::class); } return new FuncCall( new Name('is_object'), - [$expr] + [$expr], ); }, - 'isInstanceOfAny' => function (Scope $scope, Arg $expr, Arg $classes): ?Expr { - return self::buildAnyOfExpr($scope, $expr, $classes, $this->resolvers['isInstanceOf']); - }, + 'isInstanceOfAny' => fn (Scope $scope, Arg $expr, Arg $classes): ?Expr => self::buildAnyOfExpr($scope, $expr, $classes, $this->resolvers['isInstanceOf']), 'notInstanceOf' => static function (Scope $scope, Arg $expr, Arg $class): ?Expr { $classType = $scope->getType($class->value); $classNames = $classType->getObjectTypeOrClassStringObjectType()->getObjectClassNames(); if (count($classNames) !== 0) { - $result = self::implodeExpr(array_map(static function (string $className) use ($expr): Expr { - return new Instanceof_($expr->value, new Name($className)); - }, $classNames), BooleanOr::class); + $result = self::implodeExpr(array_map(static fn (string $className): Expr => new Instanceof_($expr->value, new Name($className)), $classNames), BooleanOr::class); if ($result !== null) { return new BooleanNot($result); @@ -446,15 +394,11 @@ private function getExpressionResolvers(): array return new FuncCall( new Name('is_a'), - [$expr, $class, new Arg(new ConstFetch(new Name($allowString ? 'true' : 'false')))] + [$expr, $class, new Arg(new ConstFetch(new Name($allowString ? 'true' : 'false')))], ); }, - 'isAnyOf' => function (Scope $scope, Arg $value, Arg $classes): ?Expr { - return self::buildAnyOfExpr($scope, $value, $classes, $this->resolvers['isAOf']); - }, - 'isNotA' => function (Scope $scope, Arg $value, Arg $class): Expr { - return new BooleanNot($this->resolvers['isAOf']($scope, $value, $class)); - }, + 'isAnyOf' => fn (Scope $scope, Arg $value, Arg $classes): ?Expr => self::buildAnyOfExpr($scope, $value, $classes, $this->resolvers['isAOf']), + 'isNotA' => fn (Scope $scope, Arg $value, Arg $class): Expr => new BooleanNot($this->resolvers['isAOf']($scope, $value, $class)), 'implementsInterface' => function (Scope $scope, Arg $expr, Arg $class): ?Expr { $classType = $scope->getType($class->value)->getClassStringObjectType(); $classNames = $classType->getObjectClassNames(); @@ -474,286 +418,220 @@ private function getExpressionResolvers(): array return $this->resolvers['subclassOf']($scope, $expr, $class); }, - 'keyExists' => static function (Scope $scope, Arg $array, Arg $key): Expr { - return new FuncCall( - new Name('array_key_exists'), - [$key, $array] - ); - }, - 'keyNotExists' => function (Scope $scope, Arg $array, Arg $key): Expr { - return new BooleanNot($this->resolvers['keyExists']($scope, $array, $key)); - }, - 'validArrayKey' => static function (Scope $scope, Arg $value): Expr { - return new BooleanOr( - new FuncCall( - new Name('is_int'), - [$value] - ), - new FuncCall( - new Name('is_string'), - [$value] - ) - ); - }, - 'true' => static function (Scope $scope, Arg $expr): Expr { - return new Identical( - $expr->value, - new ConstFetch(new Name('true')) - ); - }, - 'false' => static function (Scope $scope, Arg $expr): Expr { - return new Identical( - $expr->value, - new ConstFetch(new Name('false')) - ); - }, - 'null' => static function (Scope $scope, Arg $expr): Expr { - return new Identical( - $expr->value, - new ConstFetch(new Name('null')) - ); - }, - 'notFalse' => static function (Scope $scope, Arg $expr): Expr { - return new NotIdentical( - $expr->value, - new ConstFetch(new Name('false')) - ); - }, - 'notNull' => static function (Scope $scope, Arg $expr): Expr { - return new NotIdentical( - $expr->value, - new ConstFetch(new Name('null')) - ); - }, - 'eq' => static function (Scope $scope, Arg $value, Arg $value2): Expr { - return new Equal( - $value->value, - $value2->value - ); - }, - 'notEq' => function (Scope $scope, Arg $value, Arg $value2): Expr { - return new BooleanNot($this->resolvers['eq']($scope, $value, $value2)); - }, - 'same' => static function (Scope $scope, Arg $value1, Arg $value2): Expr { - return new Identical( - $value1->value, - $value2->value - ); - }, - 'notSame' => static function (Scope $scope, Arg $value1, Arg $value2): Expr { - return new NotIdentical( - $value1->value, - $value2->value - ); - }, - 'greaterThan' => static function (Scope $scope, Arg $value, Arg $limit): Expr { - return new Greater( - $value->value, - $limit->value - ); - }, - 'greaterThanEq' => static function (Scope $scope, Arg $value, Arg $limit): Expr { - return new GreaterOrEqual( - $value->value, - $limit->value - ); - }, - 'lessThan' => static function (Scope $scope, Arg $value, Arg $limit): Expr { - return new Smaller( + 'keyExists' => static fn (Scope $scope, Arg $array, Arg $key): Expr => new FuncCall( + new Name('array_key_exists'), + [$key, $array], + ), + 'keyNotExists' => fn (Scope $scope, Arg $array, Arg $key): Expr => new BooleanNot($this->resolvers['keyExists']($scope, $array, $key)), + 'validArrayKey' => static fn (Scope $scope, Arg $value): Expr => new BooleanOr( + new FuncCall( + new Name('is_int'), + [$value], + ), + new FuncCall( + new Name('is_string'), + [$value], + ), + ), + 'true' => static fn (Scope $scope, Arg $expr): Expr => new Identical( + $expr->value, + new ConstFetch(new Name('true')), + ), + 'false' => static fn (Scope $scope, Arg $expr): Expr => new Identical( + $expr->value, + new ConstFetch(new Name('false')), + ), + 'null' => static fn (Scope $scope, Arg $expr): Expr => new Identical( + $expr->value, + new ConstFetch(new Name('null')), + ), + 'notFalse' => static fn (Scope $scope, Arg $expr): Expr => new NotIdentical( + $expr->value, + new ConstFetch(new Name('false')), + ), + 'notNull' => static fn (Scope $scope, Arg $expr): Expr => new NotIdentical( + $expr->value, + new ConstFetch(new Name('null')), + ), + 'eq' => static fn (Scope $scope, Arg $value, Arg $value2): Expr => new Equal( + $value->value, + $value2->value, + ), + 'notEq' => fn (Scope $scope, Arg $value, Arg $value2): Expr => new BooleanNot($this->resolvers['eq']($scope, $value, $value2)), + 'same' => static fn (Scope $scope, Arg $value1, Arg $value2): Expr => new Identical( + $value1->value, + $value2->value, + ), + 'notSame' => static fn (Scope $scope, Arg $value1, Arg $value2): Expr => new NotIdentical( + $value1->value, + $value2->value, + ), + 'greaterThan' => static fn (Scope $scope, Arg $value, Arg $limit): Expr => new Greater( + $value->value, + $limit->value, + ), + 'greaterThanEq' => static fn (Scope $scope, Arg $value, Arg $limit): Expr => new GreaterOrEqual( + $value->value, + $limit->value, + ), + 'lessThan' => static fn (Scope $scope, Arg $value, Arg $limit): Expr => new Smaller( + $value->value, + $limit->value, + ), + 'lessThanEq' => static fn (Scope $scope, Arg $value, Arg $limit): Expr => new SmallerOrEqual( + $value->value, + $limit->value, + ), + 'range' => static fn (Scope $scope, Arg $value, Arg $min, Arg $max): Expr => new BooleanAnd( + new GreaterOrEqual( $value->value, - $limit->value - ); - }, - 'lessThanEq' => static function (Scope $scope, Arg $value, Arg $limit): Expr { - return new SmallerOrEqual( + $min->value, + ), + new SmallerOrEqual( $value->value, - $limit->value - ); - }, - 'range' => static function (Scope $scope, Arg $value, Arg $min, Arg $max): Expr { - return new BooleanAnd( - new GreaterOrEqual( - $value->value, - $min->value - ), - new SmallerOrEqual( - $value->value, - $max->value - ) - ); - }, - 'subclassOf' => static function (Scope $scope, Arg $expr, Arg $class): Expr { - return new FuncCall( - new Name('is_subclass_of'), - [ - new Arg($expr->value), - $class, - ] - ); - }, - 'classExists' => static function (Scope $scope, Arg $class): Expr { - return new FuncCall( - new Name('class_exists'), - [$class] - ); - }, - 'interfaceExists' => static function (Scope $scope, Arg $class): Expr { - return new FuncCall( - new Name('interface_exists'), - [$class] - ); - }, - 'count' => static function (Scope $scope, Arg $array, Arg $number): Expr { - return new Identical( + $max->value, + ), + ), + 'subclassOf' => static fn (Scope $scope, Arg $expr, Arg $class): Expr => new FuncCall( + new Name('is_subclass_of'), + [ + new Arg($expr->value), + $class, + ], + ), + 'classExists' => static fn (Scope $scope, Arg $class): Expr => new FuncCall( + new Name('class_exists'), + [$class], + ), + 'interfaceExists' => static fn (Scope $scope, Arg $class): Expr => new FuncCall( + new Name('interface_exists'), + [$class], + ), + 'count' => static fn (Scope $scope, Arg $array, Arg $number): Expr => new Identical( + new FuncCall( + new Name('count'), + [$array], + ), + $number->value, + ), + 'minCount' => static fn (Scope $scope, Arg $array, Arg $min): Expr => new GreaterOrEqual( + new FuncCall( + new Name('count'), + [$array], + ), + $min->value, + ), + 'maxCount' => static fn (Scope $scope, Arg $array, Arg $max): Expr => new SmallerOrEqual( + new FuncCall( + new Name('count'), + [$array], + ), + $max->value, + ), + 'countBetween' => static fn (Scope $scope, Arg $array, Arg $min, Arg $max): Expr => new BooleanAnd( + new GreaterOrEqual( new FuncCall( new Name('count'), - [$array] + [$array], ), - $number->value - ); - }, - 'minCount' => static function (Scope $scope, Arg $array, Arg $min): Expr { - return new GreaterOrEqual( + $min->value, + ), + new SmallerOrEqual( new FuncCall( new Name('count'), - [$array] + [$array], ), - $min->value - ); - }, - 'maxCount' => static function (Scope $scope, Arg $array, Arg $max): Expr { - return new SmallerOrEqual( + $max->value, + ), + ), + 'length' => static fn (Scope $scope, Arg $value, Arg $length): Expr => new BooleanAnd( + new FuncCall( + new Name('is_string'), + [$value], + ), + new Identical( new FuncCall( - new Name('count'), - [$array] + new Name('strlen'), + [$value], ), - $max->value - ); - }, - 'countBetween' => static function (Scope $scope, Arg $array, Arg $min, Arg $max): Expr { - return new BooleanAnd( - new GreaterOrEqual( - new FuncCall( - new Name('count'), - [$array] - ), - $min->value - ), - new SmallerOrEqual( - new FuncCall( - new Name('count'), - [$array] - ), - $max->value - ) - ); - }, - 'length' => static function (Scope $scope, Arg $value, Arg $length): Expr { - return new BooleanAnd( + $length->value, + ), + ), + 'minLength' => static fn (Scope $scope, Arg $value, Arg $min): Expr => new BooleanAnd( + new FuncCall( + new Name('is_string'), + [$value], + ), + new GreaterOrEqual( new FuncCall( - new Name('is_string'), - [$value] + new Name('strlen'), + [$value], ), - new Identical( - new FuncCall( - new Name('strlen'), - [$value] - ), - $length->value - ) - ); - }, - 'minLength' => static function (Scope $scope, Arg $value, Arg $min): Expr { - return new BooleanAnd( + $min->value, + ), + ), + 'maxLength' => static fn (Scope $scope, Arg $value, Arg $max): Expr => new BooleanAnd( + new FuncCall( + new Name('is_string'), + [$value], + ), + new SmallerOrEqual( new FuncCall( - new Name('is_string'), - [$value] + new Name('strlen'), + [$value], ), + $max->value, + ), + ), + 'lengthBetween' => static fn (Scope $scope, Arg $value, Arg $min, Arg $max): Expr => new BooleanAnd( + new FuncCall( + new Name('is_string'), + [$value], + ), + new BooleanAnd( new GreaterOrEqual( new FuncCall( new Name('strlen'), - [$value] + [$value], ), - $min->value - ) - ); - }, - 'maxLength' => static function (Scope $scope, Arg $value, Arg $max): Expr { - return new BooleanAnd( - new FuncCall( - new Name('is_string'), - [$value] + $min->value, ), new SmallerOrEqual( new FuncCall( new Name('strlen'), - [$value] - ), - $max->value - ) - ); - }, - 'lengthBetween' => static function (Scope $scope, Arg $value, Arg $min, Arg $max): Expr { - return new BooleanAnd( - new FuncCall( - new Name('is_string'), - [$value] - ), - new BooleanAnd( - new GreaterOrEqual( - new FuncCall( - new Name('strlen'), - [$value] - ), - $min->value + [$value], ), - new SmallerOrEqual( - new FuncCall( - new Name('strlen'), - [$value] - ), - $max->value - ) - ) - ); - }, - 'inArray' => static function (Scope $scope, Arg $needle, Arg $array): Expr { - return new FuncCall( - new Name('in_array'), - [ - $needle, - $array, - new Arg(new ConstFetch(new Name('true'))), - ] - ); - }, - 'oneOf' => function (Scope $scope, Arg $needle, Arg $array): Expr { - return $this->resolvers['inArray']($scope, $needle, $array); - }, - 'methodExists' => static function (Scope $scope, Arg $object, Arg $method): Expr { - return new FuncCall( - new Name('method_exists'), - [$object, $method] - ); - }, - 'propertyExists' => static function (Scope $scope, Arg $object, Arg $property): Expr { - return new FuncCall( - new Name('property_exists'), - [$object, $property] - ); - }, - 'isArrayAccessible' => static function (Scope $scope, Arg $expr): Expr { - return new BooleanOr( - new FuncCall( - new Name('is_array'), - [$expr] + $max->value, ), - new Instanceof_( - $expr->value, - new Name(ArrayAccess::class) - ) - ); - }, + ), + ), + 'inArray' => static fn (Scope $scope, Arg $needle, Arg $array): Expr => new FuncCall( + new Name('in_array'), + [ + $needle, + $array, + new Arg(new ConstFetch(new Name('true'))), + ], + ), + 'oneOf' => fn (Scope $scope, Arg $needle, Arg $array): Expr => $this->resolvers['inArray']($scope, $needle, $array), + 'methodExists' => static fn (Scope $scope, Arg $object, Arg $method): Expr => new FuncCall( + new Name('method_exists'), + [$object, $method], + ), + 'propertyExists' => static fn (Scope $scope, Arg $object, Arg $property): Expr => new FuncCall( + new Name('property_exists'), + [$object, $property], + ), + 'isArrayAccessible' => static fn (Scope $scope, Arg $expr): Expr => new BooleanOr( + new FuncCall( + new Name('is_array'), + [$expr], + ), + new Instanceof_( + $expr->value, + new Name(ArrayAccess::class), + ), + ), ]; foreach (['contains', 'startsWith', 'endsWith'] as $name) { @@ -764,12 +642,12 @@ private function getExpressionResolvers(): array $expr = new FuncCall( new Name('is_string'), - [$value] + [$value], ); $rootExpr = new BooleanAnd( $expr, - new FuncCall(new Name('FAUX_FUNCTION_ ' . $name), [$value, $subString]) + new FuncCall(new Name('FAUX_FUNCTION_ ' . $name), [$value, $subString]), ); return [$expr, $rootExpr]; @@ -792,9 +670,7 @@ private function getExpressionResolvers(): array 'notWhitespaceOnly', ]; foreach ($assertionsResultingAtLeastInNonEmptyString as $name) { - $this->resolvers[$name] = static function (Scope $scope, Arg $value) use ($name): array { - return self::createIsNonEmptyStringAndSomethingExprPair($name, [$value]); - }; + $this->resolvers[$name] = static fn (Scope $scope, Arg $value): array => self::createIsNonEmptyStringAndSomethingExprPair($name, [$value]); } } @@ -811,9 +687,7 @@ private function handleAllNot( return $this->allArrayOrIterable( $scope, $node->getArgs()[0]->value, - static function (Type $type): Type { - return TypeCombinator::removeNull($type); - } + static fn (Type $type): Type => TypeCombinator::removeNull($type), ); } @@ -828,9 +702,7 @@ static function (Type $type): Type { return $this->allArrayOrIterable( $scope, $node->getArgs()[0]->value, - static function (Type $type) use ($classNameType): Type { - return TypeCombinator::remove($type, $classNameType); - } + static fn (Type $type): Type => TypeCombinator::remove($type, $classNameType), ); } @@ -839,9 +711,7 @@ static function (Type $type) use ($classNameType): Type { return $this->allArrayOrIterable( $scope, $node->getArgs()[0]->value, - static function (Type $type) use ($valueType): Type { - return TypeCombinator::remove($type, $valueType); - } + static fn (Type $type): Type => TypeCombinator::remove($type, $valueType), ); } @@ -869,7 +739,7 @@ private function handleAll( $scope, $expr, TypeSpecifierContext::createTruthy(), - $rootExpr + $rootExpr, ); $sureNotTypes = $specifiedTypes->getSureNotTypes(); @@ -886,10 +756,8 @@ private function handleAll( return $this->allArrayOrIterable( $scope, $node->getArgs()[0]->value, - static function () use ($type): Type { - return $type; - }, - $rootExpr + static fn (): Type => $type, + $rootExpr, ); } @@ -946,7 +814,7 @@ private function allArrayOrIterable( TypeSpecifierContext::createTruthy(), false, $scope, - $rootExpr + $rootExpr, ); return $this->specifyRootExprIfSet($rootExpr, $specifiedTypes); @@ -965,10 +833,8 @@ private static function implodeExpr(array $expressions, string $binaryOp): ?Expr return array_reduce( $expressions, - static function (Expr $carry, Expr $item) use ($binaryOp) { - return new $binaryOp($carry, $item); - }, - $firstExpression + static fn (Expr $carry, Expr $item) => new $binaryOp($carry, $item), + $firstExpression, ); } @@ -1004,17 +870,17 @@ private static function createIsNonEmptyStringAndSomethingExprPair(string $name, $expr = new BooleanAnd( new FuncCall( new Name('is_string'), - [$args[0]] + [$args[0]], ), new NotIdentical( $args[0]->value, - new String_('') - ) + new String_(''), + ), ); $rootExpr = new BooleanAnd( $expr, - new FuncCall(new Name('FAUX_FUNCTION_ ' . $name), $args) + new FuncCall(new Name('FAUX_FUNCTION_ ' . $name), $args), ); return [$expr, $rootExpr]; @@ -1028,7 +894,7 @@ private function specifyRootExprIfSet(?Expr $rootExpr, SpecifiedTypes $specified // Makes consecutive calls with a rootExpr adding unknown info via FAUX_FUNCTION evaluate to true return $specifiedTypes->unionWith( - $this->typeSpecifier->create($rootExpr, new ConstantBooleanType(true), TypeSpecifierContext::createTruthy()) + $this->typeSpecifier->create($rootExpr, new ConstantBooleanType(true), TypeSpecifierContext::createTruthy()), ); } From 4dad6d17383d65b0a88ab8400b6671281754be8e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Sep 2024 15:58:03 +0200 Subject: [PATCH 04/18] Fixes --- .../WebMozartAssert/AssertTypeSpecifyingExtension.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php b/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php index 8d82409..817dfa1 100644 --- a/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php +++ b/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php @@ -215,7 +215,7 @@ private function createExpression( */ private function getExpressionResolvers(): array { - if ($this->resolvers === null) { + if (!isset($this->resolvers)) { $this->resolvers = [ 'integer' => static fn (Scope $scope, Arg $value): Expr => new FuncCall( new Name('is_int'), @@ -840,16 +840,12 @@ private static function implodeExpr(array $expressions, string $binaryOp): ?Expr private static function buildAnyOfExpr(Scope $scope, Arg $value, Arg $items, callable $resolver): ?Expr { - if (!$items->value instanceof Array_ || $items->value->items === null) { + if (!$items->value instanceof Array_) { return null; } $resolvers = []; foreach ($items->value->items as $key => $item) { - if ($item === null) { - continue; - } - $resolved = $resolver($scope, $value, new Arg($item->value)); if ($resolved === null) { continue; From ffd93f502aced99f9fc186f64092454213d268e9 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 6 Sep 2024 15:59:38 +0200 Subject: [PATCH 05/18] Pin nikic/php-parser:^5.1 --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 2d84af6..ecb5ddb 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "phpstan/phpstan": "^2.0" }, "require-dev": { + "nikic/php-parser": "^5.1", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-phpunit": "^2.0", From 879890edede8d650f3f99a34d3365d707d9afc9b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 13 Sep 2024 15:00:20 +0200 Subject: [PATCH 06/18] Fixes for TypeSpecifier BC breaks --- .../AssertTypeSpecifyingExtension.php | 23 +++++++++---------- .../ImpossibleCheckTypeMethodCallRuleTest.php | 11 +++++++++ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php b/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php index 817dfa1..b03a105 100644 --- a/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php +++ b/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php @@ -165,10 +165,9 @@ public function specifyTypes( $scope, $expr, TypeSpecifierContext::createTruthy(), - $rootExpr, - ); + )->setRootExpr($rootExpr ?? $expr); - return $this->specifyRootExprIfSet($rootExpr, $specifiedTypes); + return $this->specifyRootExprIfSet($rootExpr, $scope, $specifiedTypes); } /** @@ -688,6 +687,7 @@ private function handleAllNot( $scope, $node->getArgs()[0]->value, static fn (Type $type): Type => TypeCombinator::removeNull($type), + null, ); } @@ -703,6 +703,7 @@ private function handleAllNot( $scope, $node->getArgs()[0]->value, static fn (Type $type): Type => TypeCombinator::remove($type, $classNameType), + null, ); } @@ -712,6 +713,7 @@ private function handleAllNot( $scope, $node->getArgs()[0]->value, static fn (Type $type): Type => TypeCombinator::remove($type, $valueType), + null, ); } @@ -739,8 +741,7 @@ private function handleAll( $scope, $expr, TypeSpecifierContext::createTruthy(), - $rootExpr, - ); + )->setRootExpr($rootExpr ?? $expr); $sureNotTypes = $specifiedTypes->getSureNotTypes(); foreach ($specifiedTypes->getSureTypes() as $exprStr => [$exprNode, $type]) { @@ -768,7 +769,7 @@ private function allArrayOrIterable( Scope $scope, Expr $expr, Closure $typeCallback, - ?Expr $rootExpr = null + ?Expr $rootExpr ): SpecifiedTypes { $currentType = TypeCombinator::intersect($scope->getType($expr), new IterableType(new MixedType(), new MixedType())); @@ -812,12 +813,10 @@ private function allArrayOrIterable( $expr, $specifiedType, TypeSpecifierContext::createTruthy(), - false, $scope, - $rootExpr, - ); + )->setRootExpr($rootExpr); - return $this->specifyRootExprIfSet($rootExpr, $specifiedTypes); + return $this->specifyRootExprIfSet($rootExpr, $scope, $specifiedTypes); } /** @@ -882,7 +881,7 @@ private static function createIsNonEmptyStringAndSomethingExprPair(string $name, return [$expr, $rootExpr]; } - private function specifyRootExprIfSet(?Expr $rootExpr, SpecifiedTypes $specifiedTypes): SpecifiedTypes + private function specifyRootExprIfSet(?Expr $rootExpr, Scope $scope, SpecifiedTypes $specifiedTypes): SpecifiedTypes { if ($rootExpr === null) { return $specifiedTypes; @@ -890,7 +889,7 @@ private function specifyRootExprIfSet(?Expr $rootExpr, SpecifiedTypes $specified // Makes consecutive calls with a rootExpr adding unknown info via FAUX_FUNCTION evaluate to true return $specifiedTypes->unionWith( - $this->typeSpecifier->create($rootExpr, new ConstantBooleanType(true), TypeSpecifierContext::createTruthy()), + $this->typeSpecifier->create($rootExpr, new ConstantBooleanType(true), TypeSpecifierContext::createTruthy(), $scope), ); } diff --git a/tests/Type/WebMozartAssert/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/Type/WebMozartAssert/ImpossibleCheckTypeMethodCallRuleTest.php index aa860de..3033b04 100644 --- a/tests/Type/WebMozartAssert/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/Type/WebMozartAssert/ImpossibleCheckTypeMethodCallRuleTest.php @@ -19,6 +19,8 @@ protected function getRule(): Rule public function testExtension(): void { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/impossible-check.php'], [ [ 'Call to static method Webmozart\Assert\Assert::stringNotEmpty() with \'\' will always evaluate to false.', @@ -92,10 +94,12 @@ public function testExtension(): void [ 'Call to static method Webmozart\Assert\Assert::implementsInterface() with class-string|WebmozartAssertImpossibleCheck\Bar and \'WebmozartAssertImpossibleCheck\\\Bar\' will always evaluate to true.', 105, + $tipText, ], [ 'Call to static method Webmozart\Assert\Assert::implementsInterface() with class-string and \'WebmozartAssertImpossibleCheck\\\Bar\' will always evaluate to true.', 108, + $tipText, ], [ 'Call to static method Webmozart\Assert\Assert::implementsInterface() with mixed and \'WebmozartAssertImpossibleCheck\\\Foo\' will always evaluate to false.', @@ -104,6 +108,7 @@ public function testExtension(): void [ 'Call to static method Webmozart\Assert\Assert::isInstanceOf() with Exception and class-string will always evaluate to true.', 119, + $tipText, ], [ 'Call to static method Webmozart\Assert\Assert::startsWith() with \'value\' and string will always evaluate to true.', @@ -184,28 +189,34 @@ public function testEqNotEq(): void public function testBug8(): void { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse([__DIR__ . '/data/bug-8.php'], [ [ 'Call to static method Webmozart\Assert\Assert::numeric() with numeric-string will always evaluate to true.', 15, + $tipText, ], [ 'Call to static method Webmozart\Assert\Assert::numeric() with \'foo\' will always evaluate to false.', 16, + $tipText, ], [ 'Call to static method Webmozart\Assert\Assert::numeric() with \'17.19\' will always evaluate to true.', 17, + $tipText, ], ]); } public function testBug17(): void { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse([__DIR__ . '/data/bug-17.php'], [ [ 'Call to static method Webmozart\Assert\Assert::implementsInterface() with \'DateTime\' and \'DateTimeInterface\' will always evaluate to true.', 9, + $tipText, ], ]); } From dd4ef585d05d6923267f7389c37f8eca0615a763 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 24 Sep 2024 20:55:27 +0200 Subject: [PATCH 07/18] Fix build --- tests/Type/WebMozartAssert/data/array.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Type/WebMozartAssert/data/array.php b/tests/Type/WebMozartAssert/data/array.php index a105357..f670b95 100644 --- a/tests/Type/WebMozartAssert/data/array.php +++ b/tests/Type/WebMozartAssert/data/array.php @@ -101,19 +101,19 @@ public function countBetween(array $a, array $b, array $c, array $d): void public function isList($a, $b): void { Assert::isList($a); - assertType('array', $a); + assertType('list', $a); Assert::nullOrIsList($b); - assertType('array|null', $b); + assertType('list|null', $b); } public function isNonEmptyList($a, $b): void { Assert::isNonEmptyList($a); - assertType('non-empty-array', $a); + assertType('non-empty-list', $a); Assert::nullOrIsNonEmptyList($b); - assertType('non-empty-array|null', $b); + assertType('non-empty-list|null', $b); } public function isMap($a, $b): void From 0c641817d2a8f05c7157f92d91986e74d3c8ab0c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 14 Oct 2024 05:45:26 +0200 Subject: [PATCH 08/18] Fix after PHPStan update --- tests/Type/WebMozartAssert/data/bug-117.php | 2 +- tests/Type/WebMozartAssert/data/bug-150.php | 2 +- .../Type/WebMozartAssert/data/collection.php | 4 ++-- tests/Type/WebMozartAssert/data/type.php | 20 +++++++++---------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/Type/WebMozartAssert/data/bug-117.php b/tests/Type/WebMozartAssert/data/bug-117.php index 501ee04..635f100 100644 --- a/tests/Type/WebMozartAssert/data/bug-117.php +++ b/tests/Type/WebMozartAssert/data/bug-117.php @@ -46,7 +46,7 @@ public function getData(int $accountId, array $requestData): array $requestData['accountId'] = $accountId; - assertType("hasOffsetValue('accountId', int)&hasOffsetValue('errorColor', string|null)&hasOffsetValue('theme', array&hasOffsetValue('backgroundColor', string|null)&hasOffsetValue('headerImage', (array&hasOffsetValue('id', int))|null)&hasOffsetValue('textColor', string|null))&non-empty-array", $requestData); + assertType("non-empty-array&hasOffsetValue('accountId', int)&hasOffsetValue('errorColor', string|null)&hasOffsetValue('theme', non-empty-array&hasOffsetValue('backgroundColor', string|null)&hasOffsetValue('headerImage', (non-empty-array&hasOffsetValue('id', int))|null)&hasOffsetValue('textColor', string|null))", $requestData); return $requestData; } diff --git a/tests/Type/WebMozartAssert/data/bug-150.php b/tests/Type/WebMozartAssert/data/bug-150.php index e2bb4fa..22efe5a 100644 --- a/tests/Type/WebMozartAssert/data/bug-150.php +++ b/tests/Type/WebMozartAssert/data/bug-150.php @@ -13,7 +13,7 @@ public function doFoo($data): void Assert::isArray($data); Assert::keyExists($data, 'sniffs'); Assert::isArray($data['sniffs']); - assertType("array&hasOffsetValue('sniffs', array)", $data); + assertType("non-empty-array&hasOffsetValue('sniffs', array)", $data); foreach ($data['sniffs'] as $sniffName) { Assert::string($sniffName); diff --git a/tests/Type/WebMozartAssert/data/collection.php b/tests/Type/WebMozartAssert/data/collection.php index a853731..73eaab2 100644 --- a/tests/Type/WebMozartAssert/data/collection.php +++ b/tests/Type/WebMozartAssert/data/collection.php @@ -155,10 +155,10 @@ public function allKeyExists(array $a, array $b, array $c, $d): void assertType('array', $a); Assert::allKeyExists($b, 'id'); - assertType('array&hasOffset(\'id\')>', $b); + assertType('array&hasOffset(\'id\')>', $b); Assert::allKeyExists($c, 'id'); - assertType('array', $c); + assertType('array', $c); } /** diff --git a/tests/Type/WebMozartAssert/data/type.php b/tests/Type/WebMozartAssert/data/type.php index 9216966..9960eed 100644 --- a/tests/Type/WebMozartAssert/data/type.php +++ b/tests/Type/WebMozartAssert/data/type.php @@ -152,37 +152,37 @@ public function isCallable($a, $b): void public function isArray($a, $b): void { Assert::isArray($a); - assertType('array', $a); + assertType('array', $a); Assert::nullOrIsArray($b); - assertType('array|null', $b); + assertType('array|null', $b); } public function isTraversable($a, $b): void { Assert::isTraversable($a); - assertType('array|Traversable', $a); + assertType('array|Traversable', $a); Assert::nullOrIsTraversable($b); - assertType('array|Traversable|null', $b); + assertType('array|Traversable|null', $b); } public function isIterable($a, $b): void { Assert::isIterable($a); - assertType('array|Traversable', $a); + assertType('array|Traversable', $a); Assert::nullOrIsIterable($b); - assertType('array|Traversable|null', $b); + assertType('array|Traversable|null', $b); } public function isCountable($a, $b): void { Assert::isCountable($a); - assertType('array|Countable', $a); + assertType('array|Countable', $a); Assert::nullOrIsCountable($b); - assertType('array|Countable|null', $b); + assertType('array|Countable|null', $b); } public function isInstanceOf($a, $b, $c, $d): void @@ -300,10 +300,10 @@ public function isNotA(object $a, string $b, ?object $c): void public function isArrayAccessible($a, $b): void { Assert::isArrayAccessible($a); - assertType('array|ArrayAccess', $a); + assertType('array|ArrayAccess', $a); Assert::nullOrIsArrayAccessible($b); - assertType('array|ArrayAccess|null', $b); + assertType('array|ArrayAccess|null', $b); } } From f8a8306851c52a9a43013cb58cee579424d06579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Tue, 28 Jan 2025 10:27:42 +0100 Subject: [PATCH 09/18] Update LICENSE --- LICENSE | 1 + 1 file changed, 1 insertion(+) diff --git a/LICENSE b/LICENSE index 7c0f2b7..e5f34e6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2016 Ondřej Mirtes +Copyright (c) 2025 PHPStan s.r.o. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From f2687ce7638e22ab8c674dff3387ebcdb4b9469e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 4 Mar 2025 17:18:01 +0100 Subject: [PATCH 10/18] Fix build --- composer.json | 2 +- tests/Type/WebMozartAssert/data/collection.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index ecb5ddb..4588136 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.0" + "phpstan/phpstan": "^2.1.7" }, "require-dev": { "nikic/php-parser": "^5.1", diff --git a/tests/Type/WebMozartAssert/data/collection.php b/tests/Type/WebMozartAssert/data/collection.php index 73eaab2..e8a7f25 100644 --- a/tests/Type/WebMozartAssert/data/collection.php +++ b/tests/Type/WebMozartAssert/data/collection.php @@ -86,12 +86,12 @@ public function allInstanceOf(array $a, array $b, array $c, $d): void * @param (CollectionFoo|stdClass)[] $b * @param CollectionFoo[] $c */ - public function allNotInstanceOf(array $a, array $b, array $c): void + public function allNotInstanceOf(array $a, array $b, array $c, \stdClass $std): void { Assert::allNotInstanceOf($a, CollectionBar::class); assertType('array', $a); - Assert::allNotInstanceOf($b, new stdClass()); + Assert::allNotInstanceOf($b, $std); assertType('array', $b); Assert::allNotInstanceOf($c, 17); From bf212ed0e94c73bc81f9f4e7d4cce51252699a29 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 02:58:09 +0000 Subject: [PATCH 11/18] Update metcalfc/changelog-generator action to v4.5.0 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1a669a..be6cad0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v4.3.1 + uses: metcalfc/changelog-generator@v4.5.0 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From 596a93a064144bcf85fbe6f9022f973cbedfa661 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 02:56:04 +0000 Subject: [PATCH 12/18] Update metcalfc/changelog-generator action to v4.6.2 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be6cad0..b8c96d4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v4.5.0 + uses: metcalfc/changelog-generator@v4.6.2 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From 698465296c6196e61ea4479112c43b279c7215cf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 14:11:50 +0000 Subject: [PATCH 13/18] Update Eomm/why-don-t-you-tweet action to v2 --- .github/workflows/release-tweet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-tweet.yml b/.github/workflows/release-tweet.yml index 09b39de..d81f34c 100644 --- a/.github/workflows/release-tweet.yml +++ b/.github/workflows/release-tweet.yml @@ -10,7 +10,7 @@ jobs: tweet: runs-on: ubuntu-latest steps: - - uses: Eomm/why-don-t-you-tweet@v1 + - uses: Eomm/why-don-t-you-tweet@v2 if: ${{ !github.event.repository.private }} with: # GitHub event payload From 56c5ead8dd86e8dc0ed77cd4c4460900a00a63d5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 00:50:36 +0000 Subject: [PATCH 14/18] Update actions/checkout action to v5 --- .github/workflows/build.yml | 10 +++++----- .github/workflows/create-tag.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 88543fb..db0977e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -49,10 +49,10 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: "Checkout build-cs" - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: "phpstan/build-cs" path: "build-cs" @@ -100,7 +100,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -139,7 +139,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index a853501..fd91816 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -21,7 +21,7 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 token: ${{ secrets.PHPSTAN_BOT_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b8c96d4..ed7e51a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Generate changelog id: changelog From c7ba7f3cbeda8494b77cd7be21bb9d1eb2d9ecce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 02:10:32 +0000 Subject: [PATCH 15/18] Update actions/checkout action to v6 --- .github/workflows/build.yml | 10 +++++----- .github/workflows/create-tag.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index db0977e..8ccf1c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -49,10 +49,10 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: "Checkout build-cs" - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: "phpstan/build-cs" path: "build-cs" @@ -100,7 +100,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -139,7 +139,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index fd91816..a946f1c 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -21,7 +21,7 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout" - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.PHPSTAN_BOT_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ed7e51a..efb9bbe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Generate changelog id: changelog From d128016b7f6b9462f5e42105df6723d1f983ffdb Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 4 Dec 2025 13:58:10 +0100 Subject: [PATCH 16/18] Consistent rule level in Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 90ca1f2..1ee557d 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ cs-fix: .PHONY: phpstan phpstan: - php vendor/bin/phpstan analyse -l 9 -c phpstan.neon src tests + php vendor/bin/phpstan analyse -l 8 -c phpstan.neon src tests .PHONY: phpstan-generate-baseline phpstan-generate-baseline: From 7087804f6b6f2036b9b4852f3325e78cc2ded1ed Mon Sep 17 00:00:00 2001 From: Markus Staab <47448731+clxmstaab@users.noreply.github.com> Date: Thu, 4 Dec 2025 16:19:24 +0100 Subject: [PATCH 17/18] Remove no longer required phpdoc types --- .../Type/WebMozartAssert/AssertTypeSpecifyingExtensionTest.php | 3 --- .../AssertTypeSpecifyingExtensionTestBleedingEdge.php | 1 - 2 files changed, 4 deletions(-) diff --git a/tests/Type/WebMozartAssert/AssertTypeSpecifyingExtensionTest.php b/tests/Type/WebMozartAssert/AssertTypeSpecifyingExtensionTest.php index a0b14dd..951b354 100644 --- a/tests/Type/WebMozartAssert/AssertTypeSpecifyingExtensionTest.php +++ b/tests/Type/WebMozartAssert/AssertTypeSpecifyingExtensionTest.php @@ -7,9 +7,6 @@ class AssertTypeSpecifyingExtensionTest extends TypeInferenceTestCase { - /** - * @return iterable - */ public function dataFileAsserts(): iterable { yield from $this->gatherAssertTypes(__DIR__ . '/data/array.php'); diff --git a/tests/Type/WebMozartAssert/AssertTypeSpecifyingExtensionTestBleedingEdge.php b/tests/Type/WebMozartAssert/AssertTypeSpecifyingExtensionTestBleedingEdge.php index 0eb16c8..bbd9dda 100644 --- a/tests/Type/WebMozartAssert/AssertTypeSpecifyingExtensionTestBleedingEdge.php +++ b/tests/Type/WebMozartAssert/AssertTypeSpecifyingExtensionTestBleedingEdge.php @@ -7,7 +7,6 @@ class AssertTypeSpecifyingExtensionTestBleedingEdge extends TypeInferenceTestCase { - /** @return iterable */ public function dataFileAsserts(): iterable { yield from $this->gatherAssertTypes(__DIR__ . '/data/array-bleeding-edge.php'); From 48082c635e7b6077f82111958f8a276273b46234 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 02:15:29 +0000 Subject: [PATCH 18/18] Update dessant/lock-threads action to v6 --- .github/workflows/lock-closed-issues.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml index 99e74d3..5ecff6c 100644 --- a/.github/workflows/lock-closed-issues.yml +++ b/.github/workflows/lock-closed-issues.yml @@ -8,7 +8,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v5 + - uses: dessant/lock-threads@v6 with: github-token: ${{ github.token }} issue-inactive-days: '31'