diff --git a/.github/workflows/test-requirements-check.yml b/.github/workflows/test-requirements-check.yml new file mode 100644 index 0000000000..db58a4e52d --- /dev/null +++ b/.github/workflows/test-requirements-check.yml @@ -0,0 +1,210 @@ +name: Test requirements check + +on: + # Run on pushes to the main branches and on pull requests which touch files related to the requirements check. + # No need to run this workflow when there are only irrelevant changes. + push: + branches: + - 4.x + tags: + - '**' + paths: + - '.github/workflows/test-requirements-check.yml' + - '.github/workflows/reusable-build-phar.yml' + - 'bin/phpcs' + - 'bin/phpcbf' + - 'phpcs.xml.dist' + - 'requirements.php' + - 'scripts/**' + pull_request: + paths: + - '.github/workflows/test-requirements-check.yml' + - '.github/workflows/reusable-build-phar.yml' + - 'bin/phpcs' + - 'bin/phpcbf' + - 'phpcs.xml.dist' + - 'requirements.php' + - 'scripts/**' + + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # Make sure that the files involved in the requirements check don't contain parse errors + # for the PHP versions supported by the requirements check to prevent the tests being run + # failing on the parse errors instead of on the requirements check + # (which would easily go unnoticed). + lint: + runs-on: ubuntu-latest + + strategy: + matrix: + php: ['5.3', '5.4', '5.5', '5.6', '7.0', '7.1'] + + name: "Lint: PHP ${{ matrix.php }}" + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - name: "Lint bin/phpcs" + run: php -l ./bin/phpcs + + - name: "Lint bin/phpcbf" + run: php -l ./bin/phpcbf + + - name: "Lint requirements.php" + run: php -l ./requirements.php + + # The matrix for these tests should be the same for the "plain" file test as for the PHAR test. + # This matrix, however, is quite complex, making maintaining it manually pretty error prone. + # So this job uses a PHP script to generate the matrix based on a fixed set of variables. + # + # The resulting matrix contains builds which combine the following variables: + # - os: ubuntu / windows + # - cmd: phpcs / phpcbf + # - php: 7.2 (minimum PHP version for 4.x), latest and nightly with the required extensions (should pass the check). + # - php: 5.3 (minimum for the requirements check) and 7.1 with the required extension (should fail the PHP version check). + # - php: 7.2 (minimum PHP version for 4.x), latest and nightly WITHOUT required extensions (should fail the check for extensions). + # + # Each combination also contains a "expect" key to set the expectations for success / failure. + # + # The scripts involved in generating the matrix can be found in the `/scripts/` directory. + prepare-matrix: + needs: lint + + name: Get test matrix + runs-on: ubuntu-latest + + outputs: + matrix: ${{ steps.set-matrix.outputs.MATRIX }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 'latest' + ini-values: 'error_reporting=-1, display_errors=On' + coverage: none + + - name: Set matrix + id: set-matrix + run: echo "MATRIX=$(php scripts/get-requirements-check-matrix.php)" >> "$GITHUB_OUTPUT" + + - name: "DEBUG: show generated matrix" + run: echo ${{ steps.set-matrix.outputs.MATRIX }} + + # Test that the requirements check works correctly when using a Composer install or git clone of PHPCS. + test-plain: + needs: prepare-matrix + + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.prepare-matrix.outputs.MATRIX) }} + + # yamllint disable-line rule:line-length + name: "Plain: ${{ matrix.cmd == 'phpcs' && 'cs' || 'cbf' }} / ${{ matrix.php }} / ${{ matrix.name }} (${{ matrix.os == 'ubuntu-latest' && 'nix' || 'Win' }})" + + continue-on-error: ${{ matrix.php == 'nightly' }} + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + ini-values: 'error_reporting=-1, display_errors=On' + extensions: ${{ matrix.extensions }} + coverage: none + env: + fail-fast: true + + # We're only testing the requirements check here, so we just need to verify that PHPCS/PHPCBF starts. Nothing more. + - name: Run the test + id: check + continue-on-error: true + run: php "bin/${{ matrix.cmd }}" --version + + - name: Check the result of a successful test against expectation + if: ${{ steps.check.outcome == 'success' && matrix.expect == 'fail' }} + run: exit 1 + + - name: Check the result of a failed test against expectation + if: ${{ steps.check.outcome != 'success' && matrix.expect == 'success' }} + run: exit 1 + + build-phars: + needs: lint + + name: "Build Phar on PHP 8.0" + + uses: ./.github/workflows/reusable-build-phar.yml + with: + uploadArtifacts: true + + # Test that the requirements check works correctly when using a PHPCS/PHPCBF PHAR file. + test-phar: + needs: [prepare-matrix, build-phars] + + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.prepare-matrix.outputs.MATRIX) }} + + # yamllint disable-line rule:line-length + name: "PHAR: ${{ matrix.cmd == 'phpcs' && 'cs' || 'cbf' }} / ${{ matrix.php }} / ${{ matrix.name }} (${{ matrix.os == 'ubuntu-latest' && 'nix' || 'Win' }})" + + continue-on-error: ${{ matrix.php == 'nightly' }} + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + ini-values: 'error_reporting=-1, display_errors=On' + extensions: ${{ matrix.extensions }} + coverage: none + env: + fail-fast: true + + - name: Download the phar + uses: actions/download-artifact@v4 + with: + name: ${{ matrix.cmd }}-phar + + # We're only testing the requirements check here, so we just need to verify that PHPCS/PHPCBF starts. Nothing more. + - name: Run the test + id: check + continue-on-error: true + run: php ${{ matrix.cmd }}.phar --version + + - name: Check the result of a successful test against expectation + if: ${{ steps.check.outcome == 'success' && matrix.expect == 'fail' }} + run: exit 1 + + - name: Check the result of a failed test against expectation + if: ${{ steps.check.outcome != 'success' && matrix.expect == 'success' }} + run: exit 1 diff --git a/bin/phpcbf b/bin/phpcbf index c804bdf10e..95a872cd9c 100755 --- a/bin/phpcbf +++ b/bin/phpcbf @@ -3,12 +3,28 @@ /** * PHP Code Beautifier and Fixer fixes violations of a defined coding standard. * + * :WARNING: + * This file MUST stay cross-version compatible with older PHP versions (min: PHP 5.3) to allow + * for the requirements check to work correctly. + * + * The PHP 5.3 minimum is set as the previous PHPCS major (3.x) already had a PHP 5.4 minimum + * requirement and didn't take parse errors caused due to the use of namespaces into account + * in its requirements check, so running PHPCS 3.x on PHP < 5.3 would have failed with a parse + * error already anyway, so PHP 5.3 seems reasonable to keep as the minimum for this. + * :WARNING: + * * @author Greg Sherwood + * @author Juliette Reinders Folmer * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600) + * @copyright 2024 PHPCSStandards and contributors * @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence */ -require_once __DIR__.'/../autoload.php'; +// Check if the PHP version and extensions comply with the minimum requirements before anything else. +require_once dirname(__DIR__).'/requirements.php'; +PHP_CodeSniffer\checkRequirements(); + +require_once dirname(__DIR__).'/autoload.php'; $runner = new PHP_CodeSniffer\Runner(); $exitCode = $runner->runPHPCBF(); diff --git a/bin/phpcs b/bin/phpcs index d098bf8783..fe3adaf58a 100755 --- a/bin/phpcs +++ b/bin/phpcs @@ -3,12 +3,28 @@ /** * PHP_CodeSniffer detects violations of a defined coding standard. * + * :WARNING: + * This file MUST stay cross-version compatible with older PHP versions (min: PHP 5.3) to allow + * for the requirements check to work correctly. + * + * The PHP 5.3 minimum is set as the previous PHPCS major (3.x) already had a PHP 5.4 minimum + * requirement and didn't take parse errors caused due to the use of namespaces into account + * in its requirements check, so running PHPCS 3.x on PHP < 5.3 would have failed with a parse + * error already anyway, so PHP 5.3 seems reasonable to keep as the minimum for this. + * :WARNING: + * * @author Greg Sherwood + * @author Juliette Reinders Folmer * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600) + * @copyright 2024 PHPCSStandards and contributors * @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence */ -require_once __DIR__.'/../autoload.php'; +// Check if the PHP version and extensions comply with the minimum requirements before anything else. +require_once dirname(__DIR__).'/requirements.php'; +PHP_CodeSniffer\checkRequirements(); + +require_once dirname(__DIR__).'/autoload.php'; $runner = new PHP_CodeSniffer\Runner(); $exitCode = $runner->runPHPCS(); diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 0fd2f38d0f..d7f885554c 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -3,6 +3,7 @@ The coding standard for PHP_CodeSniffer itself. autoload.php + requirements.php bin scripts src @@ -58,7 +59,11 @@ - + + + bin/phpc(s|bf)$ + requirements\.php$ + diff --git a/phpstan.neon.dist b/phpstan.neon.dist index bdc3e983d3..4060a3800c 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -4,6 +4,7 @@ parameters: max: 80499 level: 0 paths: + - requirements.php - src bootstrapFiles: - tests/bootstrap.php diff --git a/requirements.php b/requirements.php new file mode 100644 index 0000000000..c15cd6524b --- /dev/null +++ b/requirements.php @@ -0,0 +1,71 @@ + + * @author Juliette Reinders Folmer + * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600) + * @copyright 2024 PHPCSStandards and contributors + * @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer; + + +/** + * Exits if the minimum requirements of PHP_CodeSniffer are not met. + * + * @return void + */ +function checkRequirements() +{ + $exitCode = 3; + + // Check the PHP version. + if (PHP_VERSION_ID < 70200) { + echo 'ERROR: PHP_CodeSniffer requires PHP version 7.2.0 or greater.'.PHP_EOL; + exit($exitCode); + } + + $requiredExtensions = array( + 'tokenizer', + 'xmlwriter', + 'SimpleXML', + ); + $missingExtensions = array(); + + foreach ($requiredExtensions as $extension) { + if (extension_loaded($extension) === false) { + $missingExtensions[] = $extension; + } + } + + if (empty($missingExtensions) === false) { + $last = array_pop($requiredExtensions); + $required = implode(', ', $requiredExtensions); + $required .= ' and '.$last; + + if (count($missingExtensions) === 1) { + $missing = $missingExtensions[0]; + } else { + $last = array_pop($missingExtensions); + $missing = implode(', ', $missingExtensions); + $missing .= ' and '.$last; + } + + $error = 'ERROR: PHP_CodeSniffer requires the %s extensions to be enabled. Please enable %s.'.PHP_EOL; + printf($error, $required, $missing); + exit($exitCode); + } + +}//end checkRequirements() diff --git a/scripts/BuildRequirementsCheckMatrix.php b/scripts/BuildRequirementsCheckMatrix.php new file mode 100644 index 0000000000..f7b395a7f5 --- /dev/null +++ b/scripts/BuildRequirementsCheckMatrix.php @@ -0,0 +1,188 @@ + + * @copyright 2024 PHPCSStandards and contributors + * @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer; + +class BuildRequirementsCheckMatrix +{ + + /** + * Range of valid PHP versions against which select tests should run. + * + * @var array + */ + private $validPhp = [ + '7.2', + 'latest', + 'nightly', + ]; + + /** + * Range of *in*valid PHP versions against which select tests should run. + * + * @var array + */ + private $invalidPhp = [ + '5.3', + '7.1', + ]; + + /** + * The PHPCS commands with which the tests should run. + * + * @var array + */ + private $cmd = [ + 'phpcs', + 'phpcbf', + ]; + + /** + * The operating systems against which the tests should run. + * + * @var array + */ + private $os = [ + 'ubuntu-latest', + 'windows-latest', + ]; + + + /** + * Get all the builds. + * + * @return array> + */ + public function getBuilds() + { + $builds = array_merge( + self::getValidBuilds(), + self::getInvalidPHPBuilds(), + self::getMissingExtensionsBuilds() + ); + + return $builds; + + }//end getBuilds() + + + /** + * Get the builds for tests which should succeed. + * + * I.e. these build comply with the minimum requirements for PHPCS. + * + * @return array> + */ + private function getValidBuilds() + { + $extensions = ['minimal' => 'none, tokenizer, xmlwriter, SimpleXML']; + + $builds = []; + foreach ($this->validPhp as $php) { + foreach ($this->cmd as $cmd) { + foreach ($extensions as $name => $exts) { + foreach ($this->os as $os) { + $builds[] = [ + 'name' => "✔ exts: $name", + 'os' => $os, + 'cmd' => $cmd, + 'php' => $php, + 'extensions' => $exts, + 'expect' => 'success', + ]; + } + } + } + } + + return $builds; + + }//end getValidBuilds() + + + /** + * Get the builds for tests which should fail because the PHP version does not comply with the minimum PHP requirement. + * + * @return array> + */ + private function getInvalidPHPBuilds() + { + $extensions = ['default' => '']; + + $builds = []; + foreach ($this->invalidPhp as $php) { + foreach ($this->cmd as $cmd) { + foreach ($extensions as $exts) { + foreach ($this->os as $os) { + $builds[] = [ + 'name' => '❌ PHP too low', + 'os' => $os, + 'cmd' => $cmd, + 'php' => $php, + 'extensions' => $exts, + 'expect' => 'fail', + ]; + } + } + } + } + + return $builds; + + }//end getInvalidPHPBuilds() + + + /** + * Get the builds for tests which should fail because the PHP extensions do not comply with the requirements of PHPCS. + * + * @return array> + */ + private function getMissingExtensionsBuilds() + { + $extensions = [ + 'missing tokenizer' => 'none, xmlwriter, SimpleXML', + 'missing xmlwriter' => ':xmlwriter', + 'missing SimpleXML' => ':SimpleXML', + 'missing both XML exts' => 'none, tokenizer', + ]; + + $builds = []; + foreach ($this->validPhp as $php) { + foreach ($this->cmd as $cmd) { + foreach ($extensions as $name => $exts) { + foreach ($this->os as $os) { + // Skip the extension requirements check on Windows as the required extensions cannot be + // disabled on Windows. They are compiled statically into the PHP binary. + // {@link https://github.com/shivammathur/setup-php/issues/887}. + if ($os === 'windows-latest') { + continue; + } + + $builds[] = [ + 'name' => "❌ $name", + 'os' => $os, + 'cmd' => $cmd, + 'php' => $php, + 'extensions' => $exts, + 'expect' => 'fail', + ]; + } + } + }//end foreach + }//end foreach + + return $builds; + + }//end getMissingExtensionsBuilds() + + +}//end class diff --git a/scripts/build-phar.php b/scripts/build-phar.php index 57bf3fd2d3..3d4aeeb251 100644 --- a/scripts/build-phar.php +++ b/scripts/build-phar.php @@ -138,6 +138,9 @@ function stripWhitespaceAndComments($fullpath, $config) ++$fileCount; }//end foreach + // Add requirements check. + $phar->addFromString('requirements.php', stripWhitespaceAndComments(realpath(__DIR__.'/../requirements.php'), $config)); + // Add autoloader. $phar->addFromString('autoload.php', stripWhitespaceAndComments(realpath(__DIR__.'/../autoload.php'), $config)); @@ -155,6 +158,8 @@ function stripWhitespaceAndComments($fullpath, $config) $stub = '#!/usr/bin/env php'."\n"; $stub .= 'run'.$script.'();'."\n"; diff --git a/scripts/get-requirements-check-matrix.php b/scripts/get-requirements-check-matrix.php new file mode 100644 index 0000000000..ea50ed5e47 --- /dev/null +++ b/scripts/get-requirements-check-matrix.php @@ -0,0 +1,18 @@ +#!/usr/bin/env php + + * @copyright 2024 PHPCSStandards and contributors + * @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +error_reporting(E_ALL); + +require_once __DIR__.'/BuildRequirementsCheckMatrix.php'; + +echo json_encode(['include' => (new PHP_CodeSniffer\BuildRequirementsCheckMatrix())->getBuilds()]); diff --git a/src/Runner.php b/src/Runner.php index d527ea575e..88f0740a7e 100644 --- a/src/Runner.php +++ b/src/Runner.php @@ -61,7 +61,6 @@ public function runPHPCS() try { Timing::startTiming(); - Runner::checkRequirements(); if (defined('PHP_CODESNIFFER_CBF') === false) { define('PHP_CODESNIFFER_CBF', false); @@ -167,7 +166,6 @@ public function runPHPCBF() try { Timing::startTiming(); - Runner::checkRequirements(); // Creating the Config object populates it with all required settings // based on the CLI arguments provided to the script and any config @@ -245,54 +243,6 @@ public function runPHPCBF() }//end runPHPCBF() - /** - * Exits if the minimum requirements of PHP_CodeSniffer are not met. - * - * @return void - * @throws \PHP_CodeSniffer\Exceptions\DeepExitException If the requirements are not met. - */ - public function checkRequirements() - { - // Check the PHP version. - if (PHP_VERSION_ID < 50400) { - $error = 'ERROR: PHP_CodeSniffer requires PHP version 5.4.0 or greater.'.PHP_EOL; - throw new DeepExitException($error, 3); - } - - $requiredExtensions = [ - 'tokenizer', - 'xmlwriter', - 'SimpleXML', - ]; - $missingExtensions = []; - - foreach ($requiredExtensions as $extension) { - if (extension_loaded($extension) === false) { - $missingExtensions[] = $extension; - } - } - - if (empty($missingExtensions) === false) { - $last = array_pop($requiredExtensions); - $required = implode(', ', $requiredExtensions); - $required .= ' and '.$last; - - if (count($missingExtensions) === 1) { - $missing = $missingExtensions[0]; - } else { - $last = array_pop($missingExtensions); - $missing = implode(', ', $missingExtensions); - $missing .= ' and '.$last; - } - - $error = 'ERROR: PHP_CodeSniffer requires the %s extensions to be enabled. Please enable %s.'.PHP_EOL; - $error = sprintf($error, $required, $missing); - throw new DeepExitException($error, 3); - } - - }//end checkRequirements() - - /** * Init the rulesets and other high-level settings. *