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;
+ }
}