diff --git a/composer.json b/composer.json
index 59d9df80..681c2237 100644
--- a/composer.json
+++ b/composer.json
@@ -27,7 +27,7 @@
"phpcs": "./vendor/bin/phpcs --standard=phpcs.xml",
"phpcs:fix": "./vendor/bin/phpcbf --standard=phpcs.xml",
"phpcs:output": "./vendor/bin/phpcs --standard=phpcs.xml --report=json lib/ 2>/dev/null | tail -1 > phpcs-output.json",
- "phpmd": "./vendor/bin/phpmd lib text phpmd.xml --baseline-file phpmd.baseline.xml",
+ "phpmd": "E=0; ./vendor/bin/phpmd lib text phpmd.xml --baseline-file phpmd.baseline.xml || E=$?; ./vendor/bin/phpmd lib text phpmd-unusedparams.xml --baseline-file phpmd.baseline.xml || E=$?; exit $E",
"phpmetrics": "./vendor/bin/phpmetrics --report-html=phpmetrics lib/",
"phpmetrics:violations": "./vendor/bin/phpmetrics --violations-xml=phpmetrics/violations.xml lib/",
"psalm": "if [ -f vendor/bin/psalm ]; then ./vendor/bin/psalm --threads=1 --no-cache; else echo 'Psalm not installed, skipping...'; fi",
diff --git a/phpmd-unusedparams.xml b/phpmd-unusedparams.xml
new file mode 100644
index 00000000..a109ae4e
--- /dev/null
+++ b/phpmd-unusedparams.xml
@@ -0,0 +1,40 @@
+
+
+
+
+ Second PHPMD leg: UnusedFormalParameter only.
+
+ This rule lives in its own ruleset because PHPMD honours <exclude-pattern>
+ ONLY as a direct child of <ruleset>; nested inside a <rule> it is parsed and
+ discarded (ConductionNL/.github#155). A direct child, however, is applied by
+ PDepend at file-collection time and drops the file from EVERY rule in the
+ ruleset. Isolating UnusedFormalParameter here means the lib/Migration
+ exclusion applies to this rule and this rule ALONE - every other rule still
+ analyses lib/Migration in the main leg.
+
+ Why lib/Migration is excluded from THIS rule: OCP\Migration\IMigrationStep
+ mandates changeSchema(IOutput $output, Closure $schemaClosure, array $options)
+ and preSchemaChange/postSchemaChange with the same three parameters. A step
+ that needs none of them still cannot drop them. The signature cannot change.
+
+ The pattern is */lib/Migration/* and deliberately NOT */Migration/* or
+ *Migration*. Those broader forms silently swallow ORDINARY CLASSES that merely
+ have "Migration" in their path - measured on openconnector, */Migration/* hides
+ a genuine finding in lib/Service/Migration/, and *Migration* additionally hides
+ every lib/Service/MigrationService.php, lib/Controller/MigrationController.php
+ and lib/Db/*Migration*.php. Those have no mandated signature and must stay
+ analysed. Only the app's own lib/Migration/ directory holds IMigrationStep
+ implementations, so only that directory is exempted.
+
+ Both legs must always run - see the "phpmd" script in composer.json, which
+ keeps the worst exit code rather than letting the first leg short-circuit
+ the second.
+
+
+ */lib/Migration/*
+
+
+
diff --git a/phpmd.xml b/phpmd.xml
index ee029854..5cea97ec 100644
--- a/phpmd.xml
+++ b/phpmd.xml
@@ -97,7 +97,27 @@
-
- *Migration*
-
+