diff --git a/.github/workflows/diagnostics.yml b/.github/workflows/diagnostics.yml index e5edebd..f5eff46 100644 --- a/.github/workflows/diagnostics.yml +++ b/.github/workflows/diagnostics.yml @@ -16,16 +16,19 @@ jobs: fail-fast: false matrix: include: - - php: 7.4 + - php: 8.3 output: '-o github-action -o default' - phpcq_install: 'install' - - php: 8.0 + phpcq_install: 'update' + - php: 8.4 + output: '-o default' + phpcq_install: 'update' + - php: 8.5 output: '-o default' phpcq_install: 'update' steps: - name: Pull source - uses: actions/checkout@v2 + uses: actions/checkout@v7 - name: Setup PHP with PECL extension uses: shivammathur/setup-php@v2 @@ -33,18 +36,15 @@ jobs: php-version: ${{ matrix.php }} - name: Cache composer cache directory - uses: actions/cache@v1 + uses: actions/cache@v6 env: cache-name: composer-cache-dir-${{ matrix.php }} with: path: ~/.cache/composer key: ${{ runner.os }}-build-${{ env.cache-name }} - - name: Install composer dependencies - run: composer install - - name: Cache vendor directory - uses: actions/cache@v1 + uses: actions/cache@v6 env: cache-name: vendor-${{ matrix.php }} with: @@ -53,11 +53,8 @@ jobs: restore-keys: | ${{ runner.os }}-build-${{ env.cache-name }}- - - name: Install phpcq toolchain - run: ./vendor/bin/phpcq ${{ matrix.phpcq_install }} -v - - name: Cache phpcq directory - uses: actions/cache@v1 + uses: actions/cache@v6 env: cache-name: phpcq-${{ matrix.php }} with: @@ -66,12 +63,18 @@ jobs: restore-keys: | ${{ runner.os }}-build-${{ env.cache-name }}- + - name: Install composer dependencies + run: composer install + + - name: Install phpcq toolchain + run: ./vendor/bin/phpcq ${{ matrix.phpcq_install }} -v + - name: Run tests run: ./vendor/bin/phpcq run -v ${{ matrix.output }} - name: Upload build directory to artifact - uses: actions/upload-artifact@v2 - if: ${{ success() }} || ${{ failure() }} + uses: actions/upload-artifact@v7 + if: ${{ success() || failure() }} with: name: phpcq-builds-php-${{ matrix.php }} path: .phpcq/build/ diff --git a/.phpcq.lock b/.phpcq.lock index b81d149..df58b3d 100644 --- a/.phpcq.lock +++ b/.phpcq.lock @@ -1 +1 @@ -{"plugins":{"phpunit":{"api-version":"1.0.0","version":"1.0.0.0","type":"php-file","url":"https://phpcq.github.io/repository/phpunit-1.0.0.0.php","signature":null,"requirements":{"php":{"php":"^7.3 || ^8.0"},"tool":{"phpunit":"^6.0 || ^7.0 || ^8.0 || ^9.0"}},"checksum":{"type":"sha-512","value":"c73f15658e3ba62665f09492ec91c3a6a715760bfaa88473a987538439fff442540148e086e46a6aa18ce55a3ea2fbf76caaa581384cb84a38859fcc609ae7e4"},"tools":{"phpunit":{"version":"9.5.20","url":"https://phar.phpunit.de/phpunit-9.5.20.phar","requirements":{"php":{"php":">=7.3","ext-dom":"*","ext-json":"*","ext-libxml":"*","ext-mbstring":"*","ext-xml":"*","ext-xmlwriter":"*"}},"checksum":{"type":"sha-256","value":"6becad2da5c37f5ad101cc665ef05a2f1a6a45d2427c8edcc74f72c92fb1e05a"},"signature":"https://phar.phpunit.de/phpunit-9.5.20.phar.asc"}},"composerLock":null},"psalm":{"api-version":"1.0.0","version":"1.1.1.0","type":"php-file","url":"https://phpcq.github.io/repository/psalm-1.1.1.0.php","signature":null,"requirements":{"php":{"php":"^7.3 || ^8.0","ext-dom":"*"},"tool":{"psalm":"^3.0 || ^4.0"}},"checksum":{"type":"sha-512","value":"4a550c9226d7bca582d7c10bd87cce01190c96398936b1613421640c83df62ed1c6e0d44c1b39635414ea8cf4a892a6458d27590793238add24e7cb5547e6ffd"},"tools":{"psalm":{"version":"4.23.0","url":"https://github.com/vimeo/psalm/releases/download/4.23.0/psalm.phar","requirements":{"php":{"php":"^7.1|^8","ext-SimpleXML":"*","ext-ctype":"*","ext-dom":"*","ext-json":"*","ext-libxml":"*","ext-mbstring":"*","ext-tokenizer":"*"}},"checksum":null,"signature":"https://github.com/vimeo/psalm/releases/download/4.23.0/psalm.phar.asc"}},"composerLock":null},"composer-require-checker":{"api-version":"1.0.0","version":"1.1.1.0","type":"php-file","url":"https://phpcq.github.io/repository/composer-require-checker-1.1.1.0.php","signature":null,"requirements":{"php":{"php":"^7.4 || ^8.0"},"tool":{"composer-require-checker":"^3.8 || ^4.0"}},"checksum":{"type":"sha-512","value":"d5415bddfe024c5749d894034583882aee4e5c3e1087815d9fdd81cb5e71630f631a0e35de0ff84b97fbbf738c16ece5f83bd8c00695913eb846aa6f04577dc2"},"tools":{"composer-require-checker":{"version":"3.8.0","url":"https://github.com/maglnet/ComposerRequireChecker/releases/download/3.8.0/composer-require-checker.phar","requirements":{"php":{"php":"^7.4 || ^8.0","ext-json":"*","ext-phar":"*"}},"checksum":null,"signature":"https://github.com/maglnet/ComposerRequireChecker/releases/download/3.8.0/composer-require-checker.phar.asc"}},"composerLock":null},"phpmd":{"api-version":"1.0.0","version":"1.0.1.0","type":"php-file","url":"https://phpcq.github.io/repository/phpmd-1.0.1.0.php","signature":null,"requirements":{"php":{"php":"^7.3 || ^8.0","ext-dom":"*"},"tool":{"phpmd":"^2.6.1"}},"checksum":{"type":"sha-512","value":"88e267b9c36b2edc85e924717606b626e005ac8d97b1f65f5331e2a3b3894dec2cf124f6187541bf759d026477b6d94daacc5d5c81bb09714a68ffabe5698dc5"},"tools":{"phpmd":{"version":"2.12.0","url":"https://github.com/phpmd/phpmd/releases/download/2.12.0/phpmd.phar","requirements":{"php":{"php":">=5.3.9","ext-xml":"*"}},"checksum":null,"signature":null}},"composerLock":null},"phpcpd":{"api-version":"1.0.0","version":"1.1.1.0","type":"php-file","url":"https://phpcq.github.io/repository/phpcpd-1.1.1.0.php","signature":null,"requirements":{"php":{"php":"^7.3 || ^8.0","ext-dom":"*"},"tool":{"phpcpd":"^6.0"}},"checksum":{"type":"sha-512","value":"1189ce0bf3fade4cb4241f1d96f915ef8fc7651f4450dc79fdf464ee3d6be3009316f0d423ce2d4af9d76ad50807b7fdf4d77bfa6d9ee2c91d6eda32ea214433"},"tools":{"phpcpd":{"version":"6.0.3","url":"https://phar.phpunit.de/phpcpd-6.0.3.phar","requirements":{"php":{"php":">=7.3","ext-dom":"*"}},"checksum":{"type":"sha-256","value":"2cbaea7cfda1bb4299d863eb075e977c3f49055dd16d88529fae5150d48a84cb"},"signature":"https://phar.phpunit.de/phpcpd-6.0.3.phar.asc"}},"composerLock":null},"phploc":{"api-version":"1.0.0","version":"1.0.0.0","type":"php-file","url":"https://phpcq.github.io/repository/phploc-1.0.0.0.php","signature":null,"requirements":{"php":{"php":"^7.3 || ^8.0","ext-dom":"*","ext-json":"*"},"tool":{"phploc":"^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0"}},"checksum":{"type":"sha-512","value":"f67b02d494796adf553cb3dd13ec06c1cb8e53c799954061749424251379541637538199afb3afa3c7a01cabd1cb6f1c53eb621f015dff9644c6c7cbf10c56d1"},"tools":{"phploc":{"version":"7.0.2","url":"https://phar.phpunit.de/phploc-7.0.2.phar","requirements":{"php":{"php":">=7.3","ext-dom":"*","ext-json":"*"}},"checksum":{"type":"sha-256","value":"3d59778ec86faf25fd00e3a329b2f9ad4a3c751ca91601ea7dab70f887b0bf46"},"signature":"https://phar.phpunit.de/phploc-7.0.2.phar.asc"}},"composerLock":null},"phpcs":{"api-version":"1.0.0","version":"1.1.0.0","type":"php-file","url":"https://phpcq.github.io/repository/phpcs-1.1.0.0.php","signature":null,"requirements":{"php":{"php":"^7.3 || ^8.0","ext-dom":"*"},"tool":{"phpcs":"^3.0 || ^2.0","phpcbf":"^3.0 || ^2.0"}},"checksum":{"type":"sha-512","value":"2737022369da1318cc4e0ea194e8a81019f7b079080d869aab878b7486052fdbe68fee3f28131f35573226def1aabd4bd005e038ee7b767c137b1107c1492a83"},"tools":{"phpcs":{"version":"3.7.0","url":"https://github.com/squizlabs/PHP_CodeSniffer/releases/download/3.7.0/phpcs.phar","requirements":{"php":{"php":">=5.4.0","ext-tokenizer":"*","ext-xmlwriter":"*","ext-simplexml":"*"}},"checksum":null,"signature":"https://github.com/squizlabs/PHP_CodeSniffer/releases/download/3.7.0/phpcs.phar.asc"},"phpcbf":{"version":"3.7.0","url":"https://github.com/squizlabs/PHP_CodeSniffer/releases/download/3.7.0/phpcbf.phar","requirements":{"php":{"php":">=5.4.0","ext-tokenizer":"*","ext-xmlwriter":"*","ext-simplexml":"*"}},"checksum":null,"signature":"https://github.com/squizlabs/PHP_CodeSniffer/releases/download/3.7.0/phpcbf.phar.asc"}},"composerLock":null},"composer-normalize":{"api-version":"1.0.0","version":"1.1.0.0","type":"php-file","url":"https://phpcq.github.io/repository/composer-normalize-1.1.0.0.php","signature":null,"requirements":{"php":{"php":"^7.3 || ^8.0","ext-json":"*"},"tool":{"composer-normalize":"^2.1"}},"checksum":{"type":"sha-512","value":"d59d3557cb20630734878a9115df5dd32d5aff815e5b15be36f6fb5d6e9d83dd36efd84215ab6529edcc924f600946f739a0d9e67723deff95c88346ab502498"},"tools":{"composer-normalize":{"version":"2.28.0","url":"https://github.com/ergebnis/composer-normalize/releases/download/2.28.0/composer-normalize.phar","requirements":{"php":{"php":"^7.4 || ^8.0"}},"checksum":null,"signature":"https://github.com/ergebnis/composer-normalize/releases/download/2.28.0/composer-normalize.phar.asc"}},"composerLock":null}},"tools":[]} \ No newline at end of file +{"plugins":{"phpunit":{"api-version":"1.0.0","version":"1.1.0.0","type":"php-file","url":"https://phpcq.github.io/repository/plugin/phpunit/phpunit-1.1.0.0.php","signature":null,"requirements":{"php":{"php":"^7.3 || ^8.0"},"tool":{"phpunit":"^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0"}},"checksum":{"type":"sha-512","value":"c73f15658e3ba62665f09492ec91c3a6a715760bfaa88473a987538439fff442540148e086e46a6aa18ce55a3ea2fbf76caaa581384cb84a38859fcc609ae7e4"},"tools":{"phpunit":{"version":"12.5.38","url":"https://phar.phpunit.de/phpunit-12.5.38.phar","requirements":{"php":{"php":">=8.3","ext-dom":"*","ext-filter":"*","ext-json":"*","ext-libxml":"*","ext-mbstring":"*","ext-xmlwriter":"*"}},"checksum":{"type":"sha-256","value":"b7817c63e0eb62b83d4f5d8cb7d720e58b236fd2f466b98a84aa14eda9075799"},"signature":"https://phar.phpunit.de/phpunit-12.5.38.phar.asc"}},"composerLock":null},"psalm":{"api-version":"1.0.0","version":"1.3.0.0","type":"php-file","url":"https://phpcq.github.io/repository/plugin/psalm/psalm-1.3.0.0.php","signature":null,"requirements":{"php":{"php":"^7.4 || ^8.0","ext-dom":"*"},"tool":{"psalm":"^3.0 || ^4.0 || ^5.0 || ^6.0"}},"checksum":{"type":"sha-512","value":"4a550c9226d7bca582d7c10bd87cce01190c96398936b1613421640c83df62ed1c6e0d44c1b39635414ea8cf4a892a6458d27590793238add24e7cb5547e6ffd"},"tools":{"psalm":{"version":"6.19.1","url":"https://github.com/vimeo/psalm/releases/download/6.19.1/psalm.phar","requirements":{"php":{"php":"~8.1.31 || ~8.2.27 || ~8.3.16 || ~8.4.3 || ~8.5.0","ext-SimpleXML":"*","ext-ctype":"*","ext-dom":"*","ext-json":"*","ext-libxml":"*","ext-mbstring":"*","ext-tokenizer":"*"}},"checksum":null,"signature":"https://github.com/vimeo/psalm/releases/download/6.19.1/psalm.phar.asc"}},"composerLock":null},"composer-require-checker":{"api-version":"1.0.0","version":"1.1.1.0","type":"php-file","url":"https://phpcq.github.io/repository/plugin/composer-require-checker/composer-require-checker-1.1.1.0.php","signature":null,"requirements":{"php":{"php":"^7.4 || ^8.0"},"tool":{"composer-require-checker":"^3.8 || ^4.0"}},"checksum":{"type":"sha-512","value":"d5415bddfe024c5749d894034583882aee4e5c3e1087815d9fdd81cb5e71630f631a0e35de0ff84b97fbbf738c16ece5f83bd8c00695913eb846aa6f04577dc2"},"tools":{"composer-require-checker":{"version":"4.24.0","url":"https://github.com/maglnet/ComposerRequireChecker/releases/download/4.24.0/composer-require-checker.phar","requirements":{"php":{"php":"~8.4.0 || ~8.5.0","ext-phar":"*"}},"checksum":null,"signature":"https://github.com/maglnet/ComposerRequireChecker/releases/download/4.24.0/composer-require-checker.phar.asc"}},"composerLock":null},"phpmd":{"api-version":"1.0.0","version":"1.0.2.0","type":"php-file","url":"https://phpcq.github.io/repository/plugin/phpmd/phpmd-1.0.2.0.php","signature":null,"requirements":{"php":{"php":"^7.3 || ^8.0","ext-dom":"*"},"tool":{"phpmd":"^2.6.1"}},"checksum":{"type":"sha-512","value":"f22280a6dec8dbdd2ec1d83b294f23237fe32c34f4a298e52038e0a7a0074d541635b2b488b1a6098a42d8418a6cd8eb804406ea82b91e362be2b5d11a0915b0"},"tools":{"phpmd":{"version":"2.15.0","url":"https://github.com/phpmd/phpmd/releases/download/2.15.0/phpmd.phar","requirements":{"php":{"php":">=5.3.9","ext-xml":"*"}},"checksum":null,"signature":"https://github.com/phpmd/phpmd/releases/download/2.15.0/phpmd.phar.asc"}},"composerLock":null},"phpcpd":{"api-version":"1.0.0","version":"1.1.1.0","type":"php-file","url":"https://phpcq.github.io/repository/plugin/phpcpd/phpcpd-1.1.1.0.php","signature":null,"requirements":{"php":{"php":"^7.3 || ^8.0","ext-dom":"*"},"tool":{"phpcpd":"^6.0"}},"checksum":{"type":"sha-512","value":"1189ce0bf3fade4cb4241f1d96f915ef8fc7651f4450dc79fdf464ee3d6be3009316f0d423ce2d4af9d76ad50807b7fdf4d77bfa6d9ee2c91d6eda32ea214433"},"tools":{"phpcpd":{"version":"6.0.3","url":"https://phar.phpunit.de/phpcpd-6.0.3.phar","requirements":{"php":{"php":">=7.3","ext-dom":"*"}},"checksum":{"type":"sha-256","value":"2cbaea7cfda1bb4299d863eb075e977c3f49055dd16d88529fae5150d48a84cb"},"signature":"https://phar.phpunit.de/phpcpd-6.0.3.phar.asc"}},"composerLock":null},"phploc":{"api-version":"1.0.0","version":"1.0.0.0","type":"php-file","url":"https://phpcq.github.io/repository/plugin/phploc/phploc-1.0.0.0.php","signature":null,"requirements":{"php":{"php":"^7.3 || ^8.0","ext-dom":"*","ext-json":"*"},"tool":{"phploc":"^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0"}},"checksum":{"type":"sha-512","value":"f67b02d494796adf553cb3dd13ec06c1cb8e53c799954061749424251379541637538199afb3afa3c7a01cabd1cb6f1c53eb621f015dff9644c6c7cbf10c56d1"},"tools":{"phploc":{"version":"7.0.2","url":"https://phar.phpunit.de/phploc-7.0.2.phar","requirements":{"php":{"php":">=7.3","ext-dom":"*","ext-json":"*"}},"checksum":{"type":"sha-256","value":"3d59778ec86faf25fd00e3a329b2f9ad4a3c751ca91601ea7dab70f887b0bf46"},"signature":"https://phar.phpunit.de/phploc-7.0.2.phar.asc"}},"composerLock":null},"phpcs":{"api-version":"1.0.0","version":"1.2.1.0","type":"php-file","url":"https://phpcq.github.io/repository/plugin/phpcs/phpcs-1.2.1.0.php","signature":null,"requirements":{"php":{"php":"^7.3 || ^8.0","ext-dom":"*"},"tool":{"phpcs":"^4.0 || ^3.0 || ^2.0","phpcbf":"^4.0 || ^3.0 || ^2.0"}},"checksum":{"type":"sha-512","value":"03f1c6c2d94b79d0e8cbd42996382e0d100c7e07f84c3138fa3a8b394e814ec18ce05cbbd257e527913219b2264f062522e4cf3e3bd402b907b9437d96982b44"},"tools":{"phpcs":{"version":"4.0.4","url":"https://github.com/PHPCSStandards/PHP_CodeSniffer/releases/download/4.0.4/phpcs.phar","requirements":{"php":{"php":">=7.2.0","ext-libxml":"*","ext-simplexml":"*","ext-tokenizer":"*","ext-xmlwriter":"*"}},"checksum":null,"signature":"https://github.com/PHPCSStandards/PHP_CodeSniffer/releases/download/4.0.4/phpcs.phar.asc"},"phpcbf":{"version":"4.0.4","url":"https://github.com/PHPCSStandards/PHP_CodeSniffer/releases/download/4.0.4/phpcbf.phar","requirements":{"php":{"php":">=7.2.0","ext-libxml":"*","ext-simplexml":"*","ext-tokenizer":"*","ext-xmlwriter":"*"}},"checksum":null,"signature":"https://github.com/PHPCSStandards/PHP_CodeSniffer/releases/download/4.0.4/phpcbf.phar.asc"}},"composerLock":null},"composer-normalize":{"api-version":"1.0.0","version":"1.1.1.0","type":"php-file","url":"https://phpcq.github.io/repository/plugin/composer-normalize/composer-normalize-1.1.1.0.php","signature":null,"requirements":{"php":{"php":"^7.3 || ^8.0","ext-json":"*"},"tool":{"composer-normalize":"^2.1"}},"checksum":{"type":"sha-512","value":"d9abda440b85d501c58abf9c81bf76f417594b397129215ffa8b777e9bb5e5eda37d7661d661db3c8d11c24f20345bc6fbe56f013b3b9435d459d2b94f086e0f"},"tools":{"composer-normalize":{"version":"2.54.0","url":"https://github.com/ergebnis/composer-normalize/releases/download/2.54.0/composer-normalize.phar","requirements":{"php":{"php":"~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0","ext-json":"*"}},"checksum":null,"signature":"https://github.com/ergebnis/composer-normalize/releases/download/2.54.0/composer-normalize.phar.asc"}},"composerLock":null}},"tools":[]} \ No newline at end of file diff --git a/.phpcq.yaml.dist b/.phpcq.yaml.dist index a9a39ef..4b41070 100644 --- a/.phpcq.yaml.dist +++ b/.phpcq.yaml.dist @@ -42,14 +42,17 @@ phpcq: # psalm - 8A03EA3B385DBAA1 - 12CE0F1D262429A5 + - 99BF4D9A33D65E1E # magl@magll.net - D2CCAC42F6295E7D # PHP_CodeSniffer - 31C7E470E2138192 + - 96E91A992CF22FF4 # Composer normalize - C00543248C87FB13 # phpmd - 0F9684B8B16B7AB0 + - 9093F8B32E4815AA tasks: fix: diff --git a/README.md b/README.md index 5c95da4..094b66d 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,26 @@ This plugin provides [phpmd](https://github.com/phpmd/phpmd) integration for phpcq. See also https://phpmd.org + +Starting with version 2.0 of this plugin, phpmd 3 is required and the option `excluded` has been renamed to `exclude`. + +## Configuration + +```yaml +tasks: + phpmd: + config: + # Built-in rulesets or paths to ruleset files. + ruleset: + - naming + - unusedcode + # Paths or patterns to exclude, passed as "--exclude" to phpmd. + exclude: + - src/Legacy + - '*src/foo/*.php' + # PHP script which is loaded before the analysis. + bootstrap: tests/bootstrap.php + # Any further flags, see "phpmd analyze --help". + custom_flags: + - --strict +``` diff --git a/composer.json b/composer.json index 25396e0..5142e12 100644 --- a/composer.json +++ b/composer.json @@ -18,13 +18,14 @@ } ], "require": { - "php": "^7.3 || ^8.0", + "php": "^8.3", "ext-dom": "*", - "phpcq/plugin-api": "^1.0@dev" + "ext-pcre": "*", + "phpcq/plugin-api": "^1.0" }, "require-dev": { "phpcq/runner-bootstrap": "^1.0@dev", - "phpcq/schema": "^1.0@dev" + "phpcq/schema": "^1.0" }, "autoload-dev": { "psr-4": { diff --git a/composer.lock b/composer.lock index 2eb5713..be8b5dc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bb0d6683fddb295f79f470983ba6e6ef", + "content-hash": "9f50927475edd31a06b7edb3d614ab46", "packages": [ { "name": "phpcq/plugin-api", - "version": "dev-master", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/phpcq/plugin-api.git", - "reference": "891b29c1415436492548771c2c6f61aedb0b0655" + "reference": "5cdbbbcea41191533e8c2bee4a61a73d731df0bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpcq/plugin-api/zipball/891b29c1415436492548771c2c6f61aedb0b0655", - "reference": "891b29c1415436492548771c2c6f61aedb0b0655", + "url": "https://api.github.com/repos/phpcq/plugin-api/zipball/5cdbbbcea41191533e8c2bee4a61a73d731df0bb", + "reference": "5cdbbbcea41191533e8c2bee4a61a73d731df0bb", "shasum": "" }, "require": { @@ -27,7 +27,6 @@ "require-dev": { "phpcq/runner-bootstrap": "^1.0@dev" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -60,7 +59,7 @@ "description": "PHP code quality project plugin API interfaces.", "support": { "issues": "https://github.com/phpcq/plugin-api/issues", - "source": "https://github.com/phpcq/plugin-api/tree/master" + "source": "https://github.com/phpcq/plugin-api/tree/1.0.3" }, "funding": [ { @@ -72,7 +71,7 @@ "type": "github" } ], - "time": "2022-03-01T09:07:09+00:00" + "time": "2024-03-21T08:35:42+00:00" } ], "packages-dev": [ @@ -140,7 +139,7 @@ }, { "name": "phpcq/schema", - "version": "dev-master", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/phpcq/schema.git", @@ -152,7 +151,6 @@ "reference": "3c0ce7ec8e4399b4800b418efe9300210c253cbe", "shasum": "" }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -198,16 +196,15 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { - "phpcq/plugin-api": 20, - "phpcq/runner-bootstrap": 20, - "phpcq/schema": 20 + "phpcq/runner-bootstrap": 20 }, "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^7.3 || ^8.0", - "ext-dom": "*" + "php": "^8.3", + "ext-dom": "*", + "ext-pcre": "*" }, - "platform-dev": [], - "plugin-api-version": "2.3.0" + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/phpcq-plugin.json b/phpcq-plugin.json index 58c963b..2fd205c 100644 --- a/phpcq-plugin.json +++ b/phpcq-plugin.json @@ -5,12 +5,12 @@ "api-version": "1.0.0", "requirements": { "php": { - "php": "^7.3 || ^8.0", + "php": "^8.3", "ext-dom": "*" }, "tool": { "phpmd": { - "constraints": "^2.6.1", + "constraints": "^3.0", "sources": [ { "type": "github", diff --git a/src/phpmd.php b/src/phpmd.php index bdcf008..3197021 100644 --- a/src/phpmd.php +++ b/src/phpmd.php @@ -28,7 +28,8 @@ public function describeConfiguration(PluginConfigurationBuilderInterface $confi $configOptionsBuilder ->describeStringListOption( 'ruleset', - 'List of rulesets (cleancode, codesize, controversial, design, naming, unusedcode).' + 'List of rulesets (cleancode, codesize, controversial, design, naming, unusedcode) or paths to ' + . 'ruleset files.' ) ->isRequired() ->withDefaultValue(['naming', 'unusedcode']); @@ -43,8 +44,15 @@ public function describeConfiguration(PluginConfigurationBuilderInterface $confi $configOptionsBuilder ->describeStringListOption( - 'excluded', - 'List of excluded paths.' + 'exclude', + 'List of paths or patterns to exclude. Use asterisks to exclude by pattern, e.g. "*src/foo/*.php" ' + . 'or "*src/foo/*".' + ); + + $configOptionsBuilder + ->describeStringOption( + 'bootstrap', + 'Path to a PHP script (relative to the project root) which is loaded before the analysis.' ); } @@ -52,28 +60,29 @@ public function createDiagnosticTasks( PluginConfigurationInterface $config, EnvironmentInterface $environment ): iterable { - $directories = $config->getStringList('directories'); - - $args = [ - implode(',', $directories), - 'xml', - implode(',', $config->getStringList('ruleset')), - ]; - - if ($config->has('excluded')) { - $paths = []; - foreach ($config->getStringList('excluded') as $path) { - if ('' === ($path = trim($path))) { + $threads = $environment->getAvailableThreads(); + $args = ['analyze', '--no-progress', '--threads', (string) $threads]; + + foreach ($config->getStringList('ruleset') as $ruleset) { + $args[] = '--ruleset'; + $args[] = $ruleset; + } + + if ($config->has('exclude')) { + foreach ($config->getStringList('exclude') as $pattern) { + if ('' === ($pattern = trim($pattern))) { continue; } - $paths[] = $path; - } - if ($paths) { $args[] = '--exclude'; - $args[] = implode(',', $paths); + $args[] = $pattern; } } + if ($config->has('bootstrap') && '' !== ($bootstrap = trim($config->getString('bootstrap')))) { + $args[] = '--bootstrap'; + $args[] = $bootstrap; + } + if ($config->has('custom_flags')) { foreach ($config->getStringList('custom_flags') as $value) { $args[] = $value; @@ -81,13 +90,19 @@ public function createDiagnosticTasks( } $xmlfile = $environment->getUniqueTempFile($this, 'xml'); - $args[] = '--report-file'; + $args[] = '--reportfile-xml'; $args[] = $xmlfile; + $args[] = '--'; + foreach ($config->getStringList('directories') as $directory) { + $args[] = $directory; + } + yield $environment ->getTaskFactory() ->buildRunPhar('phpmd', $args) ->withoutXDebug() + ->withCosts($threads) ->withWorkingDirectory($environment->getProjectConfiguration()->getProjectRootPath()) ->withOutputTransformer( $this->createOutputTransformer($xmlfile, $environment->getProjectConfiguration()->getProjectRootPath()) @@ -157,15 +172,21 @@ public function finish(int $exitCode): void } foreach ($rootNode->childNodes as $childNode) { - if (!$childNode instanceof DOMElement || $childNode->nodeName !== 'file') { + if (!$childNode instanceof DOMElement) { continue; } - $fileName = $childNode->getAttribute('name'); - if (strpos($fileName, $this->rootDir) === 0) { - $fileName = substr($fileName, strlen($this->rootDir) + 1); + if ($childNode->nodeName === 'error') { + $this->addError($childNode); + continue; } + if ($childNode->nodeName !== 'file') { + continue; + } + + $fileName = $this->relativeFileName($childNode->getAttribute('name')); + foreach ($childNode->childNodes as $violationNode) { if (!$violationNode instanceof DOMElement) { continue; @@ -239,6 +260,34 @@ public function finish(int $exitCode): void ); } + /** + * Processing errors are reported as . The message may contain a + * stack trace which is stripped (line breaks are normalized to spaces by the XML parser). + */ + private function addError(DOMElement $errorNode): void + { + $message = (string) $this->getXmlAttribute($errorNode, 'msg', ''); + if (false !== ($parts = preg_split('/\s+#0 /', $message, 2))) { + $message = $parts[0]; + } + + $builder = $this->report->addDiagnostic(TaskReportInterface::SEVERITY_FATAL, trim($message)); + $fileName = (string) $this->getXmlAttribute($errorNode, 'filename', ''); + if ('' !== $fileName) { + $builder->forFile($this->relativeFileName($fileName))->end(); + } + $builder->end(); + } + + private function relativeFileName(string $fileName): string + { + if (strpos($fileName, $this->rootDir) === 0) { + return substr($fileName, strlen($this->rootDir) + 1); + } + + return $fileName; + } + private function openReportFile(): ?DOMDocument { if (is_readable($this->xmlFile) && filesize($this->xmlFile)) { diff --git a/tests/PhpmdPluginTest.php b/tests/PhpmdPluginTest.php index 2430772..34d735a 100644 --- a/tests/PhpmdPluginTest.php +++ b/tests/PhpmdPluginTest.php @@ -8,10 +8,27 @@ use Phpcq\PluginApi\Version10\Configuration\PluginConfigurationInterface; use Phpcq\PluginApi\Version10\DiagnosticsPluginInterface; use Phpcq\PluginApi\Version10\EnvironmentInterface; +use Phpcq\PluginApi\Version10\Output\OutputTransformerFactoryInterface; +use Phpcq\PluginApi\Version10\ProjectConfigInterface; +use Phpcq\PluginApi\Version10\Report\AttachmentBuilderInterface; +use Phpcq\PluginApi\Version10\Report\DiagnosticBuilderInterface; +use Phpcq\PluginApi\Version10\Report\FileDiagnosticBuilderInterface; +use Phpcq\PluginApi\Version10\Report\TaskReportInterface; +use Phpcq\PluginApi\Version10\Task\PhpTaskBuilderInterface; +use Phpcq\PluginApi\Version10\Task\TaskFactoryInterface; +use Phpcq\PluginApi\Version10\Task\TaskInterface; use PHPUnit\Framework\TestCase; +use function dirname; +use function file_put_contents; +use function iterator_to_array; +use function sys_get_temp_dir; +use function tempnam; +use function unlink; + /** * @coversNothing + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ final class PhpmdPluginTest extends TestCase { @@ -27,7 +44,7 @@ public function testPluginName(): void public function testPluginDescribesConfig(): void { - $configOptionsBuilder = $this->getMockForAbstractClass(PluginConfigurationBuilderInterface::class); + $configOptionsBuilder = $this->createMock(PluginConfigurationBuilderInterface::class); $this->instantiate()->describeConfiguration($configOptionsBuilder); @@ -37,12 +54,282 @@ public function testPluginDescribesConfig(): void public function testPluginCreatesDiagnosticTasks(): void { - $config = $this->getMockForAbstractClass(PluginConfigurationInterface::class); - $environment = $this->getMockForAbstractClass(EnvironmentInterface::class); + $config = $this->createMock(PluginConfigurationInterface::class); + $environment = $this->createMock(EnvironmentInterface::class); $this->instantiate()->createDiagnosticTasks($config, $environment); // We assume it worked out as the plugin did execute correctly. $this->addToAssertionCount(1); } + + public function testBuildsAnalyzeArguments(): void + { + $config = $this->mockConfig( + [ + 'directories' => ['src', 'tests'], + 'ruleset' => ['naming', 'phpmd.xml'], + 'exclude' => ['vendor/', ' ', '*src/foo/*'], + 'custom_flags' => ['--strict'], + ], + ['bootstrap' => 'tests/bootstrap.php'] + ); + + $this->runWithExpectedArguments($config, [ + 'analyze', + '--no-progress', + '--threads', + '4', + '--ruleset', + 'naming', + '--ruleset', + 'phpmd.xml', + '--exclude', + 'vendor/', + '--exclude', + '*src/foo/*', + '--bootstrap', + 'tests/bootstrap.php', + '--strict', + '--reportfile-xml', + '/tmp/report.xml', + '--', + 'src', + 'tests', + ]); + } + + public function testOmitsUnconfiguredOptionalArguments(): void + { + $config = $this->mockConfig( + [ + 'directories' => ['src'], + 'ruleset' => ['naming', 'unusedcode'], + 'custom_flags' => [], + ], + ['bootstrap' => ' '] + ); + + $this->runWithExpectedArguments($config, [ + 'analyze', + '--no-progress', + '--threads', + '4', + '--ruleset', + 'naming', + '--ruleset', + 'unusedcode', + '--reportfile-xml', + '/tmp/report.xml', + '--', + 'src', + ]); + } + + public function testReportsViolationsAndProcessingErrors(): void + { + $xmlFile = tempnam(sys_get_temp_dir(), 'phpcq-phpmd'); + file_put_contents( + $xmlFile, + <<<'XML' + + + + + Avoid using short method names like Foo::x(). + + + + + + XML + ); + + $diagnostics = []; + $report = $this->mockReport($diagnostics); + + try { + $transformerFactory = $this->captureOutputTransformerFactory($xmlFile); + $transformerFactory->createFor($report)->finish(3); + } finally { + unlink($xmlFile); + } + + self::assertSame( + [ + [ + 'severity' => TaskReportInterface::SEVERITY_MAJOR, + 'message' => "Avoid using short method names like Foo::x().\n" + . '(Ruleset: Naming Rules, https://phpmd.org/rules/naming.html#shortmethodname)', + 'file' => 'src/Foo.php', + 'range' => [4, null, 7], + 'source' => 'ShortMethodName', + ], + [ + 'severity' => TaskReportInterface::SEVERITY_FATAL, + 'message' => 'Unexpected token: {, line: 1.', + 'file' => 'src/Broken.php', + ], + [ + 'severity' => TaskReportInterface::SEVERITY_FATAL, + 'message' => 'Something went wrong.', + ], + ], + $diagnostics + ); + } + + /** + * @param array> $stringLists + * @param array $strings + */ + private function mockConfig(array $stringLists, array $strings): PluginConfigurationInterface + { + $config = $this->createMock(PluginConfigurationInterface::class); + $config->method('has')->willReturnCallback( + static fn (string $key): bool => isset($stringLists[$key]) || isset($strings[$key]) + ); + $config->method('getStringList')->willReturnCallback( + static fn (string $key): array => $stringLists[$key] ?? [] + ); + $config->method('getString')->willReturnCallback( + static fn (string $key): string => $strings[$key] ?? '' + ); + + return $config; + } + + /** @param list $expectedArguments */ + private function runWithExpectedArguments(PluginConfigurationInterface $config, array $expectedArguments): void + { + $task = $this->createMock(TaskInterface::class); + $builder = $this->createMock(PhpTaskBuilderInterface::class); + $builder->method('withoutXDebug')->willReturnSelf(); + $builder->expects(self::once())->method('withCosts')->with(4)->willReturnSelf(); + $builder->method('withWorkingDirectory')->willReturnSelf(); + $builder->method('withOutputTransformer')->willReturnSelf(); + $builder->method('build')->willReturn($task); + + $factory = $this->createMock(TaskFactoryInterface::class); + $factory->expects(self::once()) + ->method('buildRunPhar') + ->with('phpmd', $expectedArguments) + ->willReturn($builder); + + $environment = $this->mockEnvironment($factory, '/tmp/report.xml'); + + self::assertSame( + [$task], + iterator_to_array($this->instantiate()->createDiagnosticTasks($config, $environment), false) + ); + } + + private function captureOutputTransformerFactory(string $xmlFile): OutputTransformerFactoryInterface + { + $transformerFactory = null; + $builder = $this->createMock(PhpTaskBuilderInterface::class); + $builder->method('withoutXDebug')->willReturnSelf(); + $builder->method('withCosts')->willReturnSelf(); + $builder->method('withWorkingDirectory')->willReturnSelf(); + $builder->method('withOutputTransformer')->willReturnCallback( + function (OutputTransformerFactoryInterface $factory) use (&$transformerFactory, $builder) { + $transformerFactory = $factory; + + return $builder; + } + ); + $builder->method('build')->willReturn($this->createMock(TaskInterface::class)); + + $factory = $this->createMock(TaskFactoryInterface::class); + $factory->method('buildRunPhar')->willReturn($builder); + + iterator_to_array( + $this->instantiate()->createDiagnosticTasks( + $this->mockConfig(['directories' => ['src'], 'ruleset' => ['naming']], []), + $this->mockEnvironment($factory, $xmlFile) + ) + ); + + self::assertInstanceOf(OutputTransformerFactoryInterface::class, $transformerFactory); + + return $transformerFactory; + } + + private function mockEnvironment(TaskFactoryInterface $factory, string $tempFile): EnvironmentInterface + { + $project = $this->createMock(ProjectConfigInterface::class); + $project->method('getProjectRootPath')->willReturn('/project'); + + $environment = $this->createMock(EnvironmentInterface::class); + $environment->method('getTaskFactory')->willReturn($factory); + $environment->method('getProjectConfiguration')->willReturn($project); + $environment->method('getAvailableThreads')->willReturn(4); + $environment->method('getUniqueTempFile')->willReturn($tempFile); + + return $environment; + } + + /** + * Mocks the report and records all added diagnostics in $diagnostics. + * + * @param list> $diagnostics + */ + private function mockReport(array &$diagnostics): TaskReportInterface + { + $report = $this->createMock(TaskReportInterface::class); + + $attachment = $this->createMock(AttachmentBuilderInterface::class); + $attachment->method('fromFile')->willReturnSelf(); + $attachment->method('setMimeType')->willReturnSelf(); + $attachment->method('end')->willReturn($report); + $report->method('addAttachment')->willReturn($attachment); + $report->expects(self::once())->method('close')->with(TaskReportInterface::STATUS_FAILED); + + $report->method('addDiagnostic')->willReturnCallback( + function (string $severity, string $message) use (&$diagnostics, $report): DiagnosticBuilderInterface { + $index = count($diagnostics); + $diagnostics[] = ['severity' => $severity, 'message' => $message]; + + $diagnostic = $this->createMock(DiagnosticBuilderInterface::class); + $file = $this->createMock(FileDiagnosticBuilderInterface::class); + $diagnostic->method('forFile')->willReturnCallback( + function (string $fileName) use (&$diagnostics, $index, $file): FileDiagnosticBuilderInterface { + $diagnostics[$index]['file'] = $fileName; + + return $file; + } + ); + $diagnostic->method('fromSource')->willReturnCallback( + function (string $source) use (&$diagnostics, $index, $diagnostic): DiagnosticBuilderInterface { + $diagnostics[$index]['source'] = $source; + + return $diagnostic; + } + ); + $diagnostic->method('end')->willReturn($report); + $file->method('forRange')->willReturnCallback( + function ( + int $line, + ?int $column = null, + ?int $endLine = null + ) use ( + &$diagnostics, + $index, + $file + ): FileDiagnosticBuilderInterface { + $diagnostics[$index]['range'] = [$line, $column, $endLine]; + + return $file; + } + ); + $file->method('end')->willReturn($diagnostic); + + return $diagnostic; + } + ); + + return $report; + } }