diff --git a/.github/workflows/phpcs.yml b/.github/workflows/phpcs.yml new file mode 100644 index 0000000..0396dee --- /dev/null +++ b/.github/workflows/phpcs.yml @@ -0,0 +1,13 @@ +name: PHP CodeSniffer + +on: + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + phpcs: + uses: wp-media/workflows/.github/workflows/phpcs.yml@main diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..fc387f7 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,13 @@ +name: PHPStan + +on: + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + phpstan: + uses: wp-media/workflows/.github/workflows/phpstan.yml@main diff --git a/.gitignore b/.gitignore index 55b84c1..dcd0a57 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /vendor/* composer.lock .phpunit.result.cache +phpcs-report.xml diff --git a/Integration/HttpRequestTrait.php b/Integration/HttpRequestTrait.php index 51a730c..5e09c5f 100644 --- a/Integration/HttpRequestTrait.php +++ b/Integration/HttpRequestTrait.php @@ -57,12 +57,22 @@ trait HttpRequestTrait { */ private $http_request_counts = []; + /** + * Starts mocking outbound HTTP requests for the current test. + * + * @return void + */ public function setup_http() { $this->reset_http(); add_filter( 'pre_http_request', [ $this, 'http_callback' ], 10, 3 ); } + /** + * Stops mocking outbound HTTP requests and fails the test if any request went unmocked. + * + * @return void + */ public function tear_down_http() { remove_filter( 'pre_http_request', [ $this, 'http_callback' ], 10 ); @@ -157,6 +167,11 @@ private function is_response_list( $mock ) { return array_keys( $mock ) === range( 0, count( $mock ) - 1 ); } + /** + * Clears the state tracked between tests. + * + * @return void + */ private function reset_http() { $this->blocked_http_requests = []; $this->http_request_counts = []; diff --git a/composer.json b/composer.json index 7e85a8a..baa9325 100644 --- a/composer.json +++ b/composer.json @@ -29,8 +29,23 @@ "vendor/antecedent/patchwork/Patchwork.php" ] }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1", + "php-stubs/wordpress-tests-stubs": "^6.8", + "phpcompatibility/phpcompatibility-wp": "^2.0", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "szepeviktor/phpstan-wordpress": "^2.0", + "wp-coding-standards/wpcs": "^3" + }, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true, + "phpstan/extension-installer": true + } }, "scripts": { "test-unit": "\"vendor/bin/phpunit\" --testsuite unit --colors=always --configuration Tests/Unit/phpunit.xml.dist", @@ -40,6 +55,9 @@ "@test-unit", "@test-integration", "@test-integration-admin" - ] + ], + "phpcs": "vendor/bin/phpcs", + "phpcs:fix": "vendor/bin/phpcbf", + "phpstan": "vendor/bin/phpstan analyze --memory-limit=2G" } } diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..3286c3e --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,37 @@ + + + The custom ruleset for WP Media projects + + + + Integration/HttpRequestTrait.php + vendor/* + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..4f28f85 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,265 @@ +parameters: + ignoreErrors: + - + rawMessage: Offset 'WPMEDIA_PHPUNIT_ROOT_DIR' does not exist on string. + identifier: offsetAccess.notFound + count: 1 + path: BootstrapManager.php + + - + rawMessage: 'Parameter #1 $root of static method WPMedia\PHPUnit\BootstrapManager::getRootDir() expects string, array|bool given.' + identifier: argument.type + count: 1 + path: BootstrapManager.php + + - + rawMessage: 'Strict comparison using === between false and string will always evaluate to false.' + identifier: identical.alwaysFalse + count: 1 + path: BootstrapManager.php + + - + rawMessage: 'Call to WPMedia\PHPUnit\Integration\AdminTestCase::initBeforeClass() on a separate line has no effect.' + identifier: staticMethod.resultUnused + count: 1 + path: Integration/AdminTestCase.php + + - + rawMessage: Constant AUTH_COOKIE not found. + identifier: constant.notFound + count: 1 + path: Integration/AdminTestCase.php + + - + rawMessage: Constant LOGGED_IN_COOKIE not found. + identifier: constant.notFound + count: 1 + path: Integration/AdminTestCase.php + + - + rawMessage: Constant PASS_COOKIE not found. + identifier: constant.notFound + count: 1 + path: Integration/AdminTestCase.php + + - + rawMessage: Constant SECURE_AUTH_COOKIE not found. + identifier: constant.notFound + count: 1 + path: Integration/AdminTestCase.php + + - + rawMessage: Constant USER_COOKIE not found. + identifier: constant.notFound + count: 1 + path: Integration/AdminTestCase.php + + - + rawMessage: 'Method WPMedia\PHPUnit\Integration\AjaxTestCase::getApiCredential() should return string but returns bool.' + identifier: return.type + count: 1 + path: Integration/AjaxTestCase.php + + - + rawMessage: 'Class WP_REST_Request referenced with incorrect case: WP_Rest_Request.' + identifier: class.nameCase + count: 1 + path: Integration/RESTTrait.php + + - + rawMessage: 'Class WP_REST_Request referenced with incorrect case: WP_Rest_Request.' + identifier: class.nameCase + count: 1 + path: Integration/RESTVfsTestCase.php + + - + rawMessage: Variable $wp_rest_server in PHPDoc tag @var does not exist. + identifier: varTag.variableNotFound + count: 1 + path: Integration/RESTVfsTestCase.php + + - + rawMessage: 'Class WP_REST_Request referenced with incorrect case: WP_Rest_Request.' + identifier: class.nameCase + count: 1 + path: Integration/RESTfulTestCase.php + + - + rawMessage: 'Method WPMedia\PHPUnit\Integration\RESTfulTestCase::getApiCredential() should return string but returns bool.' + identifier: return.type + count: 1 + path: Integration/RESTfulTestCase.php + + - + rawMessage: Variable $wp_rest_server in PHPDoc tag @var does not exist. + identifier: varTag.variableNotFound + count: 1 + path: Integration/RESTfulTestCase.php + + - + rawMessage: Constant WPMEDIA_PHPUNIT_ROOT_DIR not found. + identifier: constant.notFound + count: 1 + path: Integration/bootstrap.php + + - + rawMessage: Constant WPMEDIA_PHPUNIT_ROOT_DIR not found. + identifier: constant.notFound + count: 1 + path: Tests/Integration/VirtualFilesystemDirect/TestCase.php + + - + rawMessage: 'Found usage of constant WP_ADMIN. Use is_admin() instead.' + identifier: phpstanWP.wpConstant.fetch + count: 1 + path: Tests/Integration/testAdminOnly.php + + - + rawMessage: Constant WPMEDIA_PHPUNIT_ROOT_DIR not found. + identifier: constant.notFound + count: 1 + path: Tests/Integration/testBootstrap.php + + - + rawMessage: Catching internal class PHPUnit\Framework\AssertionFailedError. + identifier: catch.internalClass + count: 1 + path: Tests/Integration/testHttpRequestTrait.php + + - + rawMessage: 'Call to method PHPUnit\Framework\Assert::assertInstanceOf() with ''WPMedia\\PHPUnit\\VirtualFilesystemDirect'' and WPMedia\PHPUnit\VirtualFilesystemDirect will always evaluate to true.' + identifier: method.alreadyNarrowedType + count: 1 + path: Tests/Integration/testHttpRequestTraitWithVirtualFilesystem.php + + - + rawMessage: Constant WPMEDIA_PHPUNIT_ROOT_DIR not found. + identifier: constant.notFound + count: 1 + path: Tests/Integration/testHttpRequestTraitWithVirtualFilesystem.php + + - + rawMessage: Constant WPMEDIA_PHPUNIT_ROOT_DIR not found. + identifier: constant.notFound + count: 2 + path: Tests/Unit/VirtualFilesystemDirect/TestCase.php + + - + rawMessage: 'Method WPMedia\PHPUnit\VirtualFilesystemDirect::rmdir() invoked with 3 parameters, 1-2 required.' + identifier: arguments.count + count: 1 + path: Tests/Unit/VirtualFilesystemDirect/rmdir.php + + - + rawMessage: 'Call to method PHPUnit\Framework\Assert::assertNull() with int will always evaluate to false.' + identifier: method.impossibleType + count: 2 + path: Tests/Unit/VirtualFilesystemDirect/setFilemtime.php + + - + rawMessage: Constant WPMEDIA_PHPUNIT_ROOT_DIR not found. + identifier: constant.notFound + count: 1 + path: Tests/Unit/VirtualFilesystemTestTrait/TestCase.php + + - + rawMessage: Constant WPMEDIA_PHPUNIT_ROOT_DIR not found. + identifier: constant.notFound + count: 1 + path: Tests/Unit/testBootstrap.php + + - + rawMessage: 'Method WPMedia\PHPUnit\Tests\Unit\ReflectionTarget::secret() is unused.' + identifier: method.unused + count: 1 + path: Tests/Unit/testTestCaseTrait.php + + - + rawMessage: 'Call to function is_array() with array will always evaluate to true.' + identifier: function.alreadyNarrowedType + count: 1 + path: Unit/VirtualFilesystemTestCase.php + + - + rawMessage: 'Call to function is_null() with string will always evaluate to false.' + identifier: function.impossibleType + count: 2 + path: Unit/VirtualFilesystemTestCase.php + + - + rawMessage: Constant WPMEDIA_PHPUNIT_ROOT_DIR not found. + identifier: constant.notFound + count: 1 + path: Unit/bootstrap.php + + - + rawMessage: Binary operation "+" between non-empty-string and non-empty-string results in an error. + identifier: binaryOp.invalid + count: 3 + path: VirtualFilesystemDirect.php + + - + rawMessage: 'Call to function is_null() with bool|org\bovigo\vfs\vfsStreamFile will always evaluate to false.' + identifier: function.impossibleType + count: 1 + path: VirtualFilesystemDirect.php + + - + rawMessage: 'Call to function is_string() with string will always evaluate to true.' + identifier: function.alreadyNarrowedType + count: 1 + path: VirtualFilesystemDirect.php + + - + rawMessage: Instanceof between org\bovigo\vfs\vfsStreamDirectory and org\bovigo\vfs\vfsStreamDirectory will always evaluate to true. + identifier: instanceof.alwaysTrue + count: 1 + path: VirtualFilesystemDirect.php + + - + rawMessage: 'Method WPMedia\PHPUnit\VirtualFilesystemDirect::getDir() should return org\bovigo\vfs\vfsStreamDirectory|null but returns org\bovigo\vfs\vfsStreamContent.' + identifier: return.type + count: 1 + path: VirtualFilesystemDirect.php + + - + rawMessage: 'Method WPMedia\PHPUnit\VirtualFilesystemDirect::getFile() should return org\bovigo\vfs\vfsStreamFile|null but returns org\bovigo\vfs\vfsStreamContent.' + identifier: return.type + count: 1 + path: VirtualFilesystemDirect.php + + - + rawMessage: 'Method WPMedia\PHPUnit\VirtualFilesystemDirect::getnumchmodfromh() should return int but returns string.' + identifier: return.type + count: 1 + path: VirtualFilesystemDirect.php + + - + rawMessage: 'Method WPMedia\PHPUnit\VirtualFilesystemDirect::setFilemtime() should return int but returns null.' + identifier: return.type + count: 1 + path: VirtualFilesystemDirect.php + + - + rawMessage: 'PHPDoc tag @param references unknown parameter: $filectime' + identifier: parameter.notFound + count: 1 + path: VirtualFilesystemDirect.php + + - + rawMessage: 'Property WPMedia\PHPUnit\VirtualFilesystemDirect::$filesystem (org\bovigo\vfs\vfsStreamDirectory) does not accept null.' + identifier: assign.propertyType + count: 1 + path: VirtualFilesystemDirect.php + + - + rawMessage: 'Property WPMedia\PHPUnit\VirtualFilesystemDirect::$root (string) does not accept null.' + identifier: assign.propertyType + count: 1 + path: VirtualFilesystemDirect.php + + - + rawMessage: Constant WPMEDIA_PHPUNIT_ROOT_DIR not found. + identifier: constant.notFound + count: 3 + path: bootstrap-functions.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..7de0661 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,28 @@ +includes: + - phar://phpstan.phar/conf/bleedingEdge.neon + - phpstan-baseline.neon +parameters: + level: 5 + inferPrivatePropertyTypeFromConstructor: true + paths: + - ArrayTrait.php + - BootstrapManager.php + - TestCaseTrait.php + - VirtualFilesystemDirect.php + - VirtualFilesystemTestTrait.php + - bootstrap-functions.php + - Integration/ + - Unit/ + - Fixtures/ + - Tests/ + scanFiles: + - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php + - vendor/php-stubs/wordpress-tests-stubs/wordpress-tests-stubs.php + ignoreErrors: + # The phpunit.coversMethod rule (enabled by bleedingEdge in phpstan-phpunit v2) + # flags pre-existing @covers annotations in the test fixtures. Suppressed here + # rather than baselined: the v2 baseline generator mangles @-leading messages + # (writes `@@covers`), so they never re-match. Cleaning up @covers usage is + # out of scope for this version bump. + - + identifier: phpunit.coversMethod