Mago 1.25.0
Mago 1.25.0 is a big release. Seven new linter rules (no-dead-store, no-redundant-variable, no-redundant-else, no-negated-ternary, no-unused-static, no-unused-global, no-unused-closure-capture), default values on @template parameters, and a config extends directive for shared mago.toml inheritance. The analyzer picks up an opt-in pipe-callable hint relaxation, branch-discriminator narrowing, integer-bound widening in loop fixpoints, and a long list of correctness fixes around shapes, by-reference params, nullsafe scope, and finally state. Performance got serious attention: CodSpeed CI is wired up, hot allocations are gone from the comparator, populator, algebra, and syntax crates, and type-syntax now arena-allocates its AST.
A new feature-gated mago language-server subcommand ships as an unstable preview behind --features language-server, and the install script now auto-verifies release attestations through gh (with --always-verify and --no-verify escape hatches) for teams that want supply-chain checks at install time.
✨ Features
Analyzer
- Default template types: support default values on
@templateparameters in docblocks. (#899, 9d592d2) - Pipe callable hint relaxation:
allow-implicit-pipe-callable-typesskips type-hint checks on|>callables. (#1634, 67d1736) - Boundary-widen literal types at property assignments: tightens shape inference when properties are mutated. (86ebd89)
- Warn on list-destructuring with string or negative integer keys: catches mismatches between target shape and source. (#1740, 85587dc)
- Detect array append after
PHP_INT_MAXkey: flags$arr[]once the implicit key would overflow. (8a47607) - Narrow through if/else branch-discriminator booleans: improves narrowing on
if ($flag) {} else {}patterns. (9828771) - Expose
AnalysisArtifactsandanalyze_with_artifacts: orchestrator API for embedders that need per-expression types. (79e332a)
Linter
no-dead-storeandno-redundant-variable: writes whose values are never read, and obvious aliases. (#938, fce1b6e)no-redundant-else: warns onelseblocks after a return/throw/etc. (#1705, dca1497)no-negated-ternary: rewrites!cond ? a : btocond ? b : a. (#1721, 39ca373)no-unused-{static,global,closure-capture}: three rules covering unread captures and unused declarations. (#1739, 3547a92)
CLI
mago format --stdin-filepath: route stdin through the right configuration and respect excludes. (#1726, c103de0)- Config
extendsdirective: shared base configurations across projects via a per-file include chain. (#1173, 485ff4a) - Log resolved baseline paths under
MAGO_LOG=debug: makes baseline troubleshooting visible. (#1694, 1d51d2e)
Reporting
- Precise begin/end positions in GitLab format: per-issue ranges instead of synthesised line spans. (#1723, d5f8ed4)
Prelude
Install
- Auto-verify release attestations via
gh: install script verifies on auto-detect, with--always-verifyand--no-verify. (cd4cf4d)
Language server (unstable preview)
mago language-server: feature-gated unstable LSP behind--features language-server. (8c3b589)- Wire
mago.tomlinto the LSP: linter / analyzer / formatter / source config flow through the language server. (b027fe2)
🐛 Bug Fixes
Analyzer
- Re-analyze global-scope files when dependencies change: incremental analysis no longer skips affected globals. (#1735, 6a144f0)
- Propagate parent class docblock changes: subclasses re-analyze when their parent's docblock changes. (#1708, 0730075)
- Seed finally scope with combined try state: avoids stale post-try locals leaking into
finally. (#1742, ec4e1db) - Widen integer bounds independently in loop fixpoint: prevents premature convergence on co-varying bounds. (#1729, 802fc74)
- Dedupe method targets via require-extends intersection: removes duplicate diagnostics on intersected method calls. (#1727, e137325)
- Narrow through low-precedence
and/or: typing now follows assignment-style boolean operators. (218a4fa) - Detect null assignment to non-nullable typed property: catches
$obj->prop = nullon declared non-nullable. (1f95315) - Flag arithmetic-on-zero and shift-by-negative: surfaces guaranteed-runtime-error operands at compile time. (e98e879)
- Compose
+operator on array shapes per PHP semantics: array union now matches PHP's left-key-wins behaviour. (73a947a) - Suppress alt-arm template violations on callable unions: stops false positives when only one branch satisfies. (#1722, eacaa33)
- Invalidate superglobals and
global-declared vars across calls: model state changes a callee can perform. (#1712, d279a7a) - Widen known items on dynamic-key array assignments: avoids false positives when the key isn't a literal. (#1709, d848132)
- Scope conflicting-clause clear to fresh assignment vars: prevents over-broad invalidation across statements. (#1707, 5b4be9f)
- Reset conflicting-clause flags between statements: same family, applied at statement boundaries. (#1696, d746e0f)
- Narrow nullsafe bases to non-null in surviving scope:
$x?->ynarrows$xafter the survivor. (#1695, 7d9b9ac) - Stop flagging
$v !== []onmixedas redundant: that comparison is meaningful when$vcould be an array. (#1703, 67be0a0) - Infer non-empty-string from
sprintfwith non-empty args: tightens return type when args are statically non-empty. (#1688, c3a422e) - Preserve declared enum-like by-ref param types: by-reference parameters keep their narrow declared types. (#1693, a9bf5bf)
- Model
array_mapnull-callback passthrough and zip: null-callback variant returns zipped pairs. (fba9d06) - Preserve user-declared narrowings through generic by-ref calls: outer narrowings survive through generics. (4c360c0)
- Clarify redundant-type-comparison wording for positive assertions: message distinguishes
===from!==. (8bf96d3)
Codex
- Reject string callables passed to Closure parameters:
'strlen'cannot match aClosure(...)parameter. (d1050f3) - Saturate array-shape offset increment past
i64::MAX: prevents overflow on absurdly long fixed-shape arrays. (d5fdbbe) - Drop
is_callablewhen widening callable-string with literals: avoids spurious callable-string contamination. (bfc8ed3) - Float scrape order-independent in the combiner: deterministic union of float literals regardless of input order. (0a1c837)
- Register type aliases before resolving
@templateconstraints: aliases defined later in the file resolve correctly. (#1691, fb0b796) - Honor resource open/closed state in truthiness checks: closed resources are truthy in PHP. (#1706, a648361)
- Preserve declared known keys when combining with spread shape: spread no longer wipes explicit keys. (#1704, bb41160)
- Flag strict callables with too few params:
Closure(int): voidrequires the callable accepts the int. (#1700, 01a128b) - Bound
Literal%Frommodulo by|dividend|: correct modulo bounds againstint<min, max>divisors. (#1701, c11c9bf) - Preserve
i64::MINin scanner's unary-negation inference:-(-9223372036854775808)no longer overflows. (#1702, c9776e9) - Enforce template variance soundness: widening at construction and
@varwidening for invariant generics. (f223d4a)
Linter
- Skip
TRUE/FALSE/NULLin lowercase-keyword under Drupal: Drupal coding standards keep these uppercase. (#1743, 5056e78) - Skip
string-stylewhen concat contains non-interpolable expressions: avoids fixers that change runtime behaviour. (#1738, f07f207) - Mark
str-starts-withannotation as primary: diagnostic location lands on the actual call site. (86724f8)
Formatter
- Force breaks at logical ops only when condition must break: comparison operators stay inline; only
&&/||split. (#1744, f63eba1) - Converge corpus idempotency on a single pass: format-then-format-again produces identical output. (715db54)
- Make keyed-array and logical-chain formatting idempotent: stops oscillation between two layouts. (cfe060f)
Syntax
- Apply recursion limit to statements: rejects pathological inputs with a clear error instead of stack overflow. (d952858)
- Handle null byte within string interpolation: lexer no longer aborts on
"\0"inside${ }blocks. (44015e4) - Resolve parser false-positives: cleans up several spurious parse errors found via fuzzing. (374575c)
- Require digits after float exponent marker:
type-syntaxlexer no longer accepts1ewithout an exponent. (3fdbb99) - Stop forcing inline on
type-syntaxlexer/stream helpers: faster builds, no measurable runtime regression. (254b40c)
Prelude
bcmathstub improvements: tightened return types and parameter contracts across the extension. (#1733, 3271032)DateTimeInterface::getTimezone()never returnsfalse: matches the actual contract. (#1687, e3b21f5)- Conditional return for
implodeandjoin: return type narrows when args are statically non-empty. (#1690, 6864306) - Preserve list-shape through
array_pad: pad result keepslist<>rather than collapsing toarray<>. (06bebab) - Tighten
array_chunklength andarray_fillcount to non-zero ranges: catchesarray_chunk($arr, 0). (27fdac7) - Require non-empty arrays for single-arg
min/max:min([])is a runtime error. (2be0a40) - Tighten
implodereturn type for single-element arrays: returns the original element. (becde73)
Reporting
- Fall back to first annotation when issue has no primary: avoids reporting issues with no location attached. (acac500)
📖 Documentation
- GitHub Actions recipe: fix CI short-circuiting and
format --dry-runexample. (#1698, 709988f) - Install script pinning example: point at the commit that shipped
--always-verify. (d02d18d)
🏗️ Internal
Performance
- Hoist container predicates in union comparator: cuts comparator overhead on hot paths. (#1741, 7922a3e)
- Optimise object expansion in codex: fewer allocations on type expansion. (#1731, bc47811)
- Cut allocations in populator hierarchy and method inheritance: lower memory pressure during populate. (#1736, 8c40ac6)
- Reduce allocations in algebra
disjoin/saturate/group_impossibilities: hot CNF paths get tighter. (#1734, 3d2bef2) - Avoid unnecessary allocations and repeated interning in analyzer: less work per analysed expression. (#1732, 220cde2)
- Cache file id and add stream early-return fast paths in syntax crates: lex/parse path becomes lighter. (#1730, 15d9c93)
- Bump arena reset per stub file in prelude: better memory locality during prelude builds. (0996bda)
- Fused span helpers, single-byte lexer dispatch in
type-syntax: faster type-syntax lexer. (57cd833)
Refactoring
- Adopt shared
syntax-coreprimitives: unifySequence,TokenSeparatedSequence,LookaheadBufacross syntax crates. (7b408f0) - Arena-allocate
type-syntaxAST: matches the rest of the parser stack. (8501f0f) Vecinstead ofHashMapfortemplate_variance: smaller and faster for typical sizes. (#1724, d709d84)- Drop dead
type_coerced_to_literalflag fromComparisonResult: removes an unused field path. (7ad0b84) - Replace standin walker with slim
definition_type_replacer: simpler indirection. (e901a11) - Drop the
configcrate: configuration parsing handled directly in the CLI. (aeb224e)
CI / Benchmarks
- CodSpeed continuous performance testing: track regressions on every PR. (#1718, d781033)
- Criterion benches across analyzer, codex, type-syntax, prelude, twig-syntax: scaffold for the perf work. (438bfc9)
- Corpus smoke test: format-then-reformat real-world corpora to surface idempotency regressions. (ed6e3e4)
- Lexer/parser fuzzing for
type-syntax,twig-syntax, andsyntax: cargo-fuzz harnesses for grammar surfaces. (18893d7) - Property-based combiner/comparator tests: 12-axiom suite covering combiner and comparator invariants. (2077d34, 55fb478)
Misc
- Swap
serde_ymlforserde_norway: replaces an unmaintained YAML dependency. (7f46ae8)
🙏 Thank You
Contributors
A huge thank you to everyone who contributed code to this release:
Issue Reporters
Thank you to everyone who reported issues that shaped this release:
- @UweOhse: #1700, #1706, #1707, #1709, #1712, #1739, #1740
- @zip-fa: #1704, #1705, #1727, #1728, #1729
- @MidnightDesign: #1701, #1702, #1703
- @mathroc: #1691, #1742
- @Bleksak: #1693, #1721
- @karoun: #1634
- @ThatOneShortGuy: #1744
- @rimi-itk: #1743
- @nikophil: #1738
- @devnix: #899
- @Dima-369: #1722
- @steffans: #1726
- @maximal: #1723
- @Kenneth-Sills: #1173
- @guvra: #938
- @llaville: #1694
- @gsteel: #1688
- @ddanielou: #1696
- @oruborus: #1695
Full Changelog: 1.24.0...1.25.0