From e00da5f7e5c25cbfe6a39d69a8f6439b93307d98 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 9 May 2022 01:36:55 +0000 Subject: [PATCH 01/64] Update dependency slevomat/coding-standard to v7.2.0 --- build-cs/composer.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 5a10fffd..6e7fb818 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -151,16 +151,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.4.2", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "4cb3021a4e10ffe3d5f94a4c34cf4b3f6de2fa3d" + "reference": "981cc368a216c988e862a75e526b6076987d1b50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/4cb3021a4e10ffe3d5f94a4c34cf4b3f6de2fa3d", - "reference": "4cb3021a4e10ffe3d5f94a4c34cf4b3f6de2fa3d", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/981cc368a216c988e862a75e526b6076987d1b50", + "reference": "981cc368a216c988e862a75e526b6076987d1b50", "shasum": "" }, "require": { @@ -189,38 +189,38 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.4.2" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.5.1" }, - "time": "2022-03-30T13:33:37+00:00" + "time": "2022-05-05T11:32:40+00:00" }, { "name": "slevomat/coding-standard", - "version": "7.1", + "version": "7.2.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "b521bd358b5f7a7d69e9637fd139e036d8adeb6f" + "reference": "b4f96a8beea515d2d89141b7b9ad72f526d84071" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/b521bd358b5f7a7d69e9637fd139e036d8adeb6f", - "reference": "b521bd358b5f7a7d69e9637fd139e036d8adeb6f", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/b4f96a8beea515d2d89141b7b9ad72f526d84071", + "reference": "b4f96a8beea515d2d89141b7b9ad72f526d84071", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": "^1.4.1", + "phpstan/phpdoc-parser": "^1.5.1", "squizlabs/php_codesniffer": "^3.6.2" }, "require-dev": { - "phing/phing": "2.17.2", + "phing/phing": "2.17.3", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.5.2", + "phpstan/phpstan": "1.4.10|1.6.7", "phpstan/phpstan-deprecation-rules": "1.0.0", - "phpstan/phpstan-phpunit": "1.0.0|1.1.0", - "phpstan/phpstan-strict-rules": "1.1.0", - "phpunit/phpunit": "7.5.20|8.5.21|9.5.19" + "phpstan/phpstan-phpunit": "1.0.0|1.1.1", + "phpstan/phpstan-strict-rules": "1.2.3", + "phpunit/phpunit": "7.5.20|8.5.21|9.5.20" }, "type": "phpcodesniffer-standard", "extra": { @@ -240,7 +240,7 @@ "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/7.1" + "source": "https://github.com/slevomat/coding-standard/tree/7.2.0" }, "funding": [ { @@ -252,7 +252,7 @@ "type": "tidelift" } ], - "time": "2022-03-29T12:44:16+00:00" + "time": "2022-05-06T10:58:42+00:00" }, { "name": "squizlabs/php_codesniffer", From 694fe403a66a02631851b840d5bd839a096e200c Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 30 May 2022 02:04:17 +0000 Subject: [PATCH 02/64] Update dependency slevomat/coding-standard to v7.2.1 --- build-cs/composer.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 6e7fb818..4bcc8de4 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -195,16 +195,16 @@ }, { "name": "slevomat/coding-standard", - "version": "7.2.0", + "version": "7.2.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "b4f96a8beea515d2d89141b7b9ad72f526d84071" + "reference": "aff06ae7a84e4534bf6f821dc982a93a5d477c90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/b4f96a8beea515d2d89141b7b9ad72f526d84071", - "reference": "b4f96a8beea515d2d89141b7b9ad72f526d84071", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/aff06ae7a84e4534bf6f821dc982a93a5d477c90", + "reference": "aff06ae7a84e4534bf6f821dc982a93a5d477c90", "shasum": "" }, "require": { @@ -216,7 +216,7 @@ "require-dev": { "phing/phing": "2.17.3", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.6.7", + "phpstan/phpstan": "1.4.10|1.7.1", "phpstan/phpstan-deprecation-rules": "1.0.0", "phpstan/phpstan-phpunit": "1.0.0|1.1.1", "phpstan/phpstan-strict-rules": "1.2.3", @@ -240,7 +240,7 @@ "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/7.2.0" + "source": "https://github.com/slevomat/coding-standard/tree/7.2.1" }, "funding": [ { @@ -252,7 +252,7 @@ "type": "tidelift" } ], - "time": "2022-05-06T10:58:42+00:00" + "time": "2022-05-25T10:58:12+00:00" }, { "name": "squizlabs/php_codesniffer", From 05098724e1476bb02a4eadf9c83b70584703a196 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 28 Jun 2022 23:05:02 +0200 Subject: [PATCH 03/64] Require PHPStan 1.8.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 2f67ed5a..9e9279da 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.5.0" + "phpstan/phpstan": "^1.8.0" }, "conflict": { "phpunit/phpunit": "<7.0" From 34a6bb5c5427955ec02ab95a60cf3cf4cb4b92ec Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 28 Jun 2022 23:05:37 +0200 Subject: [PATCH 04/64] PHPStan baseline --- Makefile | 4 ++++ phpstan-baseline.neon | 11 +++++++++++ phpstan.neon | 1 + 3 files changed, 16 insertions(+) create mode 100644 phpstan-baseline.neon diff --git a/Makefile b/Makefile index fe917d3b..b34d0fec 100644 --- a/Makefile +++ b/Makefile @@ -21,3 +21,7 @@ cs-fix: .PHONY: phpstan phpstan: php vendor/bin/phpstan analyse -l 8 -c phpstan.neon src tests + +.PHONY: phpstan-generate-baseline +phpstan-generate-baseline: + php vendor/bin/phpstan analyse -l 8 -c phpstan.neon src tests -b phpstan-baseline.neon diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..f0464def --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,11 @@ +parameters: + ignoreErrors: + - + message: "#^Accessing PHPStan\\\\Rules\\\\Comparison\\\\ImpossibleCheckTypeMethodCallRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + count: 1 + path: tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php + + - + message: "#^Accessing PHPStan\\\\Rules\\\\Comparison\\\\ImpossibleCheckTypeStaticMethodCallRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + count: 1 + path: tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php diff --git a/phpstan.neon b/phpstan.neon index d71fc5ab..d1a581ac 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,6 +3,7 @@ includes: - rules.neon - vendor/phpstan/phpstan-strict-rules/rules.neon - phar://phpstan.phar/conf/bleedingEdge.neon + - phpstan-baseline.neon parameters: excludePaths: From 2ca1b46190275fda8d9c9d8bb1b16fdd2b2c9c47 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 28 Jun 2022 23:11:54 +0200 Subject: [PATCH 05/64] Update .gitattributes --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 615bf05f..9d7c518b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,4 +10,5 @@ tmp export-ignore Makefile export-ignore phpcs.xml export-ignore phpstan.neon export-ignore +phpstan-baseline.neon export-ignore phpunit.xml export-ignore From 52bdce81af70c8c3b1cb995e7442ca64aed562ef Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 13 Jul 2022 12:53:45 +0200 Subject: [PATCH 06/64] Create tag workflow --- .github/workflows/create-tag.yml | 53 ++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/create-tag.yml diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml new file mode 100644 index 00000000..8452d986 --- /dev/null +++ b/.github/workflows/create-tag.yml @@ -0,0 +1,53 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Create tag" + +on: + # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch + workflow_dispatch: + inputs: + version: + description: 'Next version' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + +jobs: + create-tag: + name: "Create tag" + runs-on: "ubuntu-latest" + steps: + - name: "Checkout" + uses: actions/checkout@v3 + with: + fetch-depth: 0 + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + + - name: 'Get Previous tag' + id: previoustag + uses: "WyriHaximus/github-action-get-previous-tag@v1" + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + - name: 'Get next versions' + id: semvers + uses: "WyriHaximus/github-action-next-semvers@v1" + with: + version: ${{ steps.previoustag.outputs.tag }} + + - name: "Create new minor tag" + uses: rickstaa/action-create-tag@v1 + if: inputs.version == 'minor' + with: + tag: ${{ steps.semvers.outputs.minor }} + message: ${{ steps.semvers.outputs.minor }} + + - name: "Create new patch tag" + uses: rickstaa/action-create-tag@v1 + if: inputs.version == 'patch' + with: + tag: ${{ steps.semvers.outputs.patch }} + message: ${{ steps.semvers.outputs.patch }} From b808cb8375c52ff611ce091aae666e1acf924d50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Tue, 19 Jul 2022 13:16:01 +0200 Subject: [PATCH 07/64] Update build.yml --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 82332bb6..35254989 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,6 +21,7 @@ jobs: - "7.4" - "8.0" - "8.1" + - "8.2" steps: - name: "Checkout" @@ -85,6 +86,7 @@ jobs: - "7.4" - "8.0" - "8.1" + - "8.2" dependencies: - "lowest" - "highest" @@ -127,6 +129,7 @@ jobs: - "7.4" - "8.0" - "8.1" + - "8.2" dependencies: - "lowest" - "highest" From d963a070beba9bac9f32f209111299bdf275b687 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 3 Oct 2022 15:23:24 +0200 Subject: [PATCH 08/64] Fix build --- composer.json | 2 +- phpstan.neon | 17 ----------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/composer.json b/composer.json index 9e9279da..a7ec4277 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.8.0" + "phpstan/phpstan": "^1.9.0" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/phpstan.neon b/phpstan.neon index d1a581ac..2b8fa1ae 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,20 +8,3 @@ includes: parameters: excludePaths: - tests/*/data/* - -services: - scopeIsInClass: - class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension - arguments: - isInMethodName: isInClass - removeNullMethodName: getClassReflection - tags: - - phpstan.typeSpecifier.methodTypeSpecifyingExtension - - scopeIsInTrait: - class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension - arguments: - isInMethodName: isInTrait - removeNullMethodName: getTraitReflection - tags: - - phpstan.typeSpecifier.methodTypeSpecifyingExtension From 5fcfe8f8d79d97848064569c087193b65575b9e2 Mon Sep 17 00:00:00 2001 From: Brad Miller <28307684+mad-briller@users.noreply.github.com> Date: Mon, 17 Oct 2022 14:10:17 +0100 Subject: [PATCH 09/64] Rules to check `@covers` and `@coversDefaultClass` for methods and classes --- extension.neon | 2 + rules.neon | 10 ++ src/Rules/PHPUnit/ClassCoversExistsRule.php | 93 ++++++++++++++ .../PHPUnit/ClassMethodCoversExistsRule.php | 118 ++++++++++++++++++ src/Rules/PHPUnit/CoversHelper.php | 101 +++++++++++++++ .../PHPUnit/ClassCoversExistsRuleTest.php | 48 +++++++ .../ClassMethodCoversExistsRuleTest.php | 65 ++++++++++ tests/Rules/PHPUnit/data/class-coverage.php | 25 ++++ tests/Rules/PHPUnit/data/method-coverage.php | 87 +++++++++++++ 9 files changed, 549 insertions(+) create mode 100644 src/Rules/PHPUnit/ClassCoversExistsRule.php create mode 100644 src/Rules/PHPUnit/ClassMethodCoversExistsRule.php create mode 100644 src/Rules/PHPUnit/CoversHelper.php create mode 100644 tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php create mode 100644 tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php create mode 100644 tests/Rules/PHPUnit/data/class-coverage.php create mode 100644 tests/Rules/PHPUnit/data/method-coverage.php diff --git a/extension.neon b/extension.neon index 93d3a9ed..d3274e55 100644 --- a/extension.neon +++ b/extension.neon @@ -51,6 +51,8 @@ services: class: PHPStan\Type\PHPUnit\MockObjectDynamicReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Rules\PHPUnit\CoversHelper conditionalTags: PHPStan\PhpDoc\PHPUnit\MockObjectTypeNodeResolverExtension: diff --git a/rules.neon b/rules.neon index 5be69279..11aa3c04 100644 --- a/rules.neon +++ b/rules.neon @@ -4,3 +4,13 @@ rules: - PHPStan\Rules\PHPUnit\AssertSameWithCountRule - PHPStan\Rules\PHPUnit\MockMethodCallRule - PHPStan\Rules\PHPUnit\ShouldCallParentMethodsRule + +services: + - class: PHPStan\Rules\PHPUnit\ClassCoversExistsRule + - class: PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule + +conditionalTags: + PHPStan\Rules\PHPUnit\ClassCoversExistsRule: + phpstan.rules.rule: %featureToggles.bleedingEdge% + PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule: + phpstan.rules.rule: %featureToggles.bleedingEdge% diff --git a/src/Rules/PHPUnit/ClassCoversExistsRule.php b/src/Rules/PHPUnit/ClassCoversExistsRule.php new file mode 100644 index 00000000..a5a0cb70 --- /dev/null +++ b/src/Rules/PHPUnit/ClassCoversExistsRule.php @@ -0,0 +1,93 @@ + + */ +class ClassCoversExistsRule implements Rule +{ + + /** + * Covers helper. + * + * @var CoversHelper + */ + private $coversHelper; + + /** + * Reflection provider. + * + * @var ReflectionProvider + */ + private $reflectionProvider; + + public function __construct( + CoversHelper $coversHelper, + ReflectionProvider $reflectionProvider + ) + { + $this->reflectionProvider = $reflectionProvider; + $this->coversHelper = $coversHelper; + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + + if (!$classReflection->isSubclassOf(TestCase::class)) { + return []; + } + + $errors = []; + $classPhpDoc = $classReflection->getResolvedPhpDoc(); + [$classCovers, $classCoversDefaultClasses] = $this->coversHelper->getCoverAnnotations($classPhpDoc); + + if (count($classCoversDefaultClasses) >= 2) { + $errors[] = RuleErrorBuilder::message(sprintf( + '@coversDefaultClass is defined multiple times.' + ))->build(); + + return $errors; + } + + $coversDefaultClass = array_shift($classCoversDefaultClasses); + + if ($coversDefaultClass !== null) { + $className = (string) $coversDefaultClass->value; + if (!$this->reflectionProvider->hasClass($className)) { + $errors[] = RuleErrorBuilder::message(sprintf( + '@coversDefaultClass references an invalid class %s.', + $className + ))->build(); + } + } + + foreach ($classCovers as $covers) { + $errors = array_merge( + $errors, + $this->coversHelper->processCovers($node, $covers, null) + ); + } + + return $errors; + } + +} diff --git a/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php b/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php new file mode 100644 index 00000000..95e6cc89 --- /dev/null +++ b/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php @@ -0,0 +1,118 @@ + + */ +class ClassMethodCoversExistsRule implements Rule +{ + + /** + * Covers helper. + * + * @var CoversHelper + */ + private $coversHelper; + + /** + * The file type mapper. + * + * @var FileTypeMapper + */ + private $fileTypeMapper; + + public function __construct( + CoversHelper $coversHelper, + FileTypeMapper $fileTypeMapper + ) + { + $this->coversHelper = $coversHelper; + $this->fileTypeMapper = $fileTypeMapper; + } + + public function getNodeType(): string + { + return Node\Stmt\ClassMethod::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $scope->getClassReflection(); + + if ($classReflection === null) { + return []; + } + + if (!$classReflection->isSubclassOf(TestCase::class)) { + return []; + } + + $errors = []; + $classPhpDoc = $classReflection->getResolvedPhpDoc(); + [$classCovers, $classCoversDefaultClasses] = $this->coversHelper->getCoverAnnotations($classPhpDoc); + + $classCoversStrings = array_map(static function (PhpDocTagNode $covers): string { + return (string) $covers->value; + }, $classCovers); + + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + + $coversDefaultClass = count($classCoversDefaultClasses) === 1 + ? array_shift($classCoversDefaultClasses) + : null; + + $methodPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $classReflection->getName(), + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $node->name->toString(), + $docComment->getText() + ); + + [$methodCovers, $methodCoversDefaultClasses] = $this->coversHelper->getCoverAnnotations($methodPhpDoc); + + $errors = []; + + if (count($methodCoversDefaultClasses) > 0) { + $errors[] = RuleErrorBuilder::message(sprintf( + '@coversDefaultClass defined on class method %s.', + $node->name + ))->build(); + } + + foreach ($methodCovers as $covers) { + if (in_array((string) $covers->value, $classCoversStrings, true)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Class already @covers %s so the method @covers is redundant.', + $covers->value + ))->build(); + } + + $errors = array_merge( + $errors, + $this->coversHelper->processCovers($node, $covers, $coversDefaultClass) + ); + } + + return $errors; + } + +} diff --git a/src/Rules/PHPUnit/CoversHelper.php b/src/Rules/PHPUnit/CoversHelper.php new file mode 100644 index 00000000..aebe000a --- /dev/null +++ b/src/Rules/PHPUnit/CoversHelper.php @@ -0,0 +1,101 @@ +reflectionProvider = $reflectionProvider; + } + + /** + * Gathers @covers and @coversDefaultClass annotations from phpdocs. + * + * @return array{PhpDocTagNode[], PhpDocTagNode[]} + */ + public function getCoverAnnotations(?ResolvedPhpDocBlock $phpDoc): array + { + if ($phpDoc === null) { + return [[], []]; + } + + $phpDocNodes = $phpDoc->getPhpDocNodes(); + + $covers = []; + $coversDefaultClasses = []; + + foreach ($phpDocNodes as $docNode) { + $covers = array_merge( + $covers, + $docNode->getTagsByName('@covers') + ); + + $coversDefaultClasses = array_merge( + $coversDefaultClasses, + $docNode->getTagsByName('@coversDefaultClass') + ); + } + + return [$covers, $coversDefaultClasses]; + } + + /** + * @return RuleError[] errors + */ + public function processCovers( + Node $node, + PhpDocTagNode $phpDocTag, + ?PhpDocTagNode $coversDefaultClass + ): array + { + $errors = []; + $covers = (string) $phpDocTag->value; + + if (strpos($covers, '::') !== false) { + [$className, $method] = explode('::', $covers); + } else { + $className = $covers; + } + + if ($className === '' && $node instanceof Node\Stmt\ClassMethod && $coversDefaultClass !== null) { + $className = (string) $coversDefaultClass->value; + } + + if ($this->reflectionProvider->hasClass($className)) { + $class = $this->reflectionProvider->getClass($className); + if (isset($method) && $method !== '' && !$class->hasMethod($method)) { + $errors[] = RuleErrorBuilder::message(sprintf( + '@covers value %s references an invalid method.', + $covers + ))->build(); + } + } else { + $errors[] = RuleErrorBuilder::message(sprintf( + '@covers value %s references an invalid class.', + $covers + ))->build(); + } + return $errors; + } + +} diff --git a/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php new file mode 100644 index 00000000..fb586555 --- /dev/null +++ b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php @@ -0,0 +1,48 @@ + + */ +class ClassCoversExistsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflection = $this->createReflectionProvider(); + + return new ClassCoversExistsRule( + new CoversHelper($reflection), + $reflection + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/class-coverage.php'], [ + [ + '@coversDefaultClass references an invalid class \Not\A\Class.', + 8, + ], + [ + '@coversDefaultClass is defined multiple times.', + 23, + ], + ]); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } + +} diff --git a/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php b/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php new file mode 100644 index 00000000..2e093269 --- /dev/null +++ b/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php @@ -0,0 +1,65 @@ + + */ +class ClassMethodCoversExistsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflection = $this->createReflectionProvider(); + + return new ClassMethodCoversExistsRule( + new CoversHelper($reflection), + self::getContainer()->getByType(FileTypeMapper::class) + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/method-coverage.php'], [ + [ + '@covers value ::ignoreThis references an invalid class.', + 14, + ], + [ + '@covers value \PHPUnit\Framework\TestCase::assertNotReal references an invalid method.', + 28, + ], + [ + '@covers value \Not\A\Class::foo references an invalid class.', + 35, + ], + [ + '@coversDefaultClass defined on class method testBadCoversDefault.', + 50, + ], + [ + '@covers value ::assertNotReal references an invalid method.', + 62, + ], + [ + 'Class already @covers \PHPUnit\Framework\TestCase so the method @covers is redundant.', + 85, + ], + ]); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } + +} diff --git a/tests/Rules/PHPUnit/data/class-coverage.php b/tests/Rules/PHPUnit/data/class-coverage.php new file mode 100644 index 00000000..c35cd26e --- /dev/null +++ b/tests/Rules/PHPUnit/data/class-coverage.php @@ -0,0 +1,25 @@ + Date: Mon, 24 Oct 2022 13:23:32 +0200 Subject: [PATCH 10/64] Implement assertEmpty extension --- .../Assert/AssertTypeSpecifyingExtensionHelper.php | 11 +++++++++++ tests/Type/PHPUnit/data/assert-function.php | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 36203e65..4444db3f 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -3,6 +3,8 @@ namespace PHPStan\Type\PHPUnit\Assert; use Closure; +use Countable; +use EmptyIterator; use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\Identical; @@ -156,6 +158,15 @@ private static function getExpressionResolvers(): array new ConstFetch(new Name('null')) ); }, + 'Empty' => static function (Scope $scope, Arg $actual): Expr\BinaryOp\BooleanOr { + return new Expr\BinaryOp\BooleanOr( + new Instanceof_($actual->value, new Name(EmptyIterator::class)), + new Expr\BinaryOp\BooleanOr( + new Instanceof_($actual->value, new Name(Countable::class)), + new Expr\Empty_($actual->value) + ) + ); + }, 'IsArray' => static function (Scope $scope, Arg $actual): FuncCall { return new FuncCall(new Name('is_array'), [$actual]); }, diff --git a/tests/Type/PHPUnit/data/assert-function.php b/tests/Type/PHPUnit/data/assert-function.php index ebfeb93a..117179ae 100644 --- a/tests/Type/PHPUnit/data/assert-function.php +++ b/tests/Type/PHPUnit/data/assert-function.php @@ -4,6 +4,7 @@ use function PHPStan\Testing\assertType; use function PHPUnit\Framework\assertArrayHasKey; +use function PHPUnit\Framework\assertEmpty; use function PHPUnit\Framework\assertInstanceOf; use function PHPUnit\Framework\assertObjectHasAttribute; @@ -36,4 +37,10 @@ public function objectHasAttribute(object $a): void assertType("object&hasProperty(property)", $a); } + public function testEmpty($a): void + { + assertEmpty($a); + assertType("0|0.0|''|'0'|array{}|Countable|EmptyIterator|false|null", $a); + } + } From 6b93db7fae6d6f3e81a5b4297f93af6fe4146785 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 24 Oct 2022 13:38:17 +0200 Subject: [PATCH 11/64] Fix assertEmpty --- .../PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 4444db3f..e2684062 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -13,6 +13,7 @@ use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Instanceof_; use PhpParser\Node\Name; +use PhpParser\Node\Scalar\LNumber; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; @@ -162,7 +163,10 @@ private static function getExpressionResolvers(): array return new Expr\BinaryOp\BooleanOr( new Instanceof_($actual->value, new Name(EmptyIterator::class)), new Expr\BinaryOp\BooleanOr( - new Instanceof_($actual->value, new Name(Countable::class)), + new Expr\BinaryOp\BooleanAnd( + new Instanceof_($actual->value, new Name(Countable::class)), + new Identical(new FuncCall(new Name('count'), [new Arg($actual->value)]), new LNumber(0)) + ), new Expr\Empty_($actual->value) ) ); From a0c136455f696d32d632c4994ea2d84df72792e7 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 26 Oct 2022 13:41:10 +0200 Subject: [PATCH 12/64] Regression tests --- .../ImpossibleCheckTypeMethodCallRuleTest.php | 45 +++++++++++++++++++ .../data/impossible-assert-method-call.php | 23 ++++++++++ 2 files changed, 68 insertions(+) create mode 100644 tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php create mode 100644 tests/Rules/PHPUnit/data/impossible-assert-method-call.php diff --git a/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php new file mode 100644 index 00000000..b50bcfce --- /dev/null +++ b/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php @@ -0,0 +1,45 @@ + + */ +class ImpossibleCheckTypeMethodCallRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return self::getContainer()->getByType(ImpossibleCheckTypeMethodCallRule::class); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/impossible-assert-method-call.php'], [ + [ + 'Call to method PHPUnit\Framework\Assert::assertEmpty() with array{} will always evaluate to true.', + 14, + ], + [ + 'Call to method PHPUnit\Framework\Assert::assertEmpty() with array{1, 2, 3} will always evaluate to false.', + 15, + ], + ]); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + __DIR__ . '/../../../vendor/phpstan/phpstan-strict-rules/rules.neon', + ]; + } + +} diff --git a/tests/Rules/PHPUnit/data/impossible-assert-method-call.php b/tests/Rules/PHPUnit/data/impossible-assert-method-call.php new file mode 100644 index 00000000..a406f1e5 --- /dev/null +++ b/tests/Rules/PHPUnit/data/impossible-assert-method-call.php @@ -0,0 +1,23 @@ +assertEmpty($c); + $this->assertEmpty([]); + $this->assertEmpty([1, 2, 3]); + } + + public function doBar(object $o): void + { + $this->assertEmpty($o); + } + +} From f92aab7b75ebbcc5b59b012de1b12cadf5286b6f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 26 Oct 2022 13:43:56 +0200 Subject: [PATCH 13/64] Regression test --- .../ImpossibleCheckTypeMethodCallRuleTest.php | 10 ++++ tests/Rules/PHPUnit/data/bug-141.php | 53 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 tests/Rules/PHPUnit/data/bug-141.php diff --git a/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php index b50bcfce..700f420d 100644 --- a/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php @@ -31,6 +31,16 @@ public function testRule(): void ]); } + public function testBug141(): void + { + $this->analyse([__DIR__ . '/data/bug-141.php'], [ + [ + "Call to method PHPUnit\Framework\Assert::assertEmpty() with non-empty-array<'0.6.0'|'1.0.0'|'1.0.x-dev'|'1.1.x-dev'|'9999999-dev'|'dev-feature-b', true> will always evaluate to false.", + 23, + ], + ]); + } + /** * @return string[] */ diff --git a/tests/Rules/PHPUnit/data/bug-141.php b/tests/Rules/PHPUnit/data/bug-141.php new file mode 100644 index 00000000..30011049 --- /dev/null +++ b/tests/Rules/PHPUnit/data/bug-141.php @@ -0,0 +1,53 @@ + $a + */ + public function doFoo(array $a): void + { + $this->assertEmpty($a); + } + + /** + * @param non-empty-array<'0.6.0'|'1.0.0'|'1.0.x-dev'|'1.1.x-dev'|'9999999-dev'|'dev-feature-b', true> $a + */ + public function doBar(array $a): void + { + $this->assertEmpty($a); + } + + public function doBaz(): void + { + $expected = [ + '0.6.0' => true, + '1.0.0' => true, + '1.0.x-dev' => true, + '1.1.x-dev' => true, + 'dev-feature-b' => true, + 'dev-feature/a-1.0-B' => true, + 'dev-master' => true, + '9999999-dev' => true, // alias of dev-master + ]; + + /** @var array */ + $packages = ['0.6.0', '1.0.0', '1']; + + foreach ($packages as $version) { + if (isset($expected[$version])) { + unset($expected[$version]); + } else { + throw new \Exception('Unexpected version '.$version); + } + } + + $this->assertEmpty($expected); + } + +} From 68017cc5780866ed85e14eba824c29c3032f00fa Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 26 Oct 2022 13:46:44 +0200 Subject: [PATCH 14/64] Fix build --- phpstan-baseline.neon | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f0464def..53fb96b8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -9,3 +9,8 @@ parameters: message: "#^Accessing PHPStan\\\\Rules\\\\Comparison\\\\ImpossibleCheckTypeStaticMethodCallRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" count: 1 path: tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php + + - + message: "#^Accessing PHPStan\\\\Rules\\\\Comparison\\\\ImpossibleCheckTypeMethodCallRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + count: 1 + path: tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php From 9b88cef57c5a5e8ddb90b69da26edd0d81eeadad Mon Sep 17 00:00:00 2001 From: PrinsFrank <25006490+PrinsFrank@users.noreply.github.com> Date: Wed, 26 Oct 2022 19:42:19 +0200 Subject: [PATCH 15/64] Add rule that checks for invalid and unrecognized annotations --- extension.neon | 2 + rules.neon | 6 ++ src/Rules/PHPUnit/AnnotationHelper.php | 66 ++++++++++++++ .../NoMissingSpaceInClassAnnotationRule.php | 49 +++++++++++ .../NoMissingSpaceInMethodAnnotationRule.php | 49 +++++++++++ ...oMissingSpaceInClassAnnotationRuleTest.php | 87 +++++++++++++++++++ ...MissingSpaceInMethodAnnotationRuleTest.php | 87 +++++++++++++++++++ .../data/InvalidClassCoversAnnotation.php | 38 ++++++++ .../data/InvalidMethodCoversAnnotation.php | 83 ++++++++++++++++++ 9 files changed, 467 insertions(+) create mode 100644 src/Rules/PHPUnit/AnnotationHelper.php create mode 100644 src/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRule.php create mode 100644 src/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRule.php create mode 100644 tests/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRuleTest.php create mode 100644 tests/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRuleTest.php create mode 100644 tests/Rules/PHPUnit/data/InvalidClassCoversAnnotation.php create mode 100644 tests/Rules/PHPUnit/data/InvalidMethodCoversAnnotation.php diff --git a/extension.neon b/extension.neon index d3274e55..f6f372eb 100644 --- a/extension.neon +++ b/extension.neon @@ -53,6 +53,8 @@ services: - phpstan.broker.dynamicMethodReturnTypeExtension - class: PHPStan\Rules\PHPUnit\CoversHelper + - + class: PHPStan\Rules\PHPUnit\AnnotationHelper conditionalTags: PHPStan\PhpDoc\PHPUnit\MockObjectTypeNodeResolverExtension: diff --git a/rules.neon b/rules.neon index 11aa3c04..24a28ea0 100644 --- a/rules.neon +++ b/rules.neon @@ -8,9 +8,15 @@ rules: services: - class: PHPStan\Rules\PHPUnit\ClassCoversExistsRule - class: PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule + - class: PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule + - class: PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule conditionalTags: PHPStan\Rules\PHPUnit\ClassCoversExistsRule: phpstan.rules.rule: %featureToggles.bleedingEdge% PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule: phpstan.rules.rule: %featureToggles.bleedingEdge% + PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule: + phpstan.rules.rule: %featureToggles.bleedingEdge% + PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule: + phpstan.rules.rule: %featureToggles.bleedingEdge% diff --git a/src/Rules/PHPUnit/AnnotationHelper.php b/src/Rules/PHPUnit/AnnotationHelper.php new file mode 100644 index 00000000..f5529a8f --- /dev/null +++ b/src/Rules/PHPUnit/AnnotationHelper.php @@ -0,0 +1,66 @@ +getText()); + if ($docCommentLines === false) { + return []; + } + + foreach ($docCommentLines as $docCommentLine) { + // These annotations can't be retrieved using the getResolvedPhpDoc method on the FileTypeMapper as they are not present when they are invalid + $annotation = preg_match('/(?@(?[a-zA-Z]+)(?\s*)(?.*))/', $docCommentLine, $matches); + if ($annotation === false) { + continue; // Line without annotation + } + + if (array_key_exists('property', $matches) === false || array_key_exists('whitespace', $matches) === false || array_key_exists('annotation', $matches) === false) { + continue; + } + + if (!in_array($matches['property'], self::ANNOTATIONS_WITH_PARAMS, true) || $matches['whitespace'] !== '') { + continue; + } + + $errors[] = RuleErrorBuilder::message( + 'Annotation "' . $matches['annotation'] . '" is invalid, "@' . $matches['property'] . '" should be followed by a space and a value.' + )->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRule.php b/src/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRule.php new file mode 100644 index 00000000..89e3e8ff --- /dev/null +++ b/src/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRule.php @@ -0,0 +1,49 @@ + + */ +class NoMissingSpaceInClassAnnotationRule implements Rule +{ + + /** + * Covers helper. + * + * @var AnnotationHelper + */ + private $annotationHelper; + + public function __construct(AnnotationHelper $annotationHelper) + { + $this->annotationHelper = $annotationHelper; + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $scope->getClassReflection(); + if ($classReflection === null || $classReflection->isSubclassOf(TestCase::class) === false) { + return []; + } + + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + + return $this->annotationHelper->processDocComment($docComment); + } + +} diff --git a/src/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRule.php b/src/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRule.php new file mode 100644 index 00000000..77577206 --- /dev/null +++ b/src/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRule.php @@ -0,0 +1,49 @@ + + */ +class NoMissingSpaceInMethodAnnotationRule implements Rule +{ + + /** + * Covers helper. + * + * @var AnnotationHelper + */ + private $annotationHelper; + + public function __construct(AnnotationHelper $annotationHelper) + { + $this->annotationHelper = $annotationHelper; + } + + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $scope->getClassReflection(); + if ($classReflection === null || $classReflection->isSubclassOf(TestCase::class) === false) { + return []; + } + + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + + return $this->annotationHelper->processDocComment($docComment); + } + +} diff --git a/tests/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRuleTest.php b/tests/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRuleTest.php new file mode 100644 index 00000000..e28fde15 --- /dev/null +++ b/tests/Rules/PHPUnit/NoMissingSpaceInClassAnnotationRuleTest.php @@ -0,0 +1,87 @@ + + */ +class NoMissingSpaceInClassAnnotationRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new NoMissingSpaceInClassAnnotationRule(new AnnotationHelper()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/InvalidClassCoversAnnotation.php'], [ + [ + 'Annotation "@backupGlobals" is invalid, "@backupGlobals" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@backupStaticAttributes" is invalid, "@backupStaticAttributes" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@covers\Dummy\Foo::assertSame" is invalid, "@covers" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@covers::assertSame" is invalid, "@covers" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@coversDefaultClass\Dummy\Foo" is invalid, "@coversDefaultClass" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@dataProvider" is invalid, "@dataProvider" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@depends" is invalid, "@depends" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@preserveGlobalState" is invalid, "@preserveGlobalState" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@requires" is invalid, "@requires" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@testDox" is invalid, "@testDox" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@testWith" is invalid, "@testWith" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@ticket" is invalid, "@ticket" should be followed by a space and a value.', + 36, + ], + [ + 'Annotation "@uses" is invalid, "@uses" should be followed by a space and a value.', + 36, + ], + ]); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } + +} diff --git a/tests/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRuleTest.php b/tests/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRuleTest.php new file mode 100644 index 00000000..2926ec93 --- /dev/null +++ b/tests/Rules/PHPUnit/NoMissingSpaceInMethodAnnotationRuleTest.php @@ -0,0 +1,87 @@ + + */ +class NoMissingSpaceInMethodAnnotationRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new NoMissingSpaceInMethodAnnotationRule(new AnnotationHelper()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/InvalidMethodCoversAnnotation.php'], [ + [ + 'Annotation "@backupGlobals" is invalid, "@backupGlobals" should be followed by a space and a value.', + 12, + ], + [ + 'Annotation "@backupStaticAttributes" is invalid, "@backupStaticAttributes" should be followed by a space and a value.', + 19, + ], + [ + 'Annotation "@covers\Dummy\Foo::assertSame" is invalid, "@covers" should be followed by a space and a value.', + 27, + ], + [ + 'Annotation "@covers::assertSame" is invalid, "@covers" should be followed by a space and a value.', + 27, + ], + [ + 'Annotation "@coversDefaultClass\Dummy\Foo" is invalid, "@coversDefaultClass" should be followed by a space and a value.', + 33, + ], + [ + 'Annotation "@dataProvider" is invalid, "@dataProvider" should be followed by a space and a value.', + 39, + ], + [ + 'Annotation "@depends" is invalid, "@depends" should be followed by a space and a value.', + 45, + ], + [ + 'Annotation "@preserveGlobalState" is invalid, "@preserveGlobalState" should be followed by a space and a value.', + 52, + ], + [ + 'Annotation "@requires" is invalid, "@requires" should be followed by a space and a value.', + 58, + ], + [ + 'Annotation "@testDox" is invalid, "@testDox" should be followed by a space and a value.', + 64, + ], + [ + 'Annotation "@testWith" is invalid, "@testWith" should be followed by a space and a value.', + 70, + ], + [ + 'Annotation "@ticket" is invalid, "@ticket" should be followed by a space and a value.', + 76, + ], + [ + 'Annotation "@uses" is invalid, "@uses" should be followed by a space and a value.', + 82, + ], + ]); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } + +} diff --git a/tests/Rules/PHPUnit/data/InvalidClassCoversAnnotation.php b/tests/Rules/PHPUnit/data/InvalidClassCoversAnnotation.php new file mode 100644 index 00000000..11f9b904 --- /dev/null +++ b/tests/Rules/PHPUnit/data/InvalidClassCoversAnnotation.php @@ -0,0 +1,38 @@ += 5.3 + * @testDox + * @testDox foo bar + * @testWith + * @testWith ['foo', 'bar'] + * @ticket + * @ticket 1234 + * @uses + * @uses foo + */ +class InvalidClassCoversAnnotation extends \PHPUnit\Framework\TestCase +{ +} diff --git a/tests/Rules/PHPUnit/data/InvalidMethodCoversAnnotation.php b/tests/Rules/PHPUnit/data/InvalidMethodCoversAnnotation.php new file mode 100644 index 00000000..9154937b --- /dev/null +++ b/tests/Rules/PHPUnit/data/InvalidMethodCoversAnnotation.php @@ -0,0 +1,83 @@ += 5.3 + */ + public function requiresAnnotation() {} + + /** + * @testDox + * @testDox foo bar + */ + public function testDox() {} + + /** + * @testWith + * @testWith ['foo', 'bar'] + */ + public function testWith() {} + + /** + * @ticket + * @ticket 1234 + */ + public function ticket() {} + + /** + * @uses + * @uses foo + */ + public function uses() {} +} From 09b5c9ab38d4d601bcff04b4c9240832b86f5dda Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 26 Oct 2022 20:02:49 +0200 Subject: [PATCH 16/64] Do not require PHPStan 1.9.0 yet This reverts commit d963a070beba9bac9f32f209111299bdf275b687. --- composer.json | 2 +- phpstan.neon | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a7ec4277..40683ead 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.9.0" + "phpstan/phpstan": "^1.8.11" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/phpstan.neon b/phpstan.neon index 2b8fa1ae..d1a581ac 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,3 +8,20 @@ includes: parameters: excludePaths: - tests/*/data/* + +services: + scopeIsInClass: + class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension + arguments: + isInMethodName: isInClass + removeNullMethodName: getClassReflection + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension + + scopeIsInTrait: + class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension + arguments: + isInMethodName: isInTrait + removeNullMethodName: getTraitReflection + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension From 273ca67d02379de5d563163a9ed74af2006ddf16 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 26 Oct 2022 21:41:48 +0200 Subject: [PATCH 17/64] Revert "Do not require PHPStan 1.9.0 yet" This reverts commit 09b5c9ab38d4d601bcff04b4c9240832b86f5dda. --- composer.json | 2 +- phpstan.neon | 17 ----------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/composer.json b/composer.json index 40683ead..a7ec4277 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.8.11" + "phpstan/phpstan": "^1.9.0" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/phpstan.neon b/phpstan.neon index d1a581ac..2b8fa1ae 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,20 +8,3 @@ includes: parameters: excludePaths: - tests/*/data/* - -services: - scopeIsInClass: - class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension - arguments: - isInMethodName: isInClass - removeNullMethodName: getClassReflection - tags: - - phpstan.typeSpecifier.methodTypeSpecifyingExtension - - scopeIsInTrait: - class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension - arguments: - isInMethodName: isInTrait - removeNullMethodName: getTraitReflection - tags: - - phpstan.typeSpecifier.methodTypeSpecifyingExtension From e431a6c9ed129f1f06281e33da957414a3dab196 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 26 Oct 2022 20:02:49 +0200 Subject: [PATCH 18/64] Do not require PHPStan 1.9.0 yet This reverts commit d963a070beba9bac9f32f209111299bdf275b687. --- composer.json | 2 +- phpstan.neon | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a7ec4277..40683ead 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.9.0" + "phpstan/phpstan": "^1.8.11" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/phpstan.neon b/phpstan.neon index 2b8fa1ae..d1a581ac 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,3 +8,20 @@ includes: parameters: excludePaths: - tests/*/data/* + +services: + scopeIsInClass: + class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension + arguments: + isInMethodName: isInClass + removeNullMethodName: getClassReflection + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension + + scopeIsInTrait: + class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension + arguments: + isInMethodName: isInTrait + removeNullMethodName: getTraitReflection + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension From c86e460e93cfe0f2d39a175c71923257d5a07d7f Mon Sep 17 00:00:00 2001 From: Brad <28307684+mad-briller@users.noreply.github.com> Date: Fri, 28 Oct 2022 10:10:02 +0100 Subject: [PATCH 19/64] Fix covers rule for functions. --- src/Rules/PHPUnit/CoversHelper.php | 14 +++++++++++--- .../PHPUnit/ClassMethodCoversExistsRuleTest.php | 4 ++-- tests/Rules/PHPUnit/data/class-coverage.php | 12 ++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/Rules/PHPUnit/CoversHelper.php b/src/Rules/PHPUnit/CoversHelper.php index aebe000a..23469469 100644 --- a/src/Rules/PHPUnit/CoversHelper.php +++ b/src/Rules/PHPUnit/CoversHelper.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\PHPUnit; use PhpParser\Node; +use PhpParser\Node\Name; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; use PHPStan\Reflection\ReflectionProvider; @@ -70,8 +71,9 @@ public function processCovers( { $errors = []; $covers = (string) $phpDocTag->value; + $isMethod = strpos($covers, '::') !== false; - if (strpos($covers, '::') !== false) { + if ($isMethod) { [$className, $method] = explode('::', $covers); } else { $className = $covers; @@ -83,6 +85,7 @@ public function processCovers( if ($this->reflectionProvider->hasClass($className)) { $class = $this->reflectionProvider->getClass($className); + if (isset($method) && $method !== '' && !$class->hasMethod($method)) { $errors[] = RuleErrorBuilder::message(sprintf( '@covers value %s references an invalid method.', @@ -90,9 +93,14 @@ public function processCovers( ))->build(); } } else { + if (!isset($method) && $this->reflectionProvider->hasFunction(new Name($covers, []), null)) { + return $errors; + } + $errors[] = RuleErrorBuilder::message(sprintf( - '@covers value %s references an invalid class.', - $covers + '@covers value %s references an invalid %s.', + $covers, + $isMethod ? 'method' : 'class or function' ))->build(); } return $errors; diff --git a/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php b/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php index 2e093269..5764a605 100644 --- a/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php +++ b/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php @@ -26,7 +26,7 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/method-coverage.php'], [ [ - '@covers value ::ignoreThis references an invalid class.', + '@covers value ::ignoreThis references an invalid method.', 14, ], [ @@ -34,7 +34,7 @@ public function testRule(): void 28, ], [ - '@covers value \Not\A\Class::foo references an invalid class.', + '@covers value \Not\A\Class::foo references an invalid method.', 35, ], [ diff --git a/tests/Rules/PHPUnit/data/class-coverage.php b/tests/Rules/PHPUnit/data/class-coverage.php index c35cd26e..44ea6b45 100644 --- a/tests/Rules/PHPUnit/data/class-coverage.php +++ b/tests/Rules/PHPUnit/data/class-coverage.php @@ -23,3 +23,15 @@ class CoversShouldExistTestCase2 extends \PHPUnit\Framework\TestCase class MultipleCoversDefaultClass extends \PHPUnit\Framework\TestCase { } + +/** + * @covers \ClassCoverage\testable + */ +class CoversFunction extends \PHPUnit\Framework\TestCase +{ +} + +function testable(): void +{ + +} From dc086300d925208b2a62017f0264a9049a524adf Mon Sep 17 00:00:00 2001 From: Brad <28307684+mad-briller@users.noreply.github.com> Date: Fri, 28 Oct 2022 10:32:16 +0100 Subject: [PATCH 20/64] Add test case for class or function scenario. --- tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php | 4 ++++ tests/Rules/PHPUnit/data/class-coverage.php | 1 + 2 files changed, 5 insertions(+) diff --git a/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php index fb586555..7ead3a7c 100644 --- a/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php +++ b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php @@ -32,6 +32,10 @@ public function testRule(): void '@coversDefaultClass is defined multiple times.', 23, ], + [ + '@covers value \Not\A\Class references an invalid class or function.', + 31, + ], ]); } diff --git a/tests/Rules/PHPUnit/data/class-coverage.php b/tests/Rules/PHPUnit/data/class-coverage.php index 44ea6b45..fca878f2 100644 --- a/tests/Rules/PHPUnit/data/class-coverage.php +++ b/tests/Rules/PHPUnit/data/class-coverage.php @@ -26,6 +26,7 @@ class MultipleCoversDefaultClass extends \PHPUnit\Framework\TestCase /** * @covers \ClassCoverage\testable + * @covers \Not\A\Class */ class CoversFunction extends \PHPUnit\Framework\TestCase { From dea1f87344c6964c607d9076dee42d891f3923f0 Mon Sep 17 00:00:00 2001 From: Brad <28307684+mad-briller@users.noreply.github.com> Date: Fri, 28 Oct 2022 10:45:46 +0100 Subject: [PATCH 21/64] Be explicit with covers messaging when using @coversDefaultClass and @covers. --- src/Rules/PHPUnit/CoversHelper.php | 6 ++++-- tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Rules/PHPUnit/CoversHelper.php b/src/Rules/PHPUnit/CoversHelper.php index 23469469..8bcf92e4 100644 --- a/src/Rules/PHPUnit/CoversHelper.php +++ b/src/Rules/PHPUnit/CoversHelper.php @@ -72,6 +72,7 @@ public function processCovers( $errors = []; $covers = (string) $phpDocTag->value; $isMethod = strpos($covers, '::') !== false; + $fullName = $covers; if ($isMethod) { [$className, $method] = explode('::', $covers); @@ -81,6 +82,7 @@ public function processCovers( if ($className === '' && $node instanceof Node\Stmt\ClassMethod && $coversDefaultClass !== null) { $className = (string) $coversDefaultClass->value; + $fullName = $className . $covers; } if ($this->reflectionProvider->hasClass($className)) { @@ -89,7 +91,7 @@ public function processCovers( if (isset($method) && $method !== '' && !$class->hasMethod($method)) { $errors[] = RuleErrorBuilder::message(sprintf( '@covers value %s references an invalid method.', - $covers + $fullName ))->build(); } } else { @@ -99,7 +101,7 @@ public function processCovers( $errors[] = RuleErrorBuilder::message(sprintf( '@covers value %s references an invalid %s.', - $covers, + $fullName, $isMethod ? 'method' : 'class or function' ))->build(); } diff --git a/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php b/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php index 5764a605..b886b460 100644 --- a/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php +++ b/tests/Rules/PHPUnit/ClassMethodCoversExistsRuleTest.php @@ -26,7 +26,7 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/method-coverage.php'], [ [ - '@covers value ::ignoreThis references an invalid method.', + '@covers value \Not\A\Class::ignoreThis references an invalid method.', 14, ], [ @@ -42,7 +42,7 @@ public function testRule(): void 50, ], [ - '@covers value ::assertNotReal references an invalid method.', + '@covers value \PHPUnit\Framework\TestCase::assertNotReal references an invalid method.', 62, ], [ From 2de71f94c4a1114c4a6bf30d66a87439c0071338 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 28 Oct 2022 13:04:01 +0200 Subject: [PATCH 22/64] Revert "Do not require PHPStan 1.9.0 yet" This reverts commit e431a6c9ed129f1f06281e33da957414a3dab196. --- composer.json | 2 +- phpstan.neon | 17 ----------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/composer.json b/composer.json index 40683ead..a7ec4277 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.8.11" + "phpstan/phpstan": "^1.9.0" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/phpstan.neon b/phpstan.neon index d1a581ac..2b8fa1ae 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,20 +8,3 @@ includes: parameters: excludePaths: - tests/*/data/* - -services: - scopeIsInClass: - class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension - arguments: - isInMethodName: isInClass - removeNullMethodName: getClassReflection - tags: - - phpstan.typeSpecifier.methodTypeSpecifyingExtension - - scopeIsInTrait: - class: PHPStan\Internal\ScopeIsInClassTypeSpecifyingExtension - arguments: - isInMethodName: isInTrait - removeNullMethodName: getTraitReflection - tags: - - phpstan.typeSpecifier.methodTypeSpecifyingExtension From a6aebda5b9206c61495801722b8329708a88a8c8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Nov 2022 02:51:01 +0000 Subject: [PATCH 23/64] Update metcalfc/changelog-generator action to v4 --- .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 5fed0458..bac4a006 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@v3.0.0 + uses: metcalfc/changelog-generator@v4.0.1 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From 8313d41c08795f81925c9b951232f483c6efe21f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 01:04:45 +0000 Subject: [PATCH 24/64] Update dessant/lock-threads action to v4 --- .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 a05d4173..4c7990df 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@v3 + - uses: dessant/lock-threads@v4 with: github-token: ${{ github.token }} issue-inactive-days: '31' From 4c06b7e3f2c40081334d86975350dda814bd064a Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Wed, 7 Dec 2022 16:46:24 +0100 Subject: [PATCH 25/64] Add rule to check `@dataProvider` --- extension.neon | 2 + rules.neon | 6 ++ .../PHPUnit/DataProviderDeclarationRule.php | 90 ++++++++++++++++ src/Rules/PHPUnit/DataProviderHelper.php | 102 ++++++++++++++++++ .../DataProviderDeclarationRuleTest.php | 51 +++++++++ .../data/data-provider-declaration.php | 70 ++++++++++++ 6 files changed, 321 insertions(+) create mode 100644 src/Rules/PHPUnit/DataProviderDeclarationRule.php create mode 100644 src/Rules/PHPUnit/DataProviderHelper.php create mode 100644 tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php create mode 100644 tests/Rules/PHPUnit/data/data-provider-declaration.php diff --git a/extension.neon b/extension.neon index f6f372eb..5c6a90d8 100644 --- a/extension.neon +++ b/extension.neon @@ -55,6 +55,8 @@ services: class: PHPStan\Rules\PHPUnit\CoversHelper - class: PHPStan\Rules\PHPUnit\AnnotationHelper + - + class: PHPStan\Rules\PHPUnit\DataProviderHelper conditionalTags: PHPStan\PhpDoc\PHPUnit\MockObjectTypeNodeResolverExtension: diff --git a/rules.neon b/rules.neon index 24a28ea0..195ace09 100644 --- a/rules.neon +++ b/rules.neon @@ -8,6 +8,10 @@ rules: services: - class: PHPStan\Rules\PHPUnit\ClassCoversExistsRule - class: PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule + - + class: PHPStan\Rules\PHPUnit\DataProviderDeclarationRule + arguments: + checkFunctionNameCase: %checkFunctionNameCase% - class: PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule - class: PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule @@ -16,6 +20,8 @@ conditionalTags: phpstan.rules.rule: %featureToggles.bleedingEdge% PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule: phpstan.rules.rule: %featureToggles.bleedingEdge% + PHPStan\Rules\PHPUnit\DataProviderDeclarationRule: + phpstan.rules.rule: %featureToggles.bleedingEdge% PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule: phpstan.rules.rule: %featureToggles.bleedingEdge% PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule: diff --git a/src/Rules/PHPUnit/DataProviderDeclarationRule.php b/src/Rules/PHPUnit/DataProviderDeclarationRule.php new file mode 100644 index 00000000..7d1afd6e --- /dev/null +++ b/src/Rules/PHPUnit/DataProviderDeclarationRule.php @@ -0,0 +1,90 @@ + + */ +class DataProviderDeclarationRule implements Rule +{ + + /** + * Data provider helper. + * + * @var DataProviderHelper + */ + private $dataProviderHelper; + + /** + * The file type mapper. + * + * @var FileTypeMapper + */ + private $fileTypeMapper; + + /** + * When set to true, it reports data provider method with incorrect name case. + * + * @var bool + */ + private $checkFunctionNameCase; + + public function __construct( + DataProviderHelper $dataProviderHelper, + FileTypeMapper $fileTypeMapper, + bool $checkFunctionNameCase + ) + { + $this->dataProviderHelper = $dataProviderHelper; + $this->fileTypeMapper = $fileTypeMapper; + $this->checkFunctionNameCase = $checkFunctionNameCase; + } + + public function getNodeType(): string + { + return Node\Stmt\ClassMethod::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $scope->getClassReflection(); + + if ($classReflection === null || !$classReflection->isSubclassOf(TestCase::class)) { + return []; + } + + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + + $methodPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $classReflection->getName(), + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $node->name->toString(), + $docComment->getText() + ); + + $annotations = $this->dataProviderHelper->getDataProviderAnnotations($methodPhpDoc); + + $errors = []; + + foreach ($annotations as $annotation) { + $errors = array_merge( + $errors, + $this->dataProviderHelper->processDataProvider($scope, $annotation, $this->checkFunctionNameCase) + ); + } + + return $errors; + } + +} diff --git a/src/Rules/PHPUnit/DataProviderHelper.php b/src/Rules/PHPUnit/DataProviderHelper.php new file mode 100644 index 00000000..ef3bfcdc --- /dev/null +++ b/src/Rules/PHPUnit/DataProviderHelper.php @@ -0,0 +1,102 @@ + + */ + public function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array + { + if ($phpDoc === null) { + return []; + } + + $phpDocNodes = $phpDoc->getPhpDocNodes(); + + $annotations = []; + + foreach ($phpDocNodes as $docNode) { + $annotations = array_merge( + $annotations, + $docNode->getTagsByName('@dataProvider') + ); + } + + return $annotations; + } + + /** + * @return RuleError[] errors + */ + public function processDataProvider( + Scope $scope, + PhpDocTagNode $phpDocTag, + bool $checkFunctionNameCase + ): array + { + $dataProviderName = $this->getDataProviderName($phpDocTag); + if ($dataProviderName === null) { + // Missing name is already handled in NoMissingSpaceInMethodAnnotationRule + return []; + } + + $classReflection = $scope->getClassReflection(); + if ($classReflection === null) { + // Should not happen + return []; + } + + try { + $dataProviderMethodReflection = $classReflection->getNativeMethod($dataProviderName); + } catch (MissingMethodFromReflectionException $missingMethodFromReflectionException) { + $error = RuleErrorBuilder::message(sprintf( + '@dataProvider %s related method not found.', + $dataProviderName + ))->build(); + + return [$error]; + } + + $errors = []; + + if ($checkFunctionNameCase && $dataProviderName !== $dataProviderMethodReflection->getName()) { + $errors[] = RuleErrorBuilder::message(sprintf( + '@dataProvider %s related method is used with incorrect case: %s.', + $dataProviderName, + $dataProviderMethodReflection->getName() + ))->build(); + } + + if (!$dataProviderMethodReflection->isPublic()) { + $errors[] = RuleErrorBuilder::message(sprintf( + '@dataProvider %s related method must be public.', + $dataProviderName + ))->build(); + } + + return $errors; + } + + private function getDataProviderName(PhpDocTagNode $phpDocTag): ?string + { + if (preg_match('/^[^ \t]+/', (string) $phpDocTag->value, $matches) !== 1) { + return null; + } + + return $matches[0]; + } + +} diff --git a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php new file mode 100644 index 00000000..44434e3b --- /dev/null +++ b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php @@ -0,0 +1,51 @@ + + */ +class DataProviderDeclarationRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DataProviderDeclarationRule( + new DataProviderHelper(), + self::getContainer()->getByType(FileTypeMapper::class), + true + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/data-provider-declaration.php'], [ + [ + '@dataProvider providebaz related method is used with incorrect case: provideBaz.', + 13, + ], + [ + '@dataProvider provideQuux related method must be public.', + 13, + ], + [ + '@dataProvider provideNonExisting related method not found.', + 66, + ], + ]); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } +} diff --git a/tests/Rules/PHPUnit/data/data-provider-declaration.php b/tests/Rules/PHPUnit/data/data-provider-declaration.php new file mode 100644 index 00000000..2690d02b --- /dev/null +++ b/tests/Rules/PHPUnit/data/data-provider-declaration.php @@ -0,0 +1,70 @@ + Date: Wed, 7 Dec 2022 18:07:04 +0100 Subject: [PATCH 26/64] Report data providers deprecated usage --- composer.json | 2 +- rules.neon | 1 + .../PHPUnit/DataProviderDeclarationRule.php | 18 ++++++++++++++++-- src/Rules/PHPUnit/DataProviderHelper.php | 10 +++++++++- .../DataProviderDeclarationRuleTest.php | 5 +++++ 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index a7ec4277..f3ce0e7f 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.9.0" + "phpstan/phpstan": "^1.9.3" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/rules.neon b/rules.neon index 195ace09..8dc7056b 100644 --- a/rules.neon +++ b/rules.neon @@ -12,6 +12,7 @@ services: class: PHPStan\Rules\PHPUnit\DataProviderDeclarationRule arguments: checkFunctionNameCase: %checkFunctionNameCase% + deprecationRulesInstalled: %deprecationRulesInstalled% - class: PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule - class: PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule diff --git a/src/Rules/PHPUnit/DataProviderDeclarationRule.php b/src/Rules/PHPUnit/DataProviderDeclarationRule.php index 7d1afd6e..612cf06d 100644 --- a/src/Rules/PHPUnit/DataProviderDeclarationRule.php +++ b/src/Rules/PHPUnit/DataProviderDeclarationRule.php @@ -36,15 +36,24 @@ class DataProviderDeclarationRule implements Rule */ private $checkFunctionNameCase; + /** + * When phpstan-deprecation-rules is installed, it reports deprecated usages. + * + * @var bool + */ + private $deprecationRulesInstalled; + public function __construct( DataProviderHelper $dataProviderHelper, FileTypeMapper $fileTypeMapper, - bool $checkFunctionNameCase + bool $checkFunctionNameCase, + bool $deprecationRulesInstalled ) { $this->dataProviderHelper = $dataProviderHelper; $this->fileTypeMapper = $fileTypeMapper; $this->checkFunctionNameCase = $checkFunctionNameCase; + $this->deprecationRulesInstalled = $deprecationRulesInstalled; } public function getNodeType(): string @@ -80,7 +89,12 @@ public function processNode(Node $node, Scope $scope): array foreach ($annotations as $annotation) { $errors = array_merge( $errors, - $this->dataProviderHelper->processDataProvider($scope, $annotation, $this->checkFunctionNameCase) + $this->dataProviderHelper->processDataProvider( + $scope, + $annotation, + $this->checkFunctionNameCase, + $this->deprecationRulesInstalled + ) ); } diff --git a/src/Rules/PHPUnit/DataProviderHelper.php b/src/Rules/PHPUnit/DataProviderHelper.php index ef3bfcdc..e88f2937 100644 --- a/src/Rules/PHPUnit/DataProviderHelper.php +++ b/src/Rules/PHPUnit/DataProviderHelper.php @@ -44,7 +44,8 @@ public function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array public function processDataProvider( Scope $scope, PhpDocTagNode $phpDocTag, - bool $checkFunctionNameCase + bool $checkFunctionNameCase, + bool $deprecationRulesInstalled ): array { $dataProviderName = $this->getDataProviderName($phpDocTag); @@ -87,6 +88,13 @@ public function processDataProvider( ))->build(); } + if ($deprecationRulesInstalled && !$dataProviderMethodReflection->isStatic()) { + $errors[] = RuleErrorBuilder::message(sprintf( + '@dataProvider %s related method must be static.', + $dataProviderName + ))->build(); + } + return $errors; } diff --git a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php index 44434e3b..cfd27ae0 100644 --- a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php +++ b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php @@ -17,6 +17,7 @@ protected function getRule(): Rule return new DataProviderDeclarationRule( new DataProviderHelper(), self::getContainer()->getByType(FileTypeMapper::class), + true, true ); } @@ -28,6 +29,10 @@ public function testRule(): void '@dataProvider providebaz related method is used with incorrect case: provideBaz.', 13, ], + [ + '@dataProvider provideQux related method must be static.', + 13, + ], [ '@dataProvider provideQuux related method must be public.', 13, From bc0a2909b00eea7104999a4af88e339c2fafbb09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Wer=C5=82os?= Date: Sun, 11 Dec 2022 12:33:47 +0100 Subject: [PATCH 27/64] Do not use "strtolower" when there is a dedicated method --- src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php | 3 +-- src/Rules/PHPUnit/AssertSameNullExpectedRule.php | 3 +-- src/Rules/PHPUnit/AssertSameWithCountRule.php | 7 +++---- src/Rules/PHPUnit/ShouldCallParentMethodsRule.php | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php index d24d4904..284f53b9 100644 --- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -10,7 +10,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use function count; -use function strtolower; /** * @implements Rule @@ -35,7 +34,7 @@ public function processNode(Node $node, Scope $scope): array if (count($node->getArgs()) < 2) { return []; } - if (!$node->name instanceof Node\Identifier || strtolower($node->name->name) !== 'assertsame') { + if (!$node->name instanceof Node\Identifier || $node->name->toLowerString() !== 'assertsame') { return []; } diff --git a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php index 672f3496..ca8ac607 100644 --- a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php @@ -10,7 +10,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use function count; -use function strtolower; /** * @implements Rule @@ -35,7 +34,7 @@ public function processNode(Node $node, Scope $scope): array if (count($node->getArgs()) < 2) { return []; } - if (!$node->name instanceof Node\Identifier || strtolower($node->name->name) !== 'assertsame') { + if (!$node->name instanceof Node\Identifier || $node->name->toLowerString() !== 'assertsame') { return []; } diff --git a/src/Rules/PHPUnit/AssertSameWithCountRule.php b/src/Rules/PHPUnit/AssertSameWithCountRule.php index 1f1a3ab2..a0a79a96 100644 --- a/src/Rules/PHPUnit/AssertSameWithCountRule.php +++ b/src/Rules/PHPUnit/AssertSameWithCountRule.php @@ -11,7 +11,6 @@ use PHPStan\Rules\Rule; use PHPStan\Type\ObjectType; use function count; -use function strtolower; /** * @implements Rule @@ -36,7 +35,7 @@ public function processNode(Node $node, Scope $scope): array if (count($node->getArgs()) < 2) { return []; } - if (!$node->name instanceof Node\Identifier || strtolower($node->name->name) !== 'assertsame') { + if (!$node->name instanceof Node\Identifier || $node->name->toLowerString() !== 'assertsame') { return []; } @@ -45,7 +44,7 @@ public function processNode(Node $node, Scope $scope): array if ( $right instanceof Node\Expr\FuncCall && $right->name instanceof Node\Name - && strtolower($right->name->toString()) === 'count' + && $right->name->toLowerString() === 'count' ) { return [ 'You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, count($variable)).', @@ -55,7 +54,7 @@ public function processNode(Node $node, Scope $scope): array if ( $right instanceof Node\Expr\MethodCall && $right->name instanceof Node\Identifier - && strtolower($right->name->toString()) === 'count' + && $right->name->toLowerString() === 'count' && count($right->getArgs()) === 0 ) { $type = $scope->getType($right->var); diff --git a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php index 01414061..5a640a1c 100644 --- a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php +++ b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php @@ -97,7 +97,7 @@ private function hasParentClassCall(?array $stmts, string $methodName): bool continue; } - if (strtolower($stmt->expr->name->name) === $methodName) { + if ($stmt->expr->name->toLowerString() === $methodName) { return true; } } From 008f5da032f441aa01b563972256808f9d8501c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Wer=C5=82os?= Date: Sun, 11 Dec 2022 16:46:20 +0100 Subject: [PATCH 28/64] Update .gitattributes --- .gitattributes | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.gitattributes b/.gitattributes index 9d7c518b..00d6b17d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,13 +2,12 @@ *.stub linguist-language=PHP *.neon linguist-language=YAML -.github export-ignore -tests export-ignore -tmp export-ignore -.gitattributes export-ignore -.gitignore export-ignore -Makefile export-ignore -phpcs.xml export-ignore -phpstan.neon export-ignore -phpstan-baseline.neon export-ignore -phpunit.xml export-ignore +/.* export-ignore +/build-cs export-ignore +/tests export-ignore +/tmp export-ignore +/Makefile export-ignore +/phpcs.xml export-ignore +/phpstan.neon export-ignore +/phpstan-baseline.neon export-ignore +/phpunit.xml export-ignore From b9827cf8df2bd97c7c07b1bb27c694ee41052754 Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Mon, 12 Dec 2022 22:02:25 +0100 Subject: [PATCH 29/64] Discover data providers from other classes --- src/Rules/PHPUnit/DataProviderHelper.php | 63 +++++++++++++++---- .../DataProviderDeclarationRuleTest.php | 16 +++-- .../data/data-provider-declaration.php | 9 +++ 3 files changed, 70 insertions(+), 18 deletions(-) diff --git a/src/Rules/PHPUnit/DataProviderHelper.php b/src/Rules/PHPUnit/DataProviderHelper.php index e88f2937..a224cb78 100644 --- a/src/Rules/PHPUnit/DataProviderHelper.php +++ b/src/Rules/PHPUnit/DataProviderHelper.php @@ -5,16 +5,32 @@ use PHPStan\Analyser\Scope; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; +use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MissingMethodFromReflectionException; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use function array_merge; +use function count; +use function explode; use function preg_match; use function sprintf; class DataProviderHelper { + /** + * Reflection provider. + * + * @var ReflectionProvider + */ + private $reflectionProvider; + + public function __construct(ReflectionProvider $reflectionProvider) + { + $this->reflectionProvider = $reflectionProvider; + } + /** * @return array */ @@ -48,24 +64,28 @@ public function processDataProvider( bool $deprecationRulesInstalled ): array { - $dataProviderName = $this->getDataProviderName($phpDocTag); - if ($dataProviderName === null) { - // Missing name is already handled in NoMissingSpaceInMethodAnnotationRule + $dataProviderValue = $this->getDataProviderValue($phpDocTag); + if ($dataProviderValue === null) { + // Missing value is already handled in NoMissingSpaceInMethodAnnotationRule return []; } - $classReflection = $scope->getClassReflection(); + [$classReflection, $method] = $this->parseDataProviderValue($scope, $dataProviderValue); if ($classReflection === null) { - // Should not happen - return []; + $error = RuleErrorBuilder::message(sprintf( + '@dataProvider %s related class not found.', + $dataProviderValue + ))->build(); + + return [$error]; } try { - $dataProviderMethodReflection = $classReflection->getNativeMethod($dataProviderName); + $dataProviderMethodReflection = $classReflection->getNativeMethod($method); } catch (MissingMethodFromReflectionException $missingMethodFromReflectionException) { $error = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method not found.', - $dataProviderName + $dataProviderValue ))->build(); return [$error]; @@ -73,10 +93,10 @@ public function processDataProvider( $errors = []; - if ($checkFunctionNameCase && $dataProviderName !== $dataProviderMethodReflection->getName()) { + if ($checkFunctionNameCase && $method !== $dataProviderMethodReflection->getName()) { $errors[] = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method is used with incorrect case: %s.', - $dataProviderName, + $dataProviderValue, $dataProviderMethodReflection->getName() ))->build(); } @@ -84,21 +104,21 @@ public function processDataProvider( if (!$dataProviderMethodReflection->isPublic()) { $errors[] = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method must be public.', - $dataProviderName + $dataProviderValue ))->build(); } if ($deprecationRulesInstalled && !$dataProviderMethodReflection->isStatic()) { $errors[] = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method must be static.', - $dataProviderName + $dataProviderValue ))->build(); } return $errors; } - private function getDataProviderName(PhpDocTagNode $phpDocTag): ?string + private function getDataProviderValue(PhpDocTagNode $phpDocTag): ?string { if (preg_match('/^[^ \t]+/', (string) $phpDocTag->value, $matches) !== 1) { return null; @@ -107,4 +127,21 @@ private function getDataProviderName(PhpDocTagNode $phpDocTag): ?string return $matches[0]; } + /** + * @return array{ClassReflection|null, string} + */ + private function parseDataProviderValue(Scope $scope, string $dataProviderValue): array + { + $parts = explode('::', $dataProviderValue, 2); + if (count($parts) <= 1) { + return [$scope->getClassReflection(), $dataProviderValue]; + } + + if ($this->reflectionProvider->hasClass($parts[0])) { + return [$this->reflectionProvider->getClass($parts[0]), $parts[1]]; + } + + return [null, $dataProviderValue]; + } + } diff --git a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php index cfd27ae0..04dad8dc 100644 --- a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php +++ b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php @@ -14,8 +14,10 @@ class DataProviderDeclarationRuleTest extends RuleTestCase protected function getRule(): Rule { + $reflection = $this->createReflectionProvider(); + return new DataProviderDeclarationRule( - new DataProviderHelper(), + new DataProviderHelper($reflection), self::getContainer()->getByType(FileTypeMapper::class), true, true @@ -27,19 +29,23 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/data-provider-declaration.php'], [ [ '@dataProvider providebaz related method is used with incorrect case: provideBaz.', - 13, + 14, ], [ '@dataProvider provideQux related method must be static.', - 13, + 14, ], [ '@dataProvider provideQuux related method must be public.', - 13, + 14, ], [ '@dataProvider provideNonExisting related method not found.', - 66, + 68, + ], + [ + '@dataProvider NonExisting::provideNonExisting related class not found.', + 68, ], ]); } diff --git a/tests/Rules/PHPUnit/data/data-provider-declaration.php b/tests/Rules/PHPUnit/data/data-provider-declaration.php index 2690d02b..23af826b 100644 --- a/tests/Rules/PHPUnit/data/data-provider-declaration.php +++ b/tests/Rules/PHPUnit/data/data-provider-declaration.php @@ -9,6 +9,7 @@ class FooTestCase extends \PHPUnit\Framework\TestCase * @dataProvider providebaz * @dataProvider provideQux * @dataProvider provideQuux + * @dataProvider \ExampleTestCase\BarTestCase::provideToOtherClass */ public function testIsNotFoo(string $subject): void { @@ -61,10 +62,18 @@ class BarTestCase extends \PHPUnit\Framework\TestCase /** * @dataProvider provideNonExisting + * @dataProvider NonExisting::provideNonExisting * @dataProvider provideCorge */ public function testIsNotBar(string $subject): void { self::assertNotSame('bar', $subject); } + + public static function provideToOtherClass(): iterable + { + return [ + ['toOtherClass'], + ]; + } } From cd9c6938f8bbfcb6da3ed5a3c7ea60873825d088 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 13 Dec 2022 15:49:24 +0100 Subject: [PATCH 30/64] DataProviderDeclarationRule - report non-static dataProvider only with PHPUnit 10+ --- extension.neon | 3 ++ src/Rules/PHPUnit/DataProviderHelper.php | 10 ++-- .../PHPUnit/DataProviderHelperFactory.php | 52 +++++++++++++++++++ .../DataProviderDeclarationRuleTest.php | 4 +- 4 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 src/Rules/PHPUnit/DataProviderHelperFactory.php diff --git a/extension.neon b/extension.neon index 5c6a90d8..cea2b155 100644 --- a/extension.neon +++ b/extension.neon @@ -57,6 +57,9 @@ services: class: PHPStan\Rules\PHPUnit\AnnotationHelper - class: PHPStan\Rules\PHPUnit\DataProviderHelper + factory: @PHPStan\Rules\PHPUnit\DataProviderHelperFactory::create() + - + class: PHPStan\Rules\PHPUnit\DataProviderHelperFactory conditionalTags: PHPStan\PhpDoc\PHPUnit\MockObjectTypeNodeResolverExtension: diff --git a/src/Rules/PHPUnit/DataProviderHelper.php b/src/Rules/PHPUnit/DataProviderHelper.php index a224cb78..30079600 100644 --- a/src/Rules/PHPUnit/DataProviderHelper.php +++ b/src/Rules/PHPUnit/DataProviderHelper.php @@ -26,9 +26,13 @@ class DataProviderHelper */ private $reflectionProvider; - public function __construct(ReflectionProvider $reflectionProvider) + /** @var bool */ + private $phpunit10OrNewer; + + public function __construct(ReflectionProvider $reflectionProvider, bool $phpunit10OrNewer) { $this->reflectionProvider = $reflectionProvider; + $this->phpunit10OrNewer = $phpunit10OrNewer; } /** @@ -108,9 +112,9 @@ public function processDataProvider( ))->build(); } - if ($deprecationRulesInstalled && !$dataProviderMethodReflection->isStatic()) { + if ($deprecationRulesInstalled && $this->phpunit10OrNewer && !$dataProviderMethodReflection->isStatic()) { $errors[] = RuleErrorBuilder::message(sprintf( - '@dataProvider %s related method must be static.', + '@dataProvider %s related method must be static in PHPUnit 10 and newer.', $dataProviderValue ))->build(); } diff --git a/src/Rules/PHPUnit/DataProviderHelperFactory.php b/src/Rules/PHPUnit/DataProviderHelperFactory.php new file mode 100644 index 00000000..a93ecfd3 --- /dev/null +++ b/src/Rules/PHPUnit/DataProviderHelperFactory.php @@ -0,0 +1,52 @@ +reflectionProvider = $reflectionProvider; + } + + public function create(): DataProviderHelper + { + $phpUnit10OrNewer = false; + if ($this->reflectionProvider->hasClass(TestCase::class)) { + $testCase = $this->reflectionProvider->getClass(TestCase::class); + $file = $testCase->getFileName(); + if ($file !== null) { + $phpUnitRoot = dirname($file, 3); + $phpUnitComposer = $phpUnitRoot . '/composer.json'; + if (is_file($phpUnitComposer)) { + $composerJson = @file_get_contents($phpUnitComposer); + if ($composerJson !== false) { + $json = json_decode($composerJson, true); + $version = $json['extra']['branch-alias']['dev-main'] ?? null; + if ($version !== null) { + $majorVersion = (int) explode('.', $version)[0]; + if ($majorVersion >= 10) { + $phpUnit10OrNewer = true; + } + } + } + } + } + } + + return new DataProviderHelper($this->reflectionProvider, $phpUnit10OrNewer); + } + +} diff --git a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php index 04dad8dc..03c44ecb 100644 --- a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php +++ b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php @@ -17,7 +17,7 @@ protected function getRule(): Rule $reflection = $this->createReflectionProvider(); return new DataProviderDeclarationRule( - new DataProviderHelper($reflection), + new DataProviderHelper($reflection, true), self::getContainer()->getByType(FileTypeMapper::class), true, true @@ -32,7 +32,7 @@ public function testRule(): void 14, ], [ - '@dataProvider provideQux related method must be static.', + '@dataProvider provideQux related method must be static in PHPUnit 10 and newer.', 14, ], [ From 64f4c56a19b0409c2c721378059d3a8eaa0f33a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Mon, 19 Dec 2022 13:25:26 +0100 Subject: [PATCH 31/64] Create release-toot.yml --- .github/workflows/release-toot.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/release-toot.yml diff --git a/.github/workflows/release-toot.yml b/.github/workflows/release-toot.yml new file mode 100644 index 00000000..2af0f176 --- /dev/null +++ b/.github/workflows/release-toot.yml @@ -0,0 +1,21 @@ +name: Toot release + +# More triggers +# https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#release +on: + release: + types: [published] + +jobs: + toot: + runs-on: ubuntu-latest + steps: + - uses: cbrgm/mastodon-github-action@v1 + if: ${{ !github.event.repository.private }} + with: + # GitHub event payload + # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#release + message: "New release: ${{ github.event.repository.name }} ${{ github.event.release.tag_name }} ${{ github.event.release.html_url }} #phpstan" + env: + MASTODON_URL: phpc.social + MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} From 7f7b59b560eac6d4eff06a2aaffc8b67dd166776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Tue, 20 Dec 2022 22:07:58 +0100 Subject: [PATCH 32/64] Update release-toot.yml --- .github/workflows/release-toot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-toot.yml b/.github/workflows/release-toot.yml index 2af0f176..6a1c8156 100644 --- a/.github/workflows/release-toot.yml +++ b/.github/workflows/release-toot.yml @@ -17,5 +17,5 @@ jobs: # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#release message: "New release: ${{ github.event.repository.name }} ${{ github.event.release.tag_name }} ${{ github.event.release.html_url }} #phpstan" env: - MASTODON_URL: phpc.social + MASTODON_URL: https://phpc.social MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} From 54a24bd23e9e80ee918cdc24f909d376c2e273f7 Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Wed, 21 Dec 2022 16:16:26 +0100 Subject: [PATCH 33/64] Add support for data provider attributes --- .../PHPUnit/DataProviderDeclarationRule.php | 33 +--- src/Rules/PHPUnit/DataProviderHelper.php | 164 +++++++++++++++--- .../PHPUnit/DataProviderHelperFactory.php | 9 +- .../DataProviderDeclarationRuleTest.php | 29 +++- .../data/data-provider-declaration.php | 14 ++ 5 files changed, 192 insertions(+), 57 deletions(-) diff --git a/src/Rules/PHPUnit/DataProviderDeclarationRule.php b/src/Rules/PHPUnit/DataProviderDeclarationRule.php index 612cf06d..37c586de 100644 --- a/src/Rules/PHPUnit/DataProviderDeclarationRule.php +++ b/src/Rules/PHPUnit/DataProviderDeclarationRule.php @@ -5,7 +5,6 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; -use PHPStan\Type\FileTypeMapper; use PHPUnit\Framework\TestCase; use function array_merge; @@ -22,13 +21,6 @@ class DataProviderDeclarationRule implements Rule */ private $dataProviderHelper; - /** - * The file type mapper. - * - * @var FileTypeMapper - */ - private $fileTypeMapper; - /** * When set to true, it reports data provider method with incorrect name case. * @@ -45,13 +37,11 @@ class DataProviderDeclarationRule implements Rule public function __construct( DataProviderHelper $dataProviderHelper, - FileTypeMapper $fileTypeMapper, bool $checkFunctionNameCase, bool $deprecationRulesInstalled ) { $this->dataProviderHelper = $dataProviderHelper; - $this->fileTypeMapper = $fileTypeMapper; $this->checkFunctionNameCase = $checkFunctionNameCase; $this->deprecationRulesInstalled = $deprecationRulesInstalled; } @@ -69,29 +59,16 @@ public function processNode(Node $node, Scope $scope): array return []; } - $docComment = $node->getDocComment(); - if ($docComment === null) { - return []; - } - - $methodPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $classReflection->getName(), - $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, - $node->name->toString(), - $docComment->getText() - ); - - $annotations = $this->dataProviderHelper->getDataProviderAnnotations($methodPhpDoc); - $errors = []; - foreach ($annotations as $annotation) { + foreach ($this->dataProviderHelper->getDataProviderMethods($scope, $node, $classReflection) as $dataProviderValue => [$dataProviderClassReflection, $dataProviderMethodName, $lineNumber]) { $errors = array_merge( $errors, $this->dataProviderHelper->processDataProvider( - $scope, - $annotation, + $dataProviderValue, + $dataProviderClassReflection, + $dataProviderMethodName, + $lineNumber, $this->checkFunctionNameCase, $this->deprecationRulesInstalled ) diff --git a/src/Rules/PHPUnit/DataProviderHelper.php b/src/Rules/PHPUnit/DataProviderHelper.php index 30079600..6651b051 100644 --- a/src/Rules/PHPUnit/DataProviderHelper.php +++ b/src/Rules/PHPUnit/DataProviderHelper.php @@ -2,6 +2,11 @@ namespace PHPStan\Rules\PHPUnit; +use PhpParser\Node\Attribute; +use PhpParser\Node\Expr\ClassConstFetch; +use PhpParser\Node\Name; +use PhpParser\Node\Scalar\String_; +use PhpParser\Node\Stmt\ClassMethod; use PHPStan\Analyser\Scope; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; @@ -10,6 +15,7 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\FileTypeMapper; use function array_merge; use function count; use function explode; @@ -26,19 +32,84 @@ class DataProviderHelper */ private $reflectionProvider; + /** + * The file type mapper. + * + * @var FileTypeMapper + */ + private $fileTypeMapper; + /** @var bool */ private $phpunit10OrNewer; - public function __construct(ReflectionProvider $reflectionProvider, bool $phpunit10OrNewer) + public function __construct( + ReflectionProvider $reflectionProvider, + FileTypeMapper $fileTypeMapper, + bool $phpunit10OrNewer + ) { $this->reflectionProvider = $reflectionProvider; + $this->fileTypeMapper = $fileTypeMapper; $this->phpunit10OrNewer = $phpunit10OrNewer; } + /** + * @return iterable + */ + public function getDataProviderMethods( + Scope $scope, + ClassMethod $node, + ClassReflection $classReflection + ): iterable + { + $docComment = $node->getDocComment(); + if ($docComment !== null) { + $methodPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $classReflection->getName(), + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $node->name->toString(), + $docComment->getText() + ); + foreach ($this->getDataProviderAnnotations($methodPhpDoc) as $annotation) { + $dataProviderValue = $this->getDataProviderAnnotationValue($annotation); + if ($dataProviderValue === null) { + // Missing value is already handled in NoMissingSpaceInMethodAnnotationRule + continue; + } + + $dataProviderMethod = $this->parseDataProviderAnnotationValue($scope, $dataProviderValue); + $dataProviderMethod[] = $node->getLine(); + + yield $dataProviderValue => $dataProviderMethod; + } + } + + if (!$this->phpunit10OrNewer) { + return; + } + + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + $dataProviderMethod = null; + if ($attr->name->toLowerString() === 'phpunit\\framework\\attributes\\dataprovider') { + $dataProviderMethod = $this->parseDataProviderAttribute($attr, $classReflection); + } elseif ($attr->name->toLowerString() === 'phpunit\\framework\\attributes\\dataproviderexternal') { + $dataProviderMethod = $this->parseDataProviderExternalAttribute($attr); + } + if ($dataProviderMethod === null) { + continue; + } + + yield from $dataProviderMethod; + } + } + } + /** * @return array */ - public function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array + private function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array { if ($phpDoc === null) { return []; @@ -62,67 +133,62 @@ public function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array * @return RuleError[] errors */ public function processDataProvider( - Scope $scope, - PhpDocTagNode $phpDocTag, + string $dataProviderValue, + ?ClassReflection $classReflection, + string $methodName, + int $lineNumber, bool $checkFunctionNameCase, bool $deprecationRulesInstalled ): array { - $dataProviderValue = $this->getDataProviderValue($phpDocTag); - if ($dataProviderValue === null) { - // Missing value is already handled in NoMissingSpaceInMethodAnnotationRule - return []; - } - - [$classReflection, $method] = $this->parseDataProviderValue($scope, $dataProviderValue); if ($classReflection === null) { $error = RuleErrorBuilder::message(sprintf( '@dataProvider %s related class not found.', $dataProviderValue - ))->build(); + ))->line($lineNumber)->build(); return [$error]; } try { - $dataProviderMethodReflection = $classReflection->getNativeMethod($method); + $dataProviderMethodReflection = $classReflection->getNativeMethod($methodName); } catch (MissingMethodFromReflectionException $missingMethodFromReflectionException) { $error = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method not found.', $dataProviderValue - ))->build(); + ))->line($lineNumber)->build(); return [$error]; } $errors = []; - if ($checkFunctionNameCase && $method !== $dataProviderMethodReflection->getName()) { + if ($checkFunctionNameCase && $methodName !== $dataProviderMethodReflection->getName()) { $errors[] = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method is used with incorrect case: %s.', $dataProviderValue, $dataProviderMethodReflection->getName() - ))->build(); + ))->line($lineNumber)->build(); } if (!$dataProviderMethodReflection->isPublic()) { $errors[] = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method must be public.', $dataProviderValue - ))->build(); + ))->line($lineNumber)->build(); } if ($deprecationRulesInstalled && $this->phpunit10OrNewer && !$dataProviderMethodReflection->isStatic()) { $errors[] = RuleErrorBuilder::message(sprintf( '@dataProvider %s related method must be static in PHPUnit 10 and newer.', $dataProviderValue - ))->build(); + ))->line($lineNumber)->build(); } return $errors; } - private function getDataProviderValue(PhpDocTagNode $phpDocTag): ?string + private function getDataProviderAnnotationValue(PhpDocTagNode $phpDocTag): ?string { if (preg_match('/^[^ \t]+/', (string) $phpDocTag->value, $matches) !== 1) { return null; @@ -134,7 +200,7 @@ private function getDataProviderValue(PhpDocTagNode $phpDocTag): ?string /** * @return array{ClassReflection|null, string} */ - private function parseDataProviderValue(Scope $scope, string $dataProviderValue): array + private function parseDataProviderAnnotationValue(Scope $scope, string $dataProviderValue): array { $parts = explode('::', $dataProviderValue, 2); if (count($parts) <= 1) { @@ -148,4 +214,62 @@ private function parseDataProviderValue(Scope $scope, string $dataProviderValue) return [null, $dataProviderValue]; } + /** + * @return array|null + */ + private function parseDataProviderExternalAttribute(Attribute $attribute): ?array + { + if (count($attribute->args) !== 2) { + return null; + } + $methodNameArg = $attribute->args[1]->value; + if (!$methodNameArg instanceof String_) { + return null; + } + $classNameArg = $attribute->args[0]->value; + if ($classNameArg instanceof ClassConstFetch && $classNameArg->class instanceof Name) { + $className = $classNameArg->class->toString(); + } elseif ($classNameArg instanceof String_) { + $className = $classNameArg->value; + } else { + return null; + } + + $dataProviderClassReflection = null; + if ($this->reflectionProvider->hasClass($className)) { + $dataProviderClassReflection = $this->reflectionProvider->getClass($className); + $className = $dataProviderClassReflection->getName(); + } + + return [ + sprintf('%s::%s', $className, $methodNameArg->value) => [ + $dataProviderClassReflection, + $methodNameArg->value, + $attribute->getLine(), + ], + ]; + } + + /** + * @return array|null + */ + private function parseDataProviderAttribute(Attribute $attribute, ClassReflection $classReflection): ?array + { + if (count($attribute->args) !== 1) { + return null; + } + $methodNameArg = $attribute->args[0]->value; + if (!$methodNameArg instanceof String_) { + return null; + } + + return [ + $methodNameArg->value => [ + $classReflection, + $methodNameArg->value, + $attribute->getLine(), + ], + ]; + } + } diff --git a/src/Rules/PHPUnit/DataProviderHelperFactory.php b/src/Rules/PHPUnit/DataProviderHelperFactory.php index a93ecfd3..7fc8af0f 100644 --- a/src/Rules/PHPUnit/DataProviderHelperFactory.php +++ b/src/Rules/PHPUnit/DataProviderHelperFactory.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\PHPUnit; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Type\FileTypeMapper; use PHPUnit\Framework\TestCase; use function dirname; use function explode; @@ -16,9 +17,13 @@ class DataProviderHelperFactory /** @var ReflectionProvider */ private $reflectionProvider; - public function __construct(ReflectionProvider $reflectionProvider) + /** @var FileTypeMapper */ + private $fileTypeMapper; + + public function __construct(ReflectionProvider $reflectionProvider, FileTypeMapper $fileTypeMapper) { $this->reflectionProvider = $reflectionProvider; + $this->fileTypeMapper = $fileTypeMapper; } public function create(): DataProviderHelper @@ -46,7 +51,7 @@ public function create(): DataProviderHelper } } - return new DataProviderHelper($this->reflectionProvider, $phpUnit10OrNewer); + return new DataProviderHelper($this->reflectionProvider, $this->fileTypeMapper, $phpUnit10OrNewer); } } diff --git a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php index 03c44ecb..18cc12b3 100644 --- a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php +++ b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php @@ -17,8 +17,7 @@ protected function getRule(): Rule $reflection = $this->createReflectionProvider(); return new DataProviderDeclarationRule( - new DataProviderHelper($reflection, true), - self::getContainer()->getByType(FileTypeMapper::class), + new DataProviderHelper($reflection, self::getContainer()->getByType(FileTypeMapper::class),true), true, true ); @@ -29,23 +28,39 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/data-provider-declaration.php'], [ [ '@dataProvider providebaz related method is used with incorrect case: provideBaz.', - 14, + 16, ], [ '@dataProvider provideQux related method must be static in PHPUnit 10 and newer.', - 14, + 16, ], [ '@dataProvider provideQuux related method must be public.', - 14, + 16, ], [ '@dataProvider provideNonExisting related method not found.', - 68, + 70, ], [ '@dataProvider NonExisting::provideNonExisting related class not found.', - 68, + 70, + ], + [ + '@dataProvider provideNonExisting related method not found.', + 85, + ], + [ + '@dataProvider provideNonExisting2 related method not found.', + 86, + ], + [ + '@dataProvider ExampleTestCase\\BarTestCase::providetootherclass related method is used with incorrect case: provideToOtherClass.', + 87, + ], + [ + '@dataProvider ExampleTestCase\\BarTestCase::providetootherclass related method is used with incorrect case: provideToOtherClass.', + 88, ], ]); } diff --git a/tests/Rules/PHPUnit/data/data-provider-declaration.php b/tests/Rules/PHPUnit/data/data-provider-declaration.php index 23af826b..176be2dc 100644 --- a/tests/Rules/PHPUnit/data/data-provider-declaration.php +++ b/tests/Rules/PHPUnit/data/data-provider-declaration.php @@ -2,6 +2,8 @@ namespace ExampleTestCase; +use \PHPUnit\Framework\Attributes\DataProvider; + class FooTestCase extends \PHPUnit\Framework\TestCase { /** @@ -77,3 +79,15 @@ public static function provideToOtherClass(): iterable ]; } } + +class BazTestCase extends \PHPUnit\Framework\TestCase +{ + #[\PHPUnit\Framework\Attributes\DataProvider('provideNonExisting')] + #[DataProvider('provideNonExisting2')] + #[\PHPUnit\Framework\Attributes\DataProviderExternal('\\ExampleTestCase\\BarTestCase', 'providetootherclass')] + #[\PHPUnit\Framework\Attributes\DataProviderExternal(\ExampleTestCase\BarTestCase::class, 'providetootherclass')] + public function testIsNotBaz(string $subject): void + { + self::assertNotSame('baz', $subject); + } +} From bf47c49afb7dc6e897d1f3c9b61b4bd82cc4d6e6 Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Wed, 21 Dec 2022 17:11:49 +0100 Subject: [PATCH 34/64] Ease the usage of AssertRuleHelper::isMethodOrStaticCallOnAssert() --- src/Rules/PHPUnit/AssertRuleHelper.php | 3 +++ src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php | 5 ----- src/Rules/PHPUnit/AssertSameNullExpectedRule.php | 5 ----- src/Rules/PHPUnit/AssertSameWithCountRule.php | 5 ----- 4 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/Rules/PHPUnit/AssertRuleHelper.php b/src/Rules/PHPUnit/AssertRuleHelper.php index 96735379..8c288b2c 100644 --- a/src/Rules/PHPUnit/AssertRuleHelper.php +++ b/src/Rules/PHPUnit/AssertRuleHelper.php @@ -11,6 +11,9 @@ class AssertRuleHelper { + /** + * @phpstan-assert-if-true Node\Expr\MethodCall|Node\Expr\StaticCall $node + */ public static function isMethodOrStaticCallOnAssert(Node $node, Scope $scope): bool { $testCaseType = new ObjectType('PHPUnit\Framework\Assert'); diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php index 284f53b9..6a8eb7dd 100644 --- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -4,8 +4,6 @@ use PhpParser\Node; use PhpParser\Node\Expr\ConstFetch; -use PhpParser\Node\Expr\MethodCall; -use PhpParser\Node\Expr\StaticCall; use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; @@ -28,9 +26,6 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** @var MethodCall|StaticCall $node */ - $node = $node; - if (count($node->getArgs()) < 2) { return []; } diff --git a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php index ca8ac607..10302233 100644 --- a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php @@ -4,8 +4,6 @@ use PhpParser\Node; use PhpParser\Node\Expr\ConstFetch; -use PhpParser\Node\Expr\MethodCall; -use PhpParser\Node\Expr\StaticCall; use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; @@ -28,9 +26,6 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** @var MethodCall|StaticCall $node */ - $node = $node; - if (count($node->getArgs()) < 2) { return []; } diff --git a/src/Rules/PHPUnit/AssertSameWithCountRule.php b/src/Rules/PHPUnit/AssertSameWithCountRule.php index a0a79a96..876dd87b 100644 --- a/src/Rules/PHPUnit/AssertSameWithCountRule.php +++ b/src/Rules/PHPUnit/AssertSameWithCountRule.php @@ -4,8 +4,6 @@ use Countable; use PhpParser\Node; -use PhpParser\Node\Expr\MethodCall; -use PhpParser\Node\Expr\StaticCall; use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; @@ -29,9 +27,6 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** @var MethodCall|StaticCall $node */ - $node = $node; - if (count($node->getArgs()) < 2) { return []; } From 87516ff05e173ca19a4c0f13c1e6324600c3820e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 14 Jan 2023 15:49:24 +0100 Subject: [PATCH 35/64] Require PHPStan 1.10 --- composer.json | 2 +- tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php | 1 + tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f3ce0e7f..594629fc 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.9.3" + "phpstan/phpstan": "^1.10" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index 87e5a3af..08986404 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -43,6 +43,7 @@ public function testRule(): void [ 'Call to method PHPUnit\Framework\Assert::assertSame() with array and array will always evaluate to false.', 39, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Call to method PHPUnit\Framework\Assert::assertSame() with 1 and 1 will always evaluate to true.', diff --git a/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php index 700f420d..4c065466 100644 --- a/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/Rules/PHPUnit/ImpossibleCheckTypeMethodCallRuleTest.php @@ -37,6 +37,7 @@ public function testBug141(): void [ "Call to method PHPUnit\Framework\Assert::assertEmpty() with non-empty-array<'0.6.0'|'1.0.0'|'1.0.x-dev'|'1.1.x-dev'|'9999999-dev'|'dev-feature-b', true> will always evaluate to false.", 23, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], ]); } From 75f87d4911377ad36aef67de528dc3dcaa9df77c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 31 Jan 2023 16:54:04 +0100 Subject: [PATCH 36/64] Fix build --- .../MockObjectTypeNodeResolverExtension.php | 7 +- src/Rules/PHPUnit/MockMethodCallRule.php | 74 ++++++++++--------- .../MockObjectDynamicReturnTypeExtension.php | 8 +- 3 files changed, 48 insertions(+), 41 deletions(-) diff --git a/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php b/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php index 4d463799..2d70b380 100644 --- a/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php +++ b/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php @@ -11,8 +11,8 @@ use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeWithClassName; use function array_key_exists; +use function count; class MockObjectTypeNodeResolverExtension implements TypeNodeResolverExtension, TypeNodeResolverAwareExtension { @@ -44,11 +44,12 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type $types = $this->typeNodeResolver->resolveMultiple($typeNode->types, $nameScope); foreach ($types as $type) { - if (!$type instanceof TypeWithClassName) { + $classNames = $type->getObjectClassNames(); + if (count($classNames) !== 1) { continue; } - if (array_key_exists($type->getClassName(), $mockClassNames)) { + if (array_key_exists($classNames[0], $mockClassNames)) { $resultType = TypeCombinator::intersect(...$types); if ($resultType instanceof NeverType) { continue; diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index ae554e1f..ab677603 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectType; @@ -44,53 +43,56 @@ public function processNode(Node $node, Scope $scope): array } $argType = $scope->getType($node->getArgs()[0]->value); - if (!($argType instanceof ConstantStringType)) { + if (count($argType->getConstantStrings()) === 0) { return []; } - $method = $argType->getValue(); - $type = $scope->getType($node->var); - - if ( - $type instanceof IntersectionType - && ( - in_array(MockObject::class, $type->getReferencedClasses(), true) - || in_array(Stub::class, $type->getReferencedClasses(), true) - ) - && !$type->hasMethod($method)->yes() - ) { - $mockClass = array_filter($type->getReferencedClasses(), static function (string $class): bool { - return $class !== MockObject::class && $class !== Stub::class; - }); - - return [ - sprintf( + $errors = []; + foreach ($argType->getConstantStrings() as $constantString) { + $method = $constantString->getValue(); + $type = $scope->getType($node->var); + + if ( + $type instanceof IntersectionType + && ( + in_array(MockObject::class, $type->getObjectClassNames(), true) + || in_array(Stub::class, $type->getObjectClassNames(), true) + ) + && !$type->hasMethod($method)->yes() + ) { + $mockClass = array_filter($type->getObjectClassNames(), static function (string $class): bool { + return $class !== MockObject::class && $class !== Stub::class; + }); + + $errors[] = sprintf( 'Trying to mock an undefined method %s() on class %s.', $method, implode('&', $mockClass) - ), - ]; - } + ); + } + + if ( + !($type instanceof GenericObjectType) + || $type->getClassName() !== InvocationMocker::class + || count($type->getTypes()) <= 0 + ) { + continue; + } - if ( - $type instanceof GenericObjectType - && $type->getClassName() === InvocationMocker::class - && count($type->getTypes()) > 0 - ) { $mockClass = $type->getTypes()[0]; - if ($mockClass instanceof ObjectType && !$mockClass->hasMethod($method)->yes()) { - return [ - sprintf( - 'Trying to mock an undefined method %s() on class %s.', - $method, - $mockClass->getClassName() - ), - ]; + if (!($mockClass instanceof ObjectType) || $mockClass->hasMethod($method)->yes()) { + continue; } + + $errors[] = sprintf( + 'Trying to mock an undefined method %s() on class %s.', + $method, + $mockClass->getClassName() + ); } - return []; + return $errors; } } diff --git a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php index cb0b3a17..6db3a73b 100644 --- a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php @@ -10,7 +10,6 @@ use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -use PHPStan\Type\TypeWithClassName; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; use PHPUnit\Framework\MockObject\MockObject; use function array_filter; @@ -38,7 +37,12 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method } $mockClasses = array_values(array_filter($type->getTypes(), static function (Type $type): bool { - return !$type instanceof TypeWithClassName || $type->getClassName() !== MockObject::class; + $classNames = $type->getObjectClassNames(); + if (count($classNames) !== 1) { + return true; + } + + return $classNames[0] !== MockObject::class; })); if (count($mockClasses) !== 1) { From d77af96c1aaec28f7c0293677132eaaad079e01b Mon Sep 17 00:00:00 2001 From: Brad <28307684+mad-briller@users.noreply.github.com> Date: Wed, 8 Feb 2023 16:39:45 +0000 Subject: [PATCH 37/64] Fix empty @covers annotation causing a crash. --- src/Rules/PHPUnit/CoversHelper.php | 6 ++++++ tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php | 4 ++++ tests/Rules/PHPUnit/data/class-coverage.php | 7 +++++++ 3 files changed, 17 insertions(+) diff --git a/src/Rules/PHPUnit/CoversHelper.php b/src/Rules/PHPUnit/CoversHelper.php index 8bcf92e4..a96257f0 100644 --- a/src/Rules/PHPUnit/CoversHelper.php +++ b/src/Rules/PHPUnit/CoversHelper.php @@ -95,6 +95,12 @@ public function processCovers( ))->build(); } } else { + if ($covers === '') { + $errors[] = RuleErrorBuilder::message('@covers value does not specify anything.')->build(); + + return $errors; + } + if (!isset($method) && $this->reflectionProvider->hasFunction(new Name($covers, []), null)) { return $errors; } diff --git a/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php index 7ead3a7c..7b84b50f 100644 --- a/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php +++ b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php @@ -36,6 +36,10 @@ public function testRule(): void '@covers value \Not\A\Class references an invalid class or function.', 31, ], + [ + '@covers value does not specify anything.', + 43, + ], ]); } diff --git a/tests/Rules/PHPUnit/data/class-coverage.php b/tests/Rules/PHPUnit/data/class-coverage.php index fca878f2..a5ddd180 100644 --- a/tests/Rules/PHPUnit/data/class-coverage.php +++ b/tests/Rules/PHPUnit/data/class-coverage.php @@ -36,3 +36,10 @@ function testable(): void { } + +/** + * @covers + */ +class CoversNothing extends \PHPUnit\Framework\TestCase +{ +} From db436df51b0de4301fd1b522f9c7aa455d8427b8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 18 Feb 2023 14:43:06 +0100 Subject: [PATCH 38/64] Do not use `instanceof *Type` --- src/Rules/PHPUnit/MockMethodCallRule.php | 17 +++-------------- .../AssertTypeSpecifyingExtensionHelper.php | 14 +++++++------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index ab677603..79da2d93 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -6,9 +6,7 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; -use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\IntersectionType; -use PHPStan\Type\ObjectType; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\Stub; @@ -71,24 +69,15 @@ public function processNode(Node $node, Scope $scope): array ); } - if ( - !($type instanceof GenericObjectType) - || $type->getClassName() !== InvocationMocker::class - || count($type->getTypes()) <= 0 - ) { - continue; - } - - $mockClass = $type->getTypes()[0]; - - if (!($mockClass instanceof ObjectType) || $mockClass->hasMethod($method)->yes()) { + $mockedClassObject = $type->getTemplateType(InvocationMocker::class, 'TMockedClass'); + if ($mockedClassObject->hasMethod($method)->yes()) { continue; } $errors[] = sprintf( 'Trying to mock an undefined method %s() on class %s.', $method, - $mockClass->getClassName() + implode('|', $mockedClassObject->getObjectClassNames()) ); } diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index e2684062..fbed91e7 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -18,7 +18,6 @@ use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; -use PHPStan\Type\Constant\ConstantStringType; use ReflectionObject; use function array_key_exists; use function count; @@ -125,14 +124,15 @@ private static function getExpressionResolvers(): array if (self::$resolvers === null) { self::$resolvers = [ 'InstanceOf' => static function (Scope $scope, Arg $class, Arg $object): ?Instanceof_ { - $classType = $scope->getType($class->value); - if (!$classType instanceof ConstantStringType) { + $classType = $scope->getType($class->value)->getClassStringObjectType(); + $classNames = $classType->getObjectClassNames(); + if (count($classNames) !== 1) { return null; } return new Instanceof_( $object->value, - new Name($classType->getValue()) + new Name($classNames[0]) ); }, 'Same' => static function (Scope $scope, Arg $expected, Arg $actual): Identical { @@ -205,12 +205,12 @@ private static function getExpressionResolvers(): array return new FuncCall(new Name('is_scalar'), [$actual]); }, 'InternalType' => static function (Scope $scope, Arg $type, Arg $value): ?FuncCall { - $typeType = $scope->getType($type->value); - if (!$typeType instanceof ConstantStringType) { + $typeNames = $scope->getType($type->value)->getConstantStrings(); + if (count($typeNames) !== 1) { return null; } - switch ($typeType->getValue()) { + switch ($typeNames[0]->getValue()) { case 'numeric': $functionName = 'is_numeric'; break; From abc2da969a8a28f22365cabfebe8b2c3ea5fabb8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 19 Feb 2023 12:12:42 +0100 Subject: [PATCH 39/64] Fix build --- src/Rules/PHPUnit/MockMethodCallRule.php | 4 +--- .../MockObjectDynamicReturnTypeExtension.php | 16 +++------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index 79da2d93..107b4ea8 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; -use PHPStan\Type\IntersectionType; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\Stub; @@ -51,8 +50,7 @@ public function processNode(Node $node, Scope $scope): array $type = $scope->getType($node->var); if ( - $type instanceof IntersectionType - && ( + ( in_array(MockObject::class, $type->getObjectClassNames(), true) || in_array(Stub::class, $type->getObjectClassNames(), true) ) diff --git a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php index 6db3a73b..4f74fe68 100644 --- a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php @@ -7,7 +7,6 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Generic\GenericObjectType; -use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; @@ -32,24 +31,15 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { $type = $scope->getType($methodCall->var); - if (!($type instanceof IntersectionType)) { - return new ObjectType(InvocationMocker::class); - } - - $mockClasses = array_values(array_filter($type->getTypes(), static function (Type $type): bool { - $classNames = $type->getObjectClassNames(); - if (count($classNames) !== 1) { - return true; - } - - return $classNames[0] !== MockObject::class; + $mockClasses = array_values(array_filter($type->getObjectClassNames(), static function (string $class): bool { + return $class !== MockObject::class; })); if (count($mockClasses) !== 1) { return new ObjectType(InvocationMocker::class); } - return new GenericObjectType(InvocationMocker::class, $mockClasses); + return new GenericObjectType(InvocationMocker::class, [new ObjectType($mockClasses[0])]); } } From 4b17a2352dd70f34bc80e4fb6147609598dd4617 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 21 Feb 2023 17:54:35 +0100 Subject: [PATCH 40/64] Fix MockMethodCallRule --- src/Rules/PHPUnit/MockMethodCallRule.php | 8 ++++++-- tests/Rules/PHPUnit/data/mock-method-call.php | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index 107b4ea8..cac776c2 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -56,15 +56,19 @@ public function processNode(Node $node, Scope $scope): array ) && !$type->hasMethod($method)->yes() ) { - $mockClass = array_filter($type->getObjectClassNames(), static function (string $class): bool { + $mockClasses = array_filter($type->getObjectClassNames(), static function (string $class): bool { return $class !== MockObject::class && $class !== Stub::class; }); + if (count($mockClasses) === 0) { + continue; + } $errors[] = sprintf( 'Trying to mock an undefined method %s() on class %s.', $method, - implode('&', $mockClass) + implode('&', $mockClasses) ); + continue; } $mockedClassObject = $type->getTemplateType(InvocationMocker::class, 'TMockedClass'); diff --git a/tests/Rules/PHPUnit/data/mock-method-call.php b/tests/Rules/PHPUnit/data/mock-method-call.php index bf0fd053..478fa443 100644 --- a/tests/Rules/PHPUnit/data/mock-method-call.php +++ b/tests/Rules/PHPUnit/data/mock-method-call.php @@ -36,6 +36,11 @@ public function testBadMethodOnStub() $this->createStub(Bar::class)->method('doBadThing'); } + public function testMockObject(\PHPUnit\Framework\MockObject\MockObject $mock) + { + $mock->method('doFoo'); + } + } class Bar { From 7e43c8f77c7e419730ead01c8dc787c6bcbe0e15 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 21 Feb 2023 19:39:03 +0100 Subject: [PATCH 41/64] Fix handling assertInstanceOf --- .../Assert/AssertTypeSpecifyingExtensionHelper.php | 6 +++--- .../Rules/PHPUnit/data/impossible-assert-method-call.php | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index fbed91e7..2276b51b 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -124,15 +124,15 @@ private static function getExpressionResolvers(): array if (self::$resolvers === null) { self::$resolvers = [ 'InstanceOf' => static function (Scope $scope, Arg $class, Arg $object): ?Instanceof_ { - $classType = $scope->getType($class->value)->getClassStringObjectType(); - $classNames = $classType->getObjectClassNames(); + $classType = $scope->getType($class->value); + $classNames = $classType->getConstantStrings(); if (count($classNames) !== 1) { return null; } return new Instanceof_( $object->value, - new Name($classNames[0]) + new Name($classNames[0]->getValue()) ); }, 'Same' => static function (Scope $scope, Arg $expected, Arg $actual): Identical { diff --git a/tests/Rules/PHPUnit/data/impossible-assert-method-call.php b/tests/Rules/PHPUnit/data/impossible-assert-method-call.php index a406f1e5..8b996725 100644 --- a/tests/Rules/PHPUnit/data/impossible-assert-method-call.php +++ b/tests/Rules/PHPUnit/data/impossible-assert-method-call.php @@ -20,4 +20,13 @@ public function doBar(object $o): void $this->assertEmpty($o); } + /** + * @param class-string<\Exception> $name + * @return void + */ + public function doBaz(\Exception $e, string $name): void + { + $this->assertInstanceOf($name, $e); + } + } From 4a19a3cb5b2d28b143f350e45e9f6e17e2cb81b5 Mon Sep 17 00:00:00 2001 From: Brad <28307684+mad-briller@users.noreply.github.com> Date: Thu, 23 Feb 2023 12:55:09 +0000 Subject: [PATCH 42/64] Add tip to error when a not fully qualified name is seen in @covers annotation. --- src/Rules/PHPUnit/CoversHelper.php | 10 ++++++++-- tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php | 5 +++++ tests/Rules/PHPUnit/data/class-coverage.php | 7 +++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Rules/PHPUnit/CoversHelper.php b/src/Rules/PHPUnit/CoversHelper.php index a96257f0..792fcc11 100644 --- a/src/Rules/PHPUnit/CoversHelper.php +++ b/src/Rules/PHPUnit/CoversHelper.php @@ -105,11 +105,17 @@ public function processCovers( return $errors; } - $errors[] = RuleErrorBuilder::message(sprintf( + $error = RuleErrorBuilder::message(sprintf( '@covers value %s references an invalid %s.', $fullName, $isMethod ? 'method' : 'class or function' - ))->build(); + )); + + if (strpos($className, '\\') === false) { + $error->tip('The @covers annotation requires a fully qualified name.'); + } + + $errors[] = $error->build(); } return $errors; } diff --git a/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php index 7b84b50f..32806edc 100644 --- a/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php +++ b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php @@ -40,6 +40,11 @@ public function testRule(): void '@covers value does not specify anything.', 43, ], + [ + '@covers value NotFullyQualified references an invalid class or function.', + 50, + 'The @covers annotation requires a fully qualified name.', + ], ]); } diff --git a/tests/Rules/PHPUnit/data/class-coverage.php b/tests/Rules/PHPUnit/data/class-coverage.php index a5ddd180..35af2049 100644 --- a/tests/Rules/PHPUnit/data/class-coverage.php +++ b/tests/Rules/PHPUnit/data/class-coverage.php @@ -43,3 +43,10 @@ function testable(): void class CoversNothing extends \PHPUnit\Framework\TestCase { } + +/** + * @covers NotFullyQualified + */ +class CoversNotFullyQualified extends \PHPUnit\Framework\TestCase +{ +} From 34ee324a2b8fcab680fbb3f3f3d6c86389df35ba Mon Sep 17 00:00:00 2001 From: Brad <28307684+mad-briller@users.noreply.github.com> Date: Tue, 28 Feb 2023 11:18:13 +0000 Subject: [PATCH 43/64] Fixed false positive when covering a global function. --- src/Rules/PHPUnit/CoversHelper.php | 22 +++++++++++---------- tests/Rules/PHPUnit/data/class-coverage.php | 7 +++++++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Rules/PHPUnit/CoversHelper.php b/src/Rules/PHPUnit/CoversHelper.php index 792fcc11..66d6edf8 100644 --- a/src/Rules/PHPUnit/CoversHelper.php +++ b/src/Rules/PHPUnit/CoversHelper.php @@ -71,6 +71,13 @@ public function processCovers( { $errors = []; $covers = (string) $phpDocTag->value; + + if ($covers === '') { + $errors[] = RuleErrorBuilder::message('@covers value does not specify anything.')->build(); + + return $errors; + } + $isMethod = strpos($covers, '::') !== false; $fullName = $covers; @@ -94,17 +101,12 @@ public function processCovers( $fullName ))->build(); } - } else { - if ($covers === '') { - $errors[] = RuleErrorBuilder::message('@covers value does not specify anything.')->build(); - - return $errors; - } - - if (!isset($method) && $this->reflectionProvider->hasFunction(new Name($covers, []), null)) { - return $errors; - } + } elseif (isset($method) && $this->reflectionProvider->hasFunction(new Name($method, []), null)) { + return $errors; + } elseif (!isset($method) && $this->reflectionProvider->hasFunction(new Name($className, []), null)) { + return $errors; + } else { $error = RuleErrorBuilder::message(sprintf( '@covers value %s references an invalid %s.', $fullName, diff --git a/tests/Rules/PHPUnit/data/class-coverage.php b/tests/Rules/PHPUnit/data/class-coverage.php index 35af2049..2d4e0ef8 100644 --- a/tests/Rules/PHPUnit/data/class-coverage.php +++ b/tests/Rules/PHPUnit/data/class-coverage.php @@ -50,3 +50,10 @@ class CoversNothing extends \PHPUnit\Framework\TestCase class CoversNotFullyQualified extends \PHPUnit\Framework\TestCase { } + +/** + * @covers ::str_replace + */ +class CoversGlobalFunction extends \PHPUnit\Framework\TestCase +{ +} From 4cc5c6cc38e56bce7ea47c4091814e516d172dc3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 2 Mar 2023 11:22:40 +0100 Subject: [PATCH 44/64] MockMethodCallRule - do not report for empty `$mockClasses` --- src/Rules/PHPUnit/MockMethodCallRule.php | 7 ++++++- tests/Rules/PHPUnit/data/mock-method-call.php | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index cac776c2..da8a95d7 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -76,10 +76,15 @@ public function processNode(Node $node, Scope $scope): array continue; } + $classNames = $mockedClassObject->getObjectClassNames(); + if (count($classNames) === 0) { + continue; + } + $errors[] = sprintf( 'Trying to mock an undefined method %s() on class %s.', $method, - implode('|', $mockedClassObject->getObjectClassNames()) + implode('|', $classNames) ); } diff --git a/tests/Rules/PHPUnit/data/mock-method-call.php b/tests/Rules/PHPUnit/data/mock-method-call.php index 478fa443..a4f5aaae 100644 --- a/tests/Rules/PHPUnit/data/mock-method-call.php +++ b/tests/Rules/PHPUnit/data/mock-method-call.php @@ -56,3 +56,18 @@ public function method(string $string) return $string; } }; + +final class FinalFoo +{ + +} + +class FinalFooTest extends \PHPUnit\Framework\TestCase +{ + + public function testMockFinalClass() + { + $this->createMock(FinalFoo::class)->method('doFoo'); + } + +} From f6592437762938425a51e6613bbac649c0bd7daa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Mar 2023 02:45:43 +0000 Subject: [PATCH 45/64] Update metcalfc/changelog-generator action to v4.1.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 bac4a006..92b72547 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.0.1 + uses: metcalfc/changelog-generator@v4.1.0 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From ceea85eb8c43e5ae22040c18d3f360ca9ad5dc4f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 22 Mar 2023 09:36:13 +0100 Subject: [PATCH 46/64] Update PHPCS --- build-cs/composer.json | 7 +-- build-cs/composer.lock | 117 ++++++++++++++++++++++------------------- 2 files changed, 67 insertions(+), 57 deletions(-) diff --git a/build-cs/composer.json b/build-cs/composer.json index e3079710..16a240bc 100644 --- a/build-cs/composer.json +++ b/build-cs/composer.json @@ -1,8 +1,9 @@ { "require-dev": { - "consistence-community/coding-standard": "^3.10", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "slevomat/coding-standard": "^7.0" + "consistence-community/coding-standard": "^3.11.0", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", + "slevomat/coding-standard": "^8.8.0", + "squizlabs/php_codesniffer": "^3.5.3" }, "config": { "allow-plugins": { diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 4bcc8de4..c25a151a 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -4,35 +4,35 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4485bbedba7bcc71ace5f69dbb9b6c47", + "content-hash": "e69c1916405a7e3c8001c1b609a0ee61", "packages": [], "packages-dev": [ { "name": "consistence-community/coding-standard", - "version": "3.11.1", + "version": "3.11.2", "source": { "type": "git", "url": "https://github.com/consistence-community/coding-standard.git", - "reference": "4632fead8c9ee8f50044fcbce9f66c797b34c0df" + "reference": "adb4be482e76990552bf624309d2acc8754ba1bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/4632fead8c9ee8f50044fcbce9f66c797b34c0df", - "reference": "4632fead8c9ee8f50044fcbce9f66c797b34c0df", + "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/adb4be482e76990552bf624309d2acc8754ba1bd", + "reference": "adb4be482e76990552bf624309d2acc8754ba1bd", "shasum": "" }, "require": { - "php": ">=7.4", - "slevomat/coding-standard": "~7.0", - "squizlabs/php_codesniffer": "~3.6.0" + "php": "~8.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "~3.7.0" }, "replace": { "consistence/coding-standard": "3.10.*" }, "require-dev": { - "phing/phing": "2.16.4", - "php-parallel-lint/php-parallel-lint": "1.3.0", - "phpunit/phpunit": "9.5.4" + "phing/phing": "2.17.0", + "php-parallel-lint/php-parallel-lint": "1.3.1", + "phpunit/phpunit": "9.5.10" }, "type": "library", "autoload": { @@ -70,41 +70,44 @@ ], "support": { "issues": "https://github.com/consistence-community/coding-standard/issues", - "source": "https://github.com/consistence-community/coding-standard/tree/3.11.1" + "source": "https://github.com/consistence-community/coding-standard/tree/3.11.2" }, - "time": "2021-05-03T18:13:22+00:00" + "time": "2022-06-21T08:36:36+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v0.7.2", + "version": "v1.0.0", "source": { "type": "git", - "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", - "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db" + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "4be43904336affa5c2f70744a348312336afd0da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", - "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", + "reference": "4be43904336affa5c2f70744a348312336afd0da", "shasum": "" }, "require": { "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.3", + "php": ">=5.4", "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" }, "require-dev": { "composer/composer": "*", + "ext-json": "*", + "ext-zip": "*", "php-parallel-lint/php-parallel-lint": "^1.3.1", - "phpcompatibility/php-compatibility": "^9.0" + "phpcompatibility/php-compatibility": "^9.0", + "yoast/phpunit-polyfills": "^1.0" }, "type": "composer-plugin", "extra": { - "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" }, "autoload": { "psr-4": { - "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -120,7 +123,7 @@ }, { "name": "Contributors", - "homepage": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer/graphs/contributors" + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", @@ -144,23 +147,23 @@ "tests" ], "support": { - "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", - "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2022-02-04T12:51:07+00:00" + "time": "2023-01-05T11:28:13+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.5.1", + "version": "1.15.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "981cc368a216c988e862a75e526b6076987d1b50" + "reference": "61800f71a5526081d1b5633766aa88341f1ade76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/981cc368a216c988e862a75e526b6076987d1b50", - "reference": "981cc368a216c988e862a75e526b6076987d1b50", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/61800f71a5526081d1b5633766aa88341f1ade76", + "reference": "61800f71a5526081d1b5633766aa88341f1ade76", "shasum": "" }, "require": { @@ -170,6 +173,7 @@ "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", "phpstan/phpstan-strict-rules": "^1.0", "phpunit/phpunit": "^9.5", "symfony/process": "^5.2" @@ -189,43 +193,43 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.5.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.15.3" }, - "time": "2022-05-05T11:32:40+00:00" + "time": "2022-12-20T20:56:55+00:00" }, { "name": "slevomat/coding-standard", - "version": "7.2.1", + "version": "8.8.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "aff06ae7a84e4534bf6f821dc982a93a5d477c90" + "reference": "59e25146a4ef0a7b194c5bc55b32dd414345db89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/aff06ae7a84e4534bf6f821dc982a93a5d477c90", - "reference": "aff06ae7a84e4534bf6f821dc982a93a5d477c90", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/59e25146a4ef0a7b194c5bc55b32dd414345db89", + "reference": "59e25146a4ef0a7b194c5bc55b32dd414345db89", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": "^1.5.1", - "squizlabs/php_codesniffer": "^3.6.2" + "phpstan/phpdoc-parser": ">=1.15.2 <1.16.0", + "squizlabs/php_codesniffer": "^3.7.1" }, "require-dev": { - "phing/phing": "2.17.3", + "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.7.1", - "phpstan/phpstan-deprecation-rules": "1.0.0", - "phpstan/phpstan-phpunit": "1.0.0|1.1.1", - "phpstan/phpstan-strict-rules": "1.2.3", - "phpunit/phpunit": "7.5.20|8.5.21|9.5.20" + "phpstan/phpstan": "1.4.10|1.9.6", + "phpstan/phpstan-deprecation-rules": "1.1.1", + "phpstan/phpstan-phpunit": "1.0.0|1.3.3", + "phpstan/phpstan-strict-rules": "1.4.4", + "phpunit/phpunit": "7.5.20|8.5.21|9.5.27" }, "type": "phpcodesniffer-standard", "extra": { "branch-alias": { - "dev-master": "7.x-dev" + "dev-master": "8.x-dev" } }, "autoload": { @@ -238,9 +242,13 @@ "MIT" ], "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "keywords": [ + "dev", + "phpcs" + ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/7.2.1" + "source": "https://github.com/slevomat/coding-standard/tree/8.8.0" }, "funding": [ { @@ -252,20 +260,20 @@ "type": "tidelift" } ], - "time": "2022-05-25T10:58:12+00:00" + "time": "2023-01-09T10:46:13+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.6.2", + "version": "3.7.2", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a" + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a", - "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", "shasum": "" }, "require": { @@ -301,14 +309,15 @@ "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", "keywords": [ "phpcs", - "standards" + "standards", + "static analysis" ], "support": { "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", "source": "https://github.com/squizlabs/PHP_CodeSniffer", "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" }, - "time": "2021-12-12T21:44:58+00:00" + "time": "2023-02-22T23:07:41+00:00" } ], "aliases": [], From 9e1b9de6d260461f6e99b6a8f2dbb0bbb98b579c Mon Sep 17 00:00:00 2001 From: Brad <28307684+mad-briller@users.noreply.github.com> Date: Fri, 24 Mar 2023 16:09:26 +0000 Subject: [PATCH 47/64] Warn when trying to cover an interface. --- src/Rules/PHPUnit/CoversHelper.php | 8 +++++++- tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php | 4 ++++ tests/Rules/PHPUnit/data/class-coverage.php | 7 +++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Rules/PHPUnit/CoversHelper.php b/src/Rules/PHPUnit/CoversHelper.php index 66d6edf8..f17a2063 100644 --- a/src/Rules/PHPUnit/CoversHelper.php +++ b/src/Rules/PHPUnit/CoversHelper.php @@ -95,6 +95,13 @@ public function processCovers( if ($this->reflectionProvider->hasClass($className)) { $class = $this->reflectionProvider->getClass($className); + if ($class->isInterface()) { + $errors[] = RuleErrorBuilder::message(sprintf( + '@covers value %s references an interface.', + $fullName + ))->build(); + } + if (isset($method) && $method !== '' && !$class->hasMethod($method)) { $errors[] = RuleErrorBuilder::message(sprintf( '@covers value %s references an invalid method.', @@ -105,7 +112,6 @@ public function processCovers( return $errors; } elseif (!isset($method) && $this->reflectionProvider->hasFunction(new Name($className, []), null)) { return $errors; - } else { $error = RuleErrorBuilder::message(sprintf( '@covers value %s references an invalid %s.', diff --git a/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php index 32806edc..69a7de2f 100644 --- a/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php +++ b/tests/Rules/PHPUnit/ClassCoversExistsRuleTest.php @@ -45,6 +45,10 @@ public function testRule(): void 50, 'The @covers annotation requires a fully qualified name.', ], + [ + '@covers value \DateTimeInterface references an interface.', + 64, + ], ]); } diff --git a/tests/Rules/PHPUnit/data/class-coverage.php b/tests/Rules/PHPUnit/data/class-coverage.php index 2d4e0ef8..c231f772 100644 --- a/tests/Rules/PHPUnit/data/class-coverage.php +++ b/tests/Rules/PHPUnit/data/class-coverage.php @@ -57,3 +57,10 @@ class CoversNotFullyQualified extends \PHPUnit\Framework\TestCase class CoversGlobalFunction extends \PHPUnit\Framework\TestCase { } + +/** + * @covers \DateTimeInterface + */ +class CoversInterface extends \PHPUnit\Framework\TestCase +{ +} From 8b82ef2c494ab779f8d5230036b28c99622e881c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 02:12:31 +0000 Subject: [PATCH 48/64] Update dependency slevomat/coding-standard to v8.9.0 --- build-cs/composer.lock | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index c25a151a..b52da6f8 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -154,16 +154,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.15.3", + "version": "1.16.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "61800f71a5526081d1b5633766aa88341f1ade76" + "reference": "e27e92d939e2e3636f0a1f0afaba59692c0bf571" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/61800f71a5526081d1b5633766aa88341f1ade76", - "reference": "61800f71a5526081d1b5633766aa88341f1ade76", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/e27e92d939e2e3636f0a1f0afaba59692c0bf571", + "reference": "e27e92d939e2e3636f0a1f0afaba59692c0bf571", "shasum": "" }, "require": { @@ -193,38 +193,38 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.15.3" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.16.1" }, - "time": "2022-12-20T20:56:55+00:00" + "time": "2023-02-07T18:11:17+00:00" }, { "name": "slevomat/coding-standard", - "version": "8.8.0", + "version": "8.9.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "59e25146a4ef0a7b194c5bc55b32dd414345db89" + "reference": "8f11e0f5ff984d6862bb9d83aa513dc05a1773ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/59e25146a4ef0a7b194c5bc55b32dd414345db89", - "reference": "59e25146a4ef0a7b194c5bc55b32dd414345db89", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/8f11e0f5ff984d6862bb9d83aa513dc05a1773ef", + "reference": "8f11e0f5ff984d6862bb9d83aa513dc05a1773ef", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": ">=1.15.2 <1.16.0", + "phpstan/phpdoc-parser": ">=1.16.0 <1.17.0", "squizlabs/php_codesniffer": "^3.7.1" }, "require-dev": { "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.9.6", - "phpstan/phpstan-deprecation-rules": "1.1.1", - "phpstan/phpstan-phpunit": "1.0.0|1.3.3", - "phpstan/phpstan-strict-rules": "1.4.4", - "phpunit/phpunit": "7.5.20|8.5.21|9.5.27" + "phpstan/phpstan": "1.4.10|1.10.8", + "phpstan/phpstan-deprecation-rules": "1.1.3", + "phpstan/phpstan-phpunit": "1.0.0|1.3.10", + "phpstan/phpstan-strict-rules": "1.5.0", + "phpunit/phpunit": "7.5.20|8.5.21|9.6.5" }, "type": "phpcodesniffer-standard", "extra": { @@ -234,7 +234,7 @@ }, "autoload": { "psr-4": { - "SlevomatCodingStandard\\": "SlevomatCodingStandard" + "SlevomatCodingStandard\\": "SlevomatCodingStandard/" } }, "notification-url": "https://packagist.org/downloads/", @@ -248,7 +248,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.8.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.9.0" }, "funding": [ { @@ -260,7 +260,7 @@ "type": "tidelift" } ], - "time": "2023-01-09T10:46:13+00:00" + "time": "2023-03-25T15:52:37+00:00" }, { "name": "squizlabs/php_codesniffer", From 4ae9e7c2c106c8aa420e5768088bcdefb7e63090 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 00:11:11 +0000 Subject: [PATCH 49/64] Update build-cs --- build-cs/composer.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index b52da6f8..d8730d8a 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -9,16 +9,16 @@ "packages-dev": [ { "name": "consistence-community/coding-standard", - "version": "3.11.2", + "version": "3.11.3", "source": { "type": "git", "url": "https://github.com/consistence-community/coding-standard.git", - "reference": "adb4be482e76990552bf624309d2acc8754ba1bd" + "reference": "f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/adb4be482e76990552bf624309d2acc8754ba1bd", - "reference": "adb4be482e76990552bf624309d2acc8754ba1bd", + "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1", + "reference": "f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1", "shasum": "" }, "require": { @@ -70,9 +70,9 @@ ], "support": { "issues": "https://github.com/consistence-community/coding-standard/issues", - "source": "https://github.com/consistence-community/coding-standard/tree/3.11.2" + "source": "https://github.com/consistence-community/coding-standard/tree/3.11.3" }, - "time": "2022-06-21T08:36:36+00:00" + "time": "2023-03-27T14:55:41+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", @@ -199,16 +199,16 @@ }, { "name": "slevomat/coding-standard", - "version": "8.9.0", + "version": "8.9.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "8f11e0f5ff984d6862bb9d83aa513dc05a1773ef" + "reference": "3d4fe0c803ae15829ef72d90d3d4eee3dd9f79b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/8f11e0f5ff984d6862bb9d83aa513dc05a1773ef", - "reference": "8f11e0f5ff984d6862bb9d83aa513dc05a1773ef", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/3d4fe0c803ae15829ef72d90d3d4eee3dd9f79b2", + "reference": "3d4fe0c803ae15829ef72d90d3d4eee3dd9f79b2", "shasum": "" }, "require": { @@ -248,7 +248,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.9.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.9.1" }, "funding": [ { @@ -260,7 +260,7 @@ "type": "tidelift" } ], - "time": "2023-03-25T15:52:37+00:00" + "time": "2023-03-27T11:00:16+00:00" }, { "name": "squizlabs/php_codesniffer", From 2365fd0df5d6c3457b30c9a0b919fd1e406f3fde Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 08:36:38 +0000 Subject: [PATCH 50/64] Update dependency slevomat/coding-standard to v8.10.0 --- build-cs/composer.lock | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index d8730d8a..2eafc159 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -154,16 +154,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.16.1", + "version": "1.18.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "e27e92d939e2e3636f0a1f0afaba59692c0bf571" + "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/e27e92d939e2e3636f0a1f0afaba59692c0bf571", - "reference": "e27e92d939e2e3636f0a1f0afaba59692c0bf571", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/22dcdfd725ddf99583bfe398fc624ad6c5004a0f", + "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f", "shasum": "" }, "require": { @@ -193,38 +193,38 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.16.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.18.1" }, - "time": "2023-02-07T18:11:17+00:00" + "time": "2023-04-07T11:51:11+00:00" }, { "name": "slevomat/coding-standard", - "version": "8.9.1", + "version": "8.10.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "3d4fe0c803ae15829ef72d90d3d4eee3dd9f79b2" + "reference": "c4e213e6e57f741451a08e68ef838802eec92287" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/3d4fe0c803ae15829ef72d90d3d4eee3dd9f79b2", - "reference": "3d4fe0c803ae15829ef72d90d3d4eee3dd9f79b2", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/c4e213e6e57f741451a08e68ef838802eec92287", + "reference": "c4e213e6e57f741451a08e68ef838802eec92287", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": ">=1.16.0 <1.17.0", + "phpstan/phpdoc-parser": ">=1.18.0 <1.19.0", "squizlabs/php_codesniffer": "^3.7.1" }, "require-dev": { "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.10.8", + "phpstan/phpstan": "1.4.10|1.10.11", "phpstan/phpstan-deprecation-rules": "1.1.3", - "phpstan/phpstan-phpunit": "1.0.0|1.3.10", - "phpstan/phpstan-strict-rules": "1.5.0", - "phpunit/phpunit": "7.5.20|8.5.21|9.6.5" + "phpstan/phpstan-phpunit": "1.0.0|1.3.11", + "phpstan/phpstan-strict-rules": "1.5.1", + "phpunit/phpunit": "7.5.20|8.5.21|9.6.6|10.0.19" }, "type": "phpcodesniffer-standard", "extra": { @@ -248,7 +248,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.9.1" + "source": "https://github.com/slevomat/coding-standard/tree/8.10.0" }, "funding": [ { @@ -260,7 +260,7 @@ "type": "tidelift" } ], - "time": "2023-03-27T11:00:16+00:00" + "time": "2023-04-10T07:39:29+00:00" }, { "name": "squizlabs/php_codesniffer", From c006a382a334ed01d68ad4422696c94b241c46b2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 01:07:57 +0000 Subject: [PATCH 51/64] Update dependency slevomat/coding-standard to v8.11.0 --- build-cs/composer.lock | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 2eafc159..0cea3f9e 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -154,16 +154,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.18.1", + "version": "1.20.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f" + "reference": "90490bd8fd8530a272043c4950c180b6d0cf5f81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/22dcdfd725ddf99583bfe398fc624ad6c5004a0f", - "reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/90490bd8fd8530a272043c4950c180b6d0cf5f81", + "reference": "90490bd8fd8530a272043c4950c180b6d0cf5f81", "shasum": "" }, "require": { @@ -193,38 +193,38 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.18.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.20.2" }, - "time": "2023-04-07T11:51:11+00:00" + "time": "2023-04-22T12:59:35+00:00" }, { "name": "slevomat/coding-standard", - "version": "8.10.0", + "version": "8.11.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "c4e213e6e57f741451a08e68ef838802eec92287" + "reference": "91428d5bcf7db93a842bcf97f465edf62527f3ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/c4e213e6e57f741451a08e68ef838802eec92287", - "reference": "c4e213e6e57f741451a08e68ef838802eec92287", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/91428d5bcf7db93a842bcf97f465edf62527f3ea", + "reference": "91428d5bcf7db93a842bcf97f465edf62527f3ea", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": ">=1.18.0 <1.19.0", + "phpstan/phpdoc-parser": ">=1.20.0 <1.21.0", "squizlabs/php_codesniffer": "^3.7.1" }, "require-dev": { "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.10.11", + "phpstan/phpstan": "1.10.14", "phpstan/phpstan-deprecation-rules": "1.1.3", - "phpstan/phpstan-phpunit": "1.0.0|1.3.11", + "phpstan/phpstan-phpunit": "1.3.11", "phpstan/phpstan-strict-rules": "1.5.1", - "phpunit/phpunit": "7.5.20|8.5.21|9.6.6|10.0.19" + "phpunit/phpunit": "7.5.20|8.5.21|9.6.6|10.1.1" }, "type": "phpcodesniffer-standard", "extra": { @@ -248,7 +248,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.10.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.11.0" }, "funding": [ { @@ -260,7 +260,7 @@ "type": "tidelift" } ], - "time": "2023-04-10T07:39:29+00:00" + "time": "2023-04-21T15:51:44+00:00" }, { "name": "squizlabs/php_codesniffer", From 9975b981899246fe7e289cf200deafcf2fefa649 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 00:53:26 +0000 Subject: [PATCH 52/64] Update dependency slevomat/coding-standard to v8.11.1 --- build-cs/composer.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 0cea3f9e..dab6b094 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -154,16 +154,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.20.2", + "version": "1.20.4", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "90490bd8fd8530a272043c4950c180b6d0cf5f81" + "reference": "7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/90490bd8fd8530a272043c4950c180b6d0cf5f81", - "reference": "90490bd8fd8530a272043c4950c180b6d0cf5f81", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd", + "reference": "7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd", "shasum": "" }, "require": { @@ -193,22 +193,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.20.2" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.20.4" }, - "time": "2023-04-22T12:59:35+00:00" + "time": "2023-05-02T09:19:37+00:00" }, { "name": "slevomat/coding-standard", - "version": "8.11.0", + "version": "8.11.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "91428d5bcf7db93a842bcf97f465edf62527f3ea" + "reference": "af87461316b257e46e15bb041dca6fca3796d822" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/91428d5bcf7db93a842bcf97f465edf62527f3ea", - "reference": "91428d5bcf7db93a842bcf97f465edf62527f3ea", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/af87461316b257e46e15bb041dca6fca3796d822", + "reference": "af87461316b257e46e15bb041dca6fca3796d822", "shasum": "" }, "require": { @@ -248,7 +248,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.11.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.11.1" }, "funding": [ { @@ -260,7 +260,7 @@ "type": "tidelift" } ], - "time": "2023-04-21T15:51:44+00:00" + "time": "2023-04-24T08:19:01+00:00" }, { "name": "squizlabs/php_codesniffer", From aaf5fe559e982f1b1dabdbf9ac99ea7ce7734595 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 15 May 2023 02:05:35 +0000 Subject: [PATCH 53/64] Update dependency slevomat/coding-standard to v8.12.0 --- build-cs/composer.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index dab6b094..db122416 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -199,16 +199,16 @@ }, { "name": "slevomat/coding-standard", - "version": "8.11.1", + "version": "8.12.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "af87461316b257e46e15bb041dca6fca3796d822" + "reference": "cc04334ed0ce5a251389112fbd2dbe1dbc931ae8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/af87461316b257e46e15bb041dca6fca3796d822", - "reference": "af87461316b257e46e15bb041dca6fca3796d822", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/cc04334ed0ce5a251389112fbd2dbe1dbc931ae8", + "reference": "cc04334ed0ce5a251389112fbd2dbe1dbc931ae8", "shasum": "" }, "require": { @@ -220,11 +220,11 @@ "require-dev": { "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.10.14", + "phpstan/phpstan": "1.10.15", "phpstan/phpstan-deprecation-rules": "1.1.3", "phpstan/phpstan-phpunit": "1.3.11", "phpstan/phpstan-strict-rules": "1.5.1", - "phpunit/phpunit": "7.5.20|8.5.21|9.6.6|10.1.1" + "phpunit/phpunit": "7.5.20|8.5.21|9.6.8|10.1.3" }, "type": "phpcodesniffer-standard", "extra": { @@ -248,7 +248,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.11.1" + "source": "https://github.com/slevomat/coding-standard/tree/8.12.0" }, "funding": [ { @@ -260,7 +260,7 @@ "type": "tidelift" } ], - "time": "2023-04-24T08:19:01+00:00" + "time": "2023-05-14T20:06:01+00:00" }, { "name": "squizlabs/php_codesniffer", From 23009eb5269c2232689f0b690d4a2b893afcde94 Mon Sep 17 00:00:00 2001 From: Alessandro Lai Date: Mon, 22 May 2023 18:52:54 +0200 Subject: [PATCH 54/64] Add regression test for #185 --- tests/Type/PHPUnit/data/assert-function.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/Type/PHPUnit/data/assert-function.php b/tests/Type/PHPUnit/data/assert-function.php index 117179ae..f3222f34 100644 --- a/tests/Type/PHPUnit/data/assert-function.php +++ b/tests/Type/PHPUnit/data/assert-function.php @@ -20,6 +20,17 @@ public function doFoo($o): void assertType(self::class, $o); } + /** + * @template T of object + * @param object $o + * @param class-string<\DateTimeInterface> $class + */ + public function assertInstanceOfWorksWithTemplate($o, $class): void + { + assertInstanceOf($class, $o); + assertType(\DateTimeInterface::class, $o); + } + public function arrayHasNumericKey(array $a): void { assertArrayHasKey(0, $a); assertType('array&hasOffset(0)', $a); From c44246879d692d3b2cf2a21d65be4b4715d6ef21 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 23 May 2023 13:55:04 +0200 Subject: [PATCH 55/64] Fix assertInstanceOf handler --- .../Assert/AssertTypeSpecifyingExtensionHelper.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 2276b51b..2e8e828e 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -123,16 +123,10 @@ private static function getExpressionResolvers(): array { if (self::$resolvers === null) { self::$resolvers = [ - 'InstanceOf' => static function (Scope $scope, Arg $class, Arg $object): ?Instanceof_ { - $classType = $scope->getType($class->value); - $classNames = $classType->getConstantStrings(); - if (count($classNames) !== 1) { - return null; - } - + 'InstanceOf' => static function (Scope $scope, Arg $class, Arg $object): Instanceof_ { return new Instanceof_( $object->value, - new Name($classNames[0]->getValue()) + $class->value ); }, 'Same' => static function (Scope $scope, Arg $expected, Arg $actual): Identical { From 9e928dd88023db780756e117b70383322247dd32 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 25 May 2023 15:27:48 +0200 Subject: [PATCH 56/64] Next-gen coding standard workflow --- .gitattributes | 2 - .github/renovate.json | 5 - .github/workflows/build.yml | 14 +- .gitignore | 1 + Makefile | 10 +- build-cs/.gitignore | 1 - build-cs/composer.json | 13 -- build-cs/composer.lock | 331 ------------------------------------ phpcs.xml | 111 ------------ 9 files changed, 21 insertions(+), 467 deletions(-) delete mode 100644 build-cs/.gitignore delete mode 100644 build-cs/composer.json delete mode 100644 build-cs/composer.lock delete mode 100644 phpcs.xml diff --git a/.gitattributes b/.gitattributes index 00d6b17d..aae3fd19 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,11 +3,9 @@ *.neon linguist-language=YAML /.* export-ignore -/build-cs export-ignore /tests export-ignore /tmp export-ignore /Makefile export-ignore -/phpcs.xml export-ignore /phpstan.neon export-ignore /phpstan-baseline.neon export-ignore /phpunit.xml export-ignore diff --git a/.github/renovate.json b/.github/renovate.json index b775cc18..d3f5961e 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -10,11 +10,6 @@ "enabled": true, "groupName": "root-composer" }, - { - "matchPaths": ["build-cs/**"], - "enabled": true, - "groupName": "build-cs" - }, { "matchPaths": [".github/**"], "enabled": true, diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 35254989..afe7e6ed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,7 +46,7 @@ jobs: - name: "Lint" run: "make lint" - coding-standards: + coding-standard: name: "Coding Standard" runs-on: "ubuntu-latest" @@ -55,11 +55,17 @@ jobs: - name: "Checkout" uses: actions/checkout@v3 + - name: "Checkout build-cs" + uses: actions/checkout@v3 + with: + repository: "phpstan/build-cs" + path: "build-cs" + - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.0" + php-version: "8.2" - name: "Validate Composer" run: "composer validate" @@ -67,6 +73,10 @@ jobs: - name: "Install dependencies" run: "composer install --no-interaction --no-progress" + - name: "Install build-cs dependencies" + working-directory: "build-cs" + run: "composer install --no-interaction --no-progress" + - name: "Lint" run: "make lint" diff --git a/.gitignore b/.gitignore index 2db21315..7de9f3c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /tests/tmp +/build-cs /vendor /composer.lock .phpunit.result.cache diff --git a/Makefile b/Makefile index b34d0fec..ecd8cfb2 100644 --- a/Makefile +++ b/Makefile @@ -10,13 +10,19 @@ lint: php vendor/bin/parallel-lint --colors \ src tests +.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/main + composer install --working-dir build-cs + .PHONY: cs cs: - composer install --working-dir build-cs && php build-cs/vendor/bin/phpcs + php build-cs/vendor/bin/phpcs --standard=build-cs/phpcs.xml src tests .PHONY: cs-fix cs-fix: - php build-cs/vendor/bin/phpcbf + php build-cs/vendor/bin/phpcbf --standard=build-cs/phpcs.xml src tests .PHONY: phpstan phpstan: diff --git a/build-cs/.gitignore b/build-cs/.gitignore deleted file mode 100644 index 61ead866..00000000 --- a/build-cs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/vendor diff --git a/build-cs/composer.json b/build-cs/composer.json deleted file mode 100644 index 16a240bc..00000000 --- a/build-cs/composer.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "require-dev": { - "consistence-community/coding-standard": "^3.11.0", - "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", - "slevomat/coding-standard": "^8.8.0", - "squizlabs/php_codesniffer": "^3.5.3" - }, - "config": { - "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true - } - } -} diff --git a/build-cs/composer.lock b/build-cs/composer.lock deleted file mode 100644 index db122416..00000000 --- a/build-cs/composer.lock +++ /dev/null @@ -1,331 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "e69c1916405a7e3c8001c1b609a0ee61", - "packages": [], - "packages-dev": [ - { - "name": "consistence-community/coding-standard", - "version": "3.11.3", - "source": { - "type": "git", - "url": "https://github.com/consistence-community/coding-standard.git", - "reference": "f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1", - "reference": "f38e06327d5bf80ff5ff523a2c05e623b5e8d8b1", - "shasum": "" - }, - "require": { - "php": "~8.0", - "slevomat/coding-standard": "~8.0", - "squizlabs/php_codesniffer": "~3.7.0" - }, - "replace": { - "consistence/coding-standard": "3.10.*" - }, - "require-dev": { - "phing/phing": "2.17.0", - "php-parallel-lint/php-parallel-lint": "1.3.1", - "phpunit/phpunit": "9.5.10" - }, - "type": "library", - "autoload": { - "psr-4": { - "Consistence\\": [ - "Consistence" - ] - }, - "classmap": [ - "Consistence" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "VaĊĦek Purchart", - "email": "me@vasekpurchart.cz", - "homepage": "http://vasekpurchart.cz/" - } - ], - "description": "Consistence - Coding Standard - PHP Code Sniffer rules", - "keywords": [ - "Coding Standard", - "PHPCodeSniffer", - "codesniffer", - "coding", - "cs", - "phpcs", - "ruleset", - "sniffer", - "standard" - ], - "support": { - "issues": "https://github.com/consistence-community/coding-standard/issues", - "source": "https://github.com/consistence-community/coding-standard/tree/3.11.3" - }, - "time": "2023-03-27T14:55:41+00:00" - }, - { - "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.0.0", - "source": { - "type": "git", - "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "4be43904336affa5c2f70744a348312336afd0da" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", - "reference": "4be43904336affa5c2f70744a348312336afd0da", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.4", - "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" - }, - "require-dev": { - "composer/composer": "*", - "ext-json": "*", - "ext-zip": "*", - "php-parallel-lint/php-parallel-lint": "^1.3.1", - "phpcompatibility/php-compatibility": "^9.0", - "yoast/phpunit-polyfills": "^1.0" - }, - "type": "composer-plugin", - "extra": { - "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" - }, - "autoload": { - "psr-4": { - "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Franck Nijhof", - "email": "franck.nijhof@dealerdirect.com", - "homepage": "http://www.frenck.nl/", - "role": "Developer / IT Manager" - }, - { - "name": "Contributors", - "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" - } - ], - "description": "PHP_CodeSniffer Standards Composer Installer Plugin", - "homepage": "http://www.dealerdirect.com/", - "keywords": [ - "PHPCodeSniffer", - "PHP_CodeSniffer", - "code quality", - "codesniffer", - "composer", - "installer", - "phpcbf", - "phpcs", - "plugin", - "qa", - "quality", - "standard", - "standards", - "style guide", - "stylecheck", - "tests" - ], - "support": { - "issues": "https://github.com/PHPCSStandards/composer-installer/issues", - "source": "https://github.com/PHPCSStandards/composer-installer" - }, - "time": "2023-01-05T11:28:13+00:00" - }, - { - "name": "phpstan/phpdoc-parser", - "version": "1.20.4", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd", - "reference": "7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.5", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5", - "symfony/process": "^5.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHPDoc parser with support for nullable, intersection and generic types", - "support": { - "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.20.4" - }, - "time": "2023-05-02T09:19:37+00:00" - }, - { - "name": "slevomat/coding-standard", - "version": "8.12.0", - "source": { - "type": "git", - "url": "https://github.com/slevomat/coding-standard.git", - "reference": "cc04334ed0ce5a251389112fbd2dbe1dbc931ae8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/cc04334ed0ce5a251389112fbd2dbe1dbc931ae8", - "reference": "cc04334ed0ce5a251389112fbd2dbe1dbc931ae8", - "shasum": "" - }, - "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", - "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": ">=1.20.0 <1.21.0", - "squizlabs/php_codesniffer": "^3.7.1" - }, - "require-dev": { - "phing/phing": "2.17.4", - "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.10.15", - "phpstan/phpstan-deprecation-rules": "1.1.3", - "phpstan/phpstan-phpunit": "1.3.11", - "phpstan/phpstan-strict-rules": "1.5.1", - "phpunit/phpunit": "7.5.20|8.5.21|9.6.8|10.1.3" - }, - "type": "phpcodesniffer-standard", - "extra": { - "branch-alias": { - "dev-master": "8.x-dev" - } - }, - "autoload": { - "psr-4": { - "SlevomatCodingStandard\\": "SlevomatCodingStandard/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", - "keywords": [ - "dev", - "phpcs" - ], - "support": { - "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.12.0" - }, - "funding": [ - { - "url": "https://github.com/kukulich", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", - "type": "tidelift" - } - ], - "time": "2023-05-14T20:06:01+00:00" - }, - { - "name": "squizlabs/php_codesniffer", - "version": "3.7.2", - "source": { - "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", - "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", - "shasum": "" - }, - "require": { - "ext-simplexml": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": ">=5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" - }, - "bin": [ - "bin/phpcs", - "bin/phpcbf" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Greg Sherwood", - "role": "lead" - } - ], - "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", - "keywords": [ - "phpcs", - "standards", - "static analysis" - ], - "support": { - "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", - "source": "https://github.com/squizlabs/PHP_CodeSniffer", - "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" - }, - "time": "2023-02-22T23:07:41+00:00" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": [], - "platform-dev": [], - "plugin-api-version": "2.3.0" -} diff --git a/phpcs.xml b/phpcs.xml deleted file mode 100644 index 95032a6e..00000000 --- a/phpcs.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - src - tests - - - - - - - - - - - - - - - - - - - - - - - - - - - - 10 - - - - - - 10 - - - - - - - - 10 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - tests/*/data - From d8bdab0218c5eb0964338d24a8511b65e9c94fa5 Mon Sep 17 00:00:00 2001 From: Brad <28307684+mad-briller@users.noreply.github.com> Date: Fri, 26 May 2023 10:38:12 +0100 Subject: [PATCH 57/64] Microoptimize AssertRuleHelper. --- src/Rules/PHPUnit/AssertRuleHelper.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Rules/PHPUnit/AssertRuleHelper.php b/src/Rules/PHPUnit/AssertRuleHelper.php index 8c288b2c..3ad79c00 100644 --- a/src/Rules/PHPUnit/AssertRuleHelper.php +++ b/src/Rules/PHPUnit/AssertRuleHelper.php @@ -16,7 +16,6 @@ class AssertRuleHelper */ public static function isMethodOrStaticCallOnAssert(Node $node, Scope $scope): bool { - $testCaseType = new ObjectType('PHPUnit\Framework\Assert'); if ($node instanceof Node\Expr\MethodCall) { $calledOnType = $scope->getType($node->var); } elseif ($node instanceof Node\Expr\StaticCall) { @@ -45,11 +44,9 @@ public static function isMethodOrStaticCallOnAssert(Node $node, Scope $scope): b return false; } - if (!$testCaseType->isSuperTypeOf($calledOnType)->yes()) { - return false; - } + $testCaseType = new ObjectType('PHPUnit\Framework\Assert'); - return true; + return $testCaseType->isSuperTypeOf($calledOnType)->yes(); } } From d96b5a45ace45892e55927d104e5089e354040c1 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 28 May 2023 17:57:41 +0200 Subject: [PATCH 58/64] Modernize rules with RuleErrorBuilder --- src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php | 5 +++-- src/Rules/PHPUnit/AssertSameNullExpectedRule.php | 3 ++- src/Rules/PHPUnit/AssertSameWithCountRule.php | 5 +++-- src/Rules/PHPUnit/ClassMethodCoversExistsRule.php | 1 - src/Rules/PHPUnit/MockMethodCallRule.php | 12 +++++------- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php index 6a8eb7dd..b2860676 100644 --- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -7,6 +7,7 @@ use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; use function count; /** @@ -40,13 +41,13 @@ public function processNode(Node $node, Scope $scope): array if ($expectedArgumentValue->name->toLowerString() === 'true') { return [ - 'You should use assertTrue() instead of assertSame() when expecting "true"', + RuleErrorBuilder::message('You should use assertTrue() instead of assertSame() when expecting "true"')->build(), ]; } if ($expectedArgumentValue->name->toLowerString() === 'false') { return [ - 'You should use assertFalse() instead of assertSame() when expecting "false"', + RuleErrorBuilder::message('You should use assertFalse() instead of assertSame() when expecting "false"')->build(), ]; } diff --git a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php index 10302233..cf6fc76e 100644 --- a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php @@ -7,6 +7,7 @@ use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; use function count; /** @@ -40,7 +41,7 @@ public function processNode(Node $node, Scope $scope): array if ($expectedArgumentValue->name->toLowerString() === 'null') { return [ - 'You should use assertNull() instead of assertSame(null, $actual).', + RuleErrorBuilder::message('You should use assertNull() instead of assertSame(null, $actual).')->build(), ]; } diff --git a/src/Rules/PHPUnit/AssertSameWithCountRule.php b/src/Rules/PHPUnit/AssertSameWithCountRule.php index 876dd87b..209614ae 100644 --- a/src/Rules/PHPUnit/AssertSameWithCountRule.php +++ b/src/Rules/PHPUnit/AssertSameWithCountRule.php @@ -7,6 +7,7 @@ use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ObjectType; use function count; @@ -42,7 +43,7 @@ public function processNode(Node $node, Scope $scope): array && $right->name->toLowerString() === 'count' ) { return [ - 'You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, count($variable)).', + RuleErrorBuilder::message('You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, count($variable)).')->build(), ]; } @@ -56,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array if ((new ObjectType(Countable::class))->isSuperTypeOf($type)->yes()) { return [ - 'You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, $variable->count()).', + RuleErrorBuilder::message('You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, $variable->count()).')->build(), ]; } } diff --git a/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php b/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php index 95e6cc89..69693dee 100644 --- a/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php +++ b/src/Rules/PHPUnit/ClassMethodCoversExistsRule.php @@ -62,7 +62,6 @@ public function processNode(Node $node, Scope $scope): array return []; } - $errors = []; $classPhpDoc = $classReflection->getResolvedPhpDoc(); [$classCovers, $classCoversDefaultClasses] = $this->coversHelper->getCoverAnnotations($classPhpDoc); diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index da8a95d7..90dc0945 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -6,6 +6,7 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\Stub; @@ -28,9 +29,6 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - /** @var Node\Expr\MethodCall $node */ - $node = $node; - if (!$node->name instanceof Node\Identifier || $node->name->name !== 'method') { return []; } @@ -63,11 +61,11 @@ public function processNode(Node $node, Scope $scope): array continue; } - $errors[] = sprintf( + $errors[] = RuleErrorBuilder::message(sprintf( 'Trying to mock an undefined method %s() on class %s.', $method, implode('&', $mockClasses) - ); + ))->build(); continue; } @@ -81,11 +79,11 @@ public function processNode(Node $node, Scope $scope): array continue; } - $errors[] = sprintf( + $errors[] = RuleErrorBuilder::message(sprintf( 'Trying to mock an undefined method %s() on class %s.', $method, implode('|', $classNames) - ); + ))->build(); } return $errors; From abc6e126eaccb792c2a2b6bd11760cee0b574c18 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 5 Jun 2023 10:05:44 +0200 Subject: [PATCH 59/64] Update phpstan/phpstan-strict-rules --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 594629fc..ea9f5cdf 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "require-dev": { "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-strict-rules": "^1.0", + "phpstan/phpstan-strict-rules": "^1.5.1", "phpunit/phpunit": "^9.5" }, "config": { From f2118dbe168588aa4a7a300c84c822a0732c485f Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Fri, 12 May 2023 17:53:05 +0200 Subject: [PATCH 60/64] Improve support of assertArrayHasKey() --- .../Assert/AssertTypeSpecifyingExtensionHelper.php | 10 ++++++++-- tests/Type/PHPUnit/data/assert-function.php | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 2e8e828e..a7edf21e 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -262,8 +262,14 @@ private static function getExpressionResolvers(): array ] ); }, - 'ArrayHasKey' => static function (Scope $scope, Arg $key, Arg $array): FuncCall { - return new FuncCall(new Name('array_key_exists'), [$key, $array]); + 'ArrayHasKey' => static function (Scope $scope, Arg $key, Arg $array): Expr { + return new Expr\BinaryOp\BooleanOr( + new Expr\BinaryOp\BooleanAnd( + new Expr\Instanceof_($array->value, new Name('ArrayAccess')), + new Expr\MethodCall($array->value, 'offsetExists', [$key]) + ), + new FuncCall(new Name('array_key_exists'), [$key, $array]) + ); }, 'ObjectHasAttribute' => static function (Scope $scope, Arg $property, Arg $object): FuncCall { return new FuncCall(new Name('property_exists'), [$object, $property]); diff --git a/tests/Type/PHPUnit/data/assert-function.php b/tests/Type/PHPUnit/data/assert-function.php index f3222f34..9af1d264 100644 --- a/tests/Type/PHPUnit/data/assert-function.php +++ b/tests/Type/PHPUnit/data/assert-function.php @@ -31,15 +31,21 @@ public function assertInstanceOfWorksWithTemplate($o, $class): void assertType(\DateTimeInterface::class, $o); } - public function arrayHasNumericKey(array $a): void { + public function arrayHasNumericKey(array $a, \ArrayAccess $b): void { assertArrayHasKey(0, $a); assertType('array&hasOffset(0)', $a); + + assertArrayHasKey(0, $b); + assertType('ArrayAccess', $b); } - public function arrayHasStringKey(array $a): void + public function arrayHasStringKey(array $a, \ArrayAccess $b): void { assertArrayHasKey('key', $a); assertType("array&hasOffset('key')", $a); + + assertArrayHasKey('key', $b); + assertType("ArrayAccess", $b); } public function objectHasAttribute(object $a): void From 2742e1c0d6924ff03fc640954ee4d25143d90699 Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Tue, 2 May 2023 14:30:28 +0200 Subject: [PATCH 61/64] Add partial support for assertContainsOnlyInstancesOf --- .../AssertTypeSpecifyingExtensionHelper.php | 24 +++++++++++++++++++ tests/Type/PHPUnit/data/assert-function.php | 10 ++++++++ 2 files changed, 34 insertions(+) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index a7edf21e..1507a135 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -13,7 +13,9 @@ use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Instanceof_; use PhpParser\Node\Name; +use PhpParser\Node\Param; use PhpParser\Node\Scalar\LNumber; +use PhpParser\Node\Stmt; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; @@ -274,6 +276,28 @@ private static function getExpressionResolvers(): array 'ObjectHasAttribute' => static function (Scope $scope, Arg $property, Arg $object): FuncCall { return new FuncCall(new Name('property_exists'), [$object, $property]); }, + 'ContainsOnlyInstancesOf' => static function (Scope $scope, Arg $className, Arg $haystack): Expr { + return new Expr\BinaryOp\BooleanOr( + new Expr\Instanceof_($haystack->value, new Name('Traversable')), + new Identical( + $haystack->value, + new FuncCall(new Name('array_filter'), [ + $haystack, + new Arg(new Expr\Closure([ + 'static' => true, + 'params' => [ + new Param(new Expr\Variable('_')), + ], + 'stmts' => [ + new Stmt\Return_( + new FuncCall(new Name('is_a'), [new Arg(new Expr\Variable('_')), $className]) + ), + ], + ])), + ]) + ) + ); + }, ]; } diff --git a/tests/Type/PHPUnit/data/assert-function.php b/tests/Type/PHPUnit/data/assert-function.php index 9af1d264..84102c97 100644 --- a/tests/Type/PHPUnit/data/assert-function.php +++ b/tests/Type/PHPUnit/data/assert-function.php @@ -4,6 +4,7 @@ use function PHPStan\Testing\assertType; use function PHPUnit\Framework\assertArrayHasKey; +use function PHPUnit\Framework\assertContainsOnlyInstancesOf; use function PHPUnit\Framework\assertEmpty; use function PHPUnit\Framework\assertInstanceOf; use function PHPUnit\Framework\assertObjectHasAttribute; @@ -60,4 +61,13 @@ public function testEmpty($a): void assertType("0|0.0|''|'0'|array{}|Countable|EmptyIterator|false|null", $a); } + public function containsOnlyInstancesOf(array $a, \Traversable $b): void + { + assertContainsOnlyInstancesOf(\stdClass::class, $a); + assertType('array', $a); + + assertContainsOnlyInstancesOf(\stdClass::class, $b); + assertType('Traversable', $b); + } + } From e468b76c2b89ce1611e2fc5ad3f6420dd49ddfbf Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Fri, 21 Jul 2023 15:03:05 +0200 Subject: [PATCH 62/64] Add support for assertContains and assertContainsEquals --- .../AssertTypeSpecifyingExtensionHelper.php | 15 ++++++++++++++ tests/Type/PHPUnit/data/assert-function.php | 20 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 1507a135..34d86ca1 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -276,6 +276,21 @@ private static function getExpressionResolvers(): array 'ObjectHasAttribute' => static function (Scope $scope, Arg $property, Arg $object): FuncCall { return new FuncCall(new Name('property_exists'), [$object, $property]); }, + 'Contains' => static function (Scope $scope, Arg $needle, Arg $haystack): Expr { + return new Expr\BinaryOp\BooleanOr( + new Expr\Instanceof_($haystack->value, new Name('Traversable')), + new FuncCall(new Name('in_array'), [$needle, $haystack, new Arg(new ConstFetch(new Name('true')))]) + ); + }, + 'ContainsEquals' => static function (Scope $scope, Arg $needle, Arg $haystack): Expr { + return new Expr\BinaryOp\BooleanOr( + new Expr\Instanceof_($haystack->value, new Name('Traversable')), + new Expr\BinaryOp\BooleanAnd( + new Expr\BooleanNot(new Expr\Empty_($haystack->value)), + new FuncCall(new Name('in_array'), [$needle, $haystack, new Arg(new ConstFetch(new Name('false')))]) + ) + ); + }, 'ContainsOnlyInstancesOf' => static function (Scope $scope, Arg $className, Arg $haystack): Expr { return new Expr\BinaryOp\BooleanOr( new Expr\Instanceof_($haystack->value, new Name('Traversable')), diff --git a/tests/Type/PHPUnit/data/assert-function.php b/tests/Type/PHPUnit/data/assert-function.php index 84102c97..15c4371a 100644 --- a/tests/Type/PHPUnit/data/assert-function.php +++ b/tests/Type/PHPUnit/data/assert-function.php @@ -4,6 +4,8 @@ use function PHPStan\Testing\assertType; use function PHPUnit\Framework\assertArrayHasKey; +use function PHPUnit\Framework\assertContains; +use function PHPUnit\Framework\assertContainsEquals; use function PHPUnit\Framework\assertContainsOnlyInstancesOf; use function PHPUnit\Framework\assertEmpty; use function PHPUnit\Framework\assertInstanceOf; @@ -61,6 +63,24 @@ public function testEmpty($a): void assertType("0|0.0|''|'0'|array{}|Countable|EmptyIterator|false|null", $a); } + public function contains(array $a, \Traversable $b): void + { + assertContains('foo', $a); + assertType('non-empty-array', $a); + + assertContains('foo', $b); + assertType('Traversable', $b); + } + + public function containsEquals(array $a, \Traversable $b): void + { + assertContainsEquals('foo', $a); + assertType('non-empty-array', $a); + + assertContainsEquals('foo', $b); + assertType('Traversable', $b); + } + public function containsOnlyInstancesOf(array $a, \Traversable $b): void { assertContainsOnlyInstancesOf(\stdClass::class, $a); From 6536e6659038f49d37bf8c550098138edc38eccc Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 25 Aug 2023 11:41:51 +0200 Subject: [PATCH 63/64] Open 1.3.x-dev --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index afe7e6ed..bc327e8c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "1.1.x" + - "1.3.x" jobs: lint: From 614acc10c522e319639bf38b0698a4a566665f04 Mon Sep 17 00:00:00 2001 From: Greg Korba Date: Fri, 25 Aug 2023 11:46:39 +0200 Subject: [PATCH 64/64] Add support for `assertObjectHasProperty` --- .../AssertTypeSpecifyingExtensionHelper.php | 3 +++ ...ssertFunctionTypeSpecifyingExtensionTest.php | 10 +++++++--- .../PHPUnit/data/assert-function-9.6.11.php | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 tests/Type/PHPUnit/data/assert-function-9.6.11.php diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 34d86ca1..963b4f57 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -276,6 +276,9 @@ private static function getExpressionResolvers(): array 'ObjectHasAttribute' => static function (Scope $scope, Arg $property, Arg $object): FuncCall { return new FuncCall(new Name('property_exists'), [$object, $property]); }, + 'ObjectHasProperty' => static function (Scope $scope, Arg $property, Arg $object): FuncCall { + return new FuncCall(new Name('property_exists'), [$object, $property]); + }, 'Contains' => static function (Scope $scope, Arg $needle, Arg $haystack): Expr { return new Expr\BinaryOp\BooleanOr( new Expr\Instanceof_($haystack->value, new Name('Traversable')), diff --git a/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php b/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php index f2a254d7..2d43f8bc 100644 --- a/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php +++ b/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php @@ -11,11 +11,15 @@ class AssertFunctionTypeSpecifyingExtensionTest extends TypeInferenceTestCase /** @return mixed[] */ public function dataFileAsserts(): iterable { - if (!function_exists('PHPUnit\\Framework\\assertInstanceOf')) { - return []; + if (function_exists('PHPUnit\\Framework\\assertInstanceOf')) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-function.php'); } - yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-function.php'); + if (function_exists('PHPUnit\\Framework\\assertObjectHasProperty')) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-function-9.6.11.php'); + } + + return []; } /** diff --git a/tests/Type/PHPUnit/data/assert-function-9.6.11.php b/tests/Type/PHPUnit/data/assert-function-9.6.11.php new file mode 100644 index 00000000..dea6b851 --- /dev/null +++ b/tests/Type/PHPUnit/data/assert-function-9.6.11.php @@ -0,0 +1,17 @@ +