Skip to content

DRAFT: Raise the modules' PHPStan level from 0 to 1 - #1513

Closed
austinderrick wants to merge 7 commits into
wintercms:wip/1.3from
austinderrick:feat/modules-phpstan-level-1
Closed

DRAFT: Raise the modules' PHPStan level from 0 to 1#1513
austinderrick wants to merge 7 commits into
wintercms:wip/1.3from
austinderrick:feat/modules-phpstan-level-1

Conversation

@austinderrick

@austinderrick austinderrick commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Stacked on #1512 — review only the last commit until that merges.

Summary

Raises the modules' PHPStan level from 0 to 1, which adds undefined-variable, unknown-method and argument-count analysis. The first run reported 180 errors; every one is accounted for below, and the analysis finishes clean with no baseline.

Real defects fixed

  • CmsException took the wrong branch for unknown error codes. The switch over the code has no default case, so $result stayed undefined, and null !== false passed the is-CMS-exception check that should have failed. It now starts as false.
  • MediaLibrary passed undefined $type and $key into library item construction for directory contents that are neither files nor folders. Those entries are now skipped.
  • Config::package() and the FileManifest constructor were each invoked with a vestigial extra argument that PHP silently discards.
  • Variables read while possibly undefined on edge paths, now initialized: $success in AutoDatasource, $result in Backend\Controller, $branch in UpdateManager::requestChangelog(), $resizer in SystemController's catch path, and $pluginId in PluginManager's catch blocks.
  • An unused closure import in BundleManager.

Magic contracts documented

  • The settings models (BrandSetting, EditorSetting, Preference, MailSetting, MailBrandSetting, LogSetting, MaintenanceSetting) carry @mixin and @method annotations for the SettingsModel behavior's API. The attachOne relations on ImportModel and ThemeImport, the Halcyon builder forwards on Page and CmsCompoundObject, and Parameter's applyKey scope are annotated where used.
  • Model attributes resolve through magic accessors, declared with universalObjectCratesClasses for the two model base classes. Typing attributes per model is the natural next ratchet rather than a level-1 gate.
  • A stub file corrects the Event facade's listen() arity: Storm's dispatcher accepts a listener priority that the resolved Laravel signature does not.
  • View partials inside model directories are excluded from analysis, matching the phpcs carve-outs.

Two scoped ignores, with reasons in the config

Winter's extendable dispatch defeats annotation in exactly two places, and both carry comments in phpstan.neon.dist rather than baseline entries: the SettingsModel behavior's get($key, $default) collides with the query builder's get($columns) arity when larastan resolves model statics, and parent::resetDefault() reaches the behavior through __call, which analysis cannot follow through a parent reference.

Analyses the classes and models of the three modules. Winter's modules are
not composer-autoloaded and extend runtime class aliases, so a small
bootstrap registers the alias map from modules/system/aliases.php and the
module directories are scanned for symbol discovery.

The first run surfaced real defects, fixed here:

- ThemeExport and ThemeImport built their not-intended-to-be-saved exception
  message with a broken sprintf placeholder ("The % model"), so the model
  class name never appeared in the message.
- FilterScope declared every configurable property except $default, which
  was created dynamically on assignment (deprecated since PHP 8.2).
- Eleven docblocks promised a return value on paths that return nothing, or
  the wrong type entirely: FormTabs::getIcon() and getPaneCssClass(),
  WidgetBase::render(), CmsObject::save(), ComponentManager's
  registerComponents() and makeComponent(), Router's findByUrl() and
  setParameters(), the four yaml-backed PluginBase register methods, and
  UpdateManager's downloadPlugin() and downloadTheme(), which claim to
  return self but return nothing (no caller chains on them).
  CombineAssets::getDeepHashFromAssets() claimed void while returning the
  hash string its only caller concatenates.

