Mago 1.22.0
Mago 1.22.0 is a small, high-impact release focused on robustness and ergonomics. The analyzer no longer panics on a combine-empty-Vec edge case in keyed-array parameter derivation, diagnostics preserve the source casing of class/function/method names, and the heredoc/nowdoc lexer accepts the full identifier alphabet when detecting closing labels. On the feature side, the strict-behavior linter rule ships an auto-fix, mago init seeds check-throws with sensible Error / LogicException defaults, the docblock type parser relaxes a handful of common tolerant forms, and the stale-baseline warning now reports the exact dead-entry count.
✨ Features
Linter
strict-behaviorauto-fix: The rule now attaches aPotentiallyUnsafefix that inserts, strict: true(orstrict: truewhen a trailing comma is already present) before the closing paren of calls toin_array,array_search,base64_decode, andarray_keysthat rely on loose comparison. Not offered whenallow_loose_behavioris enabled or when the call already has an explicit non-truestrict:value, so deliberate opt-outs stay intact (#1656)
Init
- Seed
unchecked-exceptionswithErrorandLogicException:mago initnow writescheck-throws.unchecked-exceptions = ["Error", "LogicException"]into the generatedmago.toml. These classes represent programmer errors (assertion failures, logic flaws) that should surface during development rather than be caught and recovered from, matching how users treat them in practice (#1661)
Type Syntax
non-zero-intkeyword: New type keyword that lowers tonegative-int | positive-int, commonly used to express "any non-zero integer".- Reserved keywords as class-constant member names:
Foo::NULL,Foo::ARRAY,Foo::INT, and other non-hyphenated reserved keywords now parse as valid class-constant references in type positions after::, matching what PHP itself accepts. - Trailing
|in unions: A dangling pipe at the end of a union (common in hand-written or machine-concatenated docblocks) now parses asType::TrailingPipeinstead of erroring; the docblock tag splitter no longer swallows the whitespace after such an operator. FOO_*/*_BARglobal-constant wildcards: Global-constant wildcard patterns now parse asType::GlobalWildcardReference, lowered to a newTReference::Globalselector that resolves against global constants. Coversint-mask-of<*_BAR>/value-of<FOO_*>idioms.
Reporting
- Dead-entry count in stale-baseline warning: When
mago lint/mago analyzedetects that the baseline file contains entries for issues that no longer exist, the warning now reports the exact count instead of a generic "contains entries for issues that no longer exist" message, so you know how many lines are safe to drop (#1662)
🐛 Bug Fixes
Analyzer
- Guard against empty key union in
TArray::Keyedparameters:get_array_parameterstripped acombine() received an empty Vecdebug assertion when a keyed array'sparametersheld an empty key union andknown_itemswas absent. Reachable through the intermediate shape produced by a narrowing read of a foreach key that is then used to write into a sibling-keyed empty array (['attrs' => []]→$p['attrs'][$name]). TheKeyedarm now mirrors theListarm's safety push ofTAtomic::Neverin the empty case - Preserve source casing of class / function / method names in diagnostics: Diagnostics previously lowercased symbol names because the analyzer carried them around as atoms normalised to lowercase. A method reported as
app\http\controllers\usercontroller::getprofilewas actually declared asApp\Http\Controllers\UserController::getProfile. The renderer now re-derives the original casing from the source file when emitting issue messages and annotations (#1660)
Codex
- Never panic on empty unions in
combine()or filter helpers: Two additional sites that could construct an empty atomic vector before callingcombine()(the combiner itself and the union-filter helper) now return a well-formednever-typed union instead of hitting thedebug_assert!. Belt-and-braces hardening around the same class of bug the analyzer fix above addresses
Syntax
- Heredoc/nowdoc closing-label terminator accepts full identifier alphabet: The lexer used
is_ascii_alphanumeric()to decide whether a byte following a potential closing label was "still part of the label", which incorrectly excluded_and high-bit UTF-8 bytes. A heredoc whose closing label was followed by a character in either of those classes would be mis-terminated. The check now usesis_part_of_identifier, matching PHP's actual identifier lexing rules
🙏 Thank You
Issue Reporters
Thank you to everyone who reported issues that shaped this release:
- @celestialaly — #1656
- @theofidry — #1660, #1661, #1662
Full Changelog: 1.21.1...1.22.0