Upgrade PHPStan to 2.x and raise analysis level from 6 to 10 #46

Merged
thatguygriff merged 1 commits from chore/phpstan-2-upgrade into main 2026-06-12 16:54:57 +00:00
Owner

Summary

Upgrades static analysis from PHPStan 1.12 (level 6) to PHPStan 2.2.2 at level 10 — the strictest setting — with every finding fixed for real rather than suppressed (3 targeted, justified phpcs:ignore comments are the only exceptions, where the WPCS sniff structurally cannot resolve closures/static calls).

Dependency changes

  • phpstan/phpstan ^1.10^2.0 (2.2.2), szepeviktor/phpstan-wordpress ^1.3^2.0
  • Analysis level now lives only in phpstan.neon (was duplicated in the composer script)

SQL-injection hardening (the big win)

All repository queries now pass table names through wpdb::prepare()'s %i identifier placeholder instead of string interpolation, so every query string is a compile-time literal and identifiers are escaped by WordPress. This raises the minimum WordPress version from 6.0 to 6.2 (where %i was introduced).

New: Val boundary-coercion helper

Level 9/10 forbids using mixed directly, and WordPress's APIs (wpdb, WP_REST_Request::get_param(), superglobals, get_option()) all return mixed. src/Val.php narrows those values with explicit runtime checks (is_numeric, is_scalar) instead of blind casts — unexpected shapes degrade to safe defaults instead of leaking garbage. Value objects, endpoints, and controllers route boundary reads through it. Covered by unit tests.

Real bugs fixed along the way

  • get_permalink() can return false — was passed unguarded to wp_login_url() in two shortcode pages
  • strtotime() can return false — was passed unguarded to gmdate() in the payment report
  • wpdb::prepare() can return null — was passed unguarded to wpdb::query() in updateTax()
  • get_users() results are now narrowed to WP_User before mapping
  • Dead ?? '' fallbacks removed where isset() was already checked

Verification

  • composer lint — PHPStan level 10, no errors
  • composer test — 221 tests, 681 assertions, all passing
  • composer cs — PHPCS clean

🤖 Generated with Claude Code

## Summary Upgrades static analysis from PHPStan 1.12 (level 6) to **PHPStan 2.2.2 at level 10** — the strictest setting — with every finding fixed for real rather than suppressed (3 targeted, justified `phpcs:ignore` comments are the only exceptions, where the WPCS sniff structurally cannot resolve closures/static calls). ### Dependency changes - `phpstan/phpstan` `^1.10` → `^2.0` (2.2.2), `szepeviktor/phpstan-wordpress` `^1.3` → `^2.0` - Analysis level now lives only in `phpstan.neon` (was duplicated in the composer script) ### SQL-injection hardening (the big win) All repository queries now pass table names through `wpdb::prepare()`'s `%i` identifier placeholder instead of string interpolation, so every query string is a compile-time literal and identifiers are escaped by WordPress. **This raises the minimum WordPress version from 6.0 to 6.2** (where `%i` was introduced). ### New: `Val` boundary-coercion helper Level 9/10 forbids using `mixed` directly, and WordPress's APIs (`wpdb`, `WP_REST_Request::get_param()`, superglobals, `get_option()`) all return `mixed`. `src/Val.php` narrows those values with explicit runtime checks (`is_numeric`, `is_scalar`) instead of blind casts — unexpected shapes degrade to safe defaults instead of leaking garbage. Value objects, endpoints, and controllers route boundary reads through it. Covered by unit tests. ### Real bugs fixed along the way - `get_permalink()` can return `false` — was passed unguarded to `wp_login_url()` in two shortcode pages - `strtotime()` can return `false` — was passed unguarded to `gmdate()` in the payment report - `wpdb::prepare()` can return `null` — was passed unguarded to `wpdb::query()` in `updateTax()` - `get_users()` results are now narrowed to `WP_User` before mapping - Dead `?? ''` fallbacks removed where `isset()` was already checked ### Verification - `composer lint` — PHPStan level 10, no errors - `composer test` — 221 tests, 681 assertions, all passing - `composer cs` — PHPCS clean 🤖 Generated with [Claude Code](https://claude.com/claude-code)
thatguygriff added 1 commit 2026-06-12 16:53:22 +00:00
Upgrade PHPStan to 2.x and raise analysis level from 6 to 10
CI / No Debug Code (pull_request) Successful in 3s
CI / Tests (PHP 8.2) (pull_request) Successful in 48s
CI / Tests (PHP 8.3) (pull_request) Successful in 52s
CI / Coding Standards (pull_request) Successful in 57s
CI / Tests (PHP 8.1) (pull_request) Successful in 1m1s
CI / PHPStan (pull_request) Successful in 1m11s
CI / Build Plugin Zip (pull_request) Has been skipped
1d6ac46ba3
- Bump phpstan/phpstan ^2.0 and szepeviktor/phpstan-wordpress ^2.0
- Move the analysis level into phpstan.neon (single source) and raise it to 10
- Add Val, a runtime coercion helper that narrows untyped WordPress boundary
  values (wpdb rows, REST params, superglobals, options) with explicit checks
  instead of blind casts, plus unit tests
- Type value-object fromRow() params as stdClass (what wpdb returns) and map
  columns through Val so unexpected shapes degrade safely
- Use %i identifier placeholders for table names in all wpdb::prepare() calls
  so every query string is a literal and identifiers are escaped by WordPress;
  raises the minimum WordPress version to 6.2 where %i was introduced
- Guard wpdb::prepare() null result before wpdb::query() in updateTax()
- Fix nullable get_permalink()/strtotime() handling, list types at REST and
  capability call sites, dead null-coalescing on checked superglobals, and
  narrow get_users() results before mapping
- Register Val method names with the ValidatedSanitizedInput sniff so it
  validates the real sanitizer around each superglobal read
- Update repository unit tests for the %i placeholder arguments

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
thatguygriff merged commit aea731c2f8 into main 2026-06-12 16:54:57 +00:00
thatguygriff deleted branch chore/phpstan-2-upgrade 2026-06-12 16:54:57 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Unsupervised/unsupervised-scheduler#46