The baseline carries three deliberate entries: BundleManager's setup handler
calls (the closures are rebound to the console command with Closure::bind, so
the methods exist at runtime), the new.static warnings on non-final
constructors, and CodeParser::handleCorruptCache(), which is a real defect
with a fix already in flight in wintercms#1511.
The workflow now installs with --no-scripts and resets the working tree the
same way tests.yml does, since composer/installers replaces the checked-out
modules with the packaged copies during install. The bootstrap gains an
autoloader for the modules' class loader convention, so alias targets resolve
without depending on how composer happened to install the module packages.
The lock file is not tracked, so the dependency costs one composer.json line,
and storm already maintains the same setup. Larastan's rules immediately paid
their way: two vestigial single-argument with() wrappers in UpdateManager and
VersionManager, replaced with the direct (new ...)->render() call the syntax
has supported since PHP 8.0. The baseline is unchanged.
…line

- The bootstrap now registers Winter's own ClassLoader for the modules,
  mirroring modules/system/tests/bootstrap/app.php, instead of a hand-rolled
  approximation of its convention.
- checkout@v7 and a 2G memory limit in the workflow job.
- One more baseline entry resolved: CodeParser::handleCorruptCache() declared
  @return void while returning the repaired cache data on every path, which
  is what made its caller's use of the result look like a defect. The
  BundleManager entries were investigated rather than assumed: typing $this
  inside the handlers converts the errors to protected-method violations,
  because the closures' static scope stays BundleManager while the runtime
  rebinding through Closure::call() is what legitimizes the access, so those
  stay baselined.
Both remaining categories had proper answers after all:

- @param-closure-this on registerSetupHandler() and registerScaffoldHandler()
  declares what Closure::call() does at runtime, resolving the fourteen
  undefined-method reports inside the handlers and giving handler authors a
  typed $this in the bargain.
- @phpstan-consistent-constructor on the seven classes using new static()
  documents the constructor contract those factories already rely on, without
  the API change final constructors would be.

PHPStan now reports zero errors with no ignores anywhere.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cbf8ab19-3ba2-4178-8f4a-844e2e81b5c4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Halcyon's inherited update() propagates save()'s return value, and Model::save()
answers bool, so swallowing the result here made every successful CmsObject
update indistinguishable from a vetoed one: callers checking the documented
bool always received null. Returning the parent result restores the contract
the docblock now correctly states. throwHalcyonSaveException() gains a
@return never annotation; every branch of it throws.
Level 1 adds undefined-variable, unknown-method and argument-count analysis,
which surfaced 180 errors. Every one is accounted for:

Real defects fixed:
- CmsException took the wrong branch for unknown error codes: the switch has
  no default, so $result stayed undefined and null !== false passed the
  is-CMS-exception check. It now starts false.
- MediaLibrary passed undefined $type and $key into item construction for
  directory contents that are neither files nor folders; those entries are
  now skipped.
- Config::package() and the FileManifest constructor were invoked with a
  vestigial extra argument PHP silently discards.
- Uninitialized variables read on edge paths: $success in AutoDatasource,
  $result in Backend\Controller, $branch in UpdateManager, $resizer in
  SystemController, and $pluginId in PluginManager's catch blocks.
- An unused closure import in BundleManager.

Magic contracts documented:
- The settings models carry @mixin and @method annotations for the
  SettingsModel behavior's API, and the attachOne relations, Halcyon builder
  forwards and the applyKey scope are annotated where used.
- Model attributes resolve through magic accessors, declared via
  universalObjectCratesClasses for the two model base classes; typing them
  per model is the next ratchet, not a level-1 gate.
- A stub corrects the Event facade's listen() arity (storm accepts a
  priority).
- View partials inside model directories are excluded, matching phpcs.

Two narrowly scoped, commented ignores remain where Winter's extendable
dispatch defeats annotation: the SettingsModel get($key, $default) arity
collision with the query builder, and parent::resetDefault() reaching the
behavior through __call.
@austinderrick
austinderrick force-pushed the feat/modules-phpstan-level-1 branch from 3934c6b to dd97c22 Compare August 14, 2026 03:46
@austinderrick
austinderrick marked this pull request as draft August 14, 2026 03:46
@austinderrick austinderrick changed the title Raise the modules' PHPStan level to 1 DRAFT: Raise the modules' PHPStan level to 1 Aug 14, 2026
@austinderrick austinderrick changed the title DRAFT: Raise the modules' PHPStan level to 1 DRAFT: Raise the modules' PHPStan level from 0 to 1 Aug 14, 2026
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