Mago 1.8.0
This release delivers major improvements to the incremental analysis engine for watch mode, new type narrowing capabilities, return type providers for filter_var() / filter_input(), and a large number of bug fixes across the analyzer, linter, formatter, and type system.
✨ Features
Analyzer
is_a()andis_subclass_of()type narrowing: The analyzer now narrows types after calls tois_a()andis_subclass_of(), including support forclass-stringparameters (#1102)- Return type providers for
filter_var()andfilter_input(): These functions now return precise types based on the filter and flags arguments (e.g.,FILTER_VALIDATE_INTreturnsint|false,FILTER_VALIDATE_EMAILwithFILTER_NULL_ON_FAILUREreturnsstring|null) (#1117) - Discriminated union narrowing: When narrowing a union of keyed arrays (e.g.,
array{valid: true, result: string}|array{valid: false, errorCode: string}), the analyzer now correctly filters out incompatible variants based on the narrowed key type, instead of blindly overwriting all variants. This also works for object property narrowing on union types (#1093)
Linter
no-issetarray access ignore option: Theno-issetrule now supports anignore-array-accessoption, allowing you to flagisset($var)while still permittingisset($array['key'])for array offset checks (#1097, #1120) by @dotdash
Semantics
- Enforce parentheses for immediately invoked closures: The semantics checker now flags
function() { ... }()as error, requiring parentheses around the closure for immediate invocation (#1118)
⚡ Performance
Incremental Analysis Engine
The watch mode (mago analyze --watch) received a complete overhaul of its incremental analysis pipeline:
- Signature-only fingerprinting: Body-only changes (e.g., changing a function implementation without modifying its signature) now skip cascade invalidation, resulting in significantly faster re-analysis cycles
- Targeted O(dirty) repopulation: Only changed symbols are re-populated, skipping safe symbols entirely
- Incremental codebase patching: New
extend_refandremove_entriesoperations allow fine-grained metadata updates without rebuilding the entire codebase - Safe symbol restoration: The reference graph now supports restoring safe symbols and targeted cleanup, reducing unnecessary re-analysis
- Body-only docblock resolution: Fixed a bug where body-only changes left docblock type references unresolved, causing spurious
non-existent-class-likeerrors in watch mode - Improved file watcher stability: Better debounce handling, stability checks, and explicit path handling for the file watcher
- Watch mode is no longer experimental: The experimental warning has been removed
🐛 Bug Fixes
Analyzer
require-extends/require-implementsresolution: Members from@require-extendsand@require-implementstypes are now correctly resolved (#1064, #1070)- Unused property false positive with trait overrides: Properties that override trait properties via constructor promotion are no longer incorrectly flagged as unused (#1119)
- FQN literal constants: Fully-qualified constant accesses
\true,\false, and\nullare now correctly recognized (#1099, #1100) by @kzmshx - Class-level template parameters for static calls: Template parameters defined at the class level are now properly resolved when making static method calls on generic types (#1103)
- Abstract method compatibility checking: The
get_substituted_methodfunction is now correctly applied to the child method when checking method signature compatibility, fixing false positives with generic abstract method inheritance - Mixin type parameters preservation: Type parameters on mixin types (e.g.,
IteratorIterator) are now preserved during method resolution, fixing incorrect return types (#1106) - Integer narrowing with non-variable expressions: Fixed incorrect narrowing when comparing integers against non-variable expressions like function calls (#1088)
- For-loop condition narrowing: Integer literals in loop conditions (e.g.,
for ($i = 0; $i < 10; $i++)) are now properly extracted from the AST for type narrowing (#1089) - Redundant type comparison in OR conditions: Fixed false positive
redundant-type-comparisonwhen using count checks or string narrowing in||conditions (#1112) - List count with unknown size:
HasAtLeastCountassertions no longer incorrectly set an exactknown_counton lists with unknown count, preventing falseunreachable-codereports (#1104) - Array spread with unknown count: Fixed false positive when spreading a list with unknown count into an array literal (#1108)
- Class constant
@vardocblock type: The analyzer now prefers@vardocblock types over inferred types for class constants, fixing cases where properly typed array values stayed asmixed(#1090, #1094)
Codex (Type System)
neveras bottom type:neveris now correctly treated as a subtype of all types inextends_or_implementschecks (#1107, #1109) by @kzmshx- Type alias forward references: All local
@psalm-type/@phpstan-typealias names are now pre-registered before parsing, so aliases can reference each other regardless of declaration order (#1116) - String casing detection: Fixed incorrect
impossible-conditionfalse positives when comparingstrtolower()/strtoupper()results with literals containing non-alphabetic characters (spaces, digits, etc.) (#1086) - Bidirectional TUnion equality: Type union equality checks are now bidirectional, preventing false positives when comparing types with different ordering (#1087)
Linter
no-redundant-usewhole-word matching: Docblock reference checking now uses whole-word matching instead of substring matching, souse Config;is correctly flagged as unused even whenConfigUsageappears in a docblock (#1078)inline-variable-returnwith by-reference assignment: The fixer no longer inlines assignments of by-reference expressions, which would produce invalid PHP (#1114)prefer-early-continuewith non-block body: Fixed the fixer for cases where the loop body is a single statement without braces (#1085) by @chrisopperwall-qz
Formatter
- First-class callable with argument unpacking: First-class callable expressions (e.g.,
$foo(...)) are no longer incorrectly treated as breaking expressions, fixing misformatted output (#1091) - Redundant grouping parentheses: The formatter now correctly removes redundant grouping parentheses in more cases (#1121) by @Michael4d45
- Heredoc span calculation: Fixed incorrect span calculation for heredoc/nowdoc strings, which could cause formatting issues (#1092)
Prelude (Type Stubs)
explode()return type: Corrected to properly returnlist<string>instead ofnon-empty-list<string>when the separator could be empty (#1095)array_slice()return type: Now correctly preserves string keys in the return type (#1096)ldap_sasl_bind()stubs: Updated all arguments except the first to be nullable (#1098)bin2hex()stubs: Improved type definition (#1101) by @veewee
🏗️ Internal
- Removed experimental warning for analyzer watch mode
- New
IncrementalAnalysisServiceencapsulating the full incremental analysis pipeline for watch mode and LSP CodebaseDiff::between()for metadata comparison andmark_safe_symbols()for incremental analysis- Signature-only fingerprint mode for detecting body-only vs. signature changes
- Applied clippy fixes across codex and linter crates
- Re-generated linter documentation
🙏 Thank You
Contributors
A huge thank you to everyone who contributed code to this release:
- @chrisopperwall-qz — #1085
- @dotdash — #1120
- @kzmshx — #1100, #1109
- @Michael4d45 — #1121
- @veewee — #1101
Issue Reporters
Thank you to everyone who reported issues and requested features that shaped this release:
- @bendavies — #1094
- @ddanielou — #1093
- @Dima-369 — #1086, #1087
- @dotdash — #1097, #1118
- @dynasource — #1090
- @erik-perri — #1078
- @karoun — #1103, #1116
- @KorvinSzanto — #1117, #1119
- @kzmshx — #1107
- @marcovmun — #1092, #1114
- @MartkCz — #1091
- @matsuo — #1098
- @mreidel-godaddy — #1095, #1096
- @Nadyita — #1104, #1106, #1108, #1112
Full Changelog: 1.7.0...1.8.0