From 320403ca77cf18768dc3afda2c9230de44e0dbbc Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Thu, 16 Dec 2021 19:32:44 +0100 Subject: [PATCH 01/69] Composer > Remove `--no-suggest` You are using the deprecated option "--no-suggest". It has no effect and will break in Composer 3. --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 59b79ed..85d9184 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,7 +37,7 @@ jobs: run: "composer validate" - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + run: "composer install --no-interaction --no-progress" - name: "Downgrade PHPUnit" if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' @@ -65,7 +65,7 @@ jobs: run: "composer validate" - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + run: "composer install --no-interaction --no-progress" - name: "Lint" run: "make lint" @@ -103,11 +103,11 @@ jobs: - name: "Install lowest dependencies" if: ${{ matrix.dependencies == 'lowest' }} - run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest" + run: "composer update --prefer-lowest --no-interaction --no-progress" - name: "Install highest dependencies" if: ${{ matrix.dependencies == 'highest' }} - run: "composer update --no-interaction --no-progress --no-suggest" + run: "composer update --no-interaction --no-progress" - name: "Downgrade PHPUnit" if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' @@ -148,11 +148,11 @@ jobs: - name: "Install lowest dependencies" if: ${{ matrix.dependencies == 'lowest' }} - run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest" + run: "composer update --prefer-lowest --no-interaction --no-progress" - name: "Install highest dependencies" if: ${{ matrix.dependencies == 'highest' }} - run: "composer update --no-interaction --no-progress --no-suggest" + run: "composer update --no-interaction --no-progress" - name: "Downgrade PHPUnit" if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' From 590a98b0658476594d5b4ec5c5bc60ce2963e8be Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Thu, 6 Jan 2022 13:41:47 +0100 Subject: [PATCH 02/69] Allow Composer plugins --- build-cs/composer.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build-cs/composer.json b/build-cs/composer.json index ed7744e..cc6a498 100644 --- a/build-cs/composer.json +++ b/build-cs/composer.json @@ -3,5 +3,10 @@ "consistence-community/coding-standard": "^3.10", "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", "slevomat/coding-standard": "^6.4" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } } } From c9ab28bb6537163f5c4dc1ec6fe791977d321707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Sun, 16 Jan 2022 09:40:34 +0100 Subject: [PATCH 03/69] Tweet release action --- .github/workflows/release-tweet.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/release-tweet.yml diff --git a/.github/workflows/release-tweet.yml b/.github/workflows/release-tweet.yml new file mode 100644 index 0000000..09b39de --- /dev/null +++ b/.github/workflows/release-tweet.yml @@ -0,0 +1,24 @@ +name: Tweet release + +# More triggers +# https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#release +on: + release: + types: [published] + +jobs: + tweet: + runs-on: ubuntu-latest + steps: + - uses: Eomm/why-don-t-you-tweet@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 + tweet-message: "New release: ${{ github.event.repository.name }} ${{ github.event.release.tag_name }} ${{ github.event.release.html_url }} #phpstan" + env: + # Get your tokens from https://developer.twitter.com/apps + TWITTER_CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }} + TWITTER_CONSUMER_API_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET }} + TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} + TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} From 59253fc2d4fecb246cf0657d9031a1d1ff1652e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Sun, 16 Jan 2022 11:53:50 +0100 Subject: [PATCH 04/69] Update release.yml --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 225470a..5ed1176 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,13 +20,13 @@ jobs: id: changelog uses: metcalfc/changelog-generator@v1.0.0 with: - myToken: ${{ secrets.GITHUB_TOKEN }} + myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} - name: "Create release" id: create-release uses: actions/create-release@v1 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} From bb6e2e2d1d7f193a7d87a35d9701f43bfb0cfbde Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 30 Jan 2022 18:04:43 +0100 Subject: [PATCH 05/69] Update phpunit.xml --- phpunit.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml b/phpunit.xml index 8d53d3f..8f71615 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,7 +10,7 @@ beStrictAboutTodoAnnotatedTests="true" failOnRisky="true" failOnWarning="true" - xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" + xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xml" > From fda8ef057ad5ec671bba3213c4eb95b0d3677482 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 1 Feb 2022 21:48:31 +0000 Subject: [PATCH 06/69] Add renovate.json --- renovate.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..f45d8f1 --- /dev/null +++ b/renovate.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "config:base" + ] +} From f59dda742b9a98e985de46394b9057de179c7b15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Tue, 1 Feb 2022 23:15:27 +0100 Subject: [PATCH 07/69] Update and rename renovate.json to .github/renovate.json --- .github/renovate.json | 23 +++++++++++++++++++++++ renovate.json | 5 ----- 2 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 .github/renovate.json delete mode 100644 renovate.json diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..597ad92 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,23 @@ +{ + "extends": [ + "config:base" + ], + "rangeStrategy": "update-lockfile", + "packageRules": [ + { + "matchPaths": ["+(composer.json)"], + "enabled": true, + "groupName": "root-composer" + }, + { + "matchPaths": ["build-cs/**"], + "enabled": true, + "groupName": "build-cs" + }, + { + "matchPaths": [".github/**"], + "enabled": true, + "groupName": "github-actions" + } + ] +} diff --git a/renovate.json b/renovate.json deleted file mode 100644 index f45d8f1..0000000 --- a/renovate.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": [ - "config:base" - ] -} From 57e52d6d81a56591776e4b5cdb4180b961837180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Tue, 1 Feb 2022 23:16:03 +0100 Subject: [PATCH 08/69] Delete dependabot.yml --- .github/dependabot.yml | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 15b7733..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: 2 -updates: - - package-ecosystem: composer - directory: "/" - schedule: - interval: monthly - open-pull-requests-limit: 10 - - package-ecosystem: composer - directory: "/build-cs" - schedule: - interval: monthly - open-pull-requests-limit: 10 - - package-ecosystem: github-actions - directory: "/" - schedule: - interval: monthly - open-pull-requests-limit: 10 From ba2d5fadf783fd7cb587df86baac91c71217705c Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 1 Feb 2022 22:15:48 +0000 Subject: [PATCH 09/69] Update metcalfc/changelog-generator action to v1.0.1 --- .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 5ed1176..24a1d30 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@v1.0.0 + uses: metcalfc/changelog-generator@v1.0.1 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From e0dcd5ffa833f8ce19679ba976879283bebc49d8 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 2 Feb 2022 00:25:03 +0000 Subject: [PATCH 10/69] Update github-actions --- .github/workflows/lock-closed-issues.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml index 960c1ba..9ea5327 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@v2 + - uses: dessant/lock-threads@v3 with: github-token: ${{ github.token }} issue-lock-inactive-days: '31' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 24a1d30..0ebed84 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@v1.0.1 + uses: metcalfc/changelog-generator@v3.0.0 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} From 576db1b23b4a00e53a21be028d3cf67d01365ec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Wed, 2 Feb 2022 15:39:40 +0100 Subject: [PATCH 11/69] Update lock-closed-issues.yml --- .github/workflows/lock-closed-issues.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml index 9ea5327..a05d417 100644 --- a/.github/workflows/lock-closed-issues.yml +++ b/.github/workflows/lock-closed-issues.yml @@ -11,11 +11,11 @@ jobs: - uses: dessant/lock-threads@v3 with: github-token: ${{ github.token }} - issue-lock-inactive-days: '31' - issue-exclude-created-before: '' - issue-exclude-labels: '' - issue-lock-labels: '' - issue-lock-comment: > + issue-inactive-days: '31' + exclude-issue-created-before: '' + exclude-any-issue-labels: '' + add-issue-labels: '' + issue-comment: > This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. From da62e3c176751378feeb87c2b10182068ee8b276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Wed, 2 Feb 2022 16:38:02 +0100 Subject: [PATCH 12/69] Update renovate.json --- .github/renovate.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index 597ad92..b775cc1 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,6 +1,7 @@ { "extends": [ - "config:base" + "config:base", + "schedule:weekly" ], "rangeStrategy": "update-lockfile", "packageRules": [ From 15dc3e0d81b711c38d02fb33fe4482dfa570951c Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 1 Feb 2022 22:15:54 +0000 Subject: [PATCH 13/69] Update dependency slevomat/coding-standard to v7 --- .gitignore | 2 +- build-cs/.gitignore | 1 - build-cs/composer.json | 2 +- build-cs/composer.lock | 327 ++++++++++++++++++ phpcs.xml | 98 ++++-- .../MockObjectTypeNodeResolverExtension.php | 7 +- src/Rules/PHPUnit/AssertRuleHelper.php | 2 + .../PHPUnit/AssertSameBooleanExpectedRule.php | 14 +- .../PHPUnit/AssertSameNullExpectedRule.php | 14 +- src/Rules/PHPUnit/AssertSameWithCountRule.php | 17 +- src/Rules/PHPUnit/MockMethodCallRule.php | 15 +- .../PHPUnit/ShouldCallParentMethodsRule.php | 10 +- .../AssertFunctionTypeSpecifyingExtension.php | 3 + .../AssertTypeSpecifyingExtensionHelper.php | 121 +++---- ...cationMockerDynamicReturnTypeExtension.php | 3 +- .../MockBuilderDynamicReturnTypeExtension.php | 4 +- .../MockObjectDynamicReturnTypeExtension.php | 8 +- .../AssertSameBooleanExpectedRuleTest.php | 5 +- ...AssertSameMethodDifferentTypesRuleTest.php | 5 +- .../AssertSameNullExpectedRuleTest.php | 5 +- ...SameStaticMethodDifferentTypesRuleTest.php | 5 +- .../PHPUnit/AssertSameWithCountRuleTest.php | 5 +- .../Rules/PHPUnit/MockMethodCallRuleTest.php | 5 +- .../ShouldCallParentMethodsRuleTest.php | 5 +- ...ertFunctionTypeSpecifyingExtensionTest.php | 3 +- ...ssertMethodTypeSpecifyingExtensionTest.php | 2 - 26 files changed, 554 insertions(+), 134 deletions(-) create mode 100644 build-cs/composer.lock diff --git a/.gitignore b/.gitignore index d6a83e5..2db2131 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /tests/tmp /vendor -composer.lock +/composer.lock .phpunit.result.cache diff --git a/build-cs/.gitignore b/build-cs/.gitignore index ff72e2d..61ead86 100644 --- a/build-cs/.gitignore +++ b/build-cs/.gitignore @@ -1,2 +1 @@ -/composer.lock /vendor diff --git a/build-cs/composer.json b/build-cs/composer.json index cc6a498..e307971 100644 --- a/build-cs/composer.json +++ b/build-cs/composer.json @@ -2,7 +2,7 @@ "require-dev": { "consistence-community/coding-standard": "^3.10", "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "slevomat/coding-standard": "^6.4" + "slevomat/coding-standard": "^7.0" }, "config": { "allow-plugins": { diff --git a/build-cs/composer.lock b/build-cs/composer.lock new file mode 100644 index 0000000..70af78f --- /dev/null +++ b/build-cs/composer.lock @@ -0,0 +1,327 @@ +{ + "_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": "4485bbedba7bcc71ace5f69dbb9b6c47", + "packages": [], + "packages-dev": [ + { + "name": "consistence-community/coding-standard", + "version": "3.11.1", + "source": { + "type": "git", + "url": "https://github.com/consistence-community/coding-standard.git", + "reference": "4632fead8c9ee8f50044fcbce9f66c797b34c0df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/4632fead8c9ee8f50044fcbce9f66c797b34c0df", + "reference": "4632fead8c9ee8f50044fcbce9f66c797b34c0df", + "shasum": "" + }, + "require": { + "php": ">=7.4", + "slevomat/coding-standard": "~7.0", + "squizlabs/php_codesniffer": "~3.6.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" + }, + "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.1" + }, + "time": "2021-05-03T18:13:22+00:00" + }, + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v0.7.2", + "source": { + "type": "git", + "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", + "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", + "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "*", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpcompatibility/php-compatibility": "^9.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "Dealerdirect\\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/Dealerdirect/phpcodesniffer-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/dealerdirect/phpcodesniffer-composer-installer/issues", + "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + }, + "time": "2022-02-04T12:51:07+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "dbc093d7af60eff5cd575d2ed761b15ed40bd08e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/dbc093d7af60eff5cd575d2ed761b15ed40bd08e", + "reference": "dbc093d7af60eff5cd575d2ed761b15ed40bd08e", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "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.2.0" + }, + "time": "2021-09-16T20:46:02+00:00" + }, + { + "name": "slevomat/coding-standard", + "version": "7.0.18", + "source": { + "type": "git", + "url": "https://github.com/slevomat/coding-standard.git", + "reference": "b81ac84f41a4797dc25c8ede1b0718e2a74be0fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/b81ac84f41a4797dc25c8ede1b0718e2a74be0fc", + "reference": "b81ac84f41a4797dc25c8ede1b0718e2a74be0fc", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", + "php": "^7.1 || ^8.0", + "phpstan/phpdoc-parser": "^1.0.0", + "squizlabs/php_codesniffer": "^3.6.1" + }, + "require-dev": { + "phing/phing": "2.17.0", + "php-parallel-lint/php-parallel-lint": "1.3.1", + "phpstan/phpstan": "1.2.0", + "phpstan/phpstan-deprecation-rules": "1.0.0", + "phpstan/phpstan-phpunit": "1.0.0", + "phpstan/phpstan-strict-rules": "1.1.0", + "phpunit/phpunit": "7.5.20|8.5.21|9.5.10" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-master": "7.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.", + "support": { + "issues": "https://github.com/slevomat/coding-standard/issues", + "source": "https://github.com/slevomat/coding-standard/tree/7.0.18" + }, + "funding": [ + { + "url": "https://github.com/kukulich", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", + "type": "tidelift" + } + ], + "time": "2021-12-07T17:19:06+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.6.2", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a", + "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a", + "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" + ], + "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" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.2.0" +} diff --git a/phpcs.xml b/phpcs.xml index ce6e811..95032a6 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,5 +1,6 @@ - + + @@ -8,58 +9,103 @@ src tests + - - + + - + + + + + - + - - - - - + + + + + 10 - - - + + 10 + - - - + + + + + 10 + + + - - + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - tests/tmp + + tests/*/data diff --git a/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php b/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php index 955b818..7225d6d 100644 --- a/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php +++ b/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php @@ -2,6 +2,7 @@ namespace PHPStan\PhpDoc\PHPUnit; +use PHPStan\Analyser\NameScope; use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\PhpDoc\TypeNodeResolverAwareExtension; use PHPStan\PhpDoc\TypeNodeResolverExtension; @@ -9,7 +10,9 @@ use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use PHPStan\Type\NeverType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeWithClassName; +use function array_key_exists; class MockObjectTypeNodeResolverExtension implements TypeNodeResolverExtension, TypeNodeResolverAwareExtension { @@ -27,7 +30,7 @@ public function getCacheKey(): string return 'phpunit-v1'; } - public function resolve(TypeNode $typeNode, \PHPStan\Analyser\NameScope $nameScope): ?Type + public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type { if (!$typeNode instanceof UnionTypeNode) { return null; @@ -45,7 +48,7 @@ public function resolve(TypeNode $typeNode, \PHPStan\Analyser\NameScope $nameSco } if (array_key_exists($type->getClassName(), $mockClassNames)) { - $resultType = \PHPStan\Type\TypeCombinator::intersect(...$types); + $resultType = TypeCombinator::intersect(...$types); if ($resultType instanceof NeverType) { continue; } diff --git a/src/Rules/PHPUnit/AssertRuleHelper.php b/src/Rules/PHPUnit/AssertRuleHelper.php index e8edc50..9673537 100644 --- a/src/Rules/PHPUnit/AssertRuleHelper.php +++ b/src/Rules/PHPUnit/AssertRuleHelper.php @@ -5,6 +5,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Type\ObjectType; +use function in_array; +use function strtolower; class AssertRuleHelper { diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php index 9712185..6be390a 100644 --- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -3,18 +3,24 @@ namespace PHPStan\Rules\PHPUnit; use PhpParser\Node; +use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\StaticCall; +use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Type\Constant\ConstantBooleanType; +use function count; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\NodeAbstract> + * @implements Rule */ -class AssertSameBooleanExpectedRule implements \PHPStan\Rules\Rule +class AssertSameBooleanExpectedRule implements Rule { public function getNodeType(): string { - return \PhpParser\NodeAbstract::class; + return NodeAbstract::class; } public function processNode(Node $node, Scope $scope): array @@ -23,7 +29,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** @var \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node */ + /** @var MethodCall|StaticCall $node */ $node = $node; if (count($node->getArgs()) < 2) { diff --git a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php index 9337cc8..8d2b2aa 100644 --- a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php @@ -3,18 +3,24 @@ namespace PHPStan\Rules\PHPUnit; use PhpParser\Node; +use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\StaticCall; +use PhpParser\NodeAbstract; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Type\NullType; +use function count; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\NodeAbstract> + * @implements Rule */ -class AssertSameNullExpectedRule implements \PHPStan\Rules\Rule +class AssertSameNullExpectedRule implements Rule { public function getNodeType(): string { - return \PhpParser\NodeAbstract::class; + return NodeAbstract::class; } public function processNode(Node $node, Scope $scope): array @@ -23,7 +29,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** @var \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node */ + /** @var MethodCall|StaticCall $node */ $node = $node; if (count($node->getArgs()) < 2) { diff --git a/src/Rules/PHPUnit/AssertSameWithCountRule.php b/src/Rules/PHPUnit/AssertSameWithCountRule.php index 3777b3f..1f1a3ab 100644 --- a/src/Rules/PHPUnit/AssertSameWithCountRule.php +++ b/src/Rules/PHPUnit/AssertSameWithCountRule.php @@ -2,19 +2,26 @@ namespace PHPStan\Rules\PHPUnit; +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; use PHPStan\Type\ObjectType; +use function count; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\NodeAbstract> + * @implements Rule */ -class AssertSameWithCountRule implements \PHPStan\Rules\Rule +class AssertSameWithCountRule implements Rule { public function getNodeType(): string { - return \PhpParser\NodeAbstract::class; + return NodeAbstract::class; } public function processNode(Node $node, Scope $scope): array @@ -23,7 +30,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** @var \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node */ + /** @var MethodCall|StaticCall $node */ $node = $node; if (count($node->getArgs()) < 2) { @@ -53,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array ) { $type = $scope->getType($right->var); - if ((new ObjectType(\Countable::class))->isSuperTypeOf($type)->yes()) { + if ((new ObjectType(Countable::class))->isSuperTypeOf($type)->yes()) { return [ 'You should use assertCount($expectedCount, $variable) instead of assertSame($expectedCount, $variable->count()).', ]; diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index d7551dc..0007aa9 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -3,18 +3,25 @@ namespace PHPStan\Rules\PHPUnit; use PhpParser\Node; +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; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; use PHPUnit\Framework\MockObject\MockObject; +use function array_filter; +use function count; +use function implode; +use function in_array; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\MethodCall> + * @implements Rule */ -class MockMethodCallRule implements \PHPStan\Rules\Rule +class MockMethodCallRule implements Rule { public function getNodeType(): string @@ -48,7 +55,7 @@ public function processNode(Node $node, Scope $scope): array && in_array(MockObject::class, $type->getReferencedClasses(), true) && !$type->hasMethod($method)->yes() ) { - $mockClass = array_filter($type->getReferencedClasses(), function (string $class): bool { + $mockClass = array_filter($type->getReferencedClasses(), static function (string $class): bool { return $class !== MockObject::class; }); @@ -56,7 +63,7 @@ public function processNode(Node $node, Scope $scope): array sprintf( 'Trying to mock an undefined method %s() on class %s.', $method, - \implode('&', $mockClass) + implode('&', $mockClass) ), ]; } diff --git a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php index de8a778..0141406 100644 --- a/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php +++ b/src/Rules/PHPUnit/ShouldCallParentMethodsRule.php @@ -5,13 +5,17 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\InClassMethodNode; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPUnit\Framework\TestCase; +use function in_array; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class ShouldCallParentMethodsRule implements \PHPStan\Rules\Rule +class ShouldCallParentMethodsRule implements Rule { public function getNodeType(): string @@ -62,9 +66,7 @@ public function processNode(Node $node, Scope $scope): array /** * @param Node\Stmt[]|null $stmts - * @param string $methodName * - * @return bool */ private function hasParentClassCall(?array $stmts, string $methodName): bool { diff --git a/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php b/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php index e20e13d..31805a3 100644 --- a/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php +++ b/src/Type/PHPUnit/Assert/AssertFunctionTypeSpecifyingExtension.php @@ -10,6 +10,9 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\FunctionTypeSpecifyingExtension; +use function strlen; +use function strpos; +use function substr; class AssertFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { diff --git a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php index 2cd354c..36203e6 100644 --- a/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php +++ b/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php @@ -2,8 +2,12 @@ namespace PHPStan\Type\PHPUnit\Assert; +use Closure; use PhpParser\Node\Arg; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\Identical; +use PhpParser\Node\Expr\BooleanNot; +use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Instanceof_; use PhpParser\Node\Name; @@ -12,17 +16,21 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Type\Constant\ConstantStringType; +use ReflectionObject; +use function array_key_exists; +use function count; +use function strlen; +use function strpos; +use function substr; class AssertTypeSpecifyingExtensionHelper { - /** @var \Closure[] */ + /** @var Closure[] */ private static $resolvers; /** - * @param string $name - * @param \PhpParser\Node\Arg[] $args - * @return bool + * @param Arg[] $args */ public static function isSupported( string $name, @@ -37,9 +45,9 @@ public static function isSupported( } $resolver = $resolvers[$trimmedName]; - $resolverReflection = new \ReflectionObject($resolver); + $resolverReflection = new ReflectionObject($resolver); - return count($args) >= (count($resolverReflection->getMethod('__invoke')->getParameters()) - 1); + return count($args) >= count($resolverReflection->getMethod('__invoke')->getParameters()) - 1; } private static function trimName(string $name): string @@ -62,11 +70,7 @@ private static function trimName(string $name): string } /** - * @param TypeSpecifier $typeSpecifier - * @param Scope $scope - * @param string $name - * @param \PhpParser\Node\Arg[] $args $args - * @return SpecifiedTypes + * @param Arg[] $args $args */ public static function specifyTypes( TypeSpecifier $typeSpecifier, @@ -87,16 +91,13 @@ public static function specifyTypes( } /** - * @param Scope $scope - * @param string $name - * @param \PhpParser\Node\Arg[] $args - * @return \PhpParser\Node\Expr|null + * @param Arg[] $args */ private static function createExpression( Scope $scope, string $name, array $args - ): ?\PhpParser\Node\Expr + ): ?Expr { $trimmedName = self::trimName($name); $resolvers = self::getExpressionResolvers(); @@ -107,88 +108,88 @@ private static function createExpression( } if (strpos($name, 'Not') !== false) { - $expression = new \PhpParser\Node\Expr\BooleanNot($expression); + $expression = new BooleanNot($expression); } return $expression; } /** - * @return \Closure[] + * @return Closure[] */ private static function getExpressionResolvers(): array { if (self::$resolvers === null) { self::$resolvers = [ - 'InstanceOf' => function (Scope $scope, Arg $class, Arg $object): ?Instanceof_ { + 'InstanceOf' => static function (Scope $scope, Arg $class, Arg $object): ?Instanceof_ { $classType = $scope->getType($class->value); if (!$classType instanceof ConstantStringType) { return null; } - return new \PhpParser\Node\Expr\Instanceof_( + return new Instanceof_( $object->value, - new \PhpParser\Node\Name($classType->getValue()) + new Name($classType->getValue()) ); }, - 'Same' => function (Scope $scope, Arg $expected, Arg $actual): Identical { - return new \PhpParser\Node\Expr\BinaryOp\Identical( + 'Same' => static function (Scope $scope, Arg $expected, Arg $actual): Identical { + return new Identical( $expected->value, $actual->value ); }, - 'True' => function (Scope $scope, Arg $actual): Identical { - return new \PhpParser\Node\Expr\BinaryOp\Identical( + 'True' => static function (Scope $scope, Arg $actual): Identical { + return new Identical( $actual->value, - new \PhpParser\Node\Expr\ConstFetch(new Name('true')) + new ConstFetch(new Name('true')) ); }, - 'False' => function (Scope $scope, Arg $actual): Identical { - return new \PhpParser\Node\Expr\BinaryOp\Identical( + 'False' => static function (Scope $scope, Arg $actual): Identical { + return new Identical( $actual->value, - new \PhpParser\Node\Expr\ConstFetch(new Name('false')) + new ConstFetch(new Name('false')) ); }, - 'Null' => function (Scope $scope, Arg $actual): Identical { - return new \PhpParser\Node\Expr\BinaryOp\Identical( + 'Null' => static function (Scope $scope, Arg $actual): Identical { + return new Identical( $actual->value, - new \PhpParser\Node\Expr\ConstFetch(new Name('null')) + new ConstFetch(new Name('null')) ); }, - 'IsArray' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_array'), [$actual]); + 'IsArray' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_array'), [$actual]); }, - 'IsBool' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_bool'), [$actual]); + 'IsBool' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_bool'), [$actual]); }, - 'IsCallable' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_callable'), [$actual]); + 'IsCallable' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_callable'), [$actual]); }, - 'IsFloat' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_float'), [$actual]); + 'IsFloat' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_float'), [$actual]); }, - 'IsInt' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_int'), [$actual]); + 'IsInt' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_int'), [$actual]); }, - 'IsIterable' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_iterable'), [$actual]); + 'IsIterable' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_iterable'), [$actual]); }, - 'IsNumeric' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_numeric'), [$actual]); + 'IsNumeric' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_numeric'), [$actual]); }, - 'IsObject' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_object'), [$actual]); + 'IsObject' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_object'), [$actual]); }, - 'IsResource' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_resource'), [$actual]); + 'IsResource' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_resource'), [$actual]); }, - 'IsString' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_string'), [$actual]); + 'IsString' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_string'), [$actual]); }, - 'IsScalar' => function (Scope $scope, Arg $actual): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('is_scalar'), [$actual]); + 'IsScalar' => static function (Scope $scope, Arg $actual): FuncCall { + return new FuncCall(new Name('is_scalar'), [$actual]); }, - 'InternalType' => function (Scope $scope, Arg $type, Arg $value): ?FuncCall { + 'InternalType' => static function (Scope $scope, Arg $type, Arg $value): ?FuncCall { $typeType = $scope->getType($type->value); if (!$typeType instanceof ConstantStringType) { return null; @@ -245,18 +246,18 @@ private static function getExpressionResolvers(): array return null; } - return new \PhpParser\Node\Expr\FuncCall( + return new FuncCall( new Name($functionName), [ $value, ] ); }, - 'ArrayHasKey' => function (Scope $scope, Arg $key, Arg $array): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('array_key_exists'), [$key, $array]); + 'ArrayHasKey' => static function (Scope $scope, Arg $key, Arg $array): FuncCall { + return new FuncCall(new Name('array_key_exists'), [$key, $array]); }, - 'ObjectHasAttribute' => function (Scope $scope, Arg $property, Arg $object): FuncCall { - return new \PhpParser\Node\Expr\FuncCall(new Name('property_exists'), [$object, $property]); + 'ObjectHasAttribute' => static function (Scope $scope, Arg $property, Arg $object): FuncCall { + return new FuncCall(new Name('property_exists'), [$object, $property]); }, ]; } diff --git a/src/Type/PHPUnit/InvocationMockerDynamicReturnTypeExtension.php b/src/Type/PHPUnit/InvocationMockerDynamicReturnTypeExtension.php index 51e2222..44764f6 100644 --- a/src/Type/PHPUnit/InvocationMockerDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/InvocationMockerDynamicReturnTypeExtension.php @@ -5,10 +5,11 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; +use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Type; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; -class InvocationMockerDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension +class InvocationMockerDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { public function getClass(): string diff --git a/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php b/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php index d6ae40d..5390b11 100644 --- a/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php @@ -5,10 +5,12 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; +use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Type; use PHPUnit\Framework\MockObject\MockBuilder; +use function in_array; -class MockBuilderDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension +class MockBuilderDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { public function getClass(): string diff --git a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php index 06e69b3..cb0b3a1 100644 --- a/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php +++ b/src/Type/PHPUnit/MockObjectDynamicReturnTypeExtension.php @@ -5,6 +5,7 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; +use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectType; @@ -12,8 +13,11 @@ use PHPStan\Type\TypeWithClassName; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; use PHPUnit\Framework\MockObject\MockObject; +use function array_filter; +use function array_values; +use function count; -class MockObjectDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension +class MockObjectDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { public function getClass(): string @@ -33,7 +37,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return new ObjectType(InvocationMocker::class); } - $mockClasses = array_values(array_filter($type->getTypes(), function (Type $type): bool { + $mockClasses = array_values(array_filter($type->getTypes(), static function (Type $type): bool { return !$type instanceof TypeWithClassName || $type->getClassName() !== MockObject::class; })); diff --git a/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php b/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php index a96aa0a..916884c 100644 --- a/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php @@ -3,11 +3,12 @@ namespace PHPStan\Rules\PHPUnit; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class AssertSameBooleanExpectedRuleTest extends \PHPStan\Testing\RuleTestCase +class AssertSameBooleanExpectedRuleTest extends RuleTestCase { protected function getRule(): Rule diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index dd96118..774223f 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -4,11 +4,12 @@ use PHPStan\Rules\Comparison\ImpossibleCheckTypeMethodCallRule; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class AssertSameMethodDifferentTypesRuleTest extends \PHPStan\Testing\RuleTestCase +class AssertSameMethodDifferentTypesRuleTest extends RuleTestCase { protected function getRule(): Rule diff --git a/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php b/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php index 009a96d..c2a2242 100644 --- a/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php @@ -3,11 +3,12 @@ namespace PHPStan\Rules\PHPUnit; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class AssertSameNullExpectedRuleTest extends \PHPStan\Testing\RuleTestCase +class AssertSameNullExpectedRuleTest extends RuleTestCase { protected function getRule(): Rule diff --git a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php index e55d3e2..cc65b27 100644 --- a/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameStaticMethodDifferentTypesRuleTest.php @@ -4,11 +4,12 @@ use PHPStan\Rules\Comparison\ImpossibleCheckTypeStaticMethodCallRule; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class AssertSameStaticMethodDifferentTypesRuleTest extends \PHPStan\Testing\RuleTestCase +class AssertSameStaticMethodDifferentTypesRuleTest extends RuleTestCase { protected function getRule(): Rule diff --git a/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php b/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php index 9a0caf9..32f564d 100644 --- a/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameWithCountRuleTest.php @@ -3,11 +3,12 @@ namespace PHPStan\Rules\PHPUnit; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class AssertSameWithCountRuleTest extends \PHPStan\Testing\RuleTestCase +class AssertSameWithCountRuleTest extends RuleTestCase { protected function getRule(): Rule diff --git a/tests/Rules/PHPUnit/MockMethodCallRuleTest.php b/tests/Rules/PHPUnit/MockMethodCallRuleTest.php index cd42678..605daae 100644 --- a/tests/Rules/PHPUnit/MockMethodCallRuleTest.php +++ b/tests/Rules/PHPUnit/MockMethodCallRuleTest.php @@ -3,11 +3,12 @@ namespace PHPStan\Rules\PHPUnit; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class MockMethodCallRuleTest extends \PHPStan\Testing\RuleTestCase +class MockMethodCallRuleTest extends RuleTestCase { protected function getRule(): Rule diff --git a/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php b/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php index 3f8d6e2..b378c67 100644 --- a/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php +++ b/tests/Rules/PHPUnit/ShouldCallParentMethodsRuleTest.php @@ -3,11 +3,12 @@ namespace PHPStan\Rules\PHPUnit; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ShouldCallParentMethodsRuleTest extends \PHPStan\Testing\RuleTestCase +class ShouldCallParentMethodsRuleTest extends RuleTestCase { protected function getRule(): Rule diff --git a/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php b/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php index c32b9d0..f2a254d 100644 --- a/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php +++ b/tests/Type/PHPUnit/AssertFunctionTypeSpecifyingExtensionTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\PHPUnit; use PHPStan\Testing\TypeInferenceTestCase; +use function function_exists; class AssertFunctionTypeSpecifyingExtensionTest extends TypeInferenceTestCase { @@ -19,8 +20,6 @@ public function dataFileAsserts(): iterable /** * @dataProvider dataFileAsserts - * @param string $assertType - * @param string $file * @param mixed ...$args */ public function testFileAsserts( diff --git a/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php b/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php index 6f6a1fc..e1841e0 100644 --- a/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php +++ b/tests/Type/PHPUnit/AssertMethodTypeSpecifyingExtensionTest.php @@ -15,8 +15,6 @@ public function dataFileAsserts(): iterable /** * @dataProvider dataFileAsserts - * @param string $assertType - * @param string $file * @param mixed ...$args */ public function testFileAsserts( From 79201b0d5269410fb6b6a0bc568f180ca9dec2ac Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 7 Mar 2022 02:00:33 +0000 Subject: [PATCH 14/69] Update actions/checkout action to v3 --- .github/workflows/build.yml | 8 ++++---- .github/workflows/release.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85d9184..49be33f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v3 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -53,7 +53,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v3 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -93,7 +93,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v3 - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -136,7 +136,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v3 - name: "Install PHP" uses: "shivammathur/setup-php@v2" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0ebed84..5fed045 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v2" + uses: actions/checkout@v3 - name: Generate changelog id: changelog From 1ca3c9814a478c2cc04111539bf72fcc48b00926 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 7 Mar 2022 02:00:29 +0000 Subject: [PATCH 15/69] Update dependency slevomat/coding-standard to v7.0.19 --- build-cs/composer.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 70af78f..96177ed 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -200,32 +200,32 @@ }, { "name": "slevomat/coding-standard", - "version": "7.0.18", + "version": "7.0.19", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "b81ac84f41a4797dc25c8ede1b0718e2a74be0fc" + "reference": "bef66a43815bbf9b5f49775e9ded3f7c6ba0cc37" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/b81ac84f41a4797dc25c8ede1b0718e2a74be0fc", - "reference": "b81ac84f41a4797dc25c8ede1b0718e2a74be0fc", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/bef66a43815bbf9b5f49775e9ded3f7c6ba0cc37", + "reference": "bef66a43815bbf9b5f49775e9ded3f7c6ba0cc37", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", "php": "^7.1 || ^8.0", "phpstan/phpdoc-parser": "^1.0.0", - "squizlabs/php_codesniffer": "^3.6.1" + "squizlabs/php_codesniffer": "^3.6.2" }, "require-dev": { - "phing/phing": "2.17.0", - "php-parallel-lint/php-parallel-lint": "1.3.1", - "phpstan/phpstan": "1.2.0", + "phing/phing": "2.17.2", + "php-parallel-lint/php-parallel-lint": "1.3.2", + "phpstan/phpstan": "1.4.6", "phpstan/phpstan-deprecation-rules": "1.0.0", "phpstan/phpstan-phpunit": "1.0.0", "phpstan/phpstan-strict-rules": "1.1.0", - "phpunit/phpunit": "7.5.20|8.5.21|9.5.10" + "phpunit/phpunit": "7.5.20|8.5.21|9.5.16" }, "type": "phpcodesniffer-standard", "extra": { @@ -245,7 +245,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.0.18" + "source": "https://github.com/slevomat/coding-standard/tree/7.0.19" }, "funding": [ { @@ -257,7 +257,7 @@ "type": "tidelift" } ], - "time": "2021-12-07T17:19:06+00:00" + "time": "2022-03-01T18:01:41+00:00" }, { "name": "squizlabs/php_codesniffer", From b5cc290488e034ddfa998874586cebaecf9626cd Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 9 Mar 2022 21:00:28 +0100 Subject: [PATCH 16/69] Require PHPStan 1.4.9 --- composer.json | 2 +- .../PHPUnit/AssertSameMethodDifferentTypesRuleTest.php | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 8c3630b..2368c93 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.0" + "phpstan/phpstan": "^1.4.9" }, "conflict": { "phpunit/phpunit": "<7.0" diff --git a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php index 774223f..87e5a3a 100644 --- a/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameMethodDifferentTypesRuleTest.php @@ -48,6 +48,10 @@ public function testRule(): void 'Call to method PHPUnit\Framework\Assert::assertSame() with 1 and 1 will always evaluate to true.', 44, ], + [ + 'Call to method PHPUnit\Framework\Assert::assertSame() with array{\'a\'} and array{\'a\', \'b\'} will always evaluate to false.', + 45, + ], [ 'Call to method PHPUnit\Framework\Assert::assertSame() with \'1\' and \'1\' will always evaluate to true.', 46, @@ -56,6 +60,10 @@ public function testRule(): void 'Call to method PHPUnit\Framework\Assert::assertSame() with \'1\' and \'2\' will always evaluate to false.', 47, ], + [ + 'Call to method PHPUnit\Framework\Assert::assertSame() with array{\'a\'} and array{\'a\', 1} will always evaluate to false.', + 51, + ], [ 'Call to method PHPUnit\Framework\Assert::assertSame() with array{\'a\', 2, 3.0} and array{\'a\', 1} will always evaluate to false.', 52, From 0fcf8217c3db211dd4a231b019cb6a1a2b8feabb Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Thu, 10 Mar 2022 09:24:10 +0100 Subject: [PATCH 17/69] Add assertSame test case with static method --- tests/Rules/PHPUnit/data/assert-same.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Rules/PHPUnit/data/assert-same.php b/tests/Rules/PHPUnit/data/assert-same.php index 48b81e4..42115b7 100644 --- a/tests/Rules/PHPUnit/data/assert-same.php +++ b/tests/Rules/PHPUnit/data/assert-same.php @@ -65,4 +65,17 @@ public function testOther() $foo->assertSame(); } + public function testStaticMethodReturnWithSameTypeIsNotReported() + { + $this->assertSame(self::createSomething('foo'), self::createSomething('foo')); + } + + /** + * @return object + */ + private static function createSomething(string $what) + { + return new \stdClass(); + } + } From 886dab73be32ade686172cb32add7045861a9a4e Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Thu, 10 Mar 2022 09:29:14 +0100 Subject: [PATCH 18/69] Add test case for assertNotSame as well --- tests/Rules/PHPUnit/data/assert-same.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Rules/PHPUnit/data/assert-same.php b/tests/Rules/PHPUnit/data/assert-same.php index 42115b7..41384e5 100644 --- a/tests/Rules/PHPUnit/data/assert-same.php +++ b/tests/Rules/PHPUnit/data/assert-same.php @@ -68,6 +68,7 @@ public function testOther() public function testStaticMethodReturnWithSameTypeIsNotReported() { $this->assertSame(self::createSomething('foo'), self::createSomething('foo')); + $this->assertNotSame(self::createSomething('bar'), self::createSomething('bar')); } /** From 8615099a0207f72c302b98b847c3bdc7c75ffe44 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 28 Mar 2022 00:22:05 +0000 Subject: [PATCH 19/69] Update dependency slevomat/coding-standard to v7.0.20 --- 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 96177ed..16f621d 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -200,16 +200,16 @@ }, { "name": "slevomat/coding-standard", - "version": "7.0.19", + "version": "7.0.20", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "bef66a43815bbf9b5f49775e9ded3f7c6ba0cc37" + "reference": "cbfadfe34c2c29473bf1e891306b3950b3b4350b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/bef66a43815bbf9b5f49775e9ded3f7c6ba0cc37", - "reference": "bef66a43815bbf9b5f49775e9ded3f7c6ba0cc37", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/cbfadfe34c2c29473bf1e891306b3950b3b4350b", + "reference": "cbfadfe34c2c29473bf1e891306b3950b3b4350b", "shasum": "" }, "require": { @@ -221,11 +221,11 @@ "require-dev": { "phing/phing": "2.17.2", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.6", + "phpstan/phpstan": "1.4.10|1.5.0", "phpstan/phpstan-deprecation-rules": "1.0.0", "phpstan/phpstan-phpunit": "1.0.0", "phpstan/phpstan-strict-rules": "1.1.0", - "phpunit/phpunit": "7.5.20|8.5.21|9.5.16" + "phpunit/phpunit": "7.5.20|8.5.21|9.5.19" }, "type": "phpcodesniffer-standard", "extra": { @@ -245,7 +245,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.0.19" + "source": "https://github.com/slevomat/coding-standard/tree/7.0.20" }, "funding": [ { @@ -257,7 +257,7 @@ "type": "tidelift" } ], - "time": "2022-03-01T18:01:41+00:00" + "time": "2022-03-25T09:43:20+00:00" }, { "name": "squizlabs/php_codesniffer", From 98bb1842ca8413193fa4ae2cef6f9cc7e8892fc8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 28 Mar 2022 11:15:51 +0200 Subject: [PATCH 20/69] Drop support for PHP 7.2, require PHPStan 1.5.0 --- .github/workflows/build.yml | 9 +++------ composer.json | 4 ++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 49be33f..8e19c3e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,6 @@ jobs: strategy: matrix: php-version: - - "7.1" - "7.2" - "7.3" - "7.4" @@ -40,7 +39,7 @@ jobs: run: "composer install --no-interaction --no-progress" - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' + if: matrix.php-version == '7.2' || matrix.php-version == '7.3' run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" - name: "Lint" @@ -81,7 +80,6 @@ jobs: fail-fast: false matrix: php-version: - - "7.1" - "7.2" - "7.3" - "7.4" @@ -110,7 +108,7 @@ jobs: run: "composer update --no-interaction --no-progress" - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' + if: matrix.php-version == '7.2' || matrix.php-version == '7.3' run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" - name: "Tests" @@ -124,7 +122,6 @@ jobs: fail-fast: false matrix: php-version: - - "7.1" - "7.2" - "7.3" - "7.4" @@ -155,7 +152,7 @@ jobs: run: "composer update --no-interaction --no-progress" - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' + if: matrix.php-version == '7.2' || matrix.php-version == '7.3' run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" - name: "PHPStan" diff --git a/composer.json b/composer.json index 2368c93..febce30 100644 --- a/composer.json +++ b/composer.json @@ -6,8 +6,8 @@ "MIT" ], "require": { - "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.4.9" + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.5.0" }, "conflict": { "phpunit/phpunit": "<7.0" From a092e4961a1a1e1509cc658e2ad6c0f202655343 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 28 Mar 2022 11:17:23 +0200 Subject: [PATCH 21/69] Update workflow --- .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 8e19c3e..82332bb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "master" + - "1.1.x" jobs: lint: From f26b2a2e7ae1ce2b94288f2d3096fcf09b93682e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 28 Mar 2022 11:17:36 +0200 Subject: [PATCH 22/69] Drop alias --- composer.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/composer.json b/composer.json index febce30..2f67ed5 100644 --- a/composer.json +++ b/composer.json @@ -25,9 +25,6 @@ "sort-packages": true }, "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - }, "phpstan": { "includes": [ "extension.neon", From 09133ce914f1388a8bb8c7f8573aaa3723cff52a Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 27 Mar 2022 10:04:28 +0200 Subject: [PATCH 23/69] CreateStub returns a Stub --- extension.neon | 1 + .../MockObjectTypeNodeResolverExtension.php | 1 + src/Rules/PHPUnit/MockMethodCallRule.php | 8 ++++++-- stubs/Stub.stub | 8 ++++++++ stubs/TestCase.stub | 3 ++- tests/Rules/PHPUnit/MockMethodCallRuleTest.php | 14 ++++++++++++-- tests/Rules/PHPUnit/data/mock-method-call.php | 10 ++++++++++ 7 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 stubs/Stub.stub diff --git a/extension.neon b/extension.neon index 1025448..93d3a9e 100644 --- a/extension.neon +++ b/extension.neon @@ -12,6 +12,7 @@ parameters: - stubs/InvocationMocker.stub - stubs/MockBuilder.stub - stubs/MockObject.stub + - stubs/Stub.stub - stubs/TestCase.stub exceptions: uncheckedExceptionRegexes: diff --git a/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php b/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php index 7225d6d..4d46379 100644 --- a/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php +++ b/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php @@ -39,6 +39,7 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type static $mockClassNames = [ 'PHPUnit_Framework_MockObject_MockObject' => true, 'PHPUnit\Framework\MockObject\MockObject' => true, + 'PHPUnit\Framework\MockObject\Stub' => true, ]; $types = $this->typeNodeResolver->resolveMultiple($typeNode->types, $nameScope); diff --git a/src/Rules/PHPUnit/MockMethodCallRule.php b/src/Rules/PHPUnit/MockMethodCallRule.php index 0007aa9..ae554e1 100644 --- a/src/Rules/PHPUnit/MockMethodCallRule.php +++ b/src/Rules/PHPUnit/MockMethodCallRule.php @@ -12,6 +12,7 @@ use PHPStan\Type\ObjectType; use PHPUnit\Framework\MockObject\Builder\InvocationMocker; use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\MockObject\Stub; use function array_filter; use function count; use function implode; @@ -52,11 +53,14 @@ public function processNode(Node $node, Scope $scope): array if ( $type instanceof IntersectionType - && in_array(MockObject::class, $type->getReferencedClasses(), true) + && ( + 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; + return $class !== MockObject::class && $class !== Stub::class; }); return [ diff --git a/stubs/Stub.stub b/stubs/Stub.stub new file mode 100644 index 0000000..62771cc --- /dev/null +++ b/stubs/Stub.stub @@ -0,0 +1,8 @@ + $originalClassName - * @phpstan-return MockObject&T + * @phpstan-return Stub&T */ public function createStub($originalClassName) {} diff --git a/tests/Rules/PHPUnit/MockMethodCallRuleTest.php b/tests/Rules/PHPUnit/MockMethodCallRuleTest.php index 605daae..c9c33e6 100644 --- a/tests/Rules/PHPUnit/MockMethodCallRuleTest.php +++ b/tests/Rules/PHPUnit/MockMethodCallRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function interface_exists; /** * @extends RuleTestCase @@ -18,7 +19,7 @@ protected function getRule(): Rule public function testRule(): void { - $this->analyse([__DIR__ . '/data/mock-method-call.php'], [ + $expectedErrors = [ [ 'Trying to mock an undefined method doBadThing() on class MockMethodCall\Bar.', 15, @@ -27,7 +28,16 @@ public function testRule(): void 'Trying to mock an undefined method doBadThing() on class MockMethodCall\Bar.', 20, ], - ]); + ]; + + if (interface_exists('PHPUnit\Framework\MockObject\Builder\InvocationStubber')) { + $expectedErrors[] = [ + 'Trying to mock an undefined method doBadThing() on class MockMethodCall\Bar.', + 36, + ]; + } + + $this->analyse([__DIR__ . '/data/mock-method-call.php'], $expectedErrors); } /** diff --git a/tests/Rules/PHPUnit/data/mock-method-call.php b/tests/Rules/PHPUnit/data/mock-method-call.php index cbe5942..bf0fd05 100644 --- a/tests/Rules/PHPUnit/data/mock-method-call.php +++ b/tests/Rules/PHPUnit/data/mock-method-call.php @@ -26,6 +26,16 @@ public function testWithAnotherObject() $bar->method('doBadThing'); } + public function testGoodMethodOnStub() + { + $this->createStub(Bar::class)->method('doThing'); + } + + public function testBadMethodOnStub() + { + $this->createStub(Bar::class)->method('doBadThing'); + } + } class Bar { From 2832aad142674a294cad11ecd5054abf7b6d54bd Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 4 Apr 2022 01:18:42 +0000 Subject: [PATCH 24/69] Update dependency slevomat/coding-standard to v7.1 --- build-cs/composer.lock | 43 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 16f621d..5a10fff 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -151,35 +151,30 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.2.0", + "version": "1.4.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "dbc093d7af60eff5cd575d2ed761b15ed40bd08e" + "reference": "4cb3021a4e10ffe3d5f94a4c34cf4b3f6de2fa3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/dbc093d7af60eff5cd575d2ed761b15ed40bd08e", - "reference": "dbc093d7af60eff5cd575d2ed761b15ed40bd08e", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/4cb3021a4e10ffe3d5f94a4c34cf4b3f6de2fa3d", + "reference": "4cb3021a4e10ffe3d5f94a4c34cf4b3f6de2fa3d", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^7.2 || ^8.0" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.0", + "phpstan/phpstan": "^1.5", "phpstan/phpstan-strict-rules": "^1.0", "phpunit/phpunit": "^9.5", "symfony/process": "^5.2" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, "autoload": { "psr-4": { "PHPStan\\PhpDocParser\\": [ @@ -194,36 +189,36 @@ "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.2.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.4.2" }, - "time": "2021-09-16T20:46:02+00:00" + "time": "2022-03-30T13:33:37+00:00" }, { "name": "slevomat/coding-standard", - "version": "7.0.20", + "version": "7.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "cbfadfe34c2c29473bf1e891306b3950b3b4350b" + "reference": "b521bd358b5f7a7d69e9637fd139e036d8adeb6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/cbfadfe34c2c29473bf1e891306b3950b3b4350b", - "reference": "cbfadfe34c2c29473bf1e891306b3950b3b4350b", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/b521bd358b5f7a7d69e9637fd139e036d8adeb6f", + "reference": "b521bd358b5f7a7d69e9637fd139e036d8adeb6f", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", - "php": "^7.1 || ^8.0", - "phpstan/phpdoc-parser": "^1.0.0", + "php": "^7.2 || ^8.0", + "phpstan/phpdoc-parser": "^1.4.1", "squizlabs/php_codesniffer": "^3.6.2" }, "require-dev": { "phing/phing": "2.17.2", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.4.10|1.5.0", + "phpstan/phpstan": "1.4.10|1.5.2", "phpstan/phpstan-deprecation-rules": "1.0.0", - "phpstan/phpstan-phpunit": "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" }, @@ -245,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.0.20" + "source": "https://github.com/slevomat/coding-standard/tree/7.1" }, "funding": [ { @@ -257,7 +252,7 @@ "type": "tidelift" } ], - "time": "2022-03-25T09:43:20+00:00" + "time": "2022-03-29T12:44:16+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -323,5 +318,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.3.0" } From 4a3c437c09075736285d1cabb5c75bf27ed0bc84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Va=C5=A1ek=20Purchart?= Date: Wed, 20 Apr 2022 17:24:25 +0200 Subject: [PATCH 25/69] AssertSameBooleanExpectedRule, AssertSameNullExpectedRule - report only ConstFetch * Add more tests for AssertSameBooleanExpectedRule * Report only direct true|false for assertTrue()|asertFalse() * Add more tests for AssertSameNullExpectedRule * Report only direct null for assertNull() --- .../PHPUnit/AssertSameBooleanExpectedRule.php | 18 +++--- .../PHPUnit/AssertSameNullExpectedRule.php | 9 ++- .../AssertSameBooleanExpectedRuleTest.php | 10 ++-- .../AssertSameNullExpectedRuleTest.php | 4 +- .../data/assert-same-boolean-expected.php | 56 ++++++++++++++++++- .../data/assert-same-null-expected.php | 40 ++++++++++++- 6 files changed, 117 insertions(+), 20 deletions(-) diff --git a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php index 6be390a..d24d490 100644 --- a/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameBooleanExpectedRule.php @@ -3,12 +3,12 @@ namespace PHPStan\Rules\PHPUnit; 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; -use PHPStan\Type\Constant\ConstantBooleanType; use function count; use function strtolower; @@ -39,20 +39,24 @@ public function processNode(Node $node, Scope $scope): array return []; } - $leftType = $scope->getType($node->getArgs()[0]->value); - if (!$leftType instanceof ConstantBooleanType) { + $expectedArgumentValue = $node->getArgs()[0]->value; + if (!($expectedArgumentValue instanceof ConstFetch)) { return []; } - if ($leftType->getValue()) { + if ($expectedArgumentValue->name->toLowerString() === 'true') { return [ 'You should use assertTrue() instead of assertSame() when expecting "true"', ]; } - return [ - 'You should use assertFalse() instead of assertSame() when expecting "false"', - ]; + if ($expectedArgumentValue->name->toLowerString() === 'false') { + return [ + 'You should use assertFalse() instead of assertSame() when expecting "false"', + ]; + } + + return []; } } diff --git a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php index 8d2b2aa..672f349 100644 --- a/src/Rules/PHPUnit/AssertSameNullExpectedRule.php +++ b/src/Rules/PHPUnit/AssertSameNullExpectedRule.php @@ -3,12 +3,12 @@ namespace PHPStan\Rules\PHPUnit; 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; -use PHPStan\Type\NullType; use function count; use function strtolower; @@ -39,9 +39,12 @@ public function processNode(Node $node, Scope $scope): array return []; } - $leftType = $scope->getType($node->getArgs()[0]->value); + $expectedArgumentValue = $node->getArgs()[0]->value; + if (!($expectedArgumentValue instanceof ConstFetch)) { + return []; + } - if ($leftType instanceof NullType) { + if ($expectedArgumentValue->name->toLowerString() === 'null') { return [ 'You should use assertNull() instead of assertSame(null, $actual).', ]; diff --git a/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php b/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php index 916884c..1fe31df 100644 --- a/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameBooleanExpectedRuleTest.php @@ -29,15 +29,15 @@ public function testRule(): void ], [ 'You should use assertTrue() instead of assertSame() when expecting "true"', - 14, + 26, ], [ - 'You should use assertFalse() instead of assertSame() when expecting "false"', - 17, + 'You should use assertTrue() instead of assertSame() when expecting "true"', + 74, ], [ - 'You should use assertTrue() instead of assertSame() when expecting "true"', - 26, + 'You should use assertFalse() instead of assertSame() when expecting "false"', + 75, ], ]); } diff --git a/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php b/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php index c2a2242..1e802dc 100644 --- a/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php +++ b/tests/Rules/PHPUnit/AssertSameNullExpectedRuleTest.php @@ -25,11 +25,11 @@ public function testRule(): void ], [ 'You should use assertNull() instead of assertSame(null, $actual).', - 13, + 24, ], [ 'You should use assertNull() instead of assertSame(null, $actual).', - 24, + 60, ], ]); } diff --git a/tests/Rules/PHPUnit/data/assert-same-boolean-expected.php b/tests/Rules/PHPUnit/data/assert-same-boolean-expected.php index 1158a57..dccd2ce 100644 --- a/tests/Rules/PHPUnit/data/assert-same-boolean-expected.php +++ b/tests/Rules/PHPUnit/data/assert-same-boolean-expected.php @@ -11,10 +11,10 @@ public function testAssertSameWithBooleanAsExpected() $this->assertSame(false, 'a'); $truish = true; - $this->assertSame($truish, true); + $this->assertSame($truish, true); // using variable is OK $falsish = false; - $this->assertSame($falsish, false); + $this->assertSame($falsish, false); // using variable is OK /** @var bool $a */ $a = null; @@ -26,4 +26,56 @@ public function testAssertSameIsDetectedWithDirectAssertAccess() \PHPUnit\Framework\Assert::assertSame(true, 'foo'); } + public function testConstants(): void + { + \PHPUnit\Framework\Assert::assertSame(PHPSTAN_PHPUNIT_TRUE, 'foo'); + \PHPUnit\Framework\Assert::assertSame(PHPSTAN_PHPUNIT_FALSE, 'foo'); + } + + private const TRUE = true; + private const FALSE = false; + + public function testClassConstants(): void + { + \PHPUnit\Framework\Assert::assertSame(self::TRUE, 'foo'); + \PHPUnit\Framework\Assert::assertSame(self::FALSE, 'foo'); + } + + public function returnBool(): bool + { + return true; + } + + /** + * @return true + */ + public function returnTrue(): bool + { + return true; + } + + /** + * @return false + */ + public function returnFalse(): bool + { + return false; + } + + public function testMethodCalls(): void + { + \PHPUnit\Framework\Assert::assertSame($this->returnTrue(), 'foo'); + \PHPUnit\Framework\Assert::assertSame($this->returnFalse(), 'foo'); + \PHPUnit\Framework\Assert::assertSame($this->returnBool(), 'foo'); + } + + public function testNonLowercase(): void + { + \PHPUnit\Framework\Assert::assertSame(True, 'foo'); + \PHPUnit\Framework\Assert::assertSame(False, 'foo'); + } + } + +const PHPSTAN_PHPUNIT_TRUE = true; +const PHPSTAN_PHPUNIT_FALSE = false; diff --git a/tests/Rules/PHPUnit/data/assert-same-null-expected.php b/tests/Rules/PHPUnit/data/assert-same-null-expected.php index 8c7be33..fedc4c9 100644 --- a/tests/Rules/PHPUnit/data/assert-same-null-expected.php +++ b/tests/Rules/PHPUnit/data/assert-same-null-expected.php @@ -10,7 +10,7 @@ public function testAssertSameWithNullAsExpected() $this->assertSame(null, 'a'); $a = null; - $this->assertSame($a, 'b'); + $this->assertSame($a, 'b'); // using variable is OK $this->assertSame('a', 'b'); // OK @@ -24,4 +24,42 @@ public function testAssertSameIsDetectedWithDirectAssertAccess() \PHPUnit\Framework\Assert::assertSame(null, 'foo'); } + public function testConstant(): void + { + \PHPUnit\Framework\Assert::assertSame(PHPSTAN_PHPUNIT_NULL, 'foo'); + } + + private const NULL = null; + + public function testClassConstant(): void + { + \PHPUnit\Framework\Assert::assertSame(self::NULL, 'foo'); + } + + public function returnNullable(): ?string + { + + } + + /** + * @return null + */ + public function returnNull() + { + return null; + } + + public function testMethodCalls(): void + { + \PHPUnit\Framework\Assert::assertSame($this->returnNull(), 'foo'); + \PHPUnit\Framework\Assert::assertSame($this->returnNullable(), 'foo'); + } + + public function testNonLowercase(): void + { + \PHPUnit\Framework\Assert::assertSame(Null, 'foo'); + } + } + +const PHPSTAN_PHPUNIT_NULL = null; From e00da5f7e5c25cbfe6a39d69a8f6439b93307d98 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 9 May 2022 01:36:55 +0000 Subject: [PATCH 26/69] 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 5a10fff..6e7fb81 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 27/69] 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 6e7fb81..4bcc8de 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 28/69] Require PHPStan 1.8.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 2f67ed5..9e9279d 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 29/69] 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 fe917d3..b34d0fe 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 0000000..f0464de --- /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 d71fc5a..d1a581a 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 30/69] Update .gitattributes --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 615bf05..9d7c518 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 31/69] 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 0000000..8452d98 --- /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 32/69] 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 82332bb..3525498 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 33/69] Fix build --- composer.json | 2 +- phpstan.neon | 17 ----------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/composer.json b/composer.json index 9e9279d..a7ec427 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 d1a581a..2b8fa1a 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 34/69] 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 93d3a9e..d3274e5 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 5be6927..11aa3c0 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 0000000..a5a0cb7 --- /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 0000000..95e6cc8 --- /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 0000000..aebe000 --- /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 0000000..fb58655 --- /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 0000000..2e09326 --- /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 0000000..c35cd26 --- /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 35/69] 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 36203e6..4444db3 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 ebfeb93..117179a 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 36/69] 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 4444db3..e268406 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 37/69] 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 0000000..b50bcfc --- /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 0000000..a406f1e --- /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 38/69] 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 b50bcfc..700f420 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 0000000..3001104 --- /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 39/69] Fix build --- phpstan-baseline.neon | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f0464de..53fb96b 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 40/69] 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 d3274e5..f6f372e 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 11aa3c0..24a28ea 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 0000000..f5529a8 --- /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 0000000..89e3e8f --- /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 0000000..7757720 --- /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 0000000..e28fde1 --- /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 0000000..2926ec9 --- /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 0000000..11f9b90 --- /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 0000000..9154937 --- /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 41/69] 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 a7ec427..40683ea 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 2b8fa1a..d1a581a 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 42/69] 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 40683ea..a7ec427 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 d1a581a..2b8fa1a 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 43/69] 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 a7ec427..40683ea 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 2b8fa1a..d1a581a 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 44/69] 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 aebe000..2346946 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 2e09326..5764a60 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 c35cd26..44ea6b4 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 45/69] 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 fb58655..7ead3a7 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 44ea6b4..fca878f 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 46/69] 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 2346946..8bcf92e 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 5764a60..b886b46 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 47/69] 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 40683ea..a7ec427 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 d1a581a..2b8fa1a 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 48/69] 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 5fed045..bac4a00 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 49/69] 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 a05d417..4c7990d 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 50/69] 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 f6f372e..5c6a90d 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 24a28ea..195ace0 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 0000000..7d1afd6 --- /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 0000000..ef3bfcd --- /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 0000000..44434e3 --- /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 0000000..2690d02 --- /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 51/69] 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 a7ec427..f3ce0e7 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 195ace0..8dc7056 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 7d1afd6..612cf06 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 ef3bfcd..e88f293 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 44434e3..cfd27ae 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 52/69] 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 d24d490..284f53b 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 672f349..ca8ac60 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 1f1a3ab..a0a79a9 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 0141406..5a640a1 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 53/69] Update .gitattributes --- .gitattributes | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.gitattributes b/.gitattributes index 9d7c518..00d6b17 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 54/69] 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 e88f293..a224cb7 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 cfd27ae..04dad8d 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 2690d02..23af826 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 55/69] 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 5c6a90d..cea2b15 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 a224cb7..3007960 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 0000000..a93ecfd --- /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 04dad8d..03c44ec 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 56/69] 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 0000000..2af0f17 --- /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 57/69] 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 2af0f17..6a1c815 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 58/69] 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 612cf06..37c586d 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 3007960..6651b05 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 a93ecfd..7fc8af0 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 03c44ec..18cc12b 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 23af826..176be2d 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 59/69] 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 9673537..8c288b2 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 284f53b..6a8eb7d 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 ca8ac60..1030223 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 a0a79a9..876dd87 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 60/69] 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 f3ce0e7..594629f 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 87e5a3a..0898640 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 700f420..4c06546 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 61/69] 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 4d46379..2d70b38 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 ae554e1..ab67760 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 cb0b3a1..6db3a73 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 62/69] 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 8bcf92e..a96257f 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 7ead3a7..7b84b50 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 fca878f..a5ddd18 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 63/69] 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 ab67760..79da2d9 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 e268406..fbed91e 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 64/69] 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 79da2d9..107b4ea 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 6db3a73..4f74fe6 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 65/69] 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 107b4ea..cac776c 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 bf0fd05..478fa44 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 66/69] 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 fbed91e..2276b51 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 a406f1e..8b99672 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 67/69] 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 a96257f..792fcc1 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 7b84b50..32806ed 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 a5ddd18..35af204 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 68/69] 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 792fcc1..66d6edf 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 35af204..2d4e0ef 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 69/69] 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 cac776c..da8a95d 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 478fa44..a4f5aaa 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'); + } + +}