carthage-software/mago
Mago is an ultra-fast PHP linter, formatter, and static analyzer written in Rust. It helps enforce code quality and consistency with a modern toolchain inspired by Rust, built for reliable checks, formatting, and analysis in PHP projects.
A maintenance release with two themes: tightening generic template inference (the #1859 false-positive cluster) and giving closures + anonymous classes stable, human-readable names. New: three WordPress security rules, regex-based ignore patterns, symlinked-vendor support, and a no-fully-qualified-global-function follow-up that migrates use function imports in one pass. Plus the usual analyzer, formatter, and language-server fixes.
{closure:src/foo.php:12:5} replaces opaque hash identifiers in messages and baselines. (#1872, 8c7751d)[analyzer] ignore entries accept pattern = "..." matched against issue text. (#1866, d769368)nonce-verification, prepared-sql, validated-sanitized-input under the wordpress integration. (#1572, 64ec53e)use function migration: under namespaced = true, bare imported calls rewrite to namespace-prefixed form and the orphan import is dropped. (#1843, e23fdfc)Column<T> matched against IntColumn pins T; nested-container literals still union freely. (#1859, 491374f)redundant-logical-operation on &&-assign: silent on short-circuit assignment idioms like $x === 't' && $y = 1. (#1869, 6f957d6)lowercase-string etc. keep their narrowing. (#1879, 61764f1)[@var](https://github.com/var) from parent class declarations. (#1885, 1e36833)new: preserved for (new Foo())->bar() shapes. (#1875, 4e86fc2)new member access: new Foo->bar() etc. surface a proper diagnostic. (#1875, 4e86fc2)$this pre-declared in variable-usage analysis: stops no-redundant-variable flagging $this reassignment. (#1868, 191902b)prefer-fake-helper inside interpolated strings: report-only, no auto-fix that would produce invalid syntax. (#1870, ce50ef1)[@var](https://github.com/var) names: accepted in identifier positions. (#1888, fafff67)[analyzer].ignore. (#1877, b26ef25)mago.dist discovery restored: distributed config file is picked up again. (#1889, 7b8e8fe)nightly-2026-05-19 / 2026-05-22. (#1867, #1876, cbc2f68, 3b43f33)A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.28.0...1.29.0
A feature release led by sound generic type-checking in the analyzer: diamond merges, variance, bounds, Liskov substitution, and constructor arity are now all enforced. The linter gains five Laravel-focused rules, and the CLI picks up a version subcommand, a configurable version-drift-fail-level, and --remove-outdated-baseline-entries for pruning stale baseline entries. Internally, PHP source handling moves to a byte-native representation, retiring mago-atom in favour of mago-word and mago-bytes. The rest is a batch of analyzer and formatter fixes.
fake(), casts, and validation. (#1838, #1837, #1836, #1834, #1833, 6156709)version subcommand: prints the version, mirroring the --version flag. (#1840, d93d75a)version-drift-fail-level: fail the run on minor or patch version drift, not just major. (#1864, 980dbfa)--remove-outdated-baseline-entries: prunes baseline entries that no longer match any issue. (#1865, d35e9d3)redundant-type-comparison false positives: impossible negated assertions are no longer flagged. (#1842, c52b5bd)mago-atom replaced by mago-word and mago-bytes. (#1862, 2a1b5a9)nightly-2026-05-18. (#1845, #1855, #1858, #1860, #1863, 489e310)A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.27.1...1.28.0
A patch release focused on false-positive cleanup: polyfills now widen builtin availability, ?? on static-local arrays no longer reports as redundant, and the array{} | list{T, ...} merge marks known positions as optional. The Composer package's src/ export-ignore is scoped to root only, and aarch64 Linux release jobs skip tests they can't execute.
array{} | list{T, ...} merge: known list positions become optional so $list[0] ?? null is no longer flagged redundant. (#1830, 5c0dc070)??=: redundant-null-coalesce no longer fires on $static[$key] ??= ... across persistent calls. (#1831, 873d9041)src/ export-ignore scoped to root: only the repo's root src/ is excluded from Composer dist archives. (#1828, a6e2ed0d)nightly-2026-05-13. (#1829, 619a8ca4)A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.27.0...1.27.1
A feature release built around Mago\Available* attributes for version-gating PHP symbols, several formatter additions for attribute ordering and multi-trait use splits under PER-CS/PSR-12, a hierarchy-aware Class::method form for class-initializers, and a namespaced mode for no-fully-qualified-global-function that imports the parent namespace instead of the function. The rest is a batch of analyzer narrowings, a mago lint --only CLI fix, the long-standing PHP 8.4 inline new X()->method() formatter fix, and docs work (playground contrast, version-switch keeping you on the same page).
Mago\Available* attributes: version-gate symbols across codex, analyzer, prelude, and composer. (#1113, 7a59a92d)Class::method in class-initializers: scope an initializer to a class hierarchy via Foo::configure. (#1684, 6fb88c6a)namespaced option on no-fully-qualified-global-function: import the namespace, not the function. (#1804, af492481)no-negated-ternary covers !==, !=, <>: rule now flags negated comparisons beyond !=. (#1825, 629cbe65)no-assign-in-condition while opt-out: optionally ignore while statements. (#1819, 47e3118f)attributes-order + separate-attributes options: control attribute ordering and grouping. (#1820, 0e835ef0)use split: one trait per use statement under PER-CS / PSR-12. (#1807, e06da322)--verify-baseline --reporting-format json emits a structured diff. (#1812, e5d26d61)array_filter / array_all callback narrowing: lift callback assertions onto array values from mixed. (#1801, 60e34a95)array_all unwrap: replaced with let-binding to avoid panicking on unexpected shapes. (ab1792c4)TIndexAccess: now expanded so T[K] resolves through bounds. (#1810, 03791963)no-is-null autofix: clean null === $x / null !== $x output across negation, parens, and double-negation. (#1821, 45f7972a)new X()->method(): emitted when both paren options are off. (#1808, a70b2ef3)use-tabs = true now counts tab columns against print-width. (#1806, a91604ad)--only stops swallowing paths: mago lint -o code path.php now lints path.php instead of treating it as another rule. (#1814, 81cae5ca)ParallelPipeline Debug: includes php_version for clearer diagnostics. (1bfce028)entrypoint override for GitLab pipelines. (#1824, 85164eee)A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.26.0...1.27.0
A feature release with two big analyzer additions: closures, arrow functions, plain functions, and methods now auto-infer if-true / if-false assertions from their bodies, so array_filter($xs, fn ($x) => $x instanceof Foo) narrows the result type without any docblock, and two new opt-in settings (strict-array-index-existence, allow-array-truthy-operand) tighten array-ops semantics. The linter gains prefer-array-spread and a deny mode for yoda-conditions. The formatter picks up inline-single-breaking-value-argument (used in the Tempest preset). On the CLI side, mago init now writes a $schema URL pinned to the running version into the generated config, and the published JSON schema is now versioned. The rest is a large batch of analyzer false-positive fixes and a --retain-code-respecting --fix dispatcher.
if_true / if_false from their body. (#1794, 14269d6)strict-array-index-existence: opt-in; tightens array indexing; deprecates allow-possibly-undefined-array-keys. (#1766, 0e563c9)allow-array-truthy-operand: opt-in setting that suppresses invalid-operand when arrays are used in logical ops. (#1765, 07f6a24)prefer-array-spread rule: detects array_merge(...) calls that can be rewritten as spread. (#1767, bc8d67a)yoda-conditions deny mode: rule gains a deny mode that flags constants on the right of comparisons. (#1785, a4e47e5)inline-single-breaking-value-argument: keeps a lone breaking value argument inline; off by default, on in Tempest. (#1148, 8964575)$schema in mago.toml: configs accept a $schema URL; mago init writes one pinned to the installed version. (#1771, e71e78b)https://mago.carthage.software/<version>/schema.json. (f1e705d)array_filter narrowing via callback: result type is now narrowed using the callback's inferred assertions. (#1794, 14269d6)list-destructure-string-key false positive: suppressed when known items already cover the destructure positions. (#1796, b3b11c5)isset / ?? redundancy on interface fields. (#1786, 9e873b0)void materialization in unions: combining void with another type now produces T|null, not T. (#1787, 47fb42a)redundant-type-comparison after impure [@assert](https://github.com/assert): suppressed for impure or value-returning assert calls. (#1781, #831, 53dad31)::class on undefined classes: now reports non-existent-class-like instead of silently typing as class-string. (#1768, 052e0cd)unset($var) no longer fires possibly-undefined-variable: unset is a write target, not a read. (#1764, 3f0e7f8)for ($i = 0; ; $i++) no longer trips the boundary check. (#1762, c64199b)new $class-string<T>(): preserves T instead of widening to the constraint. (#1760, 64f3df8)!= <falsy>: drops false-positive narrowings that ignored PHP's loose comparison rules. (#1757, 6b45a66)undefined-variable after conditional assignment. (#1759, 659a8d3)Closure named-object cast: now routes through a closure-flagged signature instead of __invoke. (#1770, 068a7eb)no-redundant-yield-from trailing-comma fix: auto-fix no longer leaves a stray , after the inlined element. (#1797, 4b49d01)prefer-static-closure and parent::: closures whose body uses parent:: are no longer suggested as static. (#1783, cf2e944)yoda-conditions and class constants: treats class constants as constant-like for the position check. (#1784, f945b75)--retain-code honored before --fix: filter applied before fix dispatch, so fixes run only for retained codes. (#1782, 35f1a37)column_number at line start: fixes wrong column offsets in gitlab / github / checkstyle / sarif reports. (#1790, 856ceca)Closure(int &$x): void parses correctly in docblocks. (#1772, be2d08f)exif_read_data signature: corrects parameter count and types. (#1758, c3fdb0f)/guide/installation and /guide/getting-started links to point at /latest/en/.... (#1788, 3f29da7)zh/**. (edd0fc3)banner.png for cleaner social cards. (ddbb0f7)/versions.json at runtime. (66f406c)/latest. (4d1f370)Vec-returning helpers; one-pass superglobal narrowing. (f43a5d1)MetadataFlags per file type: extracts the per-file-type metadata flag construction into a single helper. (#1769, 1fdb15f)A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.25.2...1.26.0
This release is a bug-fix pass focused on the linter and analyzer, plus a full rewrite of the documentation site. Several false positives reported against 1.25.1 are fixed (no-redundant-variable, no-dead-store, array_map conditional returns, bitwise ops, [@psalm-param](https://github.com/psalm-param) overrides).
use-item names in ResolvedNames: records imported FQNs so the LSP can resolve hover/go-to-definition on use items. (2cc57ee)array_map conditional returns: resolves the callback's conditional return per element instead of collapsing it. (#1748, fcb81b7)bool true/floats: matches PHP semantics (string XOR/AND/OR, true→1, float→int). (#1755, a8cf8bb)[@psalm-param](https://github.com/psalm-param) priority: applies [@psalm-param](https://github.com/psalm-param)/[@phpstan-param](https://github.com/phpstan-param) after [@param](https://github.com/param) so the more specific docblock wins. (#1756, 51c255e)no-dead-store on foreach targets: exempts conditional overwrites of the loop-variable binding. (#1749, 259f871)no-redundant-variable by-ref closure captures: drops the synthetic write that masked legitimate uses. (#1751, f713125)no-redundant-variable loop bodies: re-walks the condition after the body so writes inside the loop are observed. (#1752, 6d519a3)no-redundant-use on use items: excludes use-item spans from the used-FQN set so the import isn't counted. (2d476c3)$, ->, ::: triggers properly, plus hover/goto resolves through use items. (c839254)Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.25.1...1.25.2
Patch release. Three correctness fixes surfaced while upgrading real codebases on 1.25.0 (an invariant-generic redundant-docblock false positive, a stale narrowing leaking from try into finally, and a no-redundant-variable false positive on loop wraparound writes), plus three previously-orphaned doc pages wired back into the sidebar.
Foo<mixed> no longer reads as identical to Foo<T> in redundancy checks. (863b796)finally inherits try call invalidations: narrowings cleared inside try no longer leak into the finally. (330a41e)no-redundant-variable respects loop wraparound: a write at the bottom of a loop body counts as observed by the top-of-body read. (#1747, 40093a6)generate-completions, format-ignore, and pre-commit-hooks are now reachable. (f8f4394)Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.25.0...1.25.1
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](https://github.com/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.
[@template](https://github.com/template) parameters in docblocks. (#899, 9d592d2)allow-implicit-pipe-callable-types skips type-hint checks on |> callables. (#1634, 67d1736)PHP_INT_MAX key: flags $arr[] once the implicit key would overflow. (8a47607)if ($flag) {} else {} patterns. (9828771)AnalysisArtifacts and analyze_with_artifacts: orchestrator API for embedders that need per-expression types. (79e332a)no-dead-store and no-redundant-variable: writes whose values are never read, and obvious aliases. (#938, fce1b6e)no-redundant-else: warns on else blocks after a return/throw/etc. (#1705, dca1497)no-negated-ternary: rewrites !cond ? a : b to cond ? b : a. (#1721, 39ca373)no-unused-{static,global,closure-capture}: three rules covering unread captures and unused declarations. (#1739, 3547a92)mago format --stdin-filepath: route stdin through the right configuration and respect excludes. (#1726, c103de0)extends directive: shared base configurations across projects via a per-file include chain. (#1173, 485ff4a)MAGO_LOG=debug: makes baseline troubleshooting visible. (#1694, 1d51d2e)memcached extension: full stub coverage for the Memcached PECL extension. (#1692, 9ac7e88)gh: install script verifies on auto-detect, with --always-verify and --no-verify. (cd4cf4d)mago language-server: feature-gated unstable LSP behind --features language-server. (8c3b589)mago.toml into the LSP: linter / analyzer / formatter / source config flow through the language server. (b027fe2)finally. (#1742, ec4e1db)and/or: typing now follows assignment-style boolean operators. (218a4fa)$obj->prop = null on declared non-nullable. (1f95315)+ operator on array shapes per PHP semantics: array union now matches PHP's left-key-wins behaviour. (73a947a)global-declared vars across calls: model state changes a callee can perform. (#1712, d279a7a)$x?->y narrows $x after the survivor. (#1695, 7d9b9ac)$v !== [] on mixed as redundant: that comparison is meaningful when $v could be an array. (#1703, 67be0a0)sprintf with non-empty args: tightens return type when args are statically non-empty. (#1688, c3a422e)array_map null-callback passthrough and zip: null-callback variant returns zipped pairs. (fba9d06)=== from !==. (8bf96d3)'strlen' cannot match a Closure(...) parameter. (d1050f3)i64::MAX: prevents overflow on absurdly long fixed-shape arrays. (d5fdbbe)is_callable when widening callable-string with literals: avoids spurious callable-string contamination. (bfc8ed3)[@template](https://github.com/template) constraints: aliases defined later in the file resolve correctly. (#1691, fb0b796)Closure(int): void requires the callable accepts the int. (#1700, 01a128b)Literal % From modulo by |dividend|: correct modulo bounds against int<min, max> divisors. (#1701, c11c9bf)i64::MIN in scanner's unary-negation inference: -(-9223372036854775808) no longer overflows. (#1702, c9776e9)[@var](https://github.com/var) widening for invariant generics. (f223d4a)TRUE/FALSE/NULL in lowercase-keyword under Drupal: Drupal coding standards keep these uppercase. (#1743, 5056e78)string-style when concat contains non-interpolable expressions: avoids fixers that change runtime behaviour. (#1738, f07f207)str-starts-with annotation as primary: diagnostic location lands on the actual call site. (86724f8)&&/|| split. (#1744, f63eba1)"\0" inside ${ } blocks. (44015e4)type-syntax lexer no longer accepts 1e without an exponent. (3fdbb99)type-syntax lexer/stream helpers: faster builds, no measurable runtime regression. (254b40c)bcmath stub improvements: tightened return types and parameter contracts across the extension. (#1733, 3271032)DateTimeInterface::getTimezone() never returns false: matches the actual contract. (#1687, e3b21f5)implode and join: return type narrows when args are statically non-empty. (#1690, 6864306)array_pad: pad result keeps list<> rather than collapsing to array<>. (06bebab)array_chunk length and array_fill count to non-zero ranges: catches array_chunk($arr, 0). (27fdac7)min/max: min([]) is a runtime error. (2be0a40)implode return type for single-element arrays: returns the original element. (becde73)format --dry-run example. (#1698, 709988f)--always-verify. (d02d18d)disjoin/saturate/group_impossibilities: hot CNF paths get tighter. (#1734, 3d2bef2)type-syntax: faster type-syntax lexer. (57cd833)syntax-core primitives: unify Sequence, TokenSeparatedSequence, LookaheadBuf across syntax crates. (7b408f0)type-syntax AST: matches the rest of the parser stack. (8501f0f)Vec instead of HashMap for template_variance: smaller and faster for typical sizes. (#1724, d709d84)type_coerced_to_literal flag from ComparisonResult: removes an unused field path. (7ad0b84)definition_type_replacer: simpler indirection. (e901a11)config crate: configuration parsing handled directly in the CLI. (aeb224e)type-syntax, twig-syntax, and syntax: cargo-fuzz harnesses for grammar surfaces. (18893d7)serde_yml for serde_norway: replaces an unmaintained YAML dependency. (7f46ae8)A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.24.0...1.25.0
Mago 1.24.0 is mostly an analyzer-correctness release: recursive literal-scalar widening in by-reference out-types, wider mixed-operand detection on templated concatenations, and targeted fixes around isset / array_key_exists, list destructuring, iterable narrowing, and __call on non-final classes. It also adds a prefer-self-return-type linter rule with FQCN-to-use autofix, match-arm => alignment in the formatter, and a service memory fix that takes mago lint --fix --unsafe on Drupal core from ~11.6 GB RSS down to 748 MB.
New: mago-twig-syntax. A lossless Twig 3 lexer, AST, and parser published to crates.io. Not wired into the Mago binary yet: the plan is Twig support across the linter, formatter, and possibly the analyzer (#303), then Blade (#304). Twig is the testing ground for multi-language support; once that plumbing is confident, adding Blade should be much cheaper.
prefer-self-return-type: new rule suggesting self instead of the declaring class name in method return types. (#1442, 9081ce8)use-statement synthesis: no-fully-qualified-global-* fixers now insert a matching use at the top of the file. (#555, #1645, d16e16c)string-style handles single-quoted strings: fixer now converts 'foo' to "foo" when the chosen style requires it. (#1674, b33a57b)=> across arms in match expressions, matching assignment alignment. (#1642, 34e0277)class_exists-guarded stubs as polyfills: user definitions are preferred over guarded fallbacks on merge. (0748989)Iterator on Generator: stub now reflects that Generator implements Iterator. (e115682)ArrayAccess on DOMNodeList: stub reflects the interface exposed by the extension. (7efae17)param-out. (#1678, 45926da)mixed and union-shaped templates as concat operands: T as mixed and mixed-scalar unions now surface mixed-operand. (a55b321)\Foo\bar() no longer misfires on global fallback. (#1680, f61f57d)isset / array_key_exists to false on provably missing keys: fixes inconsistent impossible-condition warnings. (#1679, 434f11e)no-boolean-literal-comparison on synthetic match-arm binaries: fixer no longer produces invalid code for match arms. (#1681, 67ea13e)__call when the class-like is final: non-final classes keep PHPUnit-style magic method calls working. (#1676, 6953e8e)undefined-string-array-index on list destructuring. (#1668, 2f248e8)undefined-string-array-index positives. (#1669, 90f9c5d)class_exists-guarded stubs win. (c3d7eb2)iterable<mixed, V> key to array-key against an array container: fixes over-wide key propagation. (ed712ba)& reference prefix outside legal positions: catches previously-accepted syntax errors. (#1683, 53800a6)[[$x]] got collapsed wrong. (#1672, #1673, de78a1f)apply_fixes batches: bounds memory during --fix --unsafe; Drupal-core run peak goes from ~11.6 GB RSS to 748 MB. (#1670, 8b551dc)mago-twig-syntax tests. (ec890a3)Option from LiteralString::kind: every literal string now carries a concrete kind. (#1675, 2fb1377)constant_fqn_key in favor of ascii_lowercase_constant_name_atom: removes a dead helper. (067fa34)A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.23.1...1.24.0
Mago 1.23.1 republishes 1.23.0 on Rust 1.95.0 to unblock PGO on the x86_64-apple-darwin, aarch64-apple-darwin, x86_64-pc-windows-msvc, and x86_64-unknown-linux-musl release builds, and fixes one analyzer issue around array-to-object casts.
stdClass: (object)$array now infers as stdClass rather than a bare object. (#1647, 037c74b)A huge thank you to everyone who contributed code to this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.23.0...1.23.1
Mago 1.23.0 adds three new CLI surface-area features and fixes two rough edges. The analyzer no longer reports spurious template-constraint violations when a constraint uses wildcard class-constant references like self::*_REFERENCE. A new --substitute ORIG=TEMP flag across mago analyze, mago lint, and mago guard lets mutation-testing frameworks swap a host file with a mutated copy for a single invocation without touching anything on disk. mago init now offers a "minimal diff" opt-in that flips every preserve-* formatter toggle on for users who want to keep their existing line breaks intact. The Composer wrapper forwards GITHUB_TOKEN / GH_TOKEN when downloading the pre-built binary, unblocking CI runners and shared networks that hit anonymous rate limits. And the braced-string-interpolation autofix no longer bails on adjacent interpolations like "$comma$y".
--substitute ORIG=TEMP for file-content substitution: New shared flag on mago analyze, mago lint, and mago guard that replaces one host file in the project with another file for a single invocation, without modifying anything on disk. Both paths must be absolute, the flag can be repeated, and under the hood TEMP is appended to the host paths while ORIG is added to the excludes for that run, so the rest of the project is still scanned and cross-file inference continues to see the replacement. Primarily designed for mutation-testing frameworks (such as Infection) that generate a mutated copy of a source file and want the analyzer or linter to evaluate it against the rest of the project without writing the mutation to its original location. Conflicts with --stdin-input, --staged, and --watch where they apply. (infection/infection#3046)mago init now asks "Opt into the smallest possible diff when formatting?" at the end of the Formatter Configuration step. Answering yes writes every preserve-breaking-* option (member-access chains, argument lists, parameter lists, attribute lists, conditional expressions, condition expressions, array-likes, and preserve-redundant-logical-binary-expression-parentheses) as true into the generated mago.toml, so existing line breaks are kept intact. Defaults to no; let Mago decide unless you have a strong preference. (#825)GITHUB_TOKEN / GH_TOKEN when downloading the binary: The Composer package is a thin wrapper that downloads the matching pre-built binary from the GitHub release on first invocation. On shared CI runners and networks that share an egress IP with many consumers, GitHub's anonymous rate limit can reject that download. The wrapper now mirrors what mago self-update already does: it reads GITHUB_TOKEN then GH_TOKEN from the environment and sends the first non-empty value as Authorization: Bearer <token> on the download request. The curl path uses CURLOPT_HTTPHEADER (and relies on curl stripping the Authorization header on the cross-origin redirect to objects.githubusercontent.com); the file_get_contents fallback sets the header through a stream_context_create HTTP context. Both paths now also send a User-Agent: mago-composer/<version> header. No token is required when rate limits aren't an issue; the wrapper still works anonymously by default. (#1665)self::*_REFERENCE patterns used to restrict an integer template to a set of *_REFERENCE class constants) were compared against inferred bounds in their raw, unexpanded form. Because int(0) is not contained by a TReference::Member::EndsWith("_REFERENCE") node, the containment check failed and produced a spurious "template constraint violation" with unknown-ref(…::*_REFERENCE) as the expected type, which also leaked through the deferred-violations path. Constraints are now expanded via expand_union (with the correct self_class derived from the template's defining entity) at every containment site in infer_templates_from_input_and_container_types: the object type-parameter path, the bare generic parameter path, and the template_result.template_types iteration. (#902, #858)braced-string-interpolation autofix merges adjacent }{ edits: For a composite string like "$comma$y", the autofix previously emitted a closing } at the end offset of $comma and an opening { at the start offset of $y. Those offsets are identical when two interpolated expressions sit flush against each other, so the fixer's overlap detector treated the two inserts as conflicting edits and skipped the fix entirely with a Overlapping edit for … warning. The fix now detects adjacency in the collected expression list and emits a single }{ insert at the shared boundary. Same shape applied to $o[a]$o[b] and similar back-to-back array-access interpolations. (#1667)Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.22.0...1.23.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.
strict-behavior auto-fix: The rule now attaches a PotentiallyUnsafe fix that inserts , strict: true (or strict: true when a trailing comma is already present) before the closing paren of calls to in_array, array_search, base64_decode, and array_keys that rely on loose comparison. Not offered when allow_loose_behavior is enabled or when the call already has an explicit non-true strict: value, so deliberate opt-outs stay intact (#1656)unchecked-exceptions with Error and LogicException: mago init now writes check-throws.unchecked-exceptions = ["Error", "LogicException"] into the generated mago.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)non-zero-int keyword: New type keyword that lowers to negative-int | positive-int, commonly used to express "any non-zero integer".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.| in unions: A dangling pipe at the end of a union (common in hand-written or machine-concatenated docblocks) now parses as Type::TrailingPipe instead of erroring; the docblock tag splitter no longer swallows the whitespace after such an operator.FOO_* / *_BAR global-constant wildcards: Global-constant wildcard patterns now parse as Type::GlobalWildcardReference, lowered to a new TReference::Global selector that resolves against global constants. Covers int-mask-of<*_BAR> / value-of<FOO_*> idioms.mago lint / mago analyze detects 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)TArray::Keyed parameters: get_array_parameters tripped a combine() received an empty Vec debug assertion when a keyed array's parameters held an empty key union and known_items was 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]). The Keyed arm now mirrors the List arm's safety push of TAtomic::Never in the empty caseapp\http\controllers\usercontroller::getprofile was actually declared as App\Http\Controllers\UserController::getProfile. The renderer now re-derives the original casing from the source file when emitting issue messages and annotations (#1660)combine() or filter helpers: Two additional sites that could construct an empty atomic vector before calling combine() (the combiner itself and the union-filter helper) now return a well-formed never-typed union instead of hitting the debug_assert!. Belt-and-braces hardening around the same class of bug the analyzer fix above addressesis_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 uses is_part_of_identifier, matching PHP's actual identifier lexing rulesThank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.21.1...1.22.0
Mago 1.21.1 is a bug-fix release that cleans up five regressions introduced by the features shipped in 1.21.0. The string-style fixer no longer emits invalid PHP for chains that aren't rooted in a variable, the prefer-explode-over-preg-split fixer emits clean double-quoted escapes for separators containing control characters instead of splatting raw newlines or tabs into the source, the analyzer now expands generics like value-of<Enum> before comparing class-constant values and property defaults, the type parser accepts new as a class-constant name after :: despite the new new<T> type construct, and OSC 8 editor-URL hyperlinks on Windows drop the \\?\ verbatim prefix so terminal-to-editor links work again.
string-style fixer only interpolates chains rooted in a variable: Fixed the concat-to-interpolation auto-fix for the string-style rule to correctly reject chains that aren't rooted in a $variable. Previously, "Hello " . SomeEnum::World->value was rewritten to "Hello {SomeEnum::World->value}" which is invalid PHP, since interpolation braces only accept chains rooted in a variable. The check now recurses through property, null-safe property, array, method, and null-safe method accesses, returning true only when the chain's root is a variable (#1658)prefer-explode-over-preg-split fixer emits proper escapes for control characters: When the extracted separator contains control bytes (\n, \t, \r, \x01, etc.), the fixer now emits a double-quoted PHP string with escape sequences instead of dropping raw control bytes into a single-quoted string. So preg_split("/\n\n/", $s) now becomes explode("\n\n", $s) instead of explode('<LF><LF>', $s) which split the call onto multiple lines. Non-ASCII bytes still use single quotes since their UTF-8 encoding round-trips verbatim (#1655)invalid-constant-value and invalid-property-default-value checks introduced in 1.21.0 did not expand derived types like value-of<Enum> before comparing against the initializer's type, producing false positives on patterns such as /** [@var](https://github.com/var) array<value-of<Color>, int> */ const WEIGHTS = [Color::Red->value => 1, ...]. The declared type is now expanded (self/static, generics, templates, conditionals, class constants) before the subtype check runs. The parameter-default path was already correctly expanding (#1657)new as a member-name identifier after ::: The new<T> type construct introduced in 1.21.0 made new a reserved keyword in the type grammar, which in turn broke class-constant and enum-case references like Action::NEW or Foo::new. The parser now treats new as an identifier in every post-:: context (plain, qualified, and fully-qualified class references; plus the array{Foo::NEW: int} shape-key pattern) when it isn't followed by <. Top-level new<T> still parses as the NewType construct unchanged (#1654)--editor-url) carried the \\?\ verbatim prefix that std::fs::canonicalize adds, which editors and file:// handlers don't accept. The prefix is now stripped before the path is templated into the URL — \\?\C:\… becomes C:\…, and \\?\UNC\server\share\… becomes \\server\share\…. Applies to both the rich and emacs formatters (#1659)Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.21.0...1.21.1
Mago 1.21.0 is a features-and-fixes release focused on diagnostic accuracy, linter ergonomics, and formatter consistency. Highlights include a new [@mago-expect](https://github.com/mago-expect) category:code(3) count shorthand so you can say "expect three occurrences" once instead of repeating the code, detection of default values that don't match their declared type on parameters / properties / constants, a new prefer-explode-over-preg-split linter rule, an opt-in formatter setting that preserves author-written parentheses around logical sub-expressions, auto-fixes for the string-style rule in both directions, an array_flip return-type provider that preserves shape, and a batch of analyzer fixes covering narrowing across union array accesses, keyed-array append type preservation, interpolated strings built from union parts, closure parameter typing in incompatible callable contexts, and more. The formatter also gets member-access chain consistency across four different starting-expression shapes and a new rule that keeps chains with trailing comments broken even when they'd otherwise fit.
invalid-parameter-default-value, invalid-property-default-value, invalid-constant-value — flag declarations whose default value doesn't match the declared type. Catches classics like function f(SomeClass $x = []), public string $s = 1, and public const int C = 'x', including narrower [@var](https://github.com/var)/[@param](https://github.com/param) docblock types such as non-empty-string $x = '' or positive-int $n = 0 (#1652)array_flip() return-type provider: array_flip() now preserves known-shape information instead of falling back to the generic stub. array_flip(['foo' => 1]) infers array{1: 'foo'}, array_flip(list<string>) infers array<string, int>, and array_flip(array<K, V>) infers array<V, K> when V is an array-key subtype. Narrowing under array_key_exists now works on flipped arrays (#1633)array<1, T> with an int no longer reports mismatched-array-index. The access is type-valid — the specific key may just not be present at runtime, which is a separate concern handled by the known-items path. Empty arrays (never key type) still reject any index (#1633)prefer-explode-over-preg-split rule: Flags preg_split() calls whose pattern has no regex meta-characters and no modifiers, suggesting explode() instead. The rule fires only when the pattern is a literal with symmetric delimiters (/ # ~ ! @ % , ; |), the inner content has no regex special chars, and the flags argument (if present) is literal 0. Ships with an auto-fix that rewrites the pattern as a plain string, renames the call to explode, and drops a trailing flags = 0 argument (#1554)string-style rule (both directions): The string-style rule now offers auto-fixes for both interpolation (converting "Hello, " . $name to "Hello, {$name}") and concatenation (the reverse). The fix handles leading/trailing expressions, adjacent expressions, multi-line input, braced and unbraced interpolations, escape preservation, and expression-only strings. Single-quoted literals are intentionally not fixed to avoid changing escape semantics (#1640)preserve-redundant-logical-binary-expression-parentheses option: New opt-in formatter setting (default false) that preserves author-written parentheses around a logical binary sub-expression (&&, ||, and, or, xor) when its enclosing binary is also logical, even though operator precedence would make them redundant. With the setting on, ($a && $b) || $c stays as written instead of being reformatted to $a && $b || $c (#1367)code(N) count shorthand for [@mago-expect](https://github.com/mago-expect) / [@mago-ignore](https://github.com/mago-ignore): Instead of repeating a code to suppress multiple occurrences, you can now write [@mago-expect](https://github.com/mago-expect) analysis:mixed-operand(3) to suppress up to three matching issues. code(1) is equivalent to a bare code. If fewer issues match than expected, the auto-fix suggests reducing the count (or dropping the suffix entirely when it falls to 1) rather than deleting the directive (#1644)[@mago-expect](https://github.com/mago-expect) from a PHPDoc that carries other content (description lines, [@param](https://github.com/param) / [@return](https://github.com/return) / other tags), the auto-fix now deletes only the pragma's own line instead of the entire comment. Single-line pragmas and pragma-only docblocks still get deleted whole as before.CodebaseMetadata::extract_owned_keys helper: New method that extracts only the symbol keys owned by a given file's metadata, used by the incremental analysis service to safely prune entries without touching shared/prelude symbols.ResolvedNames APIs: ResolvedNames now records the end offset of each identifier in addition to its start, enabling direct "what name is at this byte offset?" lookups without rescanning source. Adds at_offset(offset) for precise range-based lookup and iter() for allocation-free iteration over (start, end, fqn, imported) tuples. The existing all() method stays around but is now #[deprecated] in favor of the new API.mixed-operand in loops and preserved keyed-array shapes on append: Appending to a keyed array in a loop no longer produces spurious mixed-operand errors, and the keyed-array shape (array{x: int, y: int}) is preserved across appends instead of being widened to a generic array<array-key, mixed> (#1639)"x=$a" with $a: 'foo'|'bar') now produce a literal union of concrete strings ('x=foo'|'x=bar') instead of a general string, letting downstream narrowing continue to work on the result (#1651)callable signature, the analyzer now preserves the closure's own declared parameter type instead of overwriting it with the incompatible target type. Previously this produced spurious errors inside the closure body even though the closure's own signature was fine — the signature-level mismatch error at the call site is already enough to surface the problem (#1641)if (!empty($vd['key'])) where $vd is false|array{key: string}, the narrowed $vd['key'] type is now correctly non-empty-string rather than truthy-mixed. The reconciler's get_value_for_key helper no longer short-circuits to mixed when it sees a non-container atomic (e.g. false, null) in a union: those atomics are skipped so iterable atomics can still contribute their element types (#1653)default cases in switch fall-through analysis: The fall-through detection now correctly handles default cases that appear mid-switch instead of last, so switches that break out of a non-terminal default no longer report spurious unreachable-arm diagnostics.Yii::app()->...), a function call (Y()->app()->...), a static property (Yiui::$app->...), or a plain variable ($obj->...) now all break consistently per access when the chain exceeds print width, instead of producing four different partial-break styles depending on the starting expression. A companion rule keeps chains with trailing comments between accesses broken regardless of line width, so Writer::default()->u8(1) // version no longer collapses onto a single line when it technically fits (#1623)class-string is not contained by known string literals: Fixed a containment check where a general class-string type was incorrectly treated as a subtype of a specific literal string, producing false positives on comparisons and narrowing (#1638)mago <command> some/path.php, the original source.paths from the config are moved into the context (so they still provide analysis context) but no longer added to the exclusion list, which was causing the overridden paths to be silently filtered out (#1648, #1650)paths and includes, causing the loader's specificity tiebreaker to reclassify the file as Vendored and hide it from Host-only tools. Context paths are now filtered against the active source paths before being added to includes (#1650)contenteditable=plaintext-only: The playground editor no longer loses its plaintext-only editing mode on re-renders, keeping caret position stable while typing (#1646)mago ast --names to ResolvedNames::iter(): The CLI's name-dump command now walks resolved names via the new allocation-free iterator instead of cloning the full map, matching how the rest of the codebase should transition away from the deprecated all().A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.20.1...1.21.0
A small but high-impact patch release. Reporting got significantly faster across the board — every output format (rich, json, count, github, gitlab, sarif, checkstyle, emacs, ariadne, code-count) now skips a wasteful copy of the issue list it used to make on every run. The bigger your project (and the more issues Mago finds), the more you save: WordPress analysis is ~3% faster end-to-end, Magento ~5%, and the savings in the reporting step itself are around 100 ms on a project of Magento's size.
count that just tallies severities. On a small project the cost was barely noticeable, but on a project with hundreds of thousands of issues it could add over 100 ms per run. The reporter now walks the original list directly, only allocating when sorting is meaningful for that format (rich, ariadne, json). End result: noticeably snappier analyze runs on large codebases, no behavior change.mixed: Using ++, --, or unary - on a value of type mixed was being reported as invalid-operand or possibly-invalid-operand, the same codes used for genuinely incompatible operands. Those operations are now reported under the dedicated mixed-operand code instead, matching how every other operator in Mago handles mixed and making baselines and rule suppressions more predictable (#1635)A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.20.0...1.20.1
Mago 1.20.0 is a big release focused on analysis accuracy, speed, and diagnostics. Highlights include new new<X> and template-type<Object, ClassName, TemplateName> utility types, detection of [@param](https://github.com/param) docblocks that silently drop native union branches, a new find-overly-wide-return-types analyzer check, six new linter rules, glob-pattern support across both analyzer ignore.in and per-rule linter excludes, a side-effects-in-condition check, a trace-gated hang watcher for diagnosing pathological inputs, and sweeping performance work — loop fixed-point depth capping, walkdir directory pruning, ptr_eq fast paths on every TType impl, Rc<TUnion> plumbing through the analyzer hot path, saturate_clauses fast paths for single-literal clauses, sealed-keyed-array bounding in the combiner, and a zero-alloc AST visitor — together dropping end-to-end psl analysis by more than 3x. On the fix side, this release ships a pile of reconciler correctness work covering chained AND-clauses, narrowing through redundant ?->, strict in_array narrowing, property-hook variance, self-referencing class constants, and more.
new<X> and template-type<Object, ClassName, TemplateName> utility types: Two new type-syntax utilities. new<X> resolves a class-string expression to the object type that would result from instantiating it, and template-type<O, C, N> extracts a named template parameter from a given object/class, matching the PHPStan semantics (#1217)find-overly-wide-return-types check: New opt-in analyzer pass that compares a function's declared return type against the union of types actually produced by its return statements, reporting a new OverlyWideReturnType issue when declared branches are never produced. Skipped for generators, abstract/unchecked methods, overriding methods, and mixed/void/never/templated returns (#1446, #1553)[@param](https://github.com/param) docblock narrowing check: Flags when a [@param](https://github.com/param) docblock type silently drops a branch of the native parameter union (e.g. annotating int|string as [@param](https://github.com/param) int), which would otherwise collapse the parameter to never inside the body (#1487)side-effects-in-condition diagnostic and matching allow-side-effects-in-conditions setting (on by default) that warns when an if/while/for/ternary/match condition calls a function or method that isn't marked [@pure](https://github.com/pure), [@mutation-free](https://github.com/mutation-free), or [@external-mutation-free](https://github.com/external-mutation-free) (#1604)ignore.in: The analyzer's ignore configuration now accepts full glob patterns (e.g. src/**/*.php, modules/*/Generated/*.php) in in = [...] alongside plain directory/file prefixes, routed through the shared ExclusionMatcher (#1619)no-literal-namespace-string rule: Flags string literals that look like fully-qualified PHP class names and suggests ::class notation. Disabled by default, warning level (#1386)no-null-property-init rule: Flags untyped public $foo = null; property declarations, since untyped properties already default to null. Ships with an auto-fix to drop the redundant initializer. Disabled by default, help level (#1315)no-side-effects-with-declarations rule: Flags files that mix top-level declarations (class, interface, trait, enum, function) with side-effecting statements. Configurable to allow class_alias, class_exists top-level calls, and conditional declarations (#1560)no-service-state-mutation rule: Flags in-place mutation of injected service state so long-lived workers (RoadRunner, FrankenPHP, Swoole) don't accidentally leak state between requests (#1582)string-style rule: Enforces a configurable preferred string style (interpolation vs concatenation), powered by a new ancestors tracking API on the lint context (get_parent, get_parent_kind, get_nth_parent, get_nth_parent_kind) (#1614)disallowed-type-instantiation rule: New security rule that flags direct new Foo() on classes listed in the rule configuration, intended to enforce factory/provider patterns. The types array accepts either plain strings or {name, help} objects so users can attach custom help text per disallowed class (#1621)excludes configuration now accepts full glob syntax via a new mago_database::matcher::ExclusionMatcher, classifying each pattern as either a glob or a plain directory prefix (#1453, #1619)align-parameters and align-named-arguments options: Two new opt-in alignment settings (both default false) that, on multiline parameter and argument lists, align columns — named arguments by their colon and parameters by the variable column (especially useful for promoted constructor properties) (#1307)preserve-breaking-member-access-chain-first-method-on-same-line option: When enabled alongside preserve-breaking-member-access-chain, keeps the first ->method() call on the same line as its receiver instead of moving the receiver onto its own line (#1319)omit-redundant-arithmetic-binary-expression-parentheses and omit-redundant-bitwise-binary-expression-parentheses, that drop parentheses around arithmetic/bitwise children under comparison and null-coalesce operators when PHP precedence already preserves meaning (e.g. $i === $retries - 1) (#1620)format --staged works with unstaged changes: mago format --staged no longer refuses to run on files with partial staging. It now reads each staged blob via git cat-file, formats it in memory, and writes the formatted version back to the index via git hash-object -w + git update-index --cacheinfo, leaving unstaged worktree changes untouched (#1629)version key in mago.toml (exact, major.minor, or major) is validated on every run — patch or minor drift logs a warning, major drift fails hard. A matching mago self-update --to-project-version flag reads the pin and installs the exact tag (or the latest release matching the constraint) (#1618)MAGO_LOG=trace is set, a dedicated watcher thread tracks in-flight analyzer workers and emits tracing::trace! messages when a file has been analyzing for more than ~5 seconds, with guidance on filing a bug report and anonymizing private code. Zero overhead when tracing is off (#1227)analyzer/orchestrator telemetry modules emit per-phase durations (clap parse, logger/config/rayon init, prelude decode, orchestrator init, database load, compile, codex merge/populate, parallel map/reduce, reduce, analyzer setup/statements/finalize) and report the 20 slowest files seen during the parallel analyze phase, all via a measure! macro that compiles to a branch-predicted zero-cost path when trace is off.redundant/impossible diagnostics on already-narrowed types (#1627)?->: For a null-safe access $obj?->prop where $obj is already known non-null, the analyzer now looks up the equivalent non-null-safe property id in block_context.locals and reuses its narrowed type, so the result of the ?-> retains the earlier narrowing instead of falling back to the widest declared type (#1627)in_array narrowing: When in_array(..., strict: true) returns true, the synthesized needle assertion is now Assertion::IsIdentical rather than Assertion::IsEqual, letting the analyzer narrow the needle to the haystack's element type (e.g. string|int|null to string against a string[] haystack). The stub also gained a [@template](https://github.com/template) T on the needle so its type is independent of the haystack value type (#1628)->sub->method() calls and readonly props: clear_object_property_narrowings now threads through the receiver variable and only wipes $this->... narrowings when the method call is actually on $this. Additionally, narrowings for readonly $this properties are preserved across self-calls and [@suspends-fiber](https://github.com/suspends-fiber) calls (#1625)const int B = self::B via a thread-local EXPANDING_CONSTANTS set and RAII guard, pushing TAtomic::Never on re-entry. The class-constant analyzer surfaces this as a new UnresolvableClassConstant issue (#1624)get hook and contravariance on properties with only a set hook, so read-only/write-only hooked properties no longer emit false type-incompatibility errors in subclasses (#1615)&&: $x === 0 && f($x) && $x !== 0 (where f takes $x by reference) now correctly drops the stale narrowing on $x from the left side by propagating parent_conflicting_clause_variables through both branches (#1608)[@param](https://github.com/param)-narrows-native-type check now skips methods that override a parent, since the docblock narrowing is constrained by the parent signature and would otherwise produce false positives.set => expr shorthand treated as virtual: A property hook like public string $test { set => strtolower($value); } was wrongly classified as virtual (and therefore write-only), because the shorthand walker only looked for explicit $this->propertyName references and missed the implicit $this->prop = expr desugaring. The walker now also tracks whether any assignment appears in the hook body: a shorthand set with no assignment in its body is correctly recognized as referencing the backing field (#1632)include_externals is false (the default for lint and format), configured includes patterns are now also compiled into excludes and merged with the user-configured excludes, so directories like vendor/ nested under a source path are correctly skipped (#1630, #913)dir_prune_globs and file-level exclude globs now match against the path relative to the canonical workspace rather than the absolute path, so workspace-anchored patterns such as src/*/Test/** and vendor/**/tests/** actually exclude matching files. Includes a new workspace_relative_str helper with Windows backslash normalization (#1143)*/packages/**/vendor/* that anchor on the workspace's absolute prefix continue to function.NO_COLOR=0 honored by the issue reporter: The color detection used by the issue reporter now treats any non-empty NO_COLOR value (including the literal "0") as disabling colors, and treats FORCE_COLOR=0 as explicitly disabling colors, aligning with the logger and the no-color.org spec (#1599)source.workspace at normalization time, so running mago from a subdirectory no longer resolves the baseline file relative to the current working directory (#1289)reset() return type preserves non-empty shapes: The reset() stub's [@param-out](https://github.com/param-out) now preserves non-empty-list/non-empty-array shapes and its [@return](https://github.com/return) narrows to V (dropping false) when the array is statically known non-empty, instead of always returning V|false (#1611)array_replace and array_replace_recursive template parameters: Added [@template](https://github.com/template) K of array-key and [@template](https://github.com/template) V to the stubs so they return array<K, V> matching the inputs instead of a lossy array<array-key, mixed> (#1605)DateTime::getLastErrors() array shape: Replaced the loose array<string, int|array>|false return type with a precise array{warning_count, warnings, error_count, errors}|false shape on both DateTime and DateTimeImmutable (#1607)DateTimeZone stub improvements: Filled in the real bitmask values for the timezone constants (AFRICA = 1 through PER_COUNTRY = 4096), tightened return types for getTransitions, getLocation, listAbbreviations, and listIdentifiers with non-empty-string/list<...>/concrete array{...} shapes, and annotated the constructor with [@throws](https://github.com/throws) DateInvalidTimeZoneException (#1612)End-to-end analysis of the Psl monorepo dropped from ~700 ms to ~160 ms in this release thanks to the combined effect of the following changes.
Rc<TUnion> in the hot path: Threaded Rc<TUnion> through assignment, binary logical, unary, variable access, invocation, loop handling, and static-statement analysis, alongside new add_optional_union_type_rc / combine_union_types_rc helpers in the codex ttype module that reuse Rc pointers when inputs are pointer-equal. Cuts redundant TUnion clones by roughly a third on Psl.loop-assignment-depth-threshold setting (default 1) caps how deep get_assignment_map_depth will recurse through loop-carried assignment chains, short-circuiting fixed-point iteration that used to re-analyse loop bodies many times. The codex combiner's DEFAULT_ARRAY_COMBINATION_THRESHOLD was also lowered from 128 to 32 so very wide keyed-array unions collapse to a general shape sooner.analyze_expression_arm now returns only the clauses newly negated by that arm, and the match driver feeds only those into find_satisfying_assignments/reconcile_keyed_types, rather than re-saturating and re-reconciling the entire running else-context on every arm.ptr_eq/cheap-field fast paths in PartialEq: Replaced the derived PartialEq on TAtomic and on the array / keyed / list variants with a hand-written impl that short-circuits on pointer equality and dispatches only to the matching variant's cheap-field compare. TUnion atoms are also sorted into canonical order in the constructor so unions built from the same set of atoms take the ordered slice-equality path instead of the O(N²) subset fallback.array-combination-threshold, the excess is flushed into a single merged keyed-array entry instead of tracking each shape separately. adjust_keyed_array_parameters also short-circuits entries whose key type cannot match, preventing quadratic blowup when unioning many mixed shapes (#1610)saturate_clauses fast paths for single-literal clauses: Pre-builds a (var, possibility-hash) HashSet index of every literal in the input so unit propagation can skip the inner O(N) scan whenever no clause contains the negation of a given literal, and adds an all-size-one fast path that skips the strict-subset redundancy walk and the consensus-rule pass entirely when every clause has a single variable. Drops exhaustive match analysis from roughly cubic to linear in arm count.GlobSet of directory-level prune patterns from the user's file-level globs (by stripping trailing /*, /**, /**/*) and applies it (plus the path-excludes) inside WalkDir::filter_entry, so excluded subtrees (vendor/, node_modules/, …) are never descended into. On the psl monorepo the walker now yields 2,975 entries instead of 144,222.Node::visit_children method drives the same match logic as Node::children but delivers each child to a caller-supplied closure instead of allocating a Vec<Node>. The linter walk, filter_map_internal, prefer_static_closure's contains_this_reference, and the cyclomatic_complexity/halstead/kan_defect metric rules all migrated to it; the linter walk is also rewritten to use an explicit Op stack instead of recursion, avoiding stack overflows on deeply nested ASTs (#1606)Config::builder: Removed the eager Configuration::from_workspace seed that was being serialized and re-fed into Config::builder(), letting the final try_deserialize::<Configuration> fall back to serde defaults. SourceConfiguration gained #[serde(default)] plus a Default impl so the workspace still resolves correctly. Saves roughly 20 ms on startup.MAGO_LOG=trace, and covers what the hang watcher, slowest-files report, and per-phase durations give you.mago init template now includes the missing workspace = "." line so the generated mago.toml matches the schema expected by the command's tests.A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.19.0...1.20.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.
[@experimental](https://github.com/experimental) usage detection: Warn when calling functions, methods, or classes marked with the [@experimental](https://github.com/experimental) docblock tag_ and * wildcard types in generic type argument positions (#1571)count() assertion negatable: !count($x) now correctly narrows the array to emptymissing-docs rule: Enforces documentation on public API elements (#1585)no-parameter-shadowing rule: Detects function parameters that shadow variables from an outer scopeno-array-accumulation-in-loop rule: Flags array accumulation patterns inside loops that may cause performance issuesno-iterator-to-array-in-foreach rule: Detects iterator_to_array() calls that could be replaced with direct iterationsorted-integer-keys rule: Flags arrays with integer keys that are not in sorted orderno-is-null rule: Suggests replacing is_null($x) with $x === null (#1557)method-name rule: Enforces method naming conventionsconstructor-threshold option for excessive-parameter-list: Configure a separate threshold for constructor parametersno-redundant-readonly detects promoted properties: The rule now catches redundant readonly on constructor promoted properties in readonly classes (#1543)indent-binary-expression-continuation option: Control indentation of continued binary expressionspreserve-breaking-conditions option: Preserve line breaks in conditions as authored (#1222){a,b,c} brace expansion syntaxmago config --schema --show baseline outputs the JSON schema for baseline files, enabling IDE integration and validation (#986)types_share_category heuristic 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)never-returning calls in switch cases: array_pop-style functions with never return types in switch default branches now correctly mark the branch as terminating, so variables defined in other branches are visible after the switch (#1578)redundant-condition in nested loops with continue/break: Variables unchanged at continue points are now recorded so they don't get overwritten by break-path assignments (#1586)$look = null from a break on null-check) no longer contaminate the next iteration's entry state in while(true) loops (#1587)inside_isset to outermost access: isset($arr[$row['key']]) now correctly reports issues on the index sub-expression $row['key'] — isset only suppresses checks on the outermost $arr[...] access (#1594)possibly-null-array-index reported inside isset(): Using a nullable value as an array key is a type-safety issue separate from undefined-key checks, so it's now reported even inside isset() (#1594)?? isset semantics for non-variable LHS: func($arr[$key]) ?? $default no longer suppresses issues in $arr[$key] — only variable/property/array-access LHS expressions get isset-like treatment (#1594)$arr[$key] += $val no longer reports the same issue twice on the index sub-expression (#1594)$row['key'] where $row is array|false now correctly includes null in the result type (PHP returns null when accessing an index on false) (#1592)possibly-undefined-variable inside isset(): isset($x) is the canonical way to check if a variable exists — no warning about it being undefined (#1603)self:: references in class constant inference: const MAP = ['a' => self::A] now correctly infers the referenced constant/enum-case types instead of degrading to mixed (#1602)array_filter return type: array_filter on a shaped array like array{a: ?string, b: ?int} now preserves the shape with entries marked optional, instead of flattening to a generic array (#1590)array_column: array_column on list<array{name: string, id: int}> with literal keys now returns the correct shaped result (#1591)&& assignments in while conditions: while (check() && ($row = fetch()) !== false) now correctly narrows $row in the loop body (#1593)$intVar == $otherInt where the types provably differ is now flagged, matching strict identity behavior for same-category comparisons (#1576)non-empty-string !== lowercase-string is no longer falsely reported as always-true — string types with independent constraint flags (casing, non-emptiness) can share concrete valuesnever arguments: Calling a templated by-reference function on a never-typed argument no longer widens the variable to mixedphpversion() prelude stub: Corrected the return type (#1584)braced-string-interpolation fixer for bareword array keys: "$o[user_id]" is now correctly fixed to "{$o['user_id']}" instead of the broken "{$o[user_id]}" which treats user_id as an undefined constant (#1588)inline-variable-return for return-by-reference functions: Functions declared with function &name() no longer get the inline suggestion, which would break the by-reference return contract (#1600)NO_COLOR not disabling linter output colors: NO_COLOR=0 --colors=auto now correctly disables colors for all output, not just the logger (#1599)line_starts now uses memchr for ~4x faster newline scanning (#1581)canonicalize(): Path exclusion checks use the already-canonicalized workspace root (#1589)is_canonical flag: Cleaned up unused path collection bookkeeping (#1596)Node::filter_map: Replaced repeated Vec::insert(0, ...) with push + reverse (#1598)mago config --schema --show baseline command in the baseline guidehrtime(), applied clippy fixesA huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.18.1...1.19.0
Patch release fixing a CI failure that prevented 1.18.0 binaries from being built for aarch64-unknown-linux-gnu and aarch64-unknown-linux-musl targets.
--stdin-input integration tests spawn the built mago binary as a subprocess. When cross-compiling (e.g., aarch64 target on an x86_64 CI runner), the binary can't execute, causing all 5 tests to fail. These tests now detect cross-compilation and skip gracefully.Full Changelog: https://github.com/carthage-software/mago/compare/1.18.0...1.18.1
Mago 1.18.0 is a packed release with 5 new features, 17 bug fixes across all tools, and improvements to the docblock parser. Highlights include full callable-string type support with function_exists/is_callable narrowing, --stdin-input for editor integrations on analyze/lint/guard, precise range() return types, a Psl\Dict\select_keys type provider, and numerous false positive fixes in the analyzer, linter, and formatter.
callable-string type support: Added callable-string, lowercase-callable-string, and uppercase-callable-string types. function_exists() narrows strings to callable-string (#1532)range() return type provider: Infers precise return types based on argument values - range(1, 5) returns non-empty-list<int<1, 5>>, range(0, 5.0) returns non-empty-list<float>, range('a', 'z') returns non-empty-list<non-empty-string> (#1510)Psl\Dict\select_keys return type provider: Narrows the return type to a shaped array when called with literal keys, handling keyed arrays, generic arrays, and union/iterable types (#1357)$v === false where $v is int is now flagged as redundant even inside foreach/for/while loops, when the types are from fundamentally incompatible categories (#1555)--stdin-input for analyze, lint, and guard: Pipe unsaved buffer content from editors and use the real file path for baseline matching and issue locations. Supports path normalization for consistent baseline behavior (#1253)--only error for analyze: Instead of a confusing clap suggestion, mago analyze --only now explains that the analyzer is not rule-based and suggests --retain-code (#774)too-many-arguments when calling dynamic callable strings, never narrowing after function_exists false branch, and unreachable-else-clause for function_exists checks. The scalar comparator now properly checks is_callable during assertion contexts, and cast_atomic_to_callable uses a mixed signature (#1561)$arr[$i++] inside an isset($arr[$i]) block now preserves the narrowed type by saving it before the increment side-effect invalidates it (#1556)!empty not removing false/null from unions: When !empty($row['key']) proves a key exists, non-array types like false and null are now removed from the parent union (#1565)count() not generating NonEmptyCountable assertion: count($arr) in truthy context now properly narrows the array to non-emptyinside_loop flag when analyzing match arm conditions to fix false non-exhaustive match errorsarray_merge with unpacked arguments: array_merge(...$lists) where $lists is list<list<int>> now correctly returns list<int> instead of array<non-negative-int, int> (#1548)net_get_interfaces return type: Corrected the prelude stub to match PHP documentation (#1550)->not leaking across ->and() boundaries: The use-specific-expectations rule no longer carries the ->not modifier past ->and() calls, which reset the expectation context in Pest chains (#1511)// [@mago-expect](https://github.com/mago-expect) lint:halstead suppresses all of them (#1452)fn(array $nums) => Calculator::sum(...$nums) is no longer incorrectly suggested to be replaced with Calculator::sum(...) (#1246)$this-> and static method chains from 3 to 5 accesses, so chains like $this->tokenStorage->getToken()->getUser()->getFoo() stay on one line when they fit within print-width (#1451)[@mago-format-ignore-next](https://github.com/mago-format-ignore-next) corrupting code in sub-expressions: The ignore marker inside match arms, arrays, or function parameters no longer leaks to the next class member, which previously duplicated raw source content (#1513)> inside && chains now get their own group, preventing unwanted line breaks (#1562)BraceStyle, MethodChainBreakingStyle, EndOfLine, and NullTypeHint now use snake_case in the generated schema, with PascalCase preserved as aliases for backwards compatibility (#1530){[@internal](https://github.com/internal) ...} and other inline tags can now span multiple lines in PHPDoc comments (#1257)[], *, ?, or {} in their path are now treated as literal paths when they exist on disk, instead of being interpreted as glob patterns (#1459)[@psalm-assert](https://github.com/psalm-assert) template narrowing (#1517)types_share_category helper for loop-aware redundant comparison detectionis_gap_insignificant helper for robust ignore-next marker validationA huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.17.0...1.18.0
Mago 1.17.0 continues the focus on analyzer accuracy with 20+ bug fixes and several new features. Highlights include detection of array access on non-array types (false, scalars), support for PHP 8.4's #[\Deprecated] attribute, literal float arithmetic tracking, a new [@suspends-fiber](https://github.com/suspends-fiber) annotation for async code, and invalidation of memoized $this properties on self-method calls. The linter gains new rules for fully-qualified global imports and ambiguous constant access, plus an auto-fixer for redundant parentheses.
false-array-access, possibly-false-array-access, possibly-invalid-array-access, and invalid-array-access issue codes report when array access is performed on false, scalars, or other non-array types, including assignment contexts (#1542)#[\Deprecated] PHP attribute: Functions, methods, classes, constants, and enums with the #[\Deprecated] attribute are now recognized as deprecated (#1541)[@suspends-fiber](https://github.com/suspends-fiber) annotation: A new docblock tag [@suspends-fiber](https://github.com/suspends-fiber) marks methods that suspend fibers. Calls to these methods invalidate all memoized $this property narrowings, preventing false positives in async code. Revolt\EventLoop\Suspension::suspend() is recognized automatically (#1536)$this->method() calls now clear memoized $this-> property types, fixing false positives where property narrowings survived across method calls that could modify them (#1536)Psl\Math\max, Psl\Math\maxva, Psl\Math\min, Psl\Math\minva, and Psl\Math\abs now use the same precise integer range providers as the built-in functions$arr[] = $val) and indexed assignment ($arr[$k] = $val) on by-reference parameters now validate against the parameter's type constraint (#1539)no-fully-qualified-global-function, no-fully-qualified-global-constant, no-fully-qualified-global-class rules: New rules that flag fully-qualified references (#1494)ambiguous-constant-access rule: Detects ambiguous constant access patterns (#1508)redundant-parentheses rule now has an auto-fixer (#1549)string('') into truthy-mixed, which caused false redundant-condition warnings (#1534)isset on string offsets: isset($s[0]) on a generic string no longer returns true unconditionally - empty strings have no characters, so the result is correctly bool (#1537)parse_url component return types: parse_url() with a component argument now correctly includes false in the return type, since malformed URLs return false regardless of the component (#1546)break no longer drop the pre-loop empty array variant, fixing false positives where count($arr) was always truthy after a while(true) loop (#1535)if (!isset($arr[$key])) { throw; } exits, empty array variants are now removed from the base variable since isset proves the array is non-emptynever-returning call (like exit()) without break are no longer incorrectly marked as unreachable - the switch can still jump directly to those cases (#1531)if ($m === 0) { echo; }) no longer leak to subsequent code, preventing incorrect type narrowing (#1509)&&: Variables passed by reference in function calls on the left side of && now correctly update their type for the right side evaluation (#1524)$arr[$dynamic] = value no longer replaces the value types of existing known keys in the array (#1527)isset/empty checks on union types with lists: The impossible-isset check now considers TArray::List variants in union types, and non-empty arrays with literal key types no longer produce false possibly-undefined warnings (#1512)int|int(0) are now properly handled by computing combined bounds (#1526)1000 * 0.5) now compute the actual result (float(500.0)) instead of returning unspecified float (#1540)fits() handling of LineSuffix: Corrected line-suffix width calculation in the formatter (#1516)getimagesize/getimagesizefromstring param-out: Added [@param-out](https://github.com/param-out) ?array annotation matching the nullable parameter type (#1523)truthy-mixed no longer contains falsy values and falsy-mixed no longer contains truthy values in type comparisons (#1534)#[\Deprecated] attribute: The codex scanner now sets the deprecated flag from PHP attributes in addition to [@deprecated](https://github.com/deprecated) docblock tags (#1541)A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.16.0...1.17.0
Mago 1.16.0 is a significant release focused on analyzer accuracy. This release fixes over 20 false positives across loop analysis, type narrowing, integer range tracking, switch fall-through handling, and comparison assertions. It also adds new features including duplicate enum value detection, argument validation hooks for setcookie, session_set_save_handler, and session_set_cookie_params, and improved return type inference for min, max, abs, and array_reverse.
setcookie / setrawcookie: When the 3rd argument is an array (options form), additional positional arguments are now flagged as errors (#1467, #1492)session_set_save_handler: Validates both the object form (1-2 args) and callable form (6-9 args), reporting errors for mismatched argument counts (#1468)session_set_cookie_params: When the 1st argument is an array, extra arguments are now flagged as errors (#1468)min / max / abs return types: Multi-argument min and max calls now return precise integer range types, and a new abs provider returns non-negative ranges (#1477, #1480)$total += $delta (#1491, #1493, #1499)$n++ inside if-conditions had their side effects discarded when the if-body always exited, causing false "impossible condition" reports (#1504)while ($row = func()) were incorrectly treated as always-true, causing variables modified inside the loop to lose their pre-loop values (#1505)default clause are no longer incorrectly flagged (#1484, #1485)non-negative-int) on the secondary variable in less-than comparisons no longer produce incorrect type narrowing when negated (#1503)count() === N not narrowing empty arrays: Empty arrays without generic parameters are now correctly removed when reconciling exact count assertions (#1506)never: Loose equality (==) with strings no longer incorrectly narrows numeric types to never (#1488)instanceof self / instanceof static: These expressions are now properly resolved for type computation (#1464)never producing mixed: Operations on never types now correctly propagate never instead of falling back to mixed (#1502)numeric in string concatenation: The numeric type is now accepted in string concatenation operations (#1500)isset return type: isset() now returns true instead of bool when all checked values are definitely set, and array keys are marked as definite after loops that always enter (#1486, #1493)array_reverse return type: Non-list arrays (e.g., array<string, int>) now correctly preserve their key type instead of being narrowed to list<V> (#1466)non-positive-int type mapping: Was incorrectly mapped to positive-int due to a copy-paste error (#1479)[@deprecated](https://github.com/deprecated) on mb_scrub: The function is not deprecated in PHP (#1476, #1481)getrusage function signature: Corrected the return type to match PHP documentation (#1501)iconv mode parameter: Corrected the mode parameter type to int<0, 3> (#1497)openssl_pkey_get_details return type: Made all keys optional since algorithm-specific keys are only present for their key typesession_set_save_handler stub: Merged duplicate declarations into a single signature with union types (#1468)session_set_cookie_params stub: Merged duplicate declarations into a single signature (#1468)A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.15.3...1.16.0
Mago 1.15.3 is a patch release that fixes formatter idempotency issues with trailing closing tags and non-standard line endings, adds a comment placement infrastructure for stable binary expression formatting, fixes incorrect OpenSSL function signatures, and resolves a CPU spin-lock in the Composer wrapper.
should_break decisions in binary expressions and keeps comments inside parenthesized subexpressions, fixing formatting oscillation between passes (#817, #1456)?> is removed: The opening tag formatting decision used source_text.contains("?>") to detect inline PHP templates. When remove_trailing_close_tag removed a trailing ?> on the first pass, the second pass saw no ?> and changed the formatting. Replaced with an AST walker that counts closing tags at any depth, excluding the trailing one that gets removed (#1350, #1457)\r not recognized as line terminator: Files with \r\r\n, bare \r (classic Mac OS), or other non-standard line endings caused the formatter to merge lines and crash on multi-line block comments. Fixed skip_newline, split_lines, replace_end_of_line, and print_comment to handle bare \r as a line terminator. Also fixed line_starts in the database crate which only scanned for \n, causing incorrect line numbering for bare-CR files (#1460, #1462)openssl_x509_* functions incorrectly accepted OpenSSLCertificate|false instead of the correct OpenSSLCertificate|string parameter type (#1463)proc_get_status in a tight loop with no sleep, causing 100% CPU usage on one core during --watch mode. Added a 10ms sleep interval and fixed signal-based exit code propagation (#1454, #1455)A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.15.2...1.15.3
Mago 1.15.2 is a re-release of 1.15.1 with corrected version metadata. The 1.15.1 release was tagged before the version bump was applied, causing binaries to report 1.15.0 in --version output and preventing publication to crates.io. This release contains no code changes beyond the version bump.
For the full list of changes since 1.15.0, see the 1.15.1 release notes.
Full Changelog: https://github.com/carthage-software/mago/compare/1.15.1...1.15.2
Mago 1.15.1 is a patch release that distinguishes $this from static return types, reverts a formatter regression with parenthesis removal in binary expressions, and restores glibc 2.17 compatibility for the Linux GNU build.
$this from static return types: Added a separate is_static flag to the type system to properly differentiate $this (same instance) from static (same class, possibly different instance). Returning new static() from a method declared as [@return](https://github.com/return) $this is now correctly flagged, while return $this remains valid. return new static() continues to be accepted for : static return types (#1429)x86_64-unknown-linux-gnu binary is now built with cross using the manylinux2014 container again, restoring compatibility with older Linux distributions. PGO optimization for this target has been removed as it was incompatible with the cross-compilation setup (#1431, #1433, #1434)A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.15.0...1.15.1
Mago 1.15.0 brings three new linter rules, configurable minimum-fail-level in TOML files, automatic watch mode restarts on config changes, PGO-optimized Linux x86 builds, and a wave of analyzer and codex bug fixes improving generics, intersection types, and type inference accuracy.
Psl\Type\nullish() return type provider: Added type narrowing support for Psl\Type\nullish(), complementing existing PSL type providers (#1390, #1391)Psl\Async\all() and Psl\Async\concurrently() return type providers: Both functions now preserve sealed array shapes, list structure, and non-empty status. For example, Psl\Async\all(['foo' => $awaitableString, 'bar' => $awaitableInt]) correctly returns array{foo: string, bar: int} (#1423)no-alternative-syntax rule: Detects alternative control structure syntax (if/endif, while/endwhile, etc.) and suggests using brace-based syntax instead (#1313)no-short-bool-cast rule: Flags !!$expr double-negation casts and suggests using (bool) $expr for clarity (#1312)prefer-pre-increment rule: Suggests ++$i over $i++ and --$i over $i-- when the return value is unused, as pre-increment avoids an unnecessary copy (#1311)no-alias-function rule: The no-alias-function rule now supports automatic fixing, replacing aliased PHP functions with their canonical equivalents (#1297)minimum-fail-level configuration option: The minimum-fail-level setting can now be configured in mago.toml under [analyzer], [linter], and [guard] sections, removing the need to pass --minimum-fail-level on every invocation. The CLI flag still overrides the config value (#1343, #1384)mago analyze --watch now monitors the configuration file, baseline file, composer.json, and composer.lock for changes and automatically restarts the analysis session when they are modified (#1402)ambiguous-object-method-access after method_exists narrowing: When method_exists($this, 'foo') adds a HasMethod intersection type, calling other methods on $this no longer falsely reports ambiguous access (#1413, #1426)stdClass&object{tags: list<Tag>} now correctly resolves the property from the shaped object part (#1387, #1421)unimplemented-abstract-property-hook with traits: Concrete properties from used traits now correctly satisfy interface abstract property hooks (#1415, #1420)$this/static return type not enforced for non-final classes: Returning new self() from a method declared as [@return](https://github.com/return) $this or : static now correctly reports a type mismatch in non-final classes. Anonymous classes are treated as effectively final (#1410, #1411, #1418)[@extends](https://github.com/extends): Template parameters inherited through [@extends](https://github.com/extends) annotations are now correctly resolved when accessing shaped array keys (#1412, #1414)i64::MAX treated as int instead of float: Very large integer literals are now correctly inferred as float (#1405)non_empty: Empty array [] used as a default parameter value is now correctly typed as non_empty=false, fixing false positive docblock-type-mismatch errors when using generic classes with default empty arrays (#1422, #1425)variable-name rule for underscore-prefixed variables: Variables starting with _ (e.g., $_unused) are no longer flagged by the variable-name naming rule (#1395, #1398)Box::leak with owned storage for rule descriptions (#1408)i64::MAX are now rejected instead of silently overflowing (#1406)[A-Z] range used in SIMD-accelerated case folding (#1393)minimum-fail-level optionA huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.14.1...1.15.0
Mago 1.14.1 is a patch release focused on fixing false positives across the analyzer's generics and type inference system. Highlights include proper template inference for spread arguments, template constraint violation detection during class instantiation, correct class-string<T> inference from $object::class, improved array_filter handling for union array types, and fixes for variable definedness tracking in try-catch blocks. The formatter also receives several fixes, and the Docker image now includes git for --staged support.
...$list where $list is list<Child> now correctly infer the template type (e.g., T=Child) instead of falling back to the constraint default (#1368)new ViewTable(models: [new Model()]), the analyzer now correctly reports an error if the inferred type doesn't satisfy the template constraint (e.g., T of Textable) (#1355)$object::class to infer class-string<T> for generic parameters: When $object is typed as a generic parameter T of object, $object::class now correctly produces class-string<T> instead of bare class-string (#1372)method_exists/property_exists narrowing string to never: When method_exists($className, 'method') is used with a string variable, the variable is now narrowed to class-string in the truthy branch instead of being discarded as never (#1374)class-string<T> identity during match exhaustiveness narrowing: Match expressions that compare a class-string<T> against class constants no longer lose the generic parameter T, preventing false positive return type mismatches (#1375)array_filter return type for union array types: array_filter now correctly removes nullable types when the input array comes from a match expression or other constructs that produce a union of array types (#1365)throw in one branch and continue in another), variables assigned in the try block are now correctly considered defined after the try-catch (#1352)incompatible-property-hook-parameter-type: The analyzer now uses the effective type (considering [@var](https://github.com/var) docblock narrowing) when checking property hook parameter compatibility (#1342)property-type-coercion for unresolved generic parameters: When instantiating generic classes without explicit type arguments (e.g., new WeakMap()), unresolved generic parameters now use placeholder types instead of constraint defaults, preventing false coercion warnings when assigned to typed properties (#1346)class-string<A>|class-string<B> passed to a class-string<T> parameter would lose one of the union members during template resolution (#1341, #1344)require, include, print, and similar constructs are now preserved when used in binary expressions (#1348, #1353)new in partial application: The formatter no longer wraps new expressions in unnecessary parentheses when used in array or argument contexts (#1370, #1373)str-contains/str-starts-with fixers as potentially unsafe: The auto-fixers for these rules are now flagged as potentially unsafe since the transformation may change behavior in edge cases (#1358)git in Docker image: The Docker image now includes git, enabling --staged options for lint and format commands (#1362, #1369)A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.14.0...1.14.1
Mago 1.14.0 is a feature-packed release that brings full parent type hint support across the analyzer and codex, two new linter rules, PER-CS 3.0 compliance for the formatter's opening tag placement, a new parameter-attribute-on-new-line formatter setting, authenticated GitHub API requests for self-update, and a batch of bug fixes across the analyzer, formatter, syntax, and prelude.
parent type hint support: The parent type hint is now fully supported across the codex, analyzer, scanner, builder, and expander — including return types, parameter types, properties, generics, arrays, and nested types (#1249)Psl\Type\int_range: Added type narrowing support for Psl\Type\int_range (#1329)switch-continue-to-break rule: Detects continue inside switch cases and suggests using break instead, since continue in a switch behaves identically to break in PHP and can mislead readers (#1314)no-redundant-binary-string-prefix rule: Flags redundant b or B prefixes on string literals that don't contain any bytes outside the ASCII range (#1324)opening-tag-on-own-line option for PER-CS 3.0 compliance: Ensures <?php is placed on its own line in pure PHP files by default, per PER-CS 3.0 Section 3. The setting defaults to true but can be set to false to preserve the previous behavior. Template files are unaffected (#1293)parameter_attribute_on_new_line setting: When enabled (default, PER-CS 12.2 compliant), parameter attributes are placed on their own line before the parameter (#1298)mago self-update now uses GitHub tokens (from GITHUB_TOKEN, or GH_TOKEN) for authenticated API requests, avoiding rate-limit failures (#1284)alpine:3 instead of scratch, providing a shell (/bin/sh) so CI runners like GitLab CI can execute commands correctly (#1285)possibly-null-property-access with null-coalesce in short-circuit evaluation: The reconciler now correctly narrows types when ($x ?? null) === null is used in short-circuit || expressions (#1278)redundant-null-coalesce on uninitialized typed properties: Typed properties without default values are now correctly marked as possibly-undefined, preventing false redundant-null-coalesce diagnostics when using ??= (#1286)[@require-extends](https://github.com/require-extends) traits: Protected methods from traits used by a [@require-extends](https://github.com/require-extends) class are now correctly resolved as accessible (#1287)else if flagged by block-statement rule: The block-statement rule no longer incorrectly flags else if (two keywords) as missing a block body (#1299)align-assignment-like no longer pads compact inline arrays into columns (#1321)require, include, include_once, require_once, and print are now preserved when used as the base of a member access chain (#1322)preserve_breaking_parameter_list is enabled (#1290)b string prefix: The b and B binary string prefixes are now correctly parsed (#1301)\\{$var} in heredoc and shell-execute strings is now correctly parsed as an escaped backslash followed by braced interpolation (#1300)A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.13.3...1.14.0
This is a patch release packed with bug fixes across the analyzer, codex, and formatter. The analyzer fixes address several false positives involving generics, union types, type inference, and unreachable code detection. The codex fixes resolve issues with type combination and [@inheritDoc](https://github.com/inheritDoc) inheritance. The formatter fixes resolve issues with concatenation chain formatting and preserve-break propagation.
incompatible-parameter-name reported multiple times) when a class implements multiple interfaces sharing a common ancestor (#1245)static type resolution in method parameters for final classes: Fixed a false positive less-specific-argument when using generics that reference static in method parameters. static is now resolved using the method context and treated as exact for final classes (#1265)T|null parameter where T was inferred as a float would collapse to just null (#1262)[@inheritDoc](https://github.com/inheritDoc) overwriting narrower child return types: Fixed a bug where [@inheritDoc](https://github.com/inheritDoc) on a child method would replace the child's more specific native return type with the parent's broader docblock type. The inherited type is now narrowed against the child's native return type when the child is more specific (#1266)preserve_breaking_* settings caused structural break propagation to parent groups, leading to different output on subsequent formatting passes. Preserve-sourced breaks are now isolated from structural breaks (#1260)A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.13.2...1.13.3
This is a patch release with several bug fixes across the analyzer, formatter, and prelude, along with a batch of performance improvements.
set hook was incorrectly reported as reading a write-only property. In PHP 8.4, backed properties always have an implicit get that returns the backing value — only virtual properties with a set-only hook are truly write-only (#1226)possibly-undefined keys when building arrays in loops: Fixed a false positive where array shape keys were inferred as optional ('key'?: type) when building arrays incrementally inside a loop with conditional branches (#1230)yield in ternary expressions: Fixed a bug where the formatter removed necessary parentheses around yield expressions used as operands in ternary or elvis expressions, producing invalid code (#1231)mago analyze multiple times on the same codebase could produce diagnostics in a different order, making diff-based CI checks unreliable (#1232)socket_* and proc_open functions in the built-in stubs (#1239)TUnion::eq to short-circuit common cases (#1225)Binary::span edge traversal: Reduced overhead when computing spans for binary expressions (#1229)load_paths (#1228)A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.13.1...1.13.2
This is a patch release that adds official Docker container image support and documentation for using Mago in containerized environments.
linux/amd64 and linux/arm64) to the GitHub Container Registry on every release. The image is built from scratch using statically-linked musl binaries, weighing only ~26 MB. Available tags include latest, exact versions (1.13.1), minor (1.13), and major (1) (#1125)Thank you to everyone who reported issues and requested features that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.13.0...1.13.1
This release adds a new no-inline linter rule, fixes a formatter idempotency bug with comments in ternary expressions, fixes missing type diagnostics for abstract methods, replaces the self_update dependency to unblock cargo install mago, and improves self-update error messages.
no-inline rule: Disallows inline content (text outside of <?php tags) in source files. Most modern PHP applications are source-code only and do not use PHP as a templating language. Inline content before <?php, after ?>, or between PHP tags is typically unintentional and can cause issues such as unexpected output or "headers already sent" errors. This rule is disabled by default and is intended for codebases that do not use PHP templates (#1220)missing-parameter-type, missing-return-type, and imprecise type diagnostics were not reported for abstract methods in classes and interfaces (#1223)?/: and their operands in ternary expressions were reordered and merged on each formatting pass, requiring up to 4 passes before stabilizing. Comments after ? and : are now correctly preserved in their original order (#1221)self_update crate with custom updater module: The self_update crate was pinned to a git ref, which blocked cargo publish and cargo install mago. It has been replaced with a minimal, purpose-built updater using ureq, self-replace, and direct archive handlingMago is developed and maintained as an independent open-source project. A special thank you to all of our sponsors whose support makes continued development possible:
Building and maintaining a tool like Mago takes a significant amount of time and effort. While we are incredibly grateful for the support we receive, it does not yet cover full-time development. If Mago is saving you time or improving your codebase, please consider supporting its development through GitHub Sponsors or an enterprise support contract with Carthage Software.
Thank you to everyone who reported issues and requested features that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.12.1...1.13.0
This is a patch release with bug fixes for the analyzer and formatter.
duplicate-array-key with spread operator: Fixed a false positive where explicit keys following a spread expression (e.g., [...self::DEFAULTS, 'title' => 'Override']) were incorrectly flagged as duplicate keys. Overriding spread keys with explicit entries is a common PHP pattern and is no longer reported (#1215)($a === 'b') === $c was flattened into the invalid $a === 'b' === $c). PHP declares comparison operators as non-associative, so chaining them without parentheses is a parse error (#1216)new keyword, aligning with the PER-CS specification (#1115, #1210).rustfmt.toml (#1212)A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.12.0...1.12.1
This release adds clickable file paths in terminal output via OSC 8 hyperlinks, a --fail-on-remaining flag for CI workflows using --fix, improved --staged handling for partially staged files, and several bug fixes in the analyzer, linter, and configuration system.
editor-url in mago.toml or the MAGO_EDITOR_URL environment variable with %file%, %line%, and %column% placeholders. Supported in rich, medium, short, and emacs reporting formats. Hyperlinks are automatically disabled when output is piped or when --colors=never is used (#1188)--fail-on-remaining flag for lint and analyze: When using --fix, Mago now exits with code 0 even if some issues could not be auto-fixed. The new --fail-on-remaining flag restores a non-zero exit code when unfixed issues remain, making it easy to enforce that all issues are resolved in CI pipelines (#1208)--staged --fix handling: When using --staged with --fix, Mago now detects partially staged files and only processes the staged content. Fixed files are automatically re-staged so that fixes are included in the commit without accidentally staging unstaged changes (#1199)object, iterable) when subtracting generic type parameters in negated assertion contexts, which could produce false redundant-condition diagnostics (#1207)===) with other typed variables. Previously, the left-hand variable could remain typed as mixed after comparison with a more precisely typed value (#1206)array-style and str-starts-with when multiple fixes applied to nearby code (#877)XDG_CONFIG_HOME is not set, Mago now correctly falls back to $HOME/.config before checking $HOME, matching the XDG Base Directory Specification. Previously, only $HOME was checked, causing global configuration files in ~/.config/mago.toml to be ignored (#1211)$XDG_CONFIG_HOME, ~/.config, ~ (#1211)editor-url configuration option with URL templates for VS Code, Cursor, Windsurf, PhpStorm/IntelliJ, Zed, Sublime Text, Emacs, and Atom (#1188)Thank you to everyone who reported issues and requested features that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.11.0...1.12.0
This release adds a --staged flag for seamless pre-commit hook integration, a new imprecise-type analyzer diagnostic for bare array/iterable type hints, support for class-like constants in array shape keys, and several bug fixes across the analyzer, formatter, and Composer installer.
--staged flag for lint and analyze: New --staged flag restricts linting and analysis to files currently staged in Git, making pre-commit hook setup straightforward. When combined with --fix, changed files are automatically re-staged so that fixes are included in the commit (#1199)imprecise-type diagnostic for bare array/iterable type hints: The analyzer now warns when parameters, returns, or properties use bare array or iterable type hints without a corresponding docblock annotation specifying the element types. This encourages more precise type documentation across your codebase (#1191)array{MyClass::FOO: string, MyEnum::Bar: int}), enabling more precise type definitions for constant-keyed arrays (#1190)class-string<T> generics: Fixed a bug where type aliases (e.g., [@psalm-type](https://github.com/psalm-type), [@phpstan-type](https://github.com/phpstan-type)) used as the generic parameter in class-string<T> annotations were not resolved, causing false positives (#1202)[@inheritDoc](https://github.com/inheritDoc) across intermediate classes: Fixed false incompatible-parameter-type positives when a child class inherited docblock types from a grandparent through an intermediate class that didn't redeclare the method (#1189)#[Override] error for trait methods: When #[Override] is used on a trait method that doesn't override any parent, the diagnostic now suggests adding a [@require-implements](https://github.com/require-implements) annotation to the trait if the method is intended to override an interface method (#1192)($a ?: $b)->method() or ($a ? $b : $c)->prop, which would change the runtime semantics (#1198)[@pure](https://github.com/pure) annotations removed: Removed incorrect [@pure](https://github.com/pure) annotations from debug_zval_dump(), error_log(), phpinfo(), and php_sapi_name() which could mask side-effect analysis (#1195).zip archives for Windows MSVC targets instead of requesting non-existent .tar.gz files, which caused 404 errors when invoking vendor/bin/mago (#1196)Mago\Internal namespace. Unsupported architecture targets have been removed and ARM v5/v6 detection has been added, with clear error messages when a platform has no pre-built binaryMAGO_ environment variable prefix: Documented that all environment variables starting with MAGO_ are reserved for configuration. Unrecognized MAGO_-prefixed variables (e.g., from CI tools) cause configuration errors. The docs now explain how to diagnose and resolve this (#844)--staged with --fix to automatically fix and re-stage files in pre-commit hooksA huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues and requested features that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.10.0...1.11.0
This release introduces wildcard pragma suppression ([@mago-ignore](https://github.com/mago-ignore) all), auto-fix for unused pragmas, a new just-in-time binary download for Composer, and a large number of bug fixes across the analyzer, linter, formatter, and type system.
[@mago-ignore](https://github.com/mago-ignore) all / [@mago-expect](https://github.com/mago-expect) all wildcard pragmas: You can now use all as the issue code in suppress pragmas to suppress all issues within a category (e.g., [@mago-ignore](https://github.com/mago-ignore) lint:all) or across all categories (e.g., [@mago-ignore](https://github.com/mago-ignore) all). This is especially useful for legacy code where listing individual codes is impractical (#1034)mago lint --fix or mago analyze --fix, unused [@mago-ignore](https://github.com/mago-ignore) and unfulfilled [@mago-expect](https://github.com/mago-expect) directives are now automatically stripped. The fixer handles three cases: removing a single code from a comma-separated list, removing a directive line from a multi-line comment, or deleting the entire comment when all pragmas are unused (#1187)check-functions option for prefer-first-class-callable: The rule now supports a check-functions config option (default: false). When disabled, the rule only suggests first-class callable conversion for method and static method calls, avoiding false positives with internal PHP functions that throw ArgumentCountError on extra arguments (#1147, #1160)exclude-setters-and-constructors option for no-boolean-flag-parameter: The rule now supports excluding setter methods and constructors from boolean flag parameter detection, reducing noise for legitimate boolean setter patterns (#1155)config output: The mago config --show formatter command now displays all resolved formatter settings flattened alongside excludes, matching the TOML configuration structure. The --schema output has also been updated to reflect the flat structure. Previously, only {"excludes": []} was shown (#1180)redundant-condition for is_float() on int|float union: The int ⊂ float containment rule is now guarded by assertion context, preventing incorrect redundant-condition and redundant-type-comparison diagnostics when is_float() or is_double() is called on int|float variables (#1186)undefined-string-array-index errors when writing to (not reading from) unknown array keys with allow-possibly-undefined-array-keys set to false (#1168, #1171)catch-type-not-throwable diagnostic now shows class names in their original casing instead of all-lowercase (#1185)invalid_dependencies during class-like metadata re-population, and the incremental analysis service properly tracks per-file issues (#1176, #1178)missing-magic-method with trait [@property](https://github.com/property)/[@method](https://github.com/method): Real inherited properties and methods from parent classes now correctly override trait pseudo [@property](https://github.com/property) and [@method](https://github.com/method) annotations, preventing false positives when a trait declares magic accessors that shadow real parent members (#1184)[@mixin](https://github.com/mixin) on parent classes are now correctly inherited by child classes during method and property resolution (#1169)prefer-arrow-function disabled inside constant expressions: Arrow function suggestions are no longer emitted inside constant expressions (e.g., class constant initializers) where closures are the only valid syntax (#1166)method-chain-semicolon-on-next-line disabled by default: The Pint preset now correctly defaults this setting to false, matching Pint's actual behavior (#1164)yield expression detected inside return statement: The parser now correctly identifies yield expressions when used as a return value (e.g., return yield $value), preventing incorrect diagnostics on generator functions (#1167)dir() second argument marked as optional: The second parameter of dir() is now correctly annotated as optional, fixing false too-few-arguments errors when calling dir() with a single argument (#1163)strtolower() behavior. This could cause mago to fail to detect errors resulting from case differences in non-ASCII class names (#1161)file-name rule disabled by default: The file-name lint rule is now disabled in the Mago playground to reduce noise for single-file examples (#1162)A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues and requested features that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.9.1...1.10.0
Patch release with several analyzer and formatter bug fixes, plus support for [@psalm-mutation-free](https://github.com/psalm-mutation-free) annotations.
[@psalm-mutation-free](https://github.com/psalm-mutation-free) and [@psalm-external-mutation-free](https://github.com/psalm-external-mutation-free) annotations: The codex scanner now recognizes these Psalm annotations and maps them to Mago's internal mutation-free flags, improving interoperability with Psalm-annotated codebases (#1157)[@pure](https://github.com/pure), [@mutation-free](https://github.com/mutation-free), or [@external-mutation-free](https://github.com/external-mutation-free), since these functions are guaranteed not to mutate the object's state (#1157)instanceof narrowing on sealed class hierarchies: When narrowing a generic type (e.g., Result<T>) with instanceof against a sealed inheritor (e.g., Success<T>), the type parameters from the parent type are now correctly carried over to the narrowed type, preventing unexpected never type results (#1156)[@var](https://github.com/var) annotations and instanceof RHS: The analyzer now reports non-existent-class-like errors for undefined types used in [@var](https://github.com/var) docblock annotations and on the right-hand side of instanceof expressions, matching the existing behavior for parameter types, return types, and property types (#1007)undefined-string-array-index errors when accessing keys on union types containing both sealed and unsealed array variants (e.g., array{foo: int, ...}|array{foo: int}). The unsealed variant's generic parameters are now properly considered when determining whether a key might exist (#1154)undefined-string-array-index error is now reported only once instead of once per union variant// [@phpstan-ignore](https://github.com/phpstan-ignore) method.unused) on method signatures with multiline parameter lists were incorrectly moved to the next line (#1153)DateTimeImmutable and DateTimeZone methods annotated as mutation-free: Methods on DateTimeImmutable and DateTimeZone that do not modify state are now annotated with [@mutation-free](https://github.com/mutation-free), preventing false positive property narrowing invalidations when calling these methods (#1157)ReflectionClass::getReflectionConstants() return type: Added missing return type information (#1152)space_after_colon_in_enum_backing_type setting: Removed a reference to a non-existent formatter setting from the configuration reference documentation (#1151)A huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.9.0...1.9.1
This release brings PHP 8.5 deprecation detection, new return type providers for sprintf() and array_map(), generator type inference, several new formatter options, and a large number of bug fixes across the analyzer, formatter, linter, and type system.
sprintf() and Psl\Str\format(): The analyzer now resolves return types for sprintf() calls with constant format strings, producing precise literal string types when all arguments are known at analysis time (#1073)array_map() preserving array shapes: When calling array_map() with a typed callback on a keyed array, the analyzer now preserves the array shape in the return type instead of widening to array<key, value> (#1144)yield but no explicit return type annotation now have their Generator<K, V, S, R> type inferred from the yielded keys, values, and return statements (#1150)deprecated-cast: Detects deprecated non-canonical type casts ((integer), (boolean), (double), and (binary)) and suggests their canonical replacementsdeprecated-shell-execute-string: Detects usage of the backtick shell execute syntax (`ls -l`), which is deprecated in PHP 8.5, and suggests using shell_exec() insteaddeprecated-switch-semicolon: Detects use of semicolons (;) as case separators in switch statements, deprecated in PHP 8.5, and suggests using colons (:) insteadmago-ignore / mago-expect diagnostics: When an ignore or expect pragma does not match any issue, the diagnostic now highlights the specific issue code that was not matched, making it easier to identify stale or incorrect pragmas (#1123)method-chain-semicolon-on-next-line setting: New option to place the semicolon on its own line when a method chain breaks across multiple lines, equivalent to PHP-CS-Fixer's multiline_whitespace_before_semicolons: new_line_for_chained_calls. Disabled by default, enabled in the Laravel/Pint preset (#1105)null_pipe_last variant for null-type-hint setting: New option that converts ?T to T|null and reorders union types to place null last, providing PER-CS 3.0 compliance for null type positioning (#1133, #1134)/**in single-line doc blocks: The formatter now ensures a space is present after the opening/**in single-line doc blocks (e.g.,/**[@var](https://github.com/var) int _/becomes/\*\* [@var](https://github.com/var) int _/) (#1077)analyze --list-codes: New flag that outputs all analyzer issue codes as a JSON array of strings, useful for tooling integration (#1146)lint --list-rules --json now includes severity: The JSON output of --list-rules now includes a level field (Error, Warning, Help, or Note) for each rule, matching the information shown in the human-readable table (#1142)redundant-nullsafe-operator from producing code-breaking false positives when the nullsafe operator is legitimately needed (#1131)paradoxical-condition / impossible-condition errors when narrowing numeric types with is_numeric() checks on multiple variables (#1130)invalid-callable errors on functions like Closure::bind() (#1127)null-argument positives (#1126)array<K, T> instead of list<T>, since named arguments can produce string keys (#1138)// [@phpstan-ignore](https://github.com/phpstan-ignore) method.unused) on method signatures were incorrectly moved to the opening brace line when using method-brace-style = "always-next-line" (#1124)<?= ?>) and single-expression echo statements within HTML templates (#1149)!(/* comment */ $x)) would oscillate between different positions on each format pass (#1135)method-chain-semicolon-on-next-line setting now correctly applies only when the method chain is the direct expression of the statement, not when a chain appears nested inside another expression (e.g., as a function argument)no-trailing-space fixer panic on CRLF files: Fixed a panic when the fixer encountered multibyte characters on lines with CRLF line endings (#1137)"{$arr[Foo\BAR]}") (#1128)parse_str() [@param-out](https://github.com/param-out) type: Fixed the output parameter type annotation for parse_str() (#1140)Closure::bind() stubs: Added proper stub definitions to prevent false invalid-callable errors (#1127)IssueCode::all() method for listing all analyzer codesRuleEntry struct for serializing linter rules with metadata and severity levelA huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues and requested features that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.8.0...1.9.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.
is_a() and is_subclass_of() type narrowing: The analyzer now narrows types after calls to is_a() and is_subclass_of(), including support for class-string parameters (#1102)filter_var() and filter_input(): These functions now return precise types based on the filter and flags arguments (e.g., FILTER_VALIDATE_INT returns int|false, FILTER_VALIDATE_EMAIL with FILTER_NULL_ON_FAILURE returns string|null) (#1117)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)no-isset array access ignore option: The no-isset rule now supports an allow-array-access option, allowing you to flag isset($var) while still permitting isset($array['key']) for array offset checks (#1097, #1120) by @dotdashfunction() { ... }() as error, requiring parentheses around the closure for immediate invocation (#1118)The watch mode (mago analyze --watch) received a complete overhaul of its incremental analysis pipeline:
extend_ref and remove_entries operations allow fine-grained metadata updates without rebuilding the entire codebasenon-existent-class-like errors in watch moderequire-extends/require-implements resolution: Members from [@require-extends](https://github.com/require-extends) and [@require-implements](https://github.com/require-implements) types are now correctly resolved (#1064, #1070)\true, \false, and \null are now correctly recognized (#1099, #1100) by @kzmshxget_substituted_method function is now correctly applied to the child method when checking method signature compatibility, fixing false positives with generic abstract method inheritanceIteratorIterator) are now preserved during method resolution, fixing incorrect return types (#1106)for ($i = 0; $i < 10; $i++)) are now properly extracted from the AST for type narrowing (#1089)redundant-type-comparison when using count checks or string narrowing in || conditions (#1112)HasAtLeastCount assertions no longer incorrectly set an exact known_count on lists with unknown count, preventing false unreachable-code reports (#1104)[@var](https://github.com/var) docblock type: The analyzer now prefers [@var](https://github.com/var) docblock types over inferred types for class constants, fixing cases where properly typed array values stayed as mixed (#1090, #1094)never as bottom type: never is now correctly treated as a subtype of all types in extends_or_implements checks (#1107, #1109) by @kzmshx[@psalm-type](https://github.com/psalm-type) / [@phpstan-type](https://github.com/phpstan-type) alias names are now pre-registered before parsing, so aliases can reference each other regardless of declaration order (#1116)impossible-condition false positives when comparing strtolower()/strtoupper() results with literals containing non-alphabetic characters (spaces, digits, etc.) (#1086)no-redundant-use whole-word matching: Docblock reference checking now uses whole-word matching instead of substring matching, so use Config; is correctly flagged as unused even when ConfigUsage appears in a docblock (#1078)inline-variable-return with by-reference assignment: The fixer no longer inlines assignments of by-reference expressions, which would produce invalid PHP (#1114)prefer-early-continue with non-block body: Fixed the fixer for cases where the loop body is a single statement without braces (#1085) by @chrisopperwall-qz$foo(...)) are no longer incorrectly treated as breaking expressions, fixing misformatted output (#1091)explode() return type: Corrected to properly return list<string> instead of non-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 @veeweeIncrementalAnalysisService encapsulating the full incremental analysis pipeline for watch mode and LSPCodebaseDiff::between() for metadata comparison and mark_safe_symbols() for incremental analysisA huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues and requested features that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.7.0...1.8.0
This release introduces new type system features, improved type inference for built-in functions, a new linter rule, and numerous bug fixes for the analyzer, formatter, and type system. A significant internal effort also went into reducing dependencies and binary size.
uppercase-string and non-empty-uppercase-string types: Full support for these PHPDoc types in type syntax, codex, and analyzer. This resolves cascading errors when these types were previously unrecognized (#1057)Return type providers for min() and max(): These functions now return precise types based on their arguments (#1074)
array_filter() callback parameter type inference: The analyzer now respects the mode argument (ARRAY_FILTER_USE_KEY, ARRAY_FILTER_USE_BOTH) when inferring closure parameter types, fixing incorrect mixed inference for callback parameters (#1031)
Switch statement fallthrough analysis: The analyzer now correctly recognizes that non-terminating code paths in a case block fall through to the next case. A case with a conditional return followed by a case that always returns is no longer flagged as missing-return-statement (#1081)
no-redundant-isset rule: New rule that detects redundant arguments in isset() calls. For example, in isset($a, $a['key'], $a['key']['nested']), the first two checks are redundant because isset on a nested access implicitly checks all parent accesses (#769)--ignore-baseline flag: New flag for lint and analyze commands that temporarily ignores the baseline file, useful for reviewing and fixing baselined issues (#1076)reqwest, openssl, num_cpus, strum_macros, derivative, strsim, bitflags, async-walkdir), replacing them with standard library equivalents or manual implementations. reqwest/openssl were replaced with ureq/rustls for a significantly smaller and faster-compiling binary[$this, 'method']) and string callbacks ('ClassName::method') are now correctly tracked as used (#1069, #1044)[@psalm-require-extends](https://github.com/psalm-require-extends) support in traits: Methods, properties, and class constants inherited from required parent classes via [@psalm-require-extends](https://github.com/psalm-require-extends) or [@phpstan-require-extends](https://github.com/phpstan-require-extends) are now properly resolved in traits, eliminating false non-existent-property, non-existent-class-constant, and unknown-ref errors (#1064, #1068, #1070)invalid-return-type errors (#1061)PHP_INT_SIZE, PHP_INT_MAX, and PHP_FLOAT_DIG now use platform-aware range/union types instead of host-specific literal values. PHP_INT_SIZE > 4 is no longer flagged as a redundant comparison (#1084)align-assignment-like is enabled, the alignment context from consecutive variable assignments no longer leaks into nested array key-value pairs (#1082)prefer-first-class-callable with reference captures: Skip suggesting first-class callable syntax when the callee variable is captured by reference in a closure's use clause, as the two forms have different semantics (#1067, #1063) by @kzmshxarray_walk generics: Fixed generic templates for array_walk to properly infer callback parameter types (#1066, #1045) by @ddanielouarray_splice type precision: Improved type definitions for array_splice to preserve list<T> types and correctly handle non-array replacement arguments (#1072, #1080)usort, uasort, uksort, etc.) to preserve non-empty array types (#1083)reqwest + openssl with ureq + rustls in self-update modulenum_cpus with std::thread::available_parallelism()bitflags with manual bit flag implementationsderivative, strum_macros, strsim, and async-walkdir dependenciesA huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues and requested features that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.6.0...1.7.0
This release brings new analyzer checks for class design enforcement, new linter rules for file organization, path-scoped ignore/exclusion support, formatter fixes, and numerous bug fixes across the board.
class-must-be-final check: New opt-in enforce-class-finality setting that reports classes not declared final, abstract, or annotated with [@api](https://github.com/api)/[@psalm-api](https://github.com/psalm-api) when they have no children (#1054)
[analyzer]
enforce-class-finality = true
missing-api-or-internal check: New opt-in require-api-or-internal setting that requires abstract classes, interfaces, and traits to have [@api](https://github.com/api) or [@internal](https://github.com/internal) annotations, forcing projects to declare extensibility intent (#1055)
[analyzer]
require-api-or-internal = true
Path-scoped ignore entries: The ignore option now supports scoping ignored codes to specific paths (#1037, #1043)
[analyzer]
ignore = [
"mixed-argument",
{ code = "missing-return-type", in = "tests/" },
{ code = "unused-parameter", in = ["tests/", "src/Generated/"] },
]
Literal types for enum properties: Enum name and value properties now return literal types instead of generic string/int, enabling more precise type inference (#1035, #952) by @veewee
Severity level in code-count format: The code-count reporting format now includes the severity level for each issue code (#987)
file-name rule: New rule that enforces file names match the class/interface/enum/trait they contain (#1049)
single-class-per-file rule: New rule that enforces each file contains at most one class-like declaration
Per-rule path exclusions: Linter rules now support path-based exclusions, allowing you to disable specific rules for specific directories (#1037)
[linter.rules]
no-isset = { exclude = ["src/Legacy/"] }
no-isset and readable-literal enabled by default: These rules are now enabled out of the box
Removed deprecated rules: The deprecated constant-type, no-boolean-literal-comparison, parameter-type, property-type, and return-type linter rules have been removed — their functionality has been moved to the analyzer
inline-empty-function-braces and inline-empty-method-braces now default to true, matching the PER Coding Style specification (#1053)--only rules: The --only flag now accepts multiple comma-separated rules (#1046)foldhash replaces ahash: Switched to foldhash for faster hashing across the codebaseArc<T> replaces Box<T> in codex: Improves cloning performance for shared metadataswitch statements no longer incorrectly narrow variable types, preventing false redundant null-check warnings (#1038)[@var](https://github.com/var) docblocks: Imported type aliases are now correctly expanded in [@var](https://github.com/var) docblock annotations (#1029, #1030)extends/implements (#1040)FORCE_COLOR support: The FORCE_COLOR environment variable is now respected in reporting output (#1042)readable-literal false positive on floats: Fixed the rule triggering on float literals like 123.45 where neither side of the decimal benefits from separators, producing a no-op suggestionarray_walk generics: Added generic templates to array_walk for proper type inference (#1045)curl_multi_exec signature: Corrected the signature of curl_multi_exec (#1033)setcookie duplicate: Removed duplicate definition for setcookie that only accepted the old syntax (#1032)atty dependencytime from 0.3.46 to 0.3.47 (#1039) by @dependabotA huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues and requested features that shaped this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.5.0...1.6.0
This release brings PSR-11 container support, custom help text for disallowed functions, and several important bug fixes for type narrowing and linter false positives.
PSR-11 container support: New psr-container plugin that infers types from class-string arguments when calling ContainerInterface::get(). This eliminates mixed-method-access and mixed-argument errors when using PSR-11 service containers (#1015) by @Noojuno
[analyzer]
plugins = ["psr-container"]
Custom help text for disallowed functions: The disallowed-functions rule now supports custom help messages per function or extension. Entries can be simple strings or objects with name and optional help fields (#1024)
[linter.rules]
disallowed-functions = {
functions = [
"eval",
{ name = "error_log", help = "Use MyLogger instead." },
],
extensions = [
"curl",
{ name = "ffi", help = "FFI is disabled for security reasons." },
],
}
Nullsafe operator type narrowing: Fixed incorrect type narrowing in the false branch of if statements using nullsafe operators (?->). Previously, the variable was incorrectly narrowed to null in the else branch. Now if ($user?->isAuthorized()) correctly preserves the original type in the else branch, matching the semantics of $user !== null && $user->isAuthorized() (#1025)
Redundant instanceof detection: Fixed false positive for undefined variables when a variable is assigned in exhaustive if/elseif branches over a union type. The analyzer now correctly detects redundant instanceof checks and tracks variable assignments across all branches (#1026)
prefer-first-class-callable: Skip suggesting first-class callable syntax for runtime-dependent call targets where conversion would change evaluation semantics. This includes method chains (adminUrlGenerator()->generateUrl()), nullsafe calls ($obj?->method()), and dynamic method names ($obj->$method()) (#1027, #1020) by @kzmshxbytes from 1.11.0 to 1.11.1 (#1023) by @dependabotA huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that were fixed in this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.4.1...1.5.0
Patch release with two bug fixes.
(void) cast expressions (#1021)JSON_THROW_ON_ERROR detection: Detect JSON_THROW_ON_ERROR flag when combined with other flags using bitwise OR (e.g., JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT) (#1022, #886) by @kzmshxFull Changelog: https://github.com/carthage-software/mago/compare/1.4.0...1.4.1
This release brings significant improvements including method call assertions, a new parser configuration section, fault-tolerant parsing, new linter fixers, and numerous bug fixes addressing false positives in the analyzer. A massive thank you to everyone who contributed!
[@assert-if-true](https://github.com/assert-if-true) and [@assert-if-false](https://github.com/assert-if-false) docblock assertions on method calls, enabling type narrowing based on method return values (#763)no-redundant-string-concat: Automatically fix redundant string concatenations (#1018) by @dotdashno-trailing-space: Automatically remove trailing whitespace (#1017) by @dotdashbraced-string-interpolation: Automatically add braces around interpolated variables (#1013) by @dotdashmax-statements-before-early-continue option to control when the early-continue rule recommends refactoring (#979, #975) by @dotdash[parser] configuration section: Configure parser-level settings including the ability to disable short opening tag recognition via enable-short-tags = false (#841)FORCE_COLOR environment variable: Force colored output even when piping to files or other commands, taking precedence over NO_COLOR (#1012)ascii_lowercase_atom for common casesprivate(set) in child classes (#985)!empty() check (#973)$_SERVER array shape typo: Fix arvc typo to argc in $_SERVER array shape (#990, #972) by @kzmshxsort-class-methods is enabled, comments now move with their associated methods instead of stacking on the first method (#994)space-around-assignment-in-declare to false in Pint preset to match actual Pint behavior (#1011, #974) by @kzmshxinline-variable-return when closure has reference capture (#997, #981) by @kzmshxend, prev, next, reset with [@param-out](https://github.com/param-out) annotations to preserve array type after by-reference calls (#980, #984)array_unshift signature: Preserve list type in [@param-out](https://github.com/param-out) annotation (#970)pathinfo() where the second argument is optional (#969).gitattributes to exclude more files from export (#1008) by @shyimconsts to typos dictionaryA huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that were fixed in this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.3.0...1.4.0
This release comes with significant performance improvements, bug fixes addressing false positives, and new formatter options. A massive thank you to everyone who reported issues and contributed!
always-next-line brace style: New always-next-line value for brace style options (method-brace-style, function-brace-style, closure-brace-style, classlike-brace-style, control-brace-style) that always places the opening brace on the next line, even for multiline signatures (#897)This release includes substantial performance optimizations:
AtomSet instead of Vec for clause key lookupsHashMap for boundsAtomMap and ControlActionSet bitfield for faster block context cloningHashMap instead of IndexMap for constants and enum_cases where ordering is not requiredIndexMap for overridden_method_ids to ensure deterministic ordering (#907)static return types correctly in method chains across inheritance (#880, #949, #964)mixed (#946, #947)isset() type narrowing (#900)&& operator (#912)never to stringNO_COLOR environment variable and --colors never flag globally across all output (#922)&$param) in [@param](https://github.com/param) tags (#955)null and bool values in sprintf, printf, sscanf, and fscanf functions (#953)$_FILES superglobal as potentially empty (#954)Vec to IndexMap with GenericTemplate struct for cleaner codeu32 bitflagsA huge thank you to everyone who contributed code to this release:
Thank you to everyone who reported issues that were fixed in this release:
Full Changelog: https://github.com/carthage-software/mago/compare/1.2.2...1.3.0
Patch release to fix binary builds broken by upstream dependency.
The 1.2.1 release binaries failed to build due to a breaking change in cargo-pgo v0.2.10 (released January 24, 2026) which changed how arguments are passed to the optimize command. This release contains no code changes-only a CI workflow fix.
Full Changelog: https://github.com/carthage-software/mago/compare/1.2.1...1.2.2
Turns out releasing at 4 AM after a break isn't the best idea. Sorry about that!
Y::create(...) now correctly returns Y instead of the parent class X)Full Changelog: https://github.com/carthage-software/mago/compare/1.2.0...1.2.1
First of all, I want to apologize for the delayed release — I've been on a break the past couple of weeks. Thank you all for your patience! 🙏
This release brings significant improvements to the analyzer, including unused code detection, better type inference, and numerous bug fixes addressing false positives reported by the community.
no-isset rule: New rule to prevent usage of the isset construct (#924) by @djschillingno-redundant-use rule (#921)lowercase-type-hint and lowercase-keyword: New fixers for these linter rules (#911) by @dotdashmago.dist.toml and mago.dist.json files (#903) by @Bleksakinline-abstract-property-hooks setting for PER-CS 4.10 compliant property hook formatting (#919)$foo->bar(...)) and partial function application ($foo->bar(?, ?)) to prevent false positive unused method errorsparent:: calls: Treat parent:: calls as instance calls for magic methods (#916)#[Override] for [@method](https://github.com/method): Don't suggest #[Override] attribute for [@method](https://github.com/method) pseudo-methods (#914)never RHS: Suppress false positive for possibly-undefined array index in null coalesce with never RHS (#923)DatePeriod type parameters: Specify IteratorAggregate type parameters for DatePeriod (#932)strpos offset type: strpos supports negative offsets, replace int<0, max> with int (#890) by @Bleksakno-insecure-comparison: Ignore CLI flags like --password in the rule (#917)NO_COLOR environment variable: Respect NO_COLOR env variable (#922)FormatterPreset config deserialization (#879) by @magic-akariA huge thank you to everyone who contributed to this release:
Thank you to everyone who reported issues that were fixed in this release:
More improvements and features coming soon! Stay tuned.
Full Changelog: https://github.com/carthage-software/mago/compare/1.1.0...1.2.0
This release includes a significant number of new features, bug fixes, and improvements across the formatter, analyzer, linter, and codex.
Note: This release includes a breaking change in the formatter's default behavior for heredoc indentation.
indent-heredoc to false in your configuration.psr-12, per-cs, drupal, etc.) (#839)following-clause-on-newline option to place else, catch, and finally on a new line (#860)uppercase-literal-keyword option for uppercase TRUE, FALSE, NULL literals (Drupal style) (#857)empty-line-before-class-like-close setting for empty line before closing brace (#855)newline-after-class-like-opening-brace setting (#853)[@mixin](https://github.com/mixin) docblock annotationsexplicit-nullable-param lint (#847)$x['foo'] ?? null)invalid-extend reporting when a readonly class extends a non-readonly class (#873)static and $this type handling and expansion in template contextsarray_filter type narrowing to support first-class callables and string literalsclone for PHP 8.3+self::TypeAlias in [@extends](https://github.com/extends))init flownon-empty-mixed type syntaxFull Changelog: https://github.com/carthage-software/mago/compare/1.0.3...1.1.0
This release includes several bug fixes across the formatter, linter, analyzer, and prelude, along with new features.
As always, we'd love to hear from you! Please keep filing bug reports and feature requests - your feedback is what drives Mago forward.
Our team will be taking a well-deserved break over the holidays. Merry Christmas to those who celebrate, and happy holidays to everyone! We'll be back in full swing in the new year, but rest assured we'll still be keeping an eye out for any critical issues.
null|array no longer causes formatting oscillation)wrap! macro to prevent node stack corruptionprefer-arrow-function and prefer-static-closure rulesprefer-anonymous-migration rule to correctly flag named migration classesno-redundant-use ruleClosure::bind scope changes for protected member access analysisgetimagesizefromstring second parameter as optionalno-assign-in-argument rule to detect assignments in call arguments (#821)Closure::bind scope changes, allowing accurate analysis of protected/private member access within rebound closuresif let to fix unnecessary_unwrap warning (#781)Full Changelog: https://github.com/carthage-software/mago/compare/1.0.2...1.0.3
inline-empty-classlike-braces default to true per PER-CS specification[@extends](https://github.com/extends) validationarray_map now correctly returns non-empty-array when given a non-empty-array (#815)current() return typenull-pipe is not a valid value for null-type-hint setting (#814)source-code.tar.gz and source-code.zip archives containing full source codeFull Changelog: https://github.com/carthage-software/mago/compare/1.0.1...1.0.2
This release includes various bug fixes across the analyzer, formatter, codex, and prelude.
[@var](https://github.com/var) docblock handling when used on top of an assignmentvoid as falsycurrent function return typeDirectoryIterator::isDot methodFull Changelog: https://github.com/carthage-software/mago/compare/1.0.0...1.0.1
After over 1,000 commits, 13 release candidates, 34 betas, and 12 alphas, we are thrilled to announce Mago 1.0.0 - the first stable release of the Mago PHP toolchain.
Mago is a comprehensive PHP toolchain written in Rust that combines a linter, formatter, and static analyzer into a single, blazingly fast binary. Whether you're working on a small project or a massive codebase with millions of lines, Mago delivers consistent, reliable feedback in seconds.
If you last used Mago at version 0.26.1, the tool has evolved dramatically:
[@throws](https://github.com/throws) validationThe linter includes 135 rules organized into 9 categories:
| Category | Focus |
|---|---|
| Best Practices | Idiomatic PHP patterns and conventions |
| Clarity | Code readability and expressiveness |
| Consistency | Uniform coding style across your codebase |
| Correctness | Logic errors and potential bugs |
| Deprecation | Outdated patterns and deprecated features |
| Maintainability | Long-term code health |
| Redundancy | Unnecessary or duplicate code |
| Safety | Type safety and null handling |
| Security | Potential security vulnerabilities |
Many rules include automatic fixes that can be applied with mago lint --fix.
The formatter produces clean, consistent code following PER Coding Style with over 50 customization options:
Run mago format --check in CI to ensure consistent formatting across your team.
The analyzer performs deep static analysis with:
check-throws) to ensure exceptions are caught or documentedThe analyzer supports plugins for library-specific type inference:
| Plugin | Description |
|---|---|
stdlib |
PHP built-in functions (enabled by default) |
psl |
azjezz/psl type providers |
flow-php |
flow-php/etl type providers |
Enable plugins in your mago.toml:
[analyzer]
plugins = ["psl", "flow-php"]
More plugins coming soon for Symfony, Laravel, Doctrine, and PHPUnit.
Guard enforces architectural rules and dependency boundaries across your codebase:
[guard.perimeter]
# Defines the architectural layers from core to infrastructure.
layering = [
"CarthageSoftware\\Domain",
"CarthageSoftware\\Application",
"CarthageSoftware\\UI",
"CarthageSoftware\\Infrastructure"
]
# Creates reusable aliases for groups of namespaces.
[guard.perimeter.layers]
core = ["[@native](https://github.com/native)", "Psl\\**"]
psr = ["Psr\\**"]
framework = ["Symfony\\**", "Doctrine\\**"]
# Defines dependency rules for specific namespaces.
[[guard.perimeter.rules]]
namespace = "CarthageSoftware\\Domain"
permit = ["[@layer](https://github.com/layer):core"]
[[guard.perimeter.rules]]
namespace = "CarthageSoftware\\Application"
permit = ["[@layer](https://github.com/layer):core", "[@layer](https://github.com/layer):psr"]
[[guard.perimeter.rules]]
namespace = "CarthageSoftware\\Infrastructure"
permit = ["[@layer](https://github.com/layer):core", "[@layer](https://github.com/layer):psr", "[@layer](https://github.com/layer):framework"]
[[guard.perimeter.rules]]
namespace = "CarthageSoftware\\Tests"
permit = ["[@all](https://github.com/all)"]
Run mago guard to check for architectural violations. This helps maintain clean architecture by:
Mago 1.0.0 is production ready and actively used by companies analyzing millions of lines of PHP code daily. The toolchain has been battle-tested across diverse codebases, from legacy monoliths to modern frameworks.
Introducing static analysis to an existing codebase can be overwhelming. Mago supports baselines that let you:
mago analyze --generate-baselinemago analyze --baseline baseline.tomlWe are committed to fixing reported issues quickly. Most bug reports are addressed within 1-2 days. While false positives may occasionally occur, we treat them as high-priority bugs.
This release includes the following changes since the last release candidate:
properties-of<T> now correctly expands to array<non-empty-string, mixed> for generic object constraintspossibly-null-array-index warnings for keyed arrays with null key coercionarray and iterable types now handled correctlyexplicit-octal ruleMago is significantly faster than traditional PHP-based tools. On the wordpress-develop codebase:
| Analyzer | Time |
|---|---|
| Mago | 3.88s |
| Psalm | 45.53s |
| PHPStan | 120.35s |
Mago analyzes the entire WordPress codebase in under 4 seconds—12x faster than Psalm and 31x faster than PHPStan.
For full benchmarks including the linter and formatter, see mago.carthage.software/benchmarks.
curl --proto '=https' --tlsv1.2 -sSfO https://carthage.software/mago.sh && bash mago.sh
brew install carthage-software/tap/mago
composer require --dev carthage-software/mago
cargo install mago
For more installation options, see the Installation Guide.
Initialize Mago in your project:
mago init
Run the linter:
mago lint
Run the analyzer:
mago analyze
Format your code:
mago format
For comprehensive documentation, visit mago.carthage.software.
The 1.0.0 release is just the beginning. Our roadmap includes:
Mago would not exist without the pioneering work of:
Mago is developed and maintained by Carthage Software. For companies interested in supporting Mago or needing expert PHP tooling consulting, get in touch.
Thank you to our sponsors who make this work possible:
Become a sponsor to support Mago's continued development.
Thank you to everyone in our Discord community who reported bugs, suggested features, and provided feedback. Your contributions have been invaluable in shaping Mago into what it is today.
Full Changelog: 0.26.1...1.0.0
This release introduces an internal plugin system for the analyzer, adds new linter rules, and continues the focus on minimizing false positives.
UnitEnum::cases() return type provider: Properly infers the return type of cases() on unit enumsinstanceof-stringable rule: Suggests using $x instanceof Stringable instead of the verbose is_object($x) && method_exists($x, '__toString') patternparent:: now works in traits with [@require-extends](https://github.com/require-extends) annotation (#732)__toString method can be cast to string without errors?->)$this handling corrected in type expansionFull Changelog: https://github.com/carthage-software/mago/compare/1.0.0-rc.12...1.0.0-rc.13
This release continues our focus on reducing false positives, improving generic type handling, and enhancing PHP 8.4+ property hooks support.
fmt --staged command for formatting only staged files, along with a pre-commit hooks guide for seamless CI integration.=== '', === 0, === 1.5, === true, === false) after type narrowing in OR conditions.unimplemented-abstract-property-hook when a concrete class inherits the hook implementation from a parent class.is_callable() Narrowing: Added support for is_callable() type narrowing on array callables with class-string elements (e.g., array{class-string<Foo>, 'method'}).isset() on Open Array Shapes: Fixed false positives when using isset() checks on open array shapes with mixed values.stdClass: Allowed dynamic property access on stdClass without triggering errors.[@method](https://github.com/method) Tag Parsing: Fixed parsing of [@method](https://github.com/method) tags with leading whitespace./** [@lang](https://github.com/lang) SQL */) were incorrectly moved after the opening identifier, resulting in invalid code.empty_line_after_opening_tag default behavior consistent across configurations.Full Changelog: https://github.com/carthage-software/mago/compare/1.0.0-rc.11...1.0.0-rc.12
This release focuses on reducing false positives, improving type system accuracy, performance optimizations, and introducing new linter rules.
New property-name rule (#703)
Added a new linter rule to enforce consistent property naming conventions across your codebase.
New use-specific-assertions rule
Introduced a new rule that encourages using specific assertion methods (e.g., assertTrue, assertNull) instead of generic assertEquals/assertSame with literal values for better clarity and intent.
function_exists(), method_exists(), property_exists(), and defined(). This eliminates false positives when conditionally using symbols after checking their existence.Fixed false positives for class-string comparisons and static variable initialization Resolved issues where the analyzer incorrectly flagged valid code involving class-string type comparisons and static variable initialization patterns.
Fixed false positive for count() comparison on non-empty-list
The analyzer no longer incorrectly reports issues when comparing the count of a non-empty-list with integer values.
Fixed list type preservation when narrowing with is_array()
Using is_array() on a union type containing a list no longer incorrectly loses the list type information.
Fixed interface method resolution for __callStatic
The analyzer now only reports interface implementation issues when resolving actual methods, not when resolving magic __callStatic calls.
Fixed invalid array access assignment value checking Improved detection of invalid values being assigned through array access expressions.
[@method](https://github.com/method) tag with static return type
The docblock parser now correctly handles [@method](https://github.com/method) tags that specify static as their return type.strict-assertions rule less strict
The strict-assertions rule has been adjusted to reduce noise while still catching problematic assertion patterns.Optimized type combiner Significant performance improvements to the type combination logic, reducing analysis time for codebases with complex type operations.
Lowered analysis thresholds Adjusted internal thresholds for formula complexity and algebra operations to improve analysis speed on large codebases without sacrificing accuracy.
Early return optimization for pragma collection Added early return when no pragmas are present, avoiding unnecessary processing.
Full Changelog: https://github.com/carthage-software/mago/compare/1.0.0-rc.10...1.0.0-rc.11
This release focuses on reducing false positives across multiple analysis scenarios, improving type system accuracy, and enhancing PHP compatibility.
Foo can be Bar at runtime (#707)$value === [] on non-null or mixed types (#701)unimplemented-abstract-property-hook incorrectly reported on interfacesIterator key/value types from key() and current() method return typesfloat and array-key in string concatenation with improved __toString trait detection$_COOKIE superglobalself type resolution to use intersection with [@require-implements](https://github.com/require-implements)/[@require-extends](https://github.com/require-extends) constraintscall_user_func and sprintf stubs with Stringable supportFull Changelog: https://github.com/carthage-software/mago/compare/1.0.0-rc.9...1.0.0-rc.10
Full Changelog: https://github.com/carthage-software/mago/compare/1.0.0-rc.8...1.0.0-rc.9
How can I help you explore Laravel packages today?