fix(quality): clear 100 real PHPMD findings and drop the 29 baseline entries that covered them - #2347
fix(quality): clear 100 real PHPMD findings and drop the 29 baseline entries that covered them#2347rubenvdlinde wants to merge 4 commits into
Conversation
…entries that covered them Second slice of the suppression audit (ConductionNL/.github#155, #2338). With phpmd.baseline.xml deleted, this repo's true PHPMD count was 749 in the main leg plus 16 UnusedFormalParameter in the second leg. This PR clears 100 of the 749 by fixing the code, not by re-baselining it. Cleared in full — these four rules now report ZERO across lib/: MissingImport 92 findings in 22 files UnusedLocalVariable 5 findings in 3 files UnusedPrivateMethod 2 findings in 1 file ShortMethodName 1 finding MissingImport: every one was a `new \Fully\Qualified\Name(...)` written inline. Each got a `use` statement and the short name at the call site. One genuine collision: Application.php already imports Service\Integration\Providers\TimeProvider, so Service\Integration\TimeProvider is imported as IntegrationTimeProvider rather than shadowing it. A `use` is compile-time aliasing, so importing the optional \Imagick / \ImagickPixel extension classes in HexIconService autoloads nothing and is safe. Shortening two class names in Application.php made CustomSniffs.Functions.NamedParameters see the HexIconService and SchemaImportService constructor calls as internal for the first time; both are now called with named parameters. UnusedLocalVariable: in DbalObjectSourceProvider all three were the `$source` slot of a `[$source, $connection, ...] = $this->writeContext(...)` destructuring. writeContext() checks write authorisation, opens the connection and can throw, so the CALL is kept and only the dead binding is dropped via list-skip syntax. In SaveObjects the dead `$defaultRegister` binding is dropped the same way — the call stays because it warms the shared static register cache that per-row code reads through loadRegisterWithCache(). In MapsOverviewService `$type` was a plain `??` array read with no side effect, so the line is gone. UnusedPrivateMethod: AuditHandler::extractSchemaId() and extractSchemaSlug() have no caller anywhere in lib/ or tests/ and the class does no dynamic dispatch. Deleted. ShortMethodName: FilesController::t() renamed to translate() at its 54 call sites. It is private, so nothing outside the class is affected. The alternative would have been adding `t` to the rule's exceptions list, which is a threshold relaxation and out of scope for this audit. Baseline: 517 entries -> 488. The 29 removed are exactly the entries for the findings fixed above, plus one that was already stale (MissingImport on lib/Service/Flow/FlowActionService.php, a file that has had no MissingImport finding for some time). Nothing was added. This matters beyond bookkeeping: a PHPMD baseline entry is scoped to a (rule, file) pair, never to a line, so each of those 29 entries was an open licence suppressing every FUTURE violation of that rule in that file too. Verified: with the baseline deleted the main leg goes 749 -> 649 and the second leg stays at 16; the four rules above report zero. phpcs is clean over all 1400 files in lib/ (serial run, exit 0). The unit suite is unchanged at 16007 tests / 35916 assertions, same exit status, before and after. Refs ConductionNL/.github#155 Refs #2338
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ❌ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ❌ | ||||
| composer | ✅ | ✅ 173/173 | |||
| npm | ✅ | ✅ 713/713 | |||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-05 15:01 UTC
Download the full PDF report from the workflow artifacts.
…r another Reverting the UnusedLocalVariable fix into a bare statement made psalm report UnusedReturnValue on resolveSafeguardRegister (SaveObjects.php:782), which was red on this PR while development is green -- so it was this PR's own red, not pre-existing. Removed the call, the now-orphaned private resolveSafeguardRegister(), and the applyBulkSafeguards() $register parameter that nothing read once the call was gone (leaving it would have traded the finding again for UnusedFormalParameter). The warm-up earned nothing: per-row code resolves each row's own register id via loadRegisterWithCache(), so a row using the bulk register warms the identical entry on first touch, a row naming a different register never reads the warmed entry, and an empty payload paid for a mapper lookup nobody needed. The removed call swallowed every Throwable, so this cannot surface a new exception. applyBulkSafeguards is private with exactly one call site, and no test references it or resolveSafeguardRegister. saveObjects() still uses $register elsewhere. php -l clean.
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ❌ | ||||
| psalm | ✅ | ||||
| phpstan | ❌ | ||||
| phpmetrics | ❌ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ❌ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 173/173 | |||
| npm | ❌ | ✅ 713/713 | |||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-05 19:50 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 173/173 | |||
| npm | ✅ | ✅ 713/713 | |||
| PHPUnit | ❌ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-05 20:15 UTC
Download the full PDF report from the workflow artifacts.
Reverting my own commit 4bb0548. It removed resolveSafeguardRegister() and the applyBulkSafeguards() $register parameter to clear a psalm UnusedReturnValue, and that broke Wave12BulkSafeguardsTest with 7 errors: TypeError: applyBulkSafeguards(): Argument #3 ($_rbac) must be of type bool, OCA\OpenRegister\Db\Schema given tests/Unit/Service/Object/Wave12BulkSafeguardsTest.php:120 invokes the private method by REFLECTION with POSITIONAL arguments, so dropping a parameter shifts every argument after it. I missed it because I checked for callers by looking only at test files whose NAME contained "SaveObjects". Wave12BulkSafeguardsTest does not, so my search returned "no references" — an absence manufactured by the wrong lookup, not a fact about the code. This restores the branch to its author's state (the psalm UnusedReturnValue finding is back and still needs fixing). Reversal verified: diff vs the parent commit ffd7f35 shows no register-related differences remain, and development's merge (logger->debug demotions, SystemOperationContext gate) is preserved. php -l clean.
|
Not merged. I made a mistake on this branch and have reverted it — the branch is back to your state. What I did, and why it was wrongPsalm was red here with one finding that is this PR's own, not pre-existing ( It came from this PR turning I "fixed" it by deleting
How I missed it: I checked for callers by looking only at test files whose filename contained Reverted in bb9f9f5. Verified: diff against the parent commit Where that leaves this PRStill not mergeable, with the original finding restored:
The real fix, for whoever picks this up: either (a) drop the parameter and update I did not attempt (a) tonight because I cannot run this suite locally to verify it (it needs a Nextcloud server tree plus openregister), and having already broken it once by guessing, a second unverified guess is not the right move. |
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ❌ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 173/173 | |||
| npm | ✅ | ✅ 713/713 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-05 21:16 UTC
Download the full PDF report from the workflow artifacts.
… 10 entries for a deleted file phpmd.baseline.xml 519 -> 506 entries, and one rule family leaves ENTIRELY. A PHPMD baseline entry is scoped to (rule, file) - optionally a method, NEVER a line - so one entry covers every current AND future violation of that rule in that file. It is an open licence, not a record. Shrinking the count is therefore not the point; getting a family to zero is, because only then does a NEW violation of that rule fail CI. CountInLoopExpression: all 3 entries removed. The 3 findings behind them were fixed in this PR, not suppressed. Verified with a single-rule ruleset over all of lib and NO baseline in play: 3 findings before, 0 after. lib/Service/Flow/FlowActionService.php: 10 entries for a file that no longer exists (WeightedMethodCount, CouplingBetweenObjects, LongVariable, ShortVariable, MissingImport, and Cyclomatic/Npath on run/runNamedFlow/runAction). Deleted with the file; suppressing nothing; free to remove. The other 509 entries are all LIVE and were left alone. I checked, and the first answer was wrong in an instructive way: matching baseline entries against the report by rule name reported FIVE families - NPath (62), LongMethod (33), WeightedMethodCount (32), LongParameterList (14), LongClass (6) - as "entirely stale", 147 free deletions. They are not. The baseline stores the rule CLASS (PHPMD\Rule\Design\LongMethod) while the XML report writes the rule NAME (ExcessiveMethodLength), and those five differ. With the mapping applied the accounting closes exactly: 767 true findings, 767 suppressed by live entries, nothing unexplained. Uniformity across five independent families was the tell. Measured with the baseline file MOVED ASIDE, not by dropping --baseline-file: PHPMD auto-discovers phpmd.baseline.xml sitting next to the ruleset and applies it either way, so un-flagging it yields a silently baselined run that looks clean. Independently corroborates #2347's 749 + 16.
…InLoopExpression, untrack the phpmd result cache (#2359) * fix(quality): scope the Migration phpmd exclude to lib/, drop a foreign copyright claim, clear the coverage ratchet phpmd-unusedparams.xml carried <exclude-pattern>*/Migration/*</exclude-pattern>. PDepend compiles an exclude-pattern into an UNANCHORED regex (Input\ExcludePathFilter preg_quote()s the pattern, then turns `\*` into `.*`), so that form matches ANY path containing a `/Migration/` segment - lib/Service/Migration/, lib/Command/Migration/, any future lib/*/Migration/. Those are ordinary classes with no interface-mandated signature, so a genuine unused parameter in one would never be reported and the run would still look clean. openconnector carried exactly such a file. openregister has no lib/*/Migration/ directory today, which is precisely why this had to be fixed before one appears: the broad form fails silently and only on the day someone adds the directory. Now `*/lib/Migration/*`, matching the 19 repos that already carry the corrected shape. Twelve files carried `SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors`, scaffolding residue from the Nextcloud app skeleton. The licence sweep in #2350 relabelled the adjacent SPDX-License-Identifier from AGPL-3.0-or-later to EUPL-1.2 - which asserts that Nextcloud GmbH's copyright is EUPL-licensed. We cannot relicense a third party's copyright. Every one of the twelve is Conduction-authored: each carries `@author Conduction Development Team` and `@copyright Conduction B.V.` in its own PHPDoc, and `git log --follow` shows only Conduction committers. The stray holder is corrected to Conduction rather than deleted, so no file loses its REUSE metadata. Side effect, measured by running phpcs on both versions at the identical path: 2 pre-existing "Missing short description in doc comment" errors go away. CountInLoopExpression retired entirely - all 3 baseline entries, all 3 findings. Two are `do { … } while (count($page) === $limit)` where the page is replaced wholesale each iteration and never mutated in the body, so the count is taken once per page into a variable; one is `for ($i = 1; $i < count($rings); $i++)` over an array the body does not touch, so the count is hoisted. Behaviour is identical in all three. .phpmd.result-cache.php untracked and gitignored. It is generated output, and a correctness hazard while committed: `composer quality:phpmd-score` passes --cache, so a stale cache in the tree makes PHPMD replay a verdict for code that has since changed - a gate reporting a result it never computed. .coverage-baseline 58.87 -> 58.93. This was the ONLY red job on development: CI measured coverage that had improved past its own committed baseline. Raising it tightens the ratchet. Unit suite before and after, same container and same vendor: 16030 tests, 35963 assertions, 0 failures, 0 errors - byte-identical totals. * fix(spec): repoint 6 @SPEC anchors that gate-46 could not resolve Hydra gate-46 (spec-anchor-existence) failed on this PR with 6 unresolved targets. All six are pre-existing debt in files this PR already touches, which is what pulled them into the gate's ADR-020 diff scope; none was introduced here. 62 of the other gates passed and coverage was 60 of 60 applicable, so this was a single real failure, not a broken run. lib/Service/VocabularyImportService.php (4 tags) pointed at openspec/changes/skos-concept-registers/... - a CHANGE directory. That change was archived on 2026-07-23, so the path stopped existing the moment it moved to openspec/changes/archive/. A @SPEC tag must target the canonical openspec/specs/ home, which is where the spec lives now; two of the four also carried "#skos-002", which is not a heading, and now name the heading that actually exists. lib/ContextChat/ContentProvider.php (4 tags, 2 distinct anchors) named "#requirement-getitemurl-must-resolve-through-the-existing-deep-link-registry" and "#requirement-initial-import-must-walk-opted-in-schemas-in-batches-and-must- be-re-runnable-via-occ". Neither heading exists; both requirements were merged into one, "Requirement: getItemUrl and initial import reuse existing OpenRegister infrastructure", and the tags were never moved with it. Every target verified against gate-46's OWN two slug rules - slugify() and gh_slugify(), which differ on punctuation inside a word - by resolving each fragment back to the heading text it matches. No overlap with #2355, which repoints a different set of anchors in file-actions. * revert(quality): drop the .coverage-baseline bump — the number drifts with development I raised .coverage-baseline 58.87 -> 58.93 because that was the value CI itself recomputed on development, and Coverage Baseline Check was development's only red job. On this PR it then failed the OTHER direction: Coverage baseline: 58.93% Coverage current: 58.88% FAIL: Coverage dropped by 0.05% Not a regression from this PR. Development moved between my two CI runs — the suite went 16030 -> 16038 tests — so the merge base this PR is measured against computes 58.88, not the 58.93 that development's own HEAD computed earlier. The two jobs also check opposite things: development's runs coverage-guard.php --update-baseline and fails when the committed value is STALE, while a PR runs it plain and fails when coverage DROPS below the committed value. Pinning a number from one tree to satisfy the other is what broke this. So the bump leaves this PR. It belongs in a one-line change computed on development's own HEAD, at a moment development is not mid-merge — not carried in on a PHPMD branch whose merge base keeps moving underneath it. The job was red before this branch existed and is unaffected by it either way. Nothing is weakened: .coverage-baseline returns to development's committed 58.87, exactly as found. * fix(quality): retire CountInLoopExpression from the baseline and drop 10 entries for a deleted file phpmd.baseline.xml 519 -> 506 entries, and one rule family leaves ENTIRELY. A PHPMD baseline entry is scoped to (rule, file) - optionally a method, NEVER a line - so one entry covers every current AND future violation of that rule in that file. It is an open licence, not a record. Shrinking the count is therefore not the point; getting a family to zero is, because only then does a NEW violation of that rule fail CI. CountInLoopExpression: all 3 entries removed. The 3 findings behind them were fixed in this PR, not suppressed. Verified with a single-rule ruleset over all of lib and NO baseline in play: 3 findings before, 0 after. lib/Service/Flow/FlowActionService.php: 10 entries for a file that no longer exists (WeightedMethodCount, CouplingBetweenObjects, LongVariable, ShortVariable, MissingImport, and Cyclomatic/Npath on run/runNamedFlow/runAction). Deleted with the file; suppressing nothing; free to remove. The other 509 entries are all LIVE and were left alone. I checked, and the first answer was wrong in an instructive way: matching baseline entries against the report by rule name reported FIVE families - NPath (62), LongMethod (33), WeightedMethodCount (32), LongParameterList (14), LongClass (6) - as "entirely stale", 147 free deletions. They are not. The baseline stores the rule CLASS (PHPMD\Rule\Design\LongMethod) while the XML report writes the rule NAME (ExcessiveMethodLength), and those five differ. With the mapping applied the accounting closes exactly: 767 true findings, 767 suppressed by live entries, nothing unexplained. Uniformity across five independent families was the tell. Measured with the baseline file MOVED ASIDE, not by dropping --baseline-file: PHPMD auto-discovers phpmd.baseline.xml sitting next to the ruleset and applies it either way, so un-flagging it yields a silently baselined run that looks clean. Independently corroborates #2347's 749 + 16.
The measurement
Second slice of the fleet suppression audit (ConductionNL/.github#155), tracked here by #2338.
Measured with PHPMD 2.15.0 / PHP 8.4.22, positive-controlled.
phpmd.xml)phpmd-unusedparams.xml)composer phpmdtodayphpmd.baseline.xmldeletedphpmd.baseline.xmlcarries 517 entries but suppresses 749 findings. That gap isgranularity, not staleness: a PHPMD baseline entry is scoped to a
(rule, file)pair —optionally a method — and never to a line. One entry can therefore cover many findings,
and every future violation of that rule in that file as well. A file-scoped entry is an
open licence, which is the real reason to delete entries rather than shrink them.
What this PR clears — 100 findings, four rules to zero
MissingImport — every one was a
new \Fully\Qualified\Name(...)written inline. Each gota
usestatement plus the short name at the call site. One genuine collision:Application.phpalready imports
Service\Integration\Providers\TimeProvider, soService\Integration\TimeProvideris imported as
IntegrationTimeProviderrather than shadowing it. Importing the optional\Imagick/\ImagickPixelextension classes inHexIconServiceis safe — auseiscompile-time aliasing and autoloads nothing.
Shortening two class names in
Application.phpmadeCustomSniffs.Functions.NamedParametersrecognise the
HexIconServiceandSchemaImportServiceconstructor calls as internal for thefirst time; both now use named parameters.
UnusedLocalVariable — the interesting half of this rule is knowing when the assignment is
dead but the call is not:
DbalObjectSourceProvider(3×): all three were the$sourceslot of[$source, $connection, …] = $this->writeContext(…).writeContext()checks writeauthorisation, opens the connection and can throw — so the call stays and only the dead
binding is dropped, via list-skip syntax
[, $connection, …].SaveObjects:$defaultRegisterwas never read, but the call warms the shared staticregister cache that per-row code reads through
loadRegisterWithCache(). Call kept as astatement, assignment dropped.
MapsOverviewService:$typewas a plain??array read with no side effect — line removed.UnusedPrivateMethod —
AuditHandler::extractSchemaId()andextractSchemaSlug()have nocaller anywhere in
lib/ortests/, and the class does no dynamic dispatch. Deleted.ShortMethodName —
FilesController::t()→translate()at its 54 call sites. It is private,so nothing outside the class is affected. The alternative was adding
tto the rule'sexceptionslist, which is a threshold relaxation and out of scope for this audit.Baseline: 517 → 488 entries, nothing added
The 29 removed entries are exactly the ones covering the findings fixed above, plus one that
was already stale —
MissingImportonlib/Service/Flow/FlowActionService.php, a file thathas had no
MissingImportfinding for some time and was being covered anyway. That staleentry is the open-licence problem in miniature.
The baseline is deliberately RETAINED. 649 + 16 findings remain and openregister cannot
reach zero in one pass; a half-deleted baseline that reddens CI helps nobody. What remains:
elseadds cyclomatic complexity, so it has to be done alongside the CC work@is a behaviour change, not a refactorDb\*entities — one protected property is one database column, and Nextcloud derives the column list, magic accessors and dirty-tracking from them. Only a destructive migration could satisfy the rule; that needs a scoped-ruleset decision, not a refactor.IEventListener::handle(),QueuedJob::run($argument), provider contracts). One is not:lib/Service/File/Pdf/PdfTextReplacer.php:320is the$strictdead-parameter defect tracked by #2339, and the baseline entryUnusedFormalParameter → PdfTextReplacer.phpis what has been hiding it. Fixing that belongs to #2339, not here.No
@SuppressWarningswas added, no threshold was changed, no test was skipped or weakened.Verification
MissingImport,UnusedLocalVariable,UnusedPrivateMethodandShortMethodNameall report zero.phpcsclean over all 1400 files inlib/(serial run, exit 0).Refs ConductionNL/.github#155. Part of #2338.