carthage-software/mago
Mago is an extremely fast PHP linter, formatter, and static analyzer written in Rust. It brings Rust-inspired speed and reliability to PHP projects with a modern toolchain and great developer experience, plus multiple install options (script, Homebrew, Composer).
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?