github carthage-software/mago 1.8.0
Mago 1.8.0

5 hours ago

Mago 1.8.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.

✨ Features

Analyzer

  • 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)
  • Return type providers for 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)
  • Discriminated union narrowing: When narrowing a union of keyed arrays (e.g., 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)

Linter

  • no-isset array access ignore option: The no-isset rule now supports an ignore-array-access option, allowing you to flag isset($var) while still permitting isset($array['key']) for array offset checks (#1097, #1120) by @dotdash

Semantics

  • Enforce parentheses for immediately invoked closures: The semantics checker now flags function() { ... }() as error, requiring parentheses around the closure for immediate invocation (#1118)

⚡ Performance

Incremental Analysis Engine

The watch mode (mago analyze --watch) received a complete overhaul of its incremental analysis pipeline:

  • Signature-only fingerprinting: Body-only changes (e.g., changing a function implementation without modifying its signature) now skip cascade invalidation, resulting in significantly faster re-analysis cycles
  • Targeted O(dirty) repopulation: Only changed symbols are re-populated, skipping safe symbols entirely
  • Incremental codebase patching: New extend_ref and remove_entries operations allow fine-grained metadata updates without rebuilding the entire codebase
  • Safe symbol restoration: The reference graph now supports restoring safe symbols and targeted cleanup, reducing unnecessary re-analysis
  • Body-only docblock resolution: Fixed a bug where body-only changes left docblock type references unresolved, causing spurious non-existent-class-like errors in watch mode
  • Improved file watcher stability: Better debounce handling, stability checks, and explicit path handling for the file watcher
  • Watch mode is no longer experimental: The experimental warning has been removed

🐛 Bug Fixes

Analyzer

  • require-extends/require-implements resolution: Members from @require-extends and @require-implements types are now correctly resolved (#1064, #1070)
  • Unused property false positive with trait overrides: Properties that override trait properties via constructor promotion are no longer incorrectly flagged as unused (#1119)
  • FQN literal constants: Fully-qualified constant accesses \true, \false, and \null are now correctly recognized (#1099, #1100) by @kzmshx
  • Class-level template parameters for static calls: Template parameters defined at the class level are now properly resolved when making static method calls on generic types (#1103)
  • Abstract method compatibility checking: The get_substituted_method function is now correctly applied to the child method when checking method signature compatibility, fixing false positives with generic abstract method inheritance
  • Mixin type parameters preservation: Type parameters on mixin types (e.g., IteratorIterator) are now preserved during method resolution, fixing incorrect return types (#1106)
  • Integer narrowing with non-variable expressions: Fixed incorrect narrowing when comparing integers against non-variable expressions like function calls (#1088)
  • For-loop condition narrowing: Integer literals in loop conditions (e.g., for ($i = 0; $i < 10; $i++)) are now properly extracted from the AST for type narrowing (#1089)
  • Redundant type comparison in OR conditions: Fixed false positive redundant-type-comparison when using count checks or string narrowing in || conditions (#1112)
  • List count with unknown size: HasAtLeastCount assertions no longer incorrectly set an exact known_count on lists with unknown count, preventing false unreachable-code reports (#1104)
  • Array spread with unknown count: Fixed false positive when spreading a list with unknown count into an array literal (#1108)
  • Class constant @var docblock type: The analyzer now prefers @var docblock types over inferred types for class constants, fixing cases where properly typed array values stayed as mixed (#1090, #1094)

Codex (Type System)

  • never as bottom type: never is now correctly treated as a subtype of all types in extends_or_implements checks (#1107, #1109) by @kzmshx
  • Type alias forward references: All local @psalm-type / @phpstan-type alias names are now pre-registered before parsing, so aliases can reference each other regardless of declaration order (#1116)
  • String casing detection: Fixed incorrect impossible-condition false positives when comparing strtolower()/strtoupper() results with literals containing non-alphabetic characters (spaces, digits, etc.) (#1086)
  • Bidirectional TUnion equality: Type union equality checks are now bidirectional, preventing false positives when comparing types with different ordering (#1087)

Linter

  • 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

Formatter

  • First-class callable with argument unpacking: First-class callable expressions (e.g., $foo(...)) are no longer incorrectly treated as breaking expressions, fixing misformatted output (#1091)
  • Redundant grouping parentheses: The formatter now correctly removes redundant grouping parentheses in more cases (#1121) by @Michael4d45
  • Heredoc span calculation: Fixed incorrect span calculation for heredoc/nowdoc strings, which could cause formatting issues (#1092)

Prelude (Type Stubs)

  • 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 @veewee

🏗️ Internal

  • Removed experimental warning for analyzer watch mode
  • New IncrementalAnalysisService encapsulating the full incremental analysis pipeline for watch mode and LSP
  • CodebaseDiff::between() for metadata comparison and mark_safe_symbols() for incremental analysis
  • Signature-only fingerprint mode for detecting body-only vs. signature changes
  • Applied clippy fixes across codex and linter crates
  • Re-generated linter documentation

🙏 Thank You

Contributors

A huge thank you to everyone who contributed code to this release:

Issue Reporters

Thank you to everyone who reported issues and requested features that shaped this release:


Full Changelog: 1.7.0...1.8.0

Don't miss a new mago release

NewReleases is sending notifications on new releases.