refactor(phpmd): larpingapp true findings 17 → 1, suppressions 25 → 1 - #264
Conversation
…pressions from 25 to 1 TRUE PHPMD findings — measured with EVERY @SuppressWarnings stripped on a throwaway copy, PHPMD 2.15.0 / PHP 8.4.22 inside nextcloud:latest — go **17 → 1**. Shipped findings stay at 0. There is no phpmd.baseline.xml in this repo and there must not be: PHPMD AUTO-DISCOVERS that filename, so a baseline stays active even after the `baseline` CLI flag is removed. phpmd.xml, phpmd-unusedparams.xml, phpcs.xml and every phpunit config are BYTE-UNCHANGED. No threshold was raised, no rule removed, no baseline added. The one surviving finding ------------------------- `Application::boot(IBootContext $context)` — UnusedFormalParameter. The signature is mandated by OCP\AppFramework\Bootstrap\IBootstrap and the body is legitimately empty: register/schema initialisation moved to the `InitializeRegister` repair step. The parameter can be neither dropped nor used. This is the floor. Clean differential as a positive control: TRUE = 1 finding / exit 2, SHIPPED = 0 findings / exit 0. The single surviving suppression demonstrably suppresses exactly one real finding, and the harness discriminates in both directions — so "0 shipped" is a measurement, not an artefact of a rule that never fired. Suppressions adjudicated: 25 → 1 -------------------------------- * DEAD, deleted — 8. Six `ShortVariable` tags (EventsController ×3, CharactersController ×2, RegisterObjectFetcher ×1) sat on methods whose only short variable is `$id`, which phpmd.xml's ShortVariable `exceptions` allowlist already covers — they suppressed nothing. One `UnusedFormalParameter` on `Application::register()`, which uses `$context`. One `NPathComplexity` on the listener's `handle()`, which never fired. * FIXABLE, fixed and deleted — 16. * JUSTIFIED, kept — 1 (`Application::boot()`). All 25 originally carried a bare tag with NO reason text; the survivor now carries a written one. What the fixes actually were ---------------------------- * EventsController (7 findings: CouplingBetweenObjects, ExcessiveClassComplexity, 3× CyclomaticComplexity, 2× NPathComplexity) — extracted `EventRosterService`, which now owns the participation, attendance and run-sheet-context rules. The controller is a thin HTTP/auth boundary again. Also collapsed the authenticate-then-authorize pair into `resolveGameMaster()` returning `[uid, refusal]`, and removed a duplicate of `resolvePlayerName` that had been inlined into the cast builder. * SkillRequirementService (4) — extracted `SkillRequirementChecker` (the four requirement kinds, with `requiredConditions`/`requiredEffects` collapsed into one shared membership check) and `IdListNormaliser`, used by both. `validate()` went from ~117 lines to orchestration. * CharacterService (1) — extracted `EffectApplier`, a stateless collaborator holding effect resolution, non-cumulative dedup and signed-modifier logic. Pure move; the arithmetic is unchanged. * CharacterRequirementListener (2) — extracted `extractEntities()` (early-return, so the `else` disappears rather than relocating) and `collectVeto()`. MEASURED, not assumed: removing an `else` is not complexity-neutral, so both the ElseExpression and the CyclomaticComplexity were re-measured and both cleared. * BooleanArgumentFlag ×2 — the rule fires on the parameter's DEFAULT VALUE, not the call site, which was confirmed before changing anything. Split into intention-revealing `loadSettings()` / `reloadSettings()` over a private `importRegister(bool $force)` with NO default. Three call sites and the test updated. Tests ----- 153 tests / 541 assertions, OK — before and after, identically conditioned, and re-run after every refactor round. Where a constructor gained a dependency the tests construct the REAL collaborator over the existing mock (e.g. `new EventRosterService($this->objectFetcher)`), so every assertion still exercises the same behaviour end to end. No assertion was weakened; the two `loadSettings` delegation tests gained an `expects($this->never())` on the sibling method. Also verified inside nextcloud:latest with `composer install` run IN this worktree: phpcs 0 errors, PHPStan "[OK] No errors", `php -l` clean across lib/ and tests/. All four new services are pure-autowired; the app registers no services explicitly, so no DI wiring was needed. Not done, and why ----------------- * `SkillRequirementService::resolveXpAbility()` is public with no caller. Left alone: it is documented as a shared resolution rule with `event-xp-award-workflow` and carries a @SPEC tag. Deleting a documented public API is not a debt burn-down. * Psalm dies before analysis in a bare container on `Cannot resolve stubfile path vendor/nextcloud/ocp/OCP.bak/...`. Pre-existing and unrelated — psalm.xml is untouched here and that directory does not exist in a fresh clone. psalm.xml was deliberately NOT edited to make it pass; PHPStan covers the same ground and is clean. * `tests/integration/EffectChainIntegrationTest.php` is run by NEITHER phpunit.xml nor phpunit-unit.xml — both include only tests/unit. Its constructor call and import were updated so it stays consistent and lints, but flagging it: that file is currently dead weight in CI and deserves its own look.
Quality Report — ConductionNL/larpingapp @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ❌ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 537/537 | |||
| PHPUnit | ✅ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-05 14:24 UTC
Download the full PDF report from the workflow artifacts.
…ared EUPL-1.2
gate-28 (license-triangle) went red on this PR, and it was MY change that
surfaced it: the gate is diff-scoped, so a file's stale header only gets
compared against composer.json once the file is edited. 11 of the files this PR
touches declared AGPL-3.0-or-later while every authoritative declaration in the
repo says EUPL-1.2 — composer.json `"license": "EUPL-1.2"`,
appinfo/info.xml `<licence>EUPL-1.2</licence>`, and the LICENSE file itself
("EUROPEAN UNION PUBLIC LICENCE v. 1.2"). 19 files under lib/ already said
EUPL-1.2; the AGPL headers are stale copy-paste from the upstream Nextcloud app
skeleton.
This is not a licence change: it makes the file headers agree with the licence
the project already grants under. Both docblock positions (file and class) were
corrected in the files this PR touches.
Also reverts lib/Controller/CharactersController.php to its development state.
WHY, in full, because reverting deliberate work needs a reason:
the only change there was deleting two DEAD
`@SuppressWarnings(PHPMD.ShortVariable)` tags (dead because the sole short
variable is `$id`, which phpmd.xml's ShortVariable `exceptions` list already
allows). But editing the file at all pulled it into the diff scope of gate-49
(controller-exception-translation), which then reported `downloadPdf()` and
`requirementReport()`: both call `RegisterObjectFetcher::getObject()` and catch
only a broad `\Exception`, with no tracked-exception catch and no `@throws`.
That is a real pre-existing debt, and the honest fix was tried: split the catch
into `DoesNotExistException` (client error, 404, silent) and `\Exception`
(server-side — an unconfigured register — same 404 so nothing leaks, but
logged). It works and gate-49 goes green. It also costs a PHPMD finding: the
`use OCP\AppFramework\Db\DoesNotExistException;` import takes
CharactersController's CouplingBetweenObjects from 12 to 13, one over the
threshold, taking SHIPPED findings from 0 to 1. MEASURED, not assumed. An FQCN
in the catch instead of an import is not a way out either — phpmd.xml enables
MissingImport.
The coupling headroom is gone because `downloadPdf()` re-implements, inline via
ContainerInterface + IAppManager, exactly what `Service\DocuDeskPdfRenderer`
already does (`isDocuDeskAvailable`, `normaliseTemplateId`, `getTemplate`,
`render`) and which `EventsController` already consumes. Consolidating onto it
would drop two dependencies, delete the duplication AND leave room for the
exception import — but it rewrites ~60 lines and the mocks in
CharactersControllerTest, which is its own change, not a rider on a PHPMD
burn-down. Rather than ship a net PHPMD regression to satisfy a different gate,
this PR leaves the file alone and the debt is written up in the PR description.
Net effect: TRUE findings stay at 1 and SHIPPED at 0 (the two reverted
suppressions were dead, so removing them never moved either number), gate-49
returns to its development state, and gate-28 is satisfied for every file this
PR does touch.
Follow-up commit: gate-28 fixed, and CharactersController deliberately taken back out of scopegate-28 (license-triangle) went red and it was this PR that surfaced it. The gate is diff-scoped, so a stale header is only compared against
|
Quality Report — ConductionNL/larpingapp @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ❌ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 537/537 | |||
| PHPUnit | ✅ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-05 14:45 UTC
Download the full PDF report from the workflow artifacts.
CI verdict, job by job — and the one red I have to account for myselfEvery Hydra gate that ran now PASSES, including the two this PR turned red and then fixed: The job is still red, but not because a gate failed — it is
The rest of the reds, all proven pre-existing on
|
| job | cause | red on development too? |
|---|---|---|
lint-check |
npm error 404 … @conduction/nextcloud-vue-3.0.0-vue3.6.tgz is not in this registry — dies at npm ci, before any lint runs |
yes (environmental; the pin is unpublished, handled separately) |
E2E Tests (Playwright) |
same npm ci 404 |
yes |
Frontend Check (test:l10n) |
pre-existing | yes |
Integration Tests (Newman) |
pre-existing | yes |
Quality Report |
rollup of the above | yes |
What passed
All four PHPUnit matrix jobs (PHP 8.3/8.4 × NC stable31/32) — pass. PHP Quality (phpmd), (phpcs), (psalm), (phpstan), (lint), (phpmetrics) — pass. Security (composer), Security (npm), License (composer), License (npm), Frontend Build, Frontend Tests (unit), Vue Quality (eslint), (stylelint), check:specs, CodeQL — pass.
Nothing red here was caused by this change.
Result
@SuppressWarningsstripped on a throwaway copy)@SuppressWarningstags inlib/phpmd.baseline.xmlpresentphpmd.xml,phpmd-unusedparams.xml,phpcs.xmland every phpunit config are byte-unchanged. No threshold raised, no rule removed, no baseline added, no assertion weakened.The one surviving finding is the floor
Application::boot(IBootContext $context)—UnusedFormalParameter. The signature is mandated byOCP\AppFramework\Bootstrap\IBootstrapand the body is legitimately empty: register/schema initialisation moved to theInitializeRegisterrepair step. The parameter can be neither dropped nor used.Clean differential as a positive control: TRUE = 1 finding / exit 2, SHIPPED = 0 findings / exit 0. The single surviving suppression demonstrably suppresses exactly one real finding, and the harness discriminates in both directions — so "0 shipped" is a measurement, not an artefact of a rule that never fired.
Suppressions adjudicated: 25 → 1
ShortVariable(EventsController ×3, CharactersController ×2, RegisterObjectFetcher ×1) on methods whose only short variable is$id, whichphpmd.xml's ShortVariableexceptionsallowlist already covers — they suppressed nothing. 1×UnusedFormalParameteronApplication::register(), which uses$context. 1×NPathComplexityon the listener'shandle(), which never fired.Application::boot()All 25 originally carried a bare tag with no reason text. The survivor now carries a written one.
What the fixes actually were
EventRosterService, which now owns the participation, attendance and run-sheet-context rules. The controller is a thin HTTP/auth boundary again. Also collapsed the authenticate-then-authorize pair intoresolveGameMaster()returning[uid, refusal], and removed a duplicate ofresolvePlayerNamethat had been inlined into the cast builder.SkillRequirementChecker(the four requirement kinds, withrequiredConditions/requiredEffectscollapsed into one shared membership check) andIdListNormaliser, used by both.validate()went from ~117 lines to orchestration.EffectApplier, a stateless collaborator holding effect resolution, non-cumulative dedup and signed-modifier logic. Pure move; the arithmetic is unchanged.extractEntities()(early-return, so theelsedisappears rather than relocating) andcollectVeto(). Measured, not assumed — removing anelseis not complexity-neutral, so both theElseExpressionand theCyclomaticComplexitywere re-measured and both cleared.BooleanArgumentFlag×2 — the rule fires on the parameter's default value, not the call site; that was confirmed before changing anything. Split into intention-revealingloadSettings()/reloadSettings()over a privateimportRegister(bool $force)with no default. Three call sites and the test updated.Tests
153 tests / 541 assertions, OK — before and after, identically conditioned, re-run after every refactor round. Where a constructor gained a dependency the tests construct the real collaborator over the existing mock (e.g.
new EventRosterService($this->objectFetcher)), so every assertion still exercises the same behaviour end to end. No assertion was weakened; the twoloadSettingsdelegation tests gained anexpects($this->never())on the sibling method.How it was measured
PHPMD 2.15.0 on PHP 8.4.22 inside
nextcloud:latest, withcomposer installrun in this worktree so nothing tests a shared checkout'slib/. Host PHP 8.2 killsvendor/bin/phpmdinplatform_check.phpwith exit 255, which reads exactly like a clean run — so no measurement was taken on the host. Exit codes read directly, never through a pipe. Findings matched on normalisedpath:line:ruletriples, because PHPMD right-padsfile:lineand raw line diffs are therefore wrong.Also verified in the same rig: phpcs 0 errors, PHPStan
[OK] No errors,php -lclean acrosslib/andtests/.Not done, and why
SkillRequirementService::resolveXpAbility()is public with no caller. Left alone — it is documented as a shared resolution rule withevent-xp-award-workflowand carries a@spectag. Deleting a documented public API is not a debt burn-down.Cannot resolve stubfile path vendor/nextcloud/ocp/OCP.bak/…. Pre-existing and unrelated —psalm.xmlis untouched here and that directory does not exist in a fresh clone.psalm.xmlwas deliberately not edited to make it pass; PHPStan covers the same ground and is clean.tests/integration/EffectChainIntegrationTest.phpis run by neitherphpunit.xmlnorphpunit-unit.xml— both include onlytests/unit. Its constructor call and import were updated so it stays consistent and lints, but flagging it: that file is currently dead weight in CI and deserves its own look.🤖 Generated with Claude Code