The CI job ran at level 0. The project shipped no phpstan.neon and set no _PHPSTAN_LEVEL, so it took the templates' default asset, which is level: 0: barely more than a parse. #3618603: Pre-alpha3 audit: give the calendar day grid an accessible name and state, throttle every path that places a hold, and refresh the figures, vocabulary and pages the last seventy commits moved pins it to level 1, which the module already meets with nothing reported, and this issue is the rest of the climb.

Measured on 1.x at ff9193d8: level 1 reports 0, level 2 reports 264, level 3 reports 356. That looks like a wall, and 219 of the 264 are one thing: method.notFound on a variable typed as EntityInterface, where phpstan cannot know the thing loaded is a booking and so cannot know it has getState(). They are not defects.

phpstan-drupal can be told. A drupal.entityMapping block naming each entity type's class, of the kind the Domain project ships, takes level 2 from 264 to 158 without touching a single call site. Twenty-seven entity types across the module and its submodules, one block. What is left at level 2 after it: 117 method.notFound, 14 property.notFound, 4 property.nonObject, 4 phpDoc.parseError, 2 arguments.count, 2 return.phpDocType, 1 class.notFound. Level 3 then adds 52 return.type, 22 offsetAccess.notFound and 11 method.childReturnType, which is where the real findings are: a docblock promising a type the code cannot return is a bug waiting for a caller to trust it.

What this did. The level lives in a shipped phpstan.neon rather than in _PHPSTAN_LEVEL, because the job reads a project's own config automatically and a contributor's local run then reads the same file: one answer instead of two that can disagree. That file carries the level, the entity mapping, the file extensions the templates' asset scanned, and the one new static ignore Drupal needs. No baseline and no other ignoreErrors, because a baseline is a level nobody meets, recorded. The gate went to level 3 in one pass rather than a step at a time: the mapping cleared most of the way and what was left was small enough to read through in one sitting.

One finding is already paid for. Level 2 is what caught a synonym sweep on #3618603: Pre-alpha3 audit: give the calendar day grid an accessible name and state, throttle every path that places a hold, and refresh the figures, vocabulary and pages the last seventy commits moved rewriting \Symfony\Component\HttpFoundation\Session\Session to Slot\Slot in four docblocks, which level 1 and level 0 both passed. That is the argument for the climb in one line: the levels above 1 see the things a reader does not.

What the levels found that a reader had not. Thirteen entity interfaces did not declare the changed time their entity tracks, though every one of them uses EntityChangedTrait and nine of the twenty-two already said so. TicketBuilder handed a readonly property to alter()'s by-reference context argument, where PHP passes a temporary, so anything a hook wrote back to it was dropped without a word. FieldRuleRegistry::options() declared string keys for options a boolean rule keys 0 and 1, which PHP holds as ints, so no caller could reach either. An order form memoized its extra columns under a docblock naming two of the three keys the row builder reads. A confirm form cast four TranslatableMarkup values to string to satisfy a return type, throwing away markup the page title wants. Two services typed their dispatcher as PSR-14, whose dispatch() takes one argument, and passed two. Four queue loops read ->data off claimItem(), which is declared object|bool. A test passed an explanation as a third argument to a two-argument assertion, so it was discarded in silence. Three array shapes in doc comments never parsed, so those types were not being checked at all.

AI-Generated: Yes (Claude Code was used to help draft this issue summary and to write the code on the merge request. The figures in it were measured on the heads named. I reviewed both before posting.)

Issue fork yoyaku-3618610

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

mably created an issue. See original summary.

mably’s picture

Status: Active » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

mably’s picture

Status: Fixed » Active

mably’s picture

Status: Active » Needs review
mably’s picture

Title: Raise the phpstan gate past level 1, starting with an entity mapping so the engine own methods resolve » Raise the phpstan gate to level 3, shipping a phpstan.neon with an entity mapping so an entity's own methods resolve
Issue summary: View changes

  • mably committed 6dba8649 on 1.x
    task: #3618610 Raise the phpstan gate to level 3, shipping a phpstan....
mably’s picture

Issue summary: View changes
Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.