diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index efc1cce..8ccf1c5 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: @@ -16,8 +16,6 @@ jobs: strategy: matrix: php-version: - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" @@ -27,7 +25,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -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" @@ -55,14 +49,14 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: "Checkout build-cs" - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: "phpstan/build-cs" path: "build-cs" - ref: "1.x" + ref: "2.x" - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -94,8 +88,6 @@ jobs: fail-fast: false matrix: php-version: - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" @@ -108,7 +100,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -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" @@ -153,7 +139,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -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" diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index a853501..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@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.PHPSTAN_BOT_TOKEN }} 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' 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1a669a..efb9bbe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,11 +14,11 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v4.3.1 + uses: metcalfc/changelog-generator@v4.6.2 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} 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 diff --git a/Makefile b/Makefile index 35e7804..1ee557d 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 @@ -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: diff --git a/composer.json b/composer.json index 3bb2343..4588136 100644 --- a/composer.json +++ b/composer.json @@ -6,16 +6,16 @@ "MIT" ], "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.12.5" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.7" }, "require-dev": { - "nikic/php-parser": "^4.13.0", + "nikic/php-parser": "^5.1", "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": { diff --git a/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php b/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php index c3afdef..b03a105 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,10 +165,9 @@ static function (Type $type) { $scope, $expr, TypeSpecifierContext::createTruthy(), - $rootExpr - ); + )->setRootExpr($rootExpr ?? $expr); - return $this->specifyRootExprIfSet($rootExpr, $specifiedTypes); + return $this->specifyRootExprIfSet($rootExpr, $scope, $specifiedTypes); } /** @@ -206,8 +201,8 @@ private function createExpression( $expr, new Identical( $args[0]->value, - new ConstFetch(new Name('null')) - ) + new ConstFetch(new Name('null')), + ), ); } @@ -219,219 +214,171 @@ private function createExpression( */ private function getExpressionResolvers(): array { - if ($this->resolvers === null) { + if (!isset($this->resolvers)) { $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 +393,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 +417,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] + [$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] + $max->value, ), - new BooleanAnd( - new GreaterOrEqual( - new FuncCall( - new Name('strlen'), - [$value] - ), - $min->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] - ), - 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 +641,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 +669,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 +686,8 @@ 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), + null, ); } @@ -828,9 +702,8 @@ 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), + null, ); } @@ -839,9 +712,8 @@ 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), + null, ); } @@ -869,8 +741,7 @@ private function handleAll( $scope, $expr, TypeSpecifierContext::createTruthy(), - $rootExpr - ); + )->setRootExpr($rootExpr ?? $expr); $sureNotTypes = $specifiedTypes->getSureNotTypes(); foreach ($specifiedTypes->getSureTypes() as $exprStr => [$exprNode, $type]) { @@ -886,10 +757,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, ); } @@ -900,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())); @@ -944,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); } /** @@ -965,25 +832,19 @@ 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, ); } 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; @@ -1004,23 +865,23 @@ 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]; } - private function specifyRootExprIfSet(?Expr $rootExpr, SpecifiedTypes $specifiedTypes): SpecifiedTypes + private function specifyRootExprIfSet(?Expr $rootExpr, Scope $scope, SpecifiedTypes $specifiedTypes): SpecifiedTypes { if ($rootExpr === null) { return $specifiedTypes; @@ -1028,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/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'); 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, ], ]); } 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 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..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); @@ -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); } }