Skip to content

phpstan-base.neon ignores every unknown OCA\OpenRegister class, fleet-wide #744

Description

@rubenvdlinde

What was measured

hydra-gates/quality-config/phpstan-base.neon carries these under ignoreErrors:

- '#unknown class OCA\\OpenRegister\\#'
- '#OCA\\OpenRegister\\[a-zA-Z\\]+.*not found#'
- '#on an unknown class OCA\\OpenRegister\\#'
- '#has invalid return type OCA\\OpenRegister\\#'
- '#has invalid type OCA\\OpenRegister\\#'
- '#implements unknown interface OCA\\OpenRegister\\#'

Every leaf app includes this base. So a green phpstan has never been evidence
that any OpenRegister type resolves, in any app.
If a leaf app typehints a
contract that does not exist, has been renamed, or has drifted, phpstan reports
nothing and exits 0.

Found while cleaning up the register-slug resolver adoption on buildiq, shillinq
and hermiq. shillinq's phpstan happened to be looking at real files through
scanDirectories; buildiq's and hermiq's never were, and all three were equally
green.

The root cause underneath it

conduction/hydra-gates ships hydra-gates/contracts/ but its composer.json
declares no autoload section at all. So nothing in that directory is
autoloadable by a consumer, which is why every app needs some private mechanism
to see the contracts: an ignoreErrors entry, a scanDirectories entry, a
psalm referencedClass suppression, a local stub copy, or a hand-written
require_once in a test bootstrap. Five mechanisms across the fleet, for one
package that could declare the mapping once.

Measured on buildiq: deleting the referencedClass suppression for
ObjectEntityInterface, which the package has shipped since long before
v1.17.0, makes psalm exit 2. A tagged release was never what made these
resolvable.

Two remedies, one of which is already in use

  1. Give the package an autoload entry for OCA\OpenRegister\Contract\.
    This is the one that would let the fleet-wide phpstan ignores go. It needs
    care: hydra-gates claims OCA\OpenRegister\Contract\ as a RUNTIME psr-4 prefix, so any app vendor can define another app contract #531 removed a runtime psr-4 prefix for this exact
    namespace because the prefix is longer than openregister's own, so a vendored
    copy in any app defined the contract for the whole process. An
    analysis-only autoload entry is not obviously the same defect, but it is
    adjacent to it and deserves a real decision rather than a quick patch.

  2. List the contract files as psalm <stubs>. Available today, no package
    change. decidesk and pipelinq already do it. buildiq, shillinq and hermiq
    have now adopted it too, in chore(deps-dev): take hydra-gates 1.18.0 and drop the tag-gap workarounds buildiq#741,
    chore(deps-dev): take hydra-gates 1.18.0 and drop the copied contracts shillinq#1567 and chore(deps-dev): take hydra-gates 1.18.0 and drop the tag-gap workarounds hermiq#841, which removed ten
    referencedClass suppressions between them.

Remedy 2 does nothing for phpstan. The ignoreErrors block above is still
there, still fleet-wide, and still means phpstan green says nothing about
OpenRegister types.

Why this is worth acting on rather than noting

A referencedClass suppression makes every value flowing through the contract
mixed, so the analyser stops checking the branch the contract exists to force.
On decidesk, swapping suppressions for stubs surfaced a real nullable that had
shipped. The phpstan ignores are the same shape at fleet scale, and nothing has
looked behind them yet.

🤖 Generated with Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions