Skip to content

fix(phpmd): scope the lib/Migration UnusedFormalParameter exclusion to its own ruleset - #434

Merged
rubenvdlinde merged 2 commits into
developmentfrom
fix/phpmd-unusedparams-ruleset
Aug 5, 2026
Merged

fix(phpmd): scope the lib/Migration UnusedFormalParameter exclusion to its own ruleset#434
rubenvdlinde merged 2 commits into
developmentfrom
fix/phpmd-unusedparams-ruleset

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Part of the fleet PHPMD ruleset fix (ConductionNL/.github#155). Propagates the shape
already merged in nextcloud-app-template #125, doriath #157, larpingapp #263 and planix #315.

The defect

phpmd.xml declared:

<rule ref="rulesets/unusedcode.xml/UnusedFormalParameter">
    <exclude-pattern>*Migration*</exclude-pattern>
</rule>

A nested <exclude-pattern> is inert. PHPMD 2.15 reads exclude-patterns in
RuleSetFactory::getIgnorePattern(), which walks $xml->children() — only elements
directly under <ruleset>. A nested one parses without error and is discarded, so
lib/Migration was scanned by the very rule the pattern was written to spare.

Reproduced directly on this repo's ruleset: a probe class in lib/Migration with three
unused formal parameters was reported, exit 2, with the nested pattern in place.

Why not simply hoist the pattern

A top-level <exclude-pattern> is applied by PDepend's ExcludePathFilter at
file-collection time, so it drops the file from every rule in the ruleset — real
complexity, StaticAccess and method-length findings in migrations would silently vanish.

The shape shipped here

  • phpmd.xml no longer declares UnusedFormalParameter; a comment records why.
  • New phpmd-unusedparams.xml holds that rule alone, with a top-level
    */Migration/* exclude — so the exclusion is scoped to that one rule and nothing else.
  • The phpmd composer script runs both legs, keeping the worst exit code so neither
    leg can short-circuit the other.

Why lib/Migration is exempt from this one 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 is not ours to change.

Measurement

PHPMD 2.15.0 (the version this repo's lockfile pins) on PHP 8.4.22, run in a
nextcloud:latest container: host PHP 8.2 makes vendor/bin/phpmd die in
platform_check.php with exit 255, which reads exactly like a clean run. Exit codes
are read directly, never through a pipe. Findings are compared as normalised
path:line:rule triples — PHPMD right-pads the file:line column in text output, so raw
line diffs are meaningless.

findings leg exits
reported, before 0 leg1=0
reported, after 0 leg1=0 leg2=0
true (every @SuppressWarnings stripped on a throwaway copy), before 855 leg1=2
true, after 855 leg1=2 leg2=2

The true count strips every @SuppressWarnings in lib/ on a throwaway copy that is
never committed
, so the comparison isolates the ruleset change from the suppressions.

Retired: 0

Newly appearing (true): 0 — must be 0, and is.
Newly appearing (reported): 0. Retired (reported): 0.

Zero newly-hidden findings: every triple present before is present after, except the
retired UnusedFormalParameter hits inside lib/Migration listed above.

lib/Migration in this repo: 0 PHP file(s).

Dead-gate proof

A new leg that exits 0 on the shipped tree is indistinguishable from a leg that does not
run. Proven otherwise on a throwaway copy (probes removed before committing) — three
probes, run through the shipped two-leg invocation:

probe expected observed
lib/Migration/ZzProbeMigration.php UnusedFormalParameter dropped not reported
lib/Migration/ZzProbeMigration.php ElseExpression still live reported, leg 1
lib/ZzProbe/ZzProbe.php UnusedFormalParameter still live reported, leg 2
leg1=2
leg2=2
leg1	lib/Migration/ZzProbeMigration.php:6:ElseExpression
leg2	lib/ZzProbe/ZzProbe.php:4:UnusedFormalParameter
leg2	lib/ZzProbe/ZzProbe.php:4:UnusedFormalParameter

Leg 2 goes from exit 0 to exit 2 under the probe, so it is live. The Migration
ElseExpression probe is still reported, so isolating the rule did not blind the
other rules to lib/Migration — which is exactly what a hoisted top-level pattern would
have done.

The rig itself was positive-controlled before the first measurement: the same probes under
the unfixed ruleset were reported with exit 2, including the three UnusedFormalParameter
hits in lib/Migration that the nested pattern was supposed to suppress.

Baseline

phpmd.baseline.xml present: yes (8 entries). None was added, deleted or shrunk.

PHPMD auto-discovers phpmd.baseline.xml from the working directory, so removing the
--baseline-file flag would be a no-op — the baseline stays active either way. Verified
empirically on this fleet: identical command, baseline file present → 0 findings / exit 0;
same command with the file absent → 93 findings / exit 2.

Suppressions

0 deleted. This repo has no lib/Migration directory at all, so it carried no @SuppressWarnings(PHPMD.UnusedFormalParameter) tags for this fix to make redundant.

This fix therefore retires nothing in this repo. Its value is the corrected shape — the old nested pattern was inert either way, and the new leg is proven live below, so the coverage is real the day a migration is added.

This repo still carries 14 UnusedFormalParameter and 321 other suppressions elsewhere in lib/; those are outside this PR's scope and untouched.

No @SuppressWarnings was added. No threshold was changed, no rule weakened, no baseline
entry added, nothing skipped.

Tests

phpunit -c phpunit-unit.xml:

  • before: 480 tests, 1564 assertions, 1 PHPUnit warning, 2 deprecations, 25 skipped — OK
  • after: 480 tests, 1564 assertions, 1 PHPUnit warning, 2 deprecations, 25 skipped — OK (unchanged)

(The default phpunit.xml suite needs a live Nextcloud runtime and errors out in a bare
checkout — a pre-existing condition this change does not touch.)

What this PR does NOT do

  • does not add or remove a baseline, a @SuppressWarnings, a threshold change or a waiver
  • does not touch any rule other than UnusedFormalParameter
  • does not burn down any of the remaining suppression-free findings — that is separate work

…o its own ruleset

The nested <exclude-pattern> inside the UnusedFormalParameter <rule> was inert:
PHPMD 2.15 honours exclude-patterns only as direct children of <ruleset>, so
lib/Migration was scanned by the very rule the pattern was written to spare.

Hoisting the pattern to the top level of phpmd.xml would have worked but is
applied at file-collection time, dropping lib/Migration from EVERY rule and
silently swallowing real complexity, StaticAccess and method-length findings.

UnusedFormalParameter now lives alone in phpmd-unusedparams.xml with a
top-level */Migration/* exclude, and the phpmd composer script runs both legs
keeping the worst exit code.
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/softwarecatalog @ 8a0b2b8

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue-demi
test-l10n
composer ✅ 121/121
npm ✅ 849/849
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-05 15:26 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/softwarecatalog @ fef581a

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue-demi
test-l10n
composer ✅ 121/121
npm ✅ 849/849
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-05 20:08 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit 473623a into development Aug 5, 2026
30 checks passed
@rubenvdlinde
rubenvdlinde deleted the fix/phpmd-unusedparams-ruleset branch August 5, 2026 20:49
rubenvdlinde added a commit that referenced this pull request Aug 15, 2026
…Db layer

ADR-083 injected OCA\OpenRegister\Db\MagicMapper into three classes here. That
is another app's DATABASE layer — the coupling ADR-022 exists to prevent — and
no leaf app can load it, so its tests could not construct their own subjects. I
had added a hand-rolled MagicMapper stub to get past that; this removes the need
for one instead.

All three sites were doing the same thing: save without schema validation. The
published contract already exposes that, and saveObject() is not a lesser
route — OpenRegister's own SaveObject calls

    metaHydrationHandler->hydrateObjectMetadata(entity:, schema:)
    objectEntityMapper->update(entity:, register:, schema:)

which IS the magic-mapper route, with the metadata hydration these callers were
performing by hand. The comment claiming a plain save touches "just the blob
table" was wrong; the code was reimplementing OpenRegister's save pipeline one
layer too deep.

The flags matter, and one of them nearly went missing. FIX #434 chose
MagicMapper for TWO reasons, not one:

    to avoid validation errors on the organisatie field (stored as UUID string
    but Schema expects object type) AND to avoid triggering ObjectUpdatedEvent
    cascades. That could interfere with the ongoing org activation process.

`_validation: false` covers the first. The second needs `silent: true`, and a
replacement carrying only the validation flag would have re-emitted those events
into an in-flight org activation — a behaviour change with no test to catch it.
Both flags are now passed at every site.

Left alone: ContactpersoonService's two `container->get(MagicMapper)` lookups.
They are lazy, not injected, so they neither block tests nor need a stub; they
carry the same FIX #434 reasoning and are worth converting on their own terms.
rubenvdlinde added a commit that referenced this pull request Aug 15, 2026
The previous commit left these deliberately: they are lazy container lookups
rather than injected dependencies, so they blocked no test and needed no stub.
They carry the same FIX #434 reasoning, and converting them needed reading what
each actually does rather than pattern-matching the first one.

Site 1 changes only the payload (username), so it is the same shape as the three
already converted: _validation: false, silent: true.

Site 2 is different, and this is why they were worth separating. It sets two
pieces of entity METADATA that the payload API expresses differently:

  organisation  travels in `@self`. SaveObject reads it and applies it via
                setOrganisation() BEHIND AN ACCESS CHECK — so an organisation
                the caller may not use is now refused rather than written. The
                direct mapper call bypassed that check entirely, which is a
                quiet authorisation improvement, not just a refactor.
  owner         is not settable from the payload at all; SaveObject derives it
                from the acting user, so the UID is resolved to an IUser and
                passed as `currentUser`.

Both keep _validation: false and silent: true, so neither the organisatie type
mismatch nor the ObjectUpdatedEvent cascade behaviour changes.

lib/ now has no code reference to OCA\OpenRegister\Db\MagicMapper. What remains
is prose in RenameDutchCatalogColumns explaining how OpenRegister's schema sync
adds columns, which is documentation of the other app's behaviour and correct
where it is.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant