Mago 1.19.0
Mago 1.19.0 is a major quality release with 20+ bug fixes, 10 new linter rules, significant performance improvements to the database layer, and several new features. Highlights include sweeping improvements to loop type inference (while/for/foreach), proper isset() scoping so sub-expressions are still checked, array_filter and array_column return type preservation for shaped arrays, resolution of self:: references in class constant inference, detection of never-returning calls in switch cases, and 6 new linter rules including missing-docs, no-parameter-shadowing, and no-array-accumulation-in-loop. The database layer received SIMD-accelerated line counting and several allocation optimizations.
✨ Features
Analyzer
@experimentalusage detection: Warn when calling functions, methods, or classes marked with the@experimentaldocblock tag- Wildcard type support: Added support for
_and*wildcard types in generic type argument positions (#1571) - Incorrect casing detection for class-likes and functions: Warn about incorrect casing when referencing class-likes and functions
count()assertion negatable:!count($x)now correctly narrows the array to empty
Linter
missing-docsrule: Enforces documentation on public API elements (#1585)no-parameter-shadowingrule: Detects function parameters that shadow variables from an outer scopeno-array-accumulation-in-looprule: Flags array accumulation patterns inside loops that may cause performance issuesno-iterator-to-array-in-foreachrule: Detectsiterator_to_array()calls that could be replaced with direct iterationsorted-integer-keysrule: Flags arrays with integer keys that are not in sorted orderno-is-nullrule: Suggests replacingis_null($x)with$x === null(#1557)method-namerule: Enforces method naming conventionsconstructor-thresholdoption forexcessive-parameter-list: Configure a separate threshold for constructor parametersno-redundant-readonlydetects promoted properties: The rule now catches redundantreadonlyon constructor promoted properties inreadonlyclasses (#1543)
Formatter
indent-binary-expression-continuationoption: Control indentation of continued binary expressionspreserve-breaking-conditionsoption: Preserve line breaks in conditions as authored (#1222)
Guard
- Brace expansion in pattern matching: Guard patterns now support
{a,b,c}brace expansion syntax
CLI
- Baseline JSON schema:
mago config --schema --show baselineoutputs the JSON schema for baseline files, enabling IDE integration and validation (#986)
Playground
- Sync all analyzer settings: The playground now exposes all analyzer configuration options with improved responsiveness
🐛 Bug Fixes
Analyzer
- Fixed loop type inference for by-reference mutations: Removed the
types_share_categoryheuristic and fixed the underlying inference — the combiner now absorbs empty arrays into lists, array append forces non-empty, and by-reference mutations propagate across loop passes (#1574, #1575) - Fixed
never-returning calls in switch cases:array_pop-style functions withneverreturn types in switch default branches now correctly mark the branch as terminating, so variables defined in other branches are visible after the switch (#1578) - Fixed false
redundant-conditionin nested loops withcontinue/break: Variables unchanged atcontinuepoints are now recorded so they don't get overwritten by break-path assignments (#1586) - Fixed break-path types leaking into loop between-pass widening: Break-path types (e.g.
$look = nullfrom abreakon null-check) no longer contaminate the next iteration's entry state inwhile(true)loops (#1587) - Scoped
inside_issetto outermost access:isset($arr[$row['key']])now correctly reports issues on the index sub-expression$row['key']—issetonly suppresses checks on the outermost$arr[...]access (#1594) possibly-null-array-indexreported insideisset(): Using a nullable value as an array key is a type-safety issue separate from undefined-key checks, so it's now reported even insideisset()(#1594)- Fixed
??isset semantics for non-variable LHS:func($arr[$key]) ?? $defaultno longer suppresses issues in$arr[$key]— only variable/property/array-access LHS expressions get isset-like treatment (#1594) - Fixed duplicate issues on compound assignments:
$arr[$key] += $valno longer reports the same issue twice on the index sub-expression (#1594) - Fixed false-array-access null propagation:
$row['key']where$rowisarray|falsenow correctly includesnullin the result type (PHP returns null when accessing an index onfalse) (#1592) - Suppressed
possibly-undefined-variableinsideisset():isset($x)is the canonical way to check if a variable exists — no warning about it being undefined (#1603) - Resolved
self::references in class constant inference:const MAP = ['a' => self::A]now correctly infers the referenced constant/enum-case types instead of degrading tomixed(#1602) - Preserved shaped arrays in
array_filterreturn type:array_filteron a shaped array likearray{a: ?string, b: ?int}now preserves the shape with entries marked optional, instead of flattening to a generic array (#1590) - Supported keyed-array shapes in
array_column:array_columnonlist<array{name: string, id: int}>with literal keys now returns the correct shaped result (#1591) - Propagated right-side
&&assignments in while conditions:while (check() && ($row = fetch()) !== false)now correctly narrows$rowin the loop body (#1593) - Detected always-false loose equality on int and bool types:
$intVar == $otherIntwhere the types provably differ is now flagged, matching strict identity behavior for same-category comparisons (#1576) - String flag overlap in identity comparisons:
non-empty-string !== lowercase-stringis no longer falsely reported as always-true — string types with independent constraint flags (casing, non-emptiness) can share concrete values - Skipped by-ref out-type update on
neverarguments: Calling a templated by-reference function on anever-typed argument no longer widens the variable tomixed - Fixed
phpversion()prelude stub: Corrected the return type (#1584)
Linter
- Fixed
braced-string-interpolationfixer for bareword array keys:"$o[user_id]"is now correctly fixed to"{$o['user_id']}"instead of the broken"{$o[user_id]}"which treatsuser_idas an undefined constant (#1588) - Fixed
inline-variable-returnfor return-by-reference functions: Functions declared withfunction &name()no longer get the inline suggestion, which would break the by-reference return contract (#1600)
Guard
- Fixed perimeter rule matching: Uses the most-specific matching rule instead of unioning all matches
CLI
- Fixed
NO_COLORnot disabling linter output colors:NO_COLOR=0 --colors=autonow correctly disables colors for all output, not just the logger (#1599)
Codex
- Fixed private property inheritance: Private properties from parent classes are no longer inherited by child classes (#1558)
⚡ Performance
Database
- SIMD-accelerated line counting:
line_startsnow usesmemchrfor ~4x faster newline scanning (#1581) - Avoided wasteful buffer allocation: File reading no longer pre-allocates based on metadata size (#1580)
- Avoided HashMap rehashing: Database building pre-allocates HashMap capacity (#1583)
- Avoided per-file
canonicalize(): Path exclusion checks use the already-canonicalized workspace root (#1589) - Removed dead
is_canonicalflag: Cleaned up unused path collection bookkeeping (#1596)
Syntax
- Fixed quadratic allocation in
Node::filter_map: Replaced repeatedVec::insert(0, ...)withpush+reverse(#1598)
📖 Documentation
- Baseline JSON schema docs: Added documentation for the new
mago config --schema --show baselinecommand in the baseline guide
🏗️ Internal
- Docblock refactoring: Removed annotation support, added tag metadata, and allowed PHP identifier chars in tag names
- Prelude: Ignored falsable return from
hrtime(), applied clippy fixes
🙏 Thank You
Contributors
A huge thank you to everyone who contributed code to this release:
- @dotdash — #1580, #1581, #1583, #1589, #1596, #1598
- @jameslkingsley — #1585
- @edsrzf — #1557
- @matthewnessworthy — #1222
Issue Reporters
Thank you to everyone who reported issues that shaped this release:
- @UweOhse — #1574, #1575, #1576, #1578, #1584, #1586, #1587, #1588, #1593, #1594, #1600, #1603
- @zip-fa — #1590, #1591, #1602
- @llaville — #1599
- @djschilling — #1547
- @nikophil — #1543
- @innocenzi — #1533
- @UweOhse — #1538
- @AJenbo — #1571
- @gennadigennadigennadi — #902
Full Changelog: 1.18.1...1.19.0