Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.45.0] - 2026-06-18
Section titled “[0.45.0] - 2026-06-18”- Comparison-driven integer-range narrowing:
<,<=,>,>=,===, and!==against literal bounds now tightenint<a,b>ranges (and named subtypes likepositive-int,non-negative-int) in each branch, narrowing toTLiteralInton a single-point match. - Integer-range inference for arithmetic and built-ins: unary negate and
abs(), modulo with a positive literal divisor, multiplication of non-negative ranges, bitwise-AND masks and right-shifts,intdiv()on non-negative dividends, andmin()/max()over all-integer arguments now produce boundedint<min,max>results.rand(),mt_rand(), andrandom_int()infer their range from literal bounds. - Literal folding at analysis time: integer arithmetic, casts, and string concatenation (including
.=) of literal operands now fold to exact literal values.strlen/mb_strlenandcount()on a sealed keyed-array shape return exact literalints;strlen/mb_strlenreturnint<1,max>fornon-empty-stringarguments. non-empty-stringpreservation and inference across string operations: case-conversion and encoding functions,(string)casts ofint/float/true,sprintfwith literal format chars,number_format,str_repeat,date/gmdate/date_format, and concatenation all preserve or producenon-empty-string.str_contains/str_starts_with/str_ends_withnarrow the haystack tonon-empty-stringin the true-branch.- Array element- and key-type-preserving inference for
array_slice,array_map,array_merge,array_unique,array_fill,array_fill_keys,array_keys,array_reverse,array_chunk(list<list<T>>),sort/rsort/usort/shuffle,array_push/array_unshift(by-ref),array_pop/array_shift(value type), andarray_key_first/array_key_last(non-null on non-empty).explode,str_split,implode,preg_split, andrange()produce typed(non-empty-)listresults.array_valuesis now@template-annotated and returnslist<TValue>. - Collection narrowing:
array_is_list,count/strlencomparisons,$arr !== [](narrows to non-empty), truthy checks on arrays/lists (narrow to non-empty variant), andin_array($needle, [...])(narrows to the literal union; the false-branch removes matched literals from a finite union). array_searchnarrows its return key type from the haystack.
- Truthy/falsy narrowing corrected across scalar types:
boolnarrows to thetrue/falseliteral (including on=== true/=== false),stringnarrows the string type,int/floatfalsy checks narrow to the zero literal, andintranges tighten their bounds around zero (int<min,0>, zero-inclusive ranges, single-point exclusion now marks branch divergence).!==/===on an int-range edge tightens the bound. non-empty-array/non-empty-listare never falsy and a closed emptyarray{}is never truthy, fixingcan_be_falsy/can_be_truthyfor these and forTNumericString,TNonNegativeInt, and zero-inclusiveTIntRange.- Named integer subtypes (
positive-int,non-negative-int, etc.) now carry their implicit bounds through arithmetic and comparisons, intersect correctly on comparison, and have correct subtype/contradiction handling — fixing missingTNumeric/TScalar/TFloatsubtype entries,DocblockTypeContradictiondetection,impossible_comparisonwith negative literals, andRedundantConditionon always-true named-int comparisons. +=/-=and++/--preserve integer-range bounds.is_numericandis_scalar/narrow_to_scalarnow handle all string and integer subtypes (including literal strings) correctly.remove_falseonTBoolyieldsTTrue(not empty), and return-type checks guard against an emptyremove_falseresult.- Static-call and method-call diagnostics:
PossiblyNullMethodCallis now suppressed againstmixedreceivers. MissingThrowsDocblockis suppressed for@template T of Exceptionparameters.TKeyedArrayproperty keys are validated against a genericarray<K,V>.Foo::classexpressions no longer emitUndefinedClass.- A PHP type hint is now preferred over a conflicting scalar
@paramdocblock. - In non-strict-mode files,
int/false→boolis no longer flagged asInvalidReturnType, and scalarint/float→stringis reclassified fromInvalidArgumenttoArgumentTypeCoercion. - A
mixed|nullargument is treated asmixed, not possibly-null. - The
analyze_sourcefile is now registered in the workspace index.
[0.44.0] - 2026-06-17
Section titled “[0.44.0] - 2026-06-17”int-rangesub-ranges are now correctly recognized as subtypes of containing int-ranges.positive-intis now a subtype ofscalar,numeric, andint<min,max>when the range contains all positive integers.list<T>subtype check forarray<K,V>now verifiesint <: K(acceptsarray-key-keyed arrays). Keyed array shapes likearray{0:Child,1:Child}now satisfylist<Base>whenChild extends Base.do-whilebodies are now known to execute at least once: variables introduced in the body are stripped ofpossibly_undefined/possibly_assignedafter the first pass, matching PHP’s guaranteed-first-iteration semantics.- Property type narrowing via
!== null/=== nullguards and direct assignment:$this->prop !== nullnow refines the property type in the true-branch, and$this->prop = $valrecords the assigned type for subsequent accesses within the same scope. $this->prop instanceof ClassNamenow narrows the property’s type in the true-branch, preventing falseInvalidArgumentandTypeMismatchdiagnostics on the narrowed access.- By-ref output parameters (e.g.
preg_match’s$matches) are now promoted from possibly-assigned to definitely-assigned in the true-branch of&&conditions. Assignment expressions inwhileconditions (e.g.while ($line = fgets($r))) stay definitely-assigned after loop-body widening. @inheritDoc-annotated methods with parameter widening (contravariant-legal in PHP) no longer emitUnusedParamfor parameters that are unused in the overriding body.- PHP
ext-bz2stubs added:bzcompress,bzdecompress,bzopen,bzread,bzwrite,bzclose,bzflush,bzerrno,bzerror,bzerrstrno longer emitUndefinedFunction. - Trait method aliases (
use Trait { method as alias; }) are now resolved before insteadof precedence, fixingUndefinedMethodfalse positives on aliased method calls. iterable<K,V>now expands toarray<K,V>|Traversable<K,V>, forwarding the key type to both sides. Previously the key was dropped, causing falseInvalidArgumentdiagnostics when aTraversableimplementation was passed to aniterable<K,V>parameter.non-empty-list<T>is now a subtype ofarray<K,V>andnon-empty-array<K,V>.- String literals that cannot be class names (e.g.
'string[]') no longer triggerUndefinedClassorInvalidArgumentwhen passed toclass-stringparameters. A complementaryTLiteralString → TClassStringsubtype rule prevents the redundantInvalidArgumentpath. array_key_exists('k', $arr)in a truthy guard now adds'k'as a non-optional entry in every sealedTKeyedArrayshape of the variable’s type, suppressing subsequentNonExistentArrayOffsetdiagnostics. Works for both plain variables and property accesses ($this->prop).Color::{$name}(dynamic enum case / const access) no longer emitsUndefinedConstant. The class sub-expression is only analyzed when it is a variable; plain identifiers are skipped, matching the existingClassConstAccessguard.
[0.43.0] - 2026-06-16
Section titled “[0.43.0] - 2026-06-16”intvalues passed tostringparameters in non-strict-mode files (withoutdeclare(strict_types=1)) are no longer flagged asInvalidArgument. PHP’s coercive typing silently casts integers to strings in this context.- Batch analysis path (
analyze_paths) now callsensure_vendor_eager_functions(), ensuring Composerautoload.filesglobals (e.g. Laravel Prompts helpers:confirm,select,suggest) are indexed before body analysis. Previously, 61 spuriousUndefinedFunctiondiagnostics were emitted on the Laravel corpus. foreach ($arr as &$val)by-reference variables no longer emitUnusedVariableor dead-write diagnostics. Writes through a reference mutate the source array and are never dead.- Dynamic method call arguments (
$obj->{$method}($arg1, $arg2)) are now analyzed so variables used only in those arguments are marked as consumed, fixing falseUnusedVariableandUnusedForeachValuediagnostics. - Variables assigned before a
tryblock and read only in thefinallyblock are no longer reported as unused. - Union-typed arguments (e.g.
Arrayable|Stringable|array|string) to matching parameters no longer emit falseImplicitToStringCastdiagnostics. catch (Exception $e)variables are never reported as unused, including when nested insideif/elseortry/catchchains.- Concat-assign (
$x .= "…") marks the prior write consumed before recording the new write, preventing false dead-write reports on the initial assignment. - Carry-forward loop variables (
$prev = $iteminsideforeach) no longer re-arm consumed writes spuriously, while$a += $ipatterns retain dead-write detection when$ais never read after the loop. $var::classand$var::CONSTaccesses now correctly mark the variable as consumed.require/includemarks all in-scope variables as consumed, since the included file can read any variable in the calling scope.- Variables assigned before a
tryblock, overwritten inside thetrybody, and read in thefinallyblock are no longer flagged as dead writes — the pre-try write is live on the exception path. UnusedVariableandUndefinedVariablediagnostics are suppressed in Blade templates (.blade.php) and PHP files underresources/views/, where variables are injected by the template engine rather than assigned in PHP.method_exists($obj, 'method')guards now suppressUndefinedMethoddiagnostics inside the guardedifbranch, including guards on property accesses.Closureobjects and keyed-array callables (e.g.[object, "method"]) are now valid callable subtypes, fixing falseInvalidReturnTypeandInvalidArgumentdiagnostics.@internalmethods called on$this(own class or via traits) no longer emitInternalMethodfalse positives.new $classStringVarwhere the variable holdsclass-string<AbstractClass>no longer emitsAbstractInstantiation— the class-string constraint guarantees a concrete subclass at the call site.(int)and(float)casts on unions that contain scalar-safe atoms (string,bool,null) no longer emitInvalidCast.- Assignment expressions inside
is_null()/is_string()/etc. guards (if (!is_null($model = $this->first(...)))) now narrow the assigned variable in the then-branch, fixingNullableReturnStatementfalse positives infirstOrFail-style methods. iterablepseudo-type now correctly expands toarray|Traversablein both the docblock parser and the AST type-hint parser. Previously it was mapped to plainarray, causingInvalidArgumentandInvalidReturnTypefalse positives whereverTraversableimplementations were used.- Absolute FQCNs in docblocks (e.g.
\Carbon\CarbonImmutable) are now preserved through alias resolution, preventing mis-resolution viauseimports that share a prefix. - Types nested inside keyed array properties (e.g.
array{"class": class-string<T>}) are now properly resolved through the file’s namespace and import context. preg_replace,preg_replace_callback,preg_replace_callback_array, andpreg_filternow returnstring|nullwhen$subjectis a string andarray<int,string>|nullwhen it is an array.var_export($val, true)now returnsstringinstead ofstring|null.
[0.42.0] - 2026-06-15
Section titled “[0.42.0] - 2026-06-15”AnalysisSession::class_imports(file)→Vec<(alias, fqcn)>— returns the use-import alias map for a file as(short_name, fully_qualified_name)pairs. Completion handlers can use this to expand a short class name written before::into its FQN before looking up static members, mirroring the alias expansion already performed bysymbol_at+definition_of.- Vendor
autoload.filesglobals (e.g. Laravel helper functions) are now lazy-loaded automatically on first analysis. Previously callers had to invoke a manual eager-index step; any consumer that omitted it received false-positiveUndefinedFunctiondiagnostics for every call to those globals.
- Diagnostic column numbers are now 0-based throughout, matching the LSP UTF-32 convention documented in
mir_types::Location. Body-analysis diagnostics were previously emitting 1-indexed columns, inconsistent with collector-stored diagnostics (which were already 0-indexed). - Classes referenced only in docblock annotations (
@param,@return,@var,@extends,@implements) are now pre-loaded during AST prioritization. Previously such classes were invisible to the pre-loader; method and property checks on the annotated variable would silently degrade tomixedwhen the class had not yet been eagerly indexed.
[0.41.0] - 2026-06-15
Section titled “[0.41.0] - 2026-06-15”IfThisIsMismatch(MIR0902) — emitted when a method’s@if-this-istype constraint is violated at a call site. Template-aware constraint checking enables precise type narrowing for receiver type refinements.DocblockTypeContradiction(MIR0406) — emitted when a comparison operator (===,<,<=,>,>=) is used with values that cannot satisfy the condition given their inferred types. Detects impossible assertions and dead code in conditionals.UnevaluatedCode(MIR0407) — emitted when aswitch/matchstatement ongettype($x)contains arms thatgettype()never returns (e.g.,"int"when the actual return is"integer"), or when the argument’s inferred type cannot produce those values.MixedReturnStatement(MIR1212) — emitted when a function with a declared non-void return type returns amixedvalue (e.g.,array_pop()in astring-returning function).- Integer range types now tracked for
count()/sizeof()→int<0, max>(orint<1, max>for non-empty),strlen()/mb_strlen()→int<0, max>, and arithmetic operations preserve range bounds. Comparison-driven narrowing (e.g.,if ($i < count($a))) now refines loop variables to their valid index ranges. array_map()andarray_filter()now infer precise result element types from their callbacks instead of returning barearray.- Vendored Redis and Memcached phpstorm-stubs extension directories, fixing ~1,400
UndefinedClassfalse positives on Laravel codebases that use these PECL extensions. - phpstorm-stubs
#[LanguageLevelTypeAware]and#[PhpStormStubsElementAvailable]attributes are now resolved against the configured target PHP version. This honors ~2,400 previously-dropped declaration sites and eliminates spurious|falsereturns for version-specific function signatures (e.g.,explode()/pack()on PHP 8.x). - Filesystem and unserialize taint sinks:
file_get_contents(),file_put_contents(),unserialize()and related functions now propagate taint inTaintedFilesystemandTaintedUnserializationissue kinds. - Symbol reference recording for static-call class name tokens, enabling go-to-definition and find-references on
ClassName::method()expressions.
- Class-level template parameters are now correctly resolved in method parameter types during generic method binding.
- Named-object arguments now satisfy bare
objectparameter types vianamed_object_subtypechecking. [object, "method"]array literals are now recognized as valid callables, fixing falseInvalidArgumentdiagnostics.- Docblock-only properties (declared via
@propertyannotations) are now correctly typed as nullable and not flagged as uninitialized. - Property
@vardocblock annotations now resolve class-level template parameters, enabling precise typing for generic class properties. - Surplus arguments to closure calls no longer emit false
TooManyArgumentsdiagnostics when the closure arity is unknown. - Array-access narrowing:
isset($arr[$key])and??operators now narrow both the array base and key existence. is_object()type guards now correctly narrowmixedtoobjectin conditional branches.unset($arr[$key])now counts as a read of the variable, fixing falseUnusedVariablediagnostics.- Static property reads (
self::$prop) now correctly count as property uses. - Bare
return;statements are now valid in functions with nullable or void-union return types. defined()andfunction_exists()guards now narrow constant/function references in conditional branches.- By-reference closure captures (
use (&$var)) now auto-create the captured variable if it doesn’t exist. - Variable assignments inside
matcharm conditions now correctly define the variable for use in the arm body. self,static,parent, and$thisresolution in trait bodies now correctly targets the consuming class instead of the trait.new staticis now allowed in abstract classes, delegating to concrete subclasses at runtime.stdClassnow permits dynamic property access and assignment without emittingUndefinedPropertydiagnostics.- Fully-qualified attribute names in
#[...]are now honored in attribute resolution. NumericandResourceare no longer treated as reserved class names in the parser.--clear-cachenow correctly targets the project-local cache directory (.mir/cache) instead of always searching the platform default cache dir.- Result cache now invalidates when the running binary, target PHP version, or user-configured stubs change.
- Wrong-case checks now extend to full FQCN namespace segments, catching case mismatches in any part of the class name.
Changed
Section titled “Changed”mir-analyzermodule structure refactored for maintainability:batch.rs,class.rs,parser/docblock.rs,session.rs, andbody_analysis.rssplit into dedicated submodules.- PHP parser suite (
php-rs-parser,php-ast,php-lexer,phpdoc-parser) upgraded to 0.18.0 for improved parsing robustness.
Performance
Section titled “Performance”- Large false-positive reduction on the Laravel reference corpus: the vendored Redis/Memcached stubs and version attributes support reduce
UndefinedClassfrom 617 to 114 (an 82% reduction on the reference benchmark).
[0.40.0] - 2026-06-13
Section titled “[0.40.0] - 2026-06-13”TypeDoesNotContainType— impossibleswitchcase values (literal cannot intersect the switch subject type) and impossiblematcharm conditions (same scalar/literal intersection check) are now reported.MixedAssignmentis now also emitted when aforeachvalue variable is bound from a mixed-typed iterable (previously this path bypassed the mixed check).- Purity enforcement:
ImpurePropertyAssignment(MIR1700),ImpureMethodCall(MIR1701),ImpureGlobalVariable(MIR1702),ImpureStaticVariable(MIR1703) — emitted when a@pure/@psalm-mutation-free-annotated function mutates a parameter’s property, calls an impure method on a parameter, or accesses a global/static variable. ImpureFunctionCall(MIR1704, Warning) — emitted when a@pure-annotated function calls a named function not itself marked@pure.UnusedClass(MIR0507, Info) —finalclass declared but never directly referenced. Restricted tofinalclasses to avoid false positives from subclassing or type-hint uses.ArgumentTypeCoercion(MIR0225, Info) — emitted when an argument is a supertype (parent class) of the expected parameter type. Previously these calls were silently accepted.PropertyTypeCoercion(MIR0226, Info) — emitted when a property assignment uses a supertype of the declared property type. Previously emitted as the higher-severityInvalidPropertyAssignment; correctly distinguished as a lower-severity coercion case.TaintedLlmPrompt(MIR0804, Error) — emitted when a value derived from tainted user input reaches a parameter annotated with@taint-sink llm_prompt. Parser now recognises@taint-sink kind $paramdocblock tags; sink params are stored onFunctionDef/MethodDef.UnusedSuppress(MIR0508, Info) — emitted when a@psalm-suppress,@suppress, or@mir-suppressannotation does not match any actual issue in the analysed file. Self-suppression (@suppress UnusedSuppress) silences its own warning.UnsupportedReferenceUsage(MIR1506, Warning) — emitted when a PHP reference assignment ($b = &$x) is used.NoInterfaceProperties(MIR1504, Info) — emitted when a property is read or written on an interface annotated with@seal-properties/@psalm-seal-propertiesbut not declared via@property/@property-read/@property-write.MissingConstructor(MIR1507, Info) — emitted when a concrete class has at least one non-nullable uninitialized property anywhere in its ancestor chain but defines no constructor.MixedFunctionCall(MIR1211, Info) — emitted when a variable of mixed type is invoked as a function via a dynamic call expression.MissingClosureReturnType(MIR1105, Info) — emitted when a closure has no native return type and no preceding@returndocblock (Full mode only).InvalidArrayOffset(MIR0300, Error) — emitted when an object, array, or closure is used as an array subscript key (types PHP cannot coerce to a valid array key).PossiblyInvalidArrayAccess(MIR0227, Info) — emitted when a union type contains some members that support[]and some that do not.DeprecatedMethod(MIR1002) — instance deprecated method calls now emitDeprecatedMethodinstead ofDeprecatedMethodCall, reservingDeprecatedMethodCallfor static calls and__clonedispatch.MixedReturnStatement(MIR1212, Info) — emitted when a function with a declared non-void, non-mixed return type returns a value that infers to mixed (e.g.array_pop()returned from astringfunction).- phpstorm-stubs
#[LanguageLevelTypeAware]and#[PhpStormStubsElementAvailable]attributes are now resolved against the configured target PHP version. This correctly models ~2,400 previously-dropped declaration sites across the stub corpus and eliminates spurious|falsereturns forexplode()/pack()on PHP 8.x (those functions throw rather than return false since 8.0). - Vendored Redis and Memcached phpstorm-stubs extension directories. These PECL extensions were previously excluded and had no fallback resolution path after the submodule loader was removed, causing false
UndefinedClassforRedis/Memcachedin Laravel codebases.
- Result cache now invalidates when the running binary, target PHP version, or user-configured stubs change. Previously the cache keyed validity on file content hash only, leaving unchanged files serving stale diagnostics after a version upgrade,
--php-versionchange, or stub set update. --clear-cachenow correctly targets the project-local cache directory ({composer_root}/.mir/cache) instead of always looking at the platform default cache dir and attempting to remove acache.jsonthat no longer exists (the format iscache.bin), making it a functional operation for normal project runs.
[0.39.0] - 2026-06-12
Section titled “[0.39.0] - 2026-06-12”UnnecessaryVarAnnotation(Info) — a@varannotation on a simple assignment whose declared type exactly matches the inferred type is flagged as redundant. The comparison is exact, with no literal widening:@var stringon$s = 'hello'changes the type (literal → base) and is therefore not reported. Narrowing annotations, mixed-typed right-hand sides, and non-assignment statements stay silent.MismatchingDocblockReturnType/MismatchingDocblockParamType(Info) — a@return/@paramdocblock that contradicts the native type hint on a top-level function is now reported. Refinements never fire: the comparison uses PHP type families (withint → floatcoercion andcallable’s string/array/object forms modeled), so e.g.literal-string/non-empty-list<…>against their base hints stay silent. Object-vs-object falls back to an inheritance-aware subtype check when every named class is known; unresolved names (templates,::classrefs, unmodeled refinement syntax) stay silent.MissingReturnType/MissingParamType(Info) — top-level functions with neither a native hint nor a docblock type are now reported (previously only interface methods were checked), on all three analysis paths (per-scope salsa, batch typed, pure per-function).
reanalyze_dependentsno longer deadlocks on workspaces with high dependent fan-out. The per-dependent warm-up (prepare_ast_for_analysis, introduced in 0.37.0) loads classes by mutating shared salsa inputs, and salsa input mutation blocks until every other database handle is released. Running the warm-up inside the parallel rayon worker meant a worker mutated the storage while sibling workers held live snapshots mid-analyze_file, so the write blocked on them forever — hanging indefinitely on high-fan-out workspaces. Warm-up now runs before the parallel read-only analyze loop, with each iteration holding only a scoped snapshot that is dropped before any input write, restoring the “no input writes while a snapshot is live” invariant. Covered by a regression test (reanalyze_dependents_lazy_load_warmup_does_not_deadlock).- Large false-positive reduction on the Laravel reference corpus across several diagnostic kinds (each fix ships with regression fixtures, including the negative cases):
- Template binding: trailing variadic params now bind every remaining argument (unwrapping
array<X>docblock types to their element type per argument); aclass-string<T>union alternative consumes class-string arguments so a sibling bareTno longer absorbs them; method-level@templateshadows a same-named class template during argument checking; and a docblock description following a@templateline is no longer misparsed as a bound. Removes ~1350 FPs (6148 → 4781), dominated by Mockery intersection mocks. InvalidStringClass:new $xwhere$xismixedor a template param no longer fires —mixedis aMixed*concern, and a template bound may be a class-string. Removes 501 FPs (4781 → 4280).UndefinedMethod:$this->m()/static::m()inside a trait body is suppressed (the consuming class may provide the method, so traits join interfaces/abstract classes), and inaccessible protected/private calls dispatched through__call(e.g.Macroable, Mockery partial mocks) no longer error. 756 → 168.UnusedVariable/UnusedForeachValue: path-accurate liveness — closureuse()captures and closure/arrow-body reads consume the outer write; branch merges no longer resurrect a write consumed on one path; multiple pending write locations per variable are tracked (pre-loop and loop-body writes); andswitchwith adefaultarm no longer merges the impossible no-match path. 938 → 376 and 106 → 54.UnusedVariable: an assignment in argument position (f($x = expr),->andReturn($mock = m::mock(...))) now counts as a use across all call shapes (function, dynamic-callee, method, static,new). 376 → 243.UndefinedFunction/InvalidTemplateParam: a string passed to a union param with non-callable alternatives is no longer validated as a function name (157 → 3); template bounds are not checked against bindings that still contain unresolved placeholders (self/static/parent, template params) (103 → 42); andclass-string<T>binding coerces class-name-shaped string literals such asm::mock('Foo\Bar')without::class.InvalidArgument: an array passed to acallable|array|nullparam matches the array alternative instead of being forced into the[object, "method"]callable shape. Removes 169 FPs (618 → 449).
- Template binding: trailing variadic params now bind every remaining argument (unwrapping
[0.38.0] - 2026-06-12
Section titled “[0.38.0] - 2026-06-12”IssueKind::default_severity_for_code— reverse lookup from a stable error code (e.g."MIR0005") to its default severity, for callers holding a bare code string (config files, suppression annotations, serialised diagnostics).
Changed
Section titled “Changed”- Property-access and method-call symbol recording now reuses the declaring class from type resolution instead of re-walking the inheritance chain, removing a redundant ancestor-chain walk per property access and per method call.
- Closure and arrow-function parameter/return type hints (
function (Foo $x) {},fn (Foo $x) => ...) now contribute reference-index entries andClassReferencesymbols, so find-references and hover cover closure usages. $x instanceof Foonow records aClassReferencesymbol at the class-name span, unblocking hover and thesymbol_at→references_toround-trip for instanceof sites.- Property references and symbols now key on the declaring class (as
find_property_in_chainreturns it) instead of the receiver type, fixing find-references andsymbol_atfor inherited properties accessed through a subtype.
[0.37.0] - 2026-06-11
Section titled “[0.37.0] - 2026-06-11”- Per-scope tracked inference queries (
file_scopes,infer_scope) for granular type inference memoization at function/class declaration and file-frame scope levels. - Batch-mode symbol collection opt-out via
BatchOptions::skip_symbolsfor performance optimization in batch analysis runs.
Changed
Section titled “Changed”analyze_filenow assembles results from per-scope memos instead of a single whole-file analysis walk, improving incremental re-analysis efficiency.- Reference locations architecture refactored:
RefIndexconsolidates three independent reference maps (reference_locations,file_references,symbol_referencers) into a single tracked structure. - Dependent re-analysis now drives through the
analyze_filequery for salsa-validated memoization, replacing per-file re-parsing and full re-analysis on every edit. - Reverse dependencies now derived from a tracked query (
file_structural_deps) instead of an in-memory map, improving incremental consistency.
- Reference-location synchronization drift eliminated by consolidating three independent maps into
RefIndex.
[0.36.0] - 2026-06-11
Section titled “[0.36.0] - 2026-06-11”MissingReturnType(MIR1201) andMissingParamType(MIR1200) — emitted for interface methods that lack@returnor@paramdocblock annotations when not otherwise typed.MixedArgument(MIR0221) andMixedAssignment(MIR0222) — emitted when amixed-typed value is passed to a parameter expecting a concrete type, or assigned to a typed property.MixedArrayAccess(MIR0223),MixedArrayOffset(MIR0224),MixedPropertyFetch(MIR0225), andMixedPropertyAssignment(MIR0226) — emitted whenmixedis used in array/property access contexts.MissingPropertyType(MIR1202) — emitted for untyped class and trait properties whenfind_dead_codeis enabled.ForbiddenCode(MIR1301) — detects code marked with#[Forbidden]attribute; use#[Forbidden("reason")]on methods/functions to flag uses as errors.@tracedocblock annotation — mark variables and expressions with/** @trace $var */to emit an@traceinformational diagnostic, aiding debugging without leaving analyzer artifacts in code. Useful for development and CI integration.PossiblyInvalidArgument(MIR0205) — enhanced to flag partial type-union overlaps, not just complete mismatches. Emitted when a union contains only some valid argument types.- Type-checking for
TClosureand__invokemethod calls: generic template parameters are now resolved at call sites, enabling precise type narrowing on closure return values. @no-named-argumentsenforcement: methods/functions marked with this attribute now emitInvalidArgumentwhen invoked with named arguments.- Duplicate declaration detection:
DuplicateClass,DuplicateInterface,DuplicateTrait,DuplicateFunction, andDuplicateConstantnow detect and report redeclarations across the entire codebase. - Psalm compatibility: all 1843 fixture tests now pass, including un-ignoring 120+ Psalm-specific test cases covering edge-case behaviors.
- Constructor-promoted property handling:
UnusedParamandUnusedVariablefalse positives eliminated for promoted properties accessed through property-assignment or constructor side effects. if-condition variable assignment detection: variables assigned inifcondition expressions (e.g.,if ($x = foo())) are no longer incorrectly flagged as unused.- Negated
instanceofguard narrowing: type refinement now correctly applies at receiver position ($obj instanceof Xand!$other instanceof $this). - User-defined stub registration now uses Salsa
Durability::HIGH, improving incremental re-analysis performance when stubs are unchanged. - Readonly promoted properties and compound-assignment edge cases in destructuring contexts.
- Operand and iteration gaps now match Psalm parity across type-checking and narrowing behaviors.
TLiteralStringsubtype narrowing: numeric literal strings now correctly matchTNumericStringbounds.- Globally-qualified type hints (
\Closure,\Generator, etc.) in namespaced files now resolve correctly without prepending the current namespace. - Generator
bare return;statements no longer emit falseInvalidReturnTypediagnostics. try-body divergence is now preserved when allcatchblocks also diverge, preventing unreachable-code false positives.ImplicitToStringCastsuppression for classes implementing\Stringableand when argument union contains non-string arms.@paramdocblock generic type hints now take precedence over plain array hints for promoted properties.
Changed
Section titled “Changed”- All 1843 fixture tests now pass without ignores, improving test coverage visibility and closing known gaps in Psalm parity.
[0.35.1] - 2026-06-10
Section titled “[0.35.1] - 2026-06-10”DuplicateClassno longer fires when two classes share the same name in separate unbraced namespace blocks.abs(int)now returnsintinstead offloat|int.- Symbol lookup now records parameter declaration sites as
Variablesymbols, enabling go-to-definition on function/method parameters. - Symbol lookup now resolves gap cursors in method chains via
expr_spanfallback, fixing missed definitions in chained calls.
Changed
Section titled “Changed”- PHP parser and phpdoc-parser updated to 0.17.0.
[0.35.0] - 2026-06-09
Section titled “[0.35.0] - 2026-06-09”UnhandledMatchCondition— emitted when amatchexpression is non-exhaustive: empty match (no arms), string literal union subject with uncovered values, or pure (non-backed) enum subject with missing cases. Enum method bodies are now included in the body-analysis pipeline, enabling exhaustiveness detection inside enum methods.AbstractMethodCallnow fires when an abstract static method is called by explicit class name (e.g.Base::bar()wherebar()is abstract). Self/static/parent calls remain exempt.InvalidDocblocknow covers three additional categories:int<min,max>ranges with invalid boundaries or wrong ordering;array<K,V>with a key type that is not a subtype ofint|string; and@methodannotations that are empty, contain invalid characters, or declare by-reference parameters.InvalidDocblockis now also emitted for@templateannotations on closure and arrow-function expressions, where they have no effect.- Trait method signatures are now checked against interface requirements: when a class implements an interface via
use T, the trait method’s signature is compared against the interface declaration andMethodSignatureMismatchis emitted for incompatible signatures. - Trait
insteadofconflict resolution is now applied during method lookup (go-to-definition and call resolution resolve to the winning trait instead of whichever was indexed first).
__getreturn type is now propagated to magic property-access inference: accesses that fall through to__getcarry the declared return type instead of always resolving tomixed.enum::cases()now synthesizeslist<EnumType>instead ofmixed, allowingforeachloop variables to be typed as the specific enum and enablingUnhandledMatchConditionto fire on enum matches.SourceFiletext is now freed on removal: theArc<str>content is nulled immediately after workspace index cleanup, releasing file content memory that was previously retained indefinitely due to Salsa 0.27 lacking a delete API.- Salsa LRU cap added to
collect_file_declarations(lru = 4096), matching the existing cap oncollect_file_definitions, preventing unbounded memo accumulation for removed files. deleted_filestracking added toMirDbStorageso removed files are explicitly auditable and provide the foundation for future tracked-struct GC.
Performance
Section titled “Performance”- Variable types stored in
FlowStateandInferredFileTypesare now deduplicated viawrap_var_type, backed by the existingintern_or_wrappool. Common scalars hit an O(1) fast path; merged types that equal a prior type are also deduplicated, makingArc::ptr_eqshortcuts in merge code fire more often. FlowState::new()no longer allocates a fresh map for the 11 PHP superglobals on every function/method scope entry. Pre-builtArcstatics are shared via COW, saving ~140 MiB of allocation churn on the project-only analysis pass (measured on Laravel).TemplateParam.boundchanged fromOption<Type>(176 B inline) toOption<Arc<Type>>viaintern_or_wrap, saving ~36 MiB of allocation churn on the project-only analysis pass.
[0.34.0] - 2026-06-08
Section titled “[0.34.0] - 2026-06-08”WrongCaseClass(MIR1009),WrongCaseFunction(MIR1010),WrongCaseMethod(MIR1011) — new Info-severity diagnostics for case-sensitive identifier references (PHP 8.6 RFC). Coversnewexpressions, static calls,instanceof, type hints,catchclauses,extends/implements/use-trait declarations, built-in and user-defined functions, instance and static method calls, anduseimport declarations.WrongCaseMethodnow fires when a magic method is defined with wrong casing (e.g.__CONSTRUCTinstead of__construct).InvalidAttribute(MIR1600) — detects invalid#[Attribute]usages: applying#[Attribute]to a function, method, property, or parameter; abstract, interface, or trait classes marked as#[Attribute]; attribute classes with a private constructor; classes used as attributes without the#[Attribute]annotation; attributes applied to elements not matching their declared target; and non-repeatable attributes used more than once on the same element.UndefinedAttributeClass— emitted when an attribute references a class that does not exist in the codebase.InaccessibleClassConstant(MIR0011) — emitted when a private or protected class constant is accessed from a context that does not have visibility.DuplicateClass(MIR1602) — emitted when the same class name is declared more than once within a file, including across braced namespace blocks.ParentNotFound(MIR0010) — emitted whenparent::is used (static call, constant access, property fetch, orparent::class) inside a class that has no declared parent.OverriddenPropertyAccess— emitted when a subclass reduces the visibility of an inherited property (public→protected,public→private,protected→private).NullableReturnStatement— emitted when a function whose return type is non-nullable has a return path that could be null (the non-null part is otherwise compatible with the declared type).InvalidClonenow also fires when cloning a named object whose__clone()method isprivateand the caller does not have access.@finaldocblock annotation is now treated as equivalent to the nativefinalkeyword forInvalidExtendClassdetection.
ATTR_TARGET_ALLcorrected from 127 to 63 (the correct sum of the sixTARGET_*flags). The wrong value accidentally set bit 6 (IS_REPEATABLE = 64), making every#[Attribute]class without explicit target flags appear repeatable and silently suppressing the “not repeatable” diagnostic.NonStaticSelfCallno longer suppresses the diagnostic when the class defines__callStatic.__callStaticonly intercepts undefined static methods, not explicitly-defined non-static ones.$thisno longer leaks into static arrow functions when resolving captured outer scope.
Changed
Section titled “Changed”FinalClassExtendedrenamed toInvalidExtendClassto align with Psalm’s naming. Update any inline@mir-suppress FinalClassExtendedannotations to@mir-suppress InvalidExtendClass.
[0.33.0] - 2026-06-05
Section titled “[0.33.0] - 2026-06-05”- Eager + background vendor indexing with configurable chunk size and memory targets (controlled via
--vendor-memoryflag; defaults to 128 MiB chunks).
- Fixed exponential memory growth when analyzing files with nested conditional branches and repeated dead-write tracking.
FlowState::merge_branchesnow deduplicates dead writes instead of concatenating, preventing allocation of gigabytes of memory on large projects like Laravel (NotificationSender.php was OOM-ing at 20GB; now uses 33MB). - Fixed workspace index singleton cache refresh when analyzing project and lazy-loaded classes, ensuring proper resolution in batch analysis.
Changed
Section titled “Changed”- Vendor indexing now uses the chunked indexing engine for more predictable memory usage and streaming behavior.
Performance
Section titled “Performance”- Subtype-check results are now cached per pass (rather than globally) in the body analysis pass, improving cache locality for concurrent analyses.
- Workspace index is now borrowed frozen during body pass analysis, eliminating write-lock contention.
PropertyDeftype fields changed fromOption<Type>toOption<Arc<Type>>, reducing per-property overhead by 168 bytes.lazy_load_missing_classesingest loop is now parallelized, speeding up vendor class loading in batch mode.
[0.32.0] - 2026-06-04
Section titled “[0.32.0] - 2026-06-04”TooManyArguments(MIR0203) is now emitted when arguments are passed to a class that has no explicit__construct()method (the implicit constructor accepts zero arguments).InvalidScope(MIR0001) is now emitted when$thisis assigned a value outside a class context.InvalidArrayAssignment(MIR0220) is now emitted when a subscript assignment ($x[] = …or$x[k] = …) is performed on a scalar type (int,bool,float).InvalidArrayAccess(MIR0219) is now emitted when subscript access is performed on a scalar type. String subscript indexing ($str[0]) remains valid.InvalidPropertyFetch(MIR0218) is now emitted when a property is accessed on a scalar or non-object type.DirectConstructorCall(MIR0217) is now emitted for explicit$obj->__construct()calls on object instances.NonStaticSelfCall(MIR0216) is now emitted whenself::/static::is used to call a non-static method in a static context.InvalidStaticInvocation(MIR0215) is now emitted when a non-static method is called with a concrete class name (ClassName::method()) and the class has no__callStatic.InterfaceInstantiation(MIR0709) is now emitted whennewis used directly on an interface.DeprecatedProperty(MIR1005) is now emitted when a property marked with@deprecatedor#[Deprecated]is read or written.DeprecatedInterface(MIR1006) is now emitted when a deprecated interface is implemented.DeprecatedTrait(MIR1007) is now emitted when a deprecated trait is used.DeprecatedConstant(MIR1008) is now emitted when a deprecated class constant or enum case is accessed.DeprecatedClass,DeprecatedMethod, andDeprecatedCalldetection expanded:#[Deprecated]is now recognised on user-defined methods and functions; deprecated classes are caught in static calls, constant access, and type hints.DeprecatedMethodCallis now emitted when cloning an object whose__clone()method is deprecated.InvalidCastis now emitted when(string)is applied to a concrete class that does not implement__toString().InvalidCatch(MIR1503) is now emitted when acatchclause names a type that does not extendThrowable.ImplicitToStringCast(MIR1501) is now emitted when aStringableobject is passed where astringis expected, making the implicit__toString()call visible.InvalidOperand(MIR0213) now covers: arithmetic on non-numeric operands, bitwise operations on objects and arrays, boolean operands in bitwise expressions, boolean increment ($b++), and array members in string concatenation.PossiblyNullOperand(MIR0214) is now emitted when a null value is used as a divisor in/or%.UnusedForeachValueis now emitted when the value variable in aforeachloop is never read.UnusedVariabledead-write detection: a variable that is assigned and then overwritten before being read is now flagged.UnusedVariableis now detected in top-level PHP scripts, not only inside functions and methods.InvalidOverride(MIR0708) is now emitted when#[Override]is applied to a method that has no overridable parent, or whose parent method isprivate.MethodSignatureMismatchnow catches: abstract re-declaration of a concrete method, multi-interface return-type conflicts, by-reference parameter mismatch, overrides that drop parent parameters, and static/non-static mismatch.- Generic type inference at instantiation:
new Box(5)now infersBox<int>by binding class@templateparameters from constructor argument types. - Unannotated generic method returns: methods whose parameters carry template types now resolve concrete return types at call sites without an explicit
@returnannotation. @readonlydocblock annotation on properties is now treated the same as the nativereadonlykeyword for theReadonlyPropertyAssignmentcheck.
@mixinproperty resolution: properties declared on@mixinclasses are now found via the full inheritance chain, eliminatingUndefinedPropertyfalse positives for mixin-based patterns.- Narrowing false positive: possibly-undefined variables no longer cause the
else/elseifbranch to be incorrectly marked as unreachable. - Narrowing in
elseif/elsechains: each failedelseifcondition is now applied as a negative narrowing to theelsebranch. UnusedVariablefalse positives in loops: pre-loop writes are cleared after the loop body iterates, preventing them from being re-introduced through the else path.UnusedVariablefalse positives for variables passed tocompact(): those variables are now marked as consumed.- Return type checking now applies inside anonymous-class methods.
- Stub cache corruption on the second analysis run:
#[serde(skip_serializing_if = "Option::is_none")]is unsafe with bincode (a non-self-describing format) — theNonediscriminant byte was omitted on write while deserialization still expected it, causing misaligned reads and a runaway allocation. Removedskip_serializing_iffrom thedeprecatedfield onPropertyDef,ConstantDef,InterfaceDef,TraitDef, andEnumCaseDef. Stub cache format version bumped to 4 to invalidate stale on-disk entries.
[0.31.0] - 2026-06-01
Section titled “[0.31.0] - 2026-06-01”- Inline issue suppression via source comments: add
// @mir-suppress DiagnosticNameon the offending line (or the line above) to silence a specific diagnostic without affecting others. NonExistentArrayOffset(MIR0301) is now emitted when a literal string or integer key is accessed on a closed keyed array (array{foo: int}) and the key is absent.ParadoxicalCondition(MIR0404, Warning) is now emitted for duplicate literal values inswitchcases andmatcharms, where the repeated branch can never be reached.- Conditionally-declared functions and classes — the
if (!function_exists('foo')) { function foo() {} }guard pattern used by Laravel helpers, Symfony polyfills, and WordPress pluggable functions — are now indexed. Resolves ~1,608UndefinedFunctionfalse positives on a standard Laravel project. - All issue locations now carry
line_end/col_endin addition to the existing start position, enabling tighter diagnostic ranges in SARIF, LSP, and playground consumers.
UnusedVariablefalse positives for variables used as dynamic property or method names ($this->$var,$this->{$var},$this->$method()).UndefinedClassfalse positives for class names used as the argument toclass_exists(),interface_exists(), ortrait_exists(), and for usages of optional classes inside the guarded true-branch.- Conditional return types (
@return ($T is null ? X : Y)) are now resolved at static method call sites, eliminating falseInvalidArgumenterrors. - Short-circuit
&&/||assignments are promoted from possibly-assigned to definitely-assigned when the branch is known to have executed (e.g. the true-branch of&&). ReducesPossiblyUndefinedVariablefalse positives in the Laravel benchmark from 31 to 7. - Composer root detection now skips
vendor/<org>/<pkg>/composer.jsonmanifests and walks up to the true project root, eliminating ~1,552UndefinedClassfalse positives on standard Laravel projects. strtr($str, $pairs)(2-argument array form) no longer firesTooFewArguments.TooManyArgumentsfalse positives eliminated when a union type contains a barecallable(unknown arity) alongside a typedTClosure.UnusedVariablefalse positives eliminated for variables read only inside afinallyblock (the save-restore pattern).- Nested
TConditionalreturn types (e.g.($v is null ? array{} : ($v is array ? array<K,V> : array{V}))) are now recursively resolved rather than returned as opaque conditional types. UndefinedPropertyfalse positives eliminated for property accesses guarded by??orisset(e.g.$this->prop ?? null).PossiblyUndefinedVariablefalse positives eliminated for variables used as the left operand of??when the coalesced result is immediately compared against the fallback literal.- A bare
Closuretype now satisfies a typedClosure(): Tparameter, eliminating falseInvalidArgumenterrors. ingest_filenow evicts dependents’ cached analysis when a file’s content changes, preventing stale results from being replayed across incremental re-analysis.Enum::Caseand class constant accesses now resolve to the correct type instead ofmixed.TooManyArgumentsfalse positives eliminated for functions and methods that usefunc_get_args()/func_num_args()/func_get_arg()in their bodies.InvalidArgumentfalse positives eliminated forStringableobjects passed asstringparameters in files withoutdeclare(strict_types=1).array_keys(array<K, V>)now returnslist<K>instead oflist<mixed>.preg_match$matchesparameter is now typed asarray<int, string>via by-ref write-back.str_replace/str_ireplacereturn type is narrowed tostringwhen the subject is a scalar.hrtime()return is narrowed toint|floatwhen$as_numberistrue.NonExistentArrayOffsetis suppressed inside existence-check contexts (isset,??,empty).- Template parameters in supertype position are now treated as wildcards in
atomic_subtype, eliminating falseInvalidTemplateParamdiagnostics for union-sub against union bounds. list<T>is now inferred for the$arr[] = $vpush notation instead ofarray<mixed, T>.$obj::classpassed as aclass-string<T>argument no longer firesInvalidArgument.- Nested array assignment (
$arr[$k][] = $v) now correctly propagates the innermost key type. - Template parameters inside array types in generic method returns are now correctly resolved.
- Reference index gaps closed: class references recorded at the class identifier in static calls,
self/static/parent/ClassNameconstant accesses, and inherited method calls use the declaring class. - PHP version filtering is now wired into the salsa database so
FileAnalyzerhonours--php-versioncorrectly. - Parser now strips quotes from array shape keys in PHPDoc (
array{'key': T}parses correctly). mysqli_init()PHP 8.0 overload (returningmysqli) added to stubs.
Performance
Section titled “Performance”- Peak cold-start memory reduced by ~22 MiB:
MethodDef/FunctionDefinferred return types changed fromOption<Type>(176 B) toOption<Arc<Type>>(8 B); class analysis no longer materializes vendor/stub classes during the analyzed-file decomposition;mimallocinstalled as the global allocator.
[0.30.0] - 2026-05-28
Section titled “[0.30.0] - 2026-05-28”$argvand$argcare now seeded as predefined globals, eliminatingUndefinedVariablefalse positives in CLI scripts.- Single-star
/* @var $this */annotations (the form PhpStorm generates for Yii2 view templates) are now recognized in addition to/**PHPDoc blocks. Fixes #290. PossiblyUndefinedVariablefalse positives eliminated for variables assigned insidewhile(true)andfor(;;)loops before everybreak. Infinite loops no longer treat the “loop never executes” path as reachable.UnusedVariableandUnusedParamfalse positives eliminated for variables read only inside a diverging if-branch (one that alwaysreturns orthrows).
Changed
Section titled “Changed”- Upgraded
php-rs-parser,php-ast,php-lexer, andphpdoc-parserto 0.15.0. Function and closure bodies are now wrapped in aBlocktype; class/enum/interface/trait members are behindClassBody/EnumBodywrappers.
[0.29.0] - 2026-05-27
Section titled “[0.29.0] - 2026-05-27”- Cache is now enabled by default without
--cache-dir. Composer projects cache to<project-root>/.mir/cache; other scans use the platform cache directory. Pass--no-cacheto opt out. @mir-checkinline type assertion directive: annotate a variable with/** @mir-check $x is SomeType */in a test fixture to emitTypeCheckMismatchif the inferred type does not match, enabling regression tests for type inference.- Short-circuit
isset/!issetnarrowing in&&and||expressions:isset($x) && $x->method()now correctly narrows$xto non-null inside the right-hand side. InvalidStringClassdiagnostic: emitted instead ofUndefinedClasswhen a dynamic class expression (new $var,$var::method()) is not a validclass-string. String literal arguments toclass-stringparameters are now validated.TCallableStringatomic type for proper callable-string validation.- Variance checking for generic return types: a method return type that widens its parent’s generic parameter now emits a diagnostic.
- Template bounds (FQN resolution): eliminated ~2,100 false-positive
InvalidTemplateParamandInvalidArgumentdiagnostics caused by bare class names in@template T of …bounds not being namespace-qualified. Fixes cover all definition collectors (class, interface, trait, function, method), intersection bounds,@varand property type annotations, and generic type arguments. - Template conditional returns:
@return (T is null ? X : Y)now parses and resolves correctly at call sites. When T is already bound in the substitution, the conditional collapses to the correct branch. When the discriminator is nullable-but-not-only-null, the conditional widens toX|Yinstead of emitting a false positive. - Intersection types: intersection-typed values are now recognized as subtypes of their parts and of
object, eliminating companionInvalidArgumentfalse positives for functions likeget_class().InvalidArgumentis also suppressed when a parameter type contains templates within an intersection. - Template inference:
Tis now correctly inferred fromclass-string<T>arguments,Closure,callable, and intersection-typed parameters. Template bounds now check inheritance chains. Array-key pseudo-type andTKeyedArrayare recognized in template binding. - Array types: empty keyed arrays (
array{}) are folded into matching generic arrays in unions, eliminating|array{}noise from loop-built arrays. Array key types are now preserved in$arr[$key] = $valassignments, fixing ~62 false-positiveInvalidReturnTypediagnostics. Mutual-reference array loops no longer cause an infinite hang during inference. - PHP built-ins:
array_walk,array_walk_recursive3rd parameter is now optional;mt_rand/randparameters are now optional. Fixes ~30TooFewArgumentsfalse positives.array_mapwith multiple arrays now accepts a callback with matching arity instead of requiring arity 1, fixing ~62 false positives. - Enum built-ins:
from()/tryFrom()are now synthesized with one parameter, eliminatingTooManyArgumentsfalse positives. - Narrowing:
UndefinedVariableis no longer emitted for variables on the left-hand side of??and??=.assigned_varsis now correctly restored afterisset-narrowed branches. - Column numbers: diagnostic column numbers are now 1-indexed (previously 0-indexed). Any tooling that parses mir output should update accordingly.
- Stubs: user-defined files now consistently override native stub definitions in the symbol index, eliminating non-deterministic false positives when shadowing PHP built-in names.
self::CONSTreferences in method parameter defaults now correctly emitUndefinedConstantwhen the constant does not exist.- First-class callable syntax (
SomeClass::method(...)) now resolves to a typedTClosureinstead of an untyped callable. InvalidStringClassfalse positives eliminated for object expressions on the left of::(e.g.$obj::CONST).
Changed
Section titled “Changed”ProjectAnalyzeris replaced byAnalysisSessionin the public API. The new type consolidates project setup and analysis into a single entry point.- Stub loading is now fully lazy: stubs for a PHP version are loaded on first reference rather than at startup, reducing cold-start memory for projects that use only a subset of built-ins.
[0.28.0] - 2026-05-17
Section titled “[0.28.0] - 2026-05-17”- Composer plugin type:
composer require jorgsowa/mirnow triggers the binary download automatically without requiring manual script wiring. Thecomposer.jsontype field is set tocomposer-plugin, and aPluginclass registers the install/update event handler.
- Composer installer now embeds the target triple in the version marker, preventing a binary installed on one platform (e.g. macOS) from being reused on a different one (e.g. Linux in Docker). The shim error message for
proc_openfailures now mentions a possible architecture mismatch. - Broken relative links in the error codes reference table (
./→../) that caused 404s when navigating from the codes page to individual issue pages. - Documentation corrections for
ImplicitToStringCast,InvalidCast,UndefinedClass,InvalidScope,DeprecatedMethod, andDeprecatedMethodCallissue pages. Added missingUndefinedTrait(MIR0009) documentation page.
[0.27.0] - 2026-05-17
Section titled “[0.27.0] - 2026-05-17”- Stable
MIR####error codes for every issue variant, organized into 16 category bands. Codes surface inDisplayoutput in rustc style:error[MIR0005] UndefinedClass: .... Thename()method is unchanged and remains the suppression and SARIF rule key. UndefinedTrait(MIR0009) diagnostic: emitted when ausestatement references a name that does not exist in the codebase.InvalidTraitUsenow also emitted when the used name resolves to a class, interface, or enum instead of a trait. Per-use-statement source locations are stored inClassStorageandClassNodeso diagnostics point at the trait name in theusestatement.- php-rs-parser 0.13.0: parse errors now carry precise source locations via
err.span()instead of hardcoded line 1 col 0;ForbiddenWarningdiagnostics emit atSeverity::Warningand do not block semantic analysis.
- Literal integer (
1,42,-3) and quoted-string ('foo',"bar") types in docblock annotations now parse asTLiteralInt/TLiteralStringinstead ofTNamedObject, making@return 2|3and similar annotations work correctly. @return/@paramdocblocks written on the line preceding a standalone function declaration (rather than attached as an ASTdoc_comment) are now applied, matching the existing behavior for class methods.@methoddocblocks on traits, interfaces, and enums are now honored. Previouslyadd_docblock_memberswas only called for classes, silently dropping virtual method declarations on other symbol kinds.@method-added methods carryis_virtual: trueand are excluded fromUnimplementedInterfaceMethodchecks.UnusedVariablenow reports the correct source location for variables first assigned via array push ($arr[] = value),static $var, orglobal $var(previously fell back to line 1, col 0).global $varassignments are now treated as externally observable side effects (matching by-reference parameter semantics), eliminating false-positiveUnusedVariablediagnostics on global variable writes.Union::intersect_withnow returnsnever()when no types overlap between the subject and the arm condition, preventing false-positive method/property errors in match arm bodies.Union::add_typenow absorbsneverinto non-empty unions (T | never = T).- Pending reference locations are now drained into
RefLocAccumulatorinsideanalyze_file(Salsa), fixing reference tracking in the incremental analysis path.
Changed
Section titled “Changed”MissingThrowsDocblockis now suppressed by default forRuntimeExceptionandLogicExceptiondescendants (PHP’s “unchecked” exceptions). Both directthrowstatements and transitive@throwspropagation are filtered. The suppression list is configurable via the newsuppressed_issue_kindsAPI.find_dead_code: boolonProjectAnalyzerreplaced withsuppressed_issue_kinds: HashSet<String>and a centralizedapply_issue_suppressions()post-filter applied on every analysis path including the cache-hit path.- Removed the
instanceofoperator-precedence workaround fromnarrowing.rs; php-rs-parser 0.13.0 correctly parses!$x instanceof Cas!($x instanceof C).
Dependencies
Section titled “Dependencies”- Bumped php-rs-parser, php-ast, php-lexer, phpdoc-parser
0.12.1→0.13.0.
[0.26.0] - 2026-05-15
Section titled “[0.26.0] - 2026-05-15”Performance
Section titled “Performance”- Persistent Pass-1 cache (
StubSliceCache): when a cache directory is configured (ProjectAnalyzer::with_cache,AnalysisSession::with_cache_dir, or--cache-dir), each file’sStubSliceis stashed in<cache_dir>/stubs/<hh>/<full_hash>.binusing a content-hash key, a bincode binary encoding, and atomic tempfile-and-rename writes. On a warm cache, files skip parse and definition collection (≈95% of the per-file cost on Laravel) and the cached slice is ingested directly. Cache header is version-gated byCARGO_PKG_VERSION, the on-disk format version, and the target PHP version, so cached data is automatically invalidated across mir or PHP-version upgrades. - Both the batch path (
ProjectAnalyzer::collect_types_only, exercised by the CLI for vendor warmup) and the per-file LSP path (AnalysisSession::ingest_fileviaSharedDb::collect_and_ingest_file) consult the cache. Measured onlaravel/framework v11.44.7(10,188 vendor files, M-series Mac), independently verified hit counters (10,185 hits / 0 misseson warm, the 3-file delta is files mir skips for parse errors and is excluded from caching):- Vendor batch collection: cold 2,224 ms / 2,822 MiB churn → warm 1,440 ms / 525 MiB churn (−35% wall, −81% churn). Repeated runs land in a −30% to −46% wall-time band depending on the OS page-cache state of the underlying vendor tree.
- LSP-style serial
ingest_filestorm viaAnalysisSession: cold 5,476 ms → warm 3,720 ms (−32% wall). The serial path is bottlenecked by Salsa write-lock + ingest cost the cache doesn’t address.
- Cache misses (or files with parse / collector errors) skip the write-back so future runs re-parse them; cache hits restore the file path field from the lookup argument so the on-disk encoding never carries a machine-specific absolute path.
ProjectAnalyzer::{with_cache_dir,set_cache_dir}andAnalysisSession::{with_cache,with_cache_dir}nowdebug_assertthey are called before any file is ingested — late attachment would silently reset the shared database and discard prior Pass-1 work.
Dependencies
Section titled “Dependencies”- Bumped all transitive crates within their compatible semver ranges (
cargo update), including thephp-rs-parser/php-ast/php-lexer/phpdoc-parserstack from0.12.0→0.12.1. - Bumped
quick-xml0.39→0.40inmir-analyzer. - Replaced
postcardwithbincode 1.3.3for theStubSliceCacheon-disk format.postcardpulledheapless→atomic-polyfill(RUSTSEC-2023-0089);bincode v2was tried next but is itself flagged unmaintained (RUSTSEC-2025-0141).bincode 1.3.3carries no advisory and is explicitly called “complete” by its authors. Cache on-disk format version bumped to 2 so existing v2-encoded entries are treated as misses.
[0.25.0] - 2026-05-15
Section titled “[0.25.0] - 2026-05-15”Performance
Section titled “Performance”- Pass 2 reference-location recording now uses per-worker staging buffers (
PendingRefLocs) instead of writing directly to sharedArc<Mutex<...>>maps. Workers accumulate locations in an isolatedparking_lot::Mutex<Vec<RefLoc>>and a single serial commit drains them with one lock acquisition per map. Pass 2 wall-clock variance reduced from 28–240 ms (8×) to 43–56 ms (±25%) on 12 threads.
analyze_dependents_of()now returns the correct dependent set after a symbol is deleted or renamed. Previously, files referencing a now-gone symbol were silently dropped becausedependency_graph()routed edges throughsymbol_defining_file(), which returnsNonefor deleted symbols. Three coordinated fixes: afile_to_defined_symbolsforward index for O(1) definition lookup on removal; asymbol_referencersreverse index that survives symbol deletion; and astale_defined_symbolsaccumulator inAnalysisSessionthat feeds deleted symbols’ referencers back into the BFS.
[0.24.0] - 2026-05-15
Section titled “[0.24.0] - 2026-05-15”Performance
Section titled “Performance”- O(1) parameter deduplication: replaced linear Vec scan with FxHashMap for ~20% faster stub ingestion on large vendor sets. Deduplication now runs in parallel within rayon Pass 1 instead of serializing the collector.
- RwLock-based atomic counter writes for Salsa db updates, reducing lock contention during batch analysis and improving 12-thread scaling.
file_referencesforward index added toMirDb:dependency_graph()cost reduced from O(S×R) to O(E) (files × edges), eliminating full-table scans during incremental re-analysis.- In-memory always-on reverse dependency map (
structural_dependents_of) for O(D) BFS over structural dependencies (imports, class hierarchy, type hints) without requiring disk cache.
- Reference location recording now complete at all five previously-missing call sites:
instanceof,catch,::class,::CONST, and type-hint declarations. Files referencing a class only via these constructs are now correctly visible to the incremental dependency graph andanalyze_dependents_of().
[0.23.0] - 2026-05-14
Section titled “[0.23.0] - 2026-05-14”- Type narrowing for
get_class($obj) === 'ClassName'comparisons, enabling precise type refinement when class identity is verified. is_resource()type guard for completeness in the type narrowing system.- Parallel Salsa pre-sweep inference pass in batch path, replacing sequential Pass 2 driver with direct rayon-based inference for improved throughput.
- Type narrowing for
$var === SomeClass::classcomparisons, refining object types when matched against class constants.
- Bare-FQN references (e.g.,
new \Service(),\Helper::go()) now correctly wired into the incremental dependency graph soanalyze_dependents_of()returns files referencing classes via unqualified absolute paths.
Changed
Section titled “Changed”- Refactored database module structure:
source_filesmap moved from SharedDb tuple into MirDb for clearer ownership. - Lazy-load optimization: avoid redundant full scans of class inheritance chains when loading missing classes.
[0.22.0] - 2026-05-12
Section titled “[0.22.0] - 2026-05-12”AnalysisSession::class_issues_for(): exposes cross-file class diagnostics (abstract-method gaps, override violations, circular inheritance) so LSP consumers can retrieve the complete diagnostic picture alongsideanalyze_dependents_of()without accessingClassAnalyzerdirectly.
[0.21.2] - 2026-05-12
Section titled “[0.21.2] - 2026-05-12”@template T as Boundsyntax now parsed correctly (previously only@template T of Boundwas recognized), enabling proper type narrowing for templates declared with theaskeyword.- Callable/closure return types in
@returnannotations (e.g.,@return \Closure(): T) now correctly capture the return type after the colon, fixing falseMixedMethodCalldiagnostics when template parameters were used as closure return types.
[0.21.1] - 2026-05-09
Section titled “[0.21.1] - 2026-05-09”cargo-denyconfiguration format migration to version 2.
[0.21.0] - 2026-05-09
Section titled “[0.21.0] - 2026-05-09”- Tier 1 & 2 parser optimizations: pre-sized arena allocators and parallel user stub discovery for improved cold-start performance (25-40% improvement expected).
cargo-denyconfiguration format corrected to use proper advisories section syntax.- Security audit findings: eliminated unwrap calls and unsafe UTF-8 conversions.
- Panic on empty generic type parameters in docblock parsing.
- Outdated lock poison
.expect()calls replaced with proper error handling. - Template parameter bounds preservation and improved generic type narrowing.
- MixedClone detection for unconstrained template parameters.
- Missing stubs directory safety check in build.rs.
- Soft stub fallback version-gating for both functions and classes.
Changed
Section titled “Changed”- Refactored AST-based stub discovery in FileAnalyzer for clarity and performance.
- Split db.rs into focused sub-modules for maintainability.
- Improved code quality with centralized test utilities.
- Eliminated HashMap/HashSet clones in cache flush hot paths.
- Reduced string clone allocations in hot paths.
- Replaced std::sync::Mutex with parking_lot::Mutex to eliminate poison panics.
Performance
Section titled “Performance”- Parallelized fixture discovery in build script.
[0.20.0] - 2026-05-08
Section titled “[0.20.0] - 2026-05-08”- Session-based per-file analysis API (
AnalysisSession+FileAnalyzer) for incremental, file-scoped analysis suitable for LSP-style consumers. mir_analyzer::location_from_span(span, file, source, source_map) -> Location: public free function that converts a parserSpan(byte-offset range) to the crate’sLocationtype (1-based lines, 0-based codepoint columns), so consumers can translate Pass-2 spans to their own protocol’s position format without re-implementing column math.- Soft fallback for unknown stubs: when Pass 2 would emit
UndefinedFunction/UndefinedClassfor a name the build-time stub index recognises as a real PHP built-in, the diagnostic is suppressed. Defends against lazy-stub timing races (auto-discovery scanner false negatives, essentials-only sessions without auto-discovery, mid-ingest reads). Genuinely unknown names still emit. - Concurrent-read benchmark: N reader threads call
definition_of()in a tight loop while a writer continuously re-ingests a fixture, reporting wall time per fixed-size batch for 1 / 4 / 8 readers. Surfaces real contention characteristics under flat-out write pressure (per-read latency: 324ns @ 1 reader, 1.4µs @ 4, 1.9µs @ 8); realistic LSP edit cadence stays at the 324ns figure. MixedCloneissue type: detectsclone/clone withexpressions onmixed-typed values inExpressionAnalyzer.
@varannotation narrowing now applies to global-scope statements, not just function bodies. Previouslyanalyze_stmt()(used for top-level statements) skipped the pre/post narrowing thatanalyze_stmts()performed for function bodies, so@varhad no effect at global scope. Fixesglobal_with_var_no_indent,function_with_var, andinvalid_mixed_clonefixtures.
Changed
Section titled “Changed”- Analyzer boilerplate simplifications:
Union::core_type()collapses 10+ chainedremove_null().remove_false()call sites in type-checking logic.DefinitionCollector::parse_docblock_from_node_or_preceding()consolidates the “checkdoc_comment, fall back to preceding docblock” pattern repeated 11+ times across class/trait/interface collectors.StatementsAnalyzer::span_to_location()replaces 7 instances of verbose span-to-location computation in flow analysis.
[0.19.0] - 2026-05-07
Section titled “[0.19.0] - 2026-05-07”- Trait method undefined function detection: diagnostics now detect when trait methods reference undefined functions, improving visibility into broken trait implementations.
- Enhanced inheritance chain checking for magic methods (
__get,__invoke): full ancestor chain is now properly examined, catching edge cases where magic methods are defined in distant parent classes.
- Magic method resolution (
__get,__invoke) now checks the complete ancestor chain instead of stopping at the immediate parent, fixing false negatives where inherited magic methods were not detected. - Unused method tests now properly handle collateral errors, improving test reliability and reducing false positives in fixture validation.
[0.18.0] - 2026-05-06
Section titled “[0.18.0] - 2026-05-06”AbstractInstantiationdiagnostic to detect attempts to instantiate abstract classes vianew ClassName().
- Closure
use()clause validation: now detects undefined variables referenced in closure use() clauses. Example:use ($i)will reportUndefinedVariableif$iis not defined in the parent scope. - Mixin method resolution with generics: docblock
@mixin Foo<T>annotations now correctly resolve to classFooinstead of attempting to look up a non-existent class namedFoo<T>. - All 17
undefined_variablefixture tests now pass with correct line/column/message expectations. - All 15
undefined_constantfixture tests now pass with correct line/column/message expectations.
[0.17.3] - 2026-05-05
Section titled “[0.17.3] - 2026-05-05”Performance
Section titled “Performance”- Deduplicate parameter types across all function/method signatures via
Arc<Union>interning, eliminating redundant type allocations. - Resolve function node once per call site instead of twice, reducing redundant database lookups.
- Use
SimpleTypefor atomic function parameters, reducing type envelope overhead. - Deduplicate return types via
Arc<Union>interning for all callables. - Deduplicate parameter lists across vendor method signatures, further reducing memory footprint.
- Skip re-caching
StubSlicein Salsa during vendor collection, improving vendor ingestion performance.
[0.17.2] - 2026-05-04
Section titled “[0.17.2] - 2026-05-04”- The published
mir-analyzercrate is no longer shipped with an empty stub set. Thestubs/directory lived at the workspace root, outside the package, socargo packageexcluded it; downstream consumers (e.g.php-lsp) sawSTUB_FILES = &[]and every PHP built-in resolved asUndefinedFunction/UndefinedClass. Stubs now live inside the crate atcrates/mir-analyzer/stubs/and are included in the published artifact.build.rspanics if the directory is missing, and a newtests/packaging.rstest assertscargo package --listincludesstubs/Core/Core.phpplus the rest of the stub set — closing the publish-time gap. - Built-in function and class lookups are now case-insensitive, matching PHP semantics.
Restore_Error_Handler(),RESTORE_ERROR_HANDLER(),new arrayobject([]), andnew ARRAYOBJECT([])no longer produce false-positiveUndefinedFunction/UndefinedClassdiagnostics. Implemented as side indices onMirDb(function_node_keys_lower,class_node_keys_lower) so the canonical-FQN storage thatactive_*_node_fqns,function_count,type_count, andclear_file_referencesdepend on is unchanged. Constants remain case-sensitive (PHP semantics).
[0.17.1] - 2026-05-03
Section titled “[0.17.1] - 2026-05-03”- Unqualified class names in namespaced files no longer silently fall back to the global namespace when the namespaced class is missing. PHP only does that fallback for functions and constants; mir’s
resolve_name_via_dbwas incorrectly extending it to classes, masking realUndefinedClassbugs. - Composer autoload parsing now covers
psr-0,classmap, andfilesin addition topsr-4, for both projectcomposer.jsonand each package invendor/composer/installed.json. Vendor packages that expose global helpers viaautoload.files(Symfony polyfills, Laravel helpers, ramsey/uuid bootstrap, etc.) and classmap-only packages no longer produce false-positiveUndefinedFunction/UndefinedClassdiagnostics.
[0.17.0] - 2026-05-03
Section titled “[0.17.0] - 2026-05-03”Removed
Section titled “Removed”mir_codebase::Codebasestruct,CodebaseBuilder,codebase_from_parts, and the internalInternermodule. The salsa db (MirDb) is the single source of truth for class/method/property/constant metadata, per-file imports/namespaces, global vars, and reference tracking. Themir-codebasecrate now exports only the serializable storage types (StubSlice,*Storage,FnParam,TemplateParam,Visibility,Location). Breaking for library consumers that importedmir_codebase::Codebase.ProjectAnalyzer::codebase()accessor (already removed in 0.16.x perf work; the Codebase deletion completes the cleanup).mir-codebaseno longer pulls indashmaporthiserror.
Performance
Section titled “Performance”- Hot-path Salsa db lookup tables (
class_nodes,function_nodes,method_nodes,property_nodes,class_constant_nodes,global_constant_nodes,file_namespaces,file_imports,global_vars,symbol_to_file,reference_locations) and the ancestor-walk visited sets inclass_ancestors/lookup_method_in_chain/method_is_concretely_implementednow useFxHashMap/FxHashSetinstead of stdHashMap/HashSet. Eliminates the per-ancestorStringallocation inclass_ancestors(now reuses the existingArc<str>). ~7% reduction in user CPU time on the Laravelsrc/benchmark.
[0.16.1] - 2026-05-01
Section titled “[0.16.1] - 2026-05-01”- CLI Composer detection now walks up from a single explicit file path to find the nearest
composer.json, so root config files such as.php-cs-fixer.phpcan resolve project PSR-4 namespaces instead of reporting false-positiveUndefinedClassdiagnostics.
[0.16.0] - 2026-04-28
Section titled “[0.16.0] - 2026-04-28”- Cross-file inferred return types (G6): a type-inference priming pass now runs all function and method bodies in parallel before the issue-emitting Pass 2, writing
inferred_return_typefor every symbol without recording reference locations. Callers no longer seemixedfor callees whose Pass 2 had not yet completed. Covers the common depth-1 case; depth-N chains are addressed by Phase 4 (Salsa). - Per-class
OnceLockfinalization (Phase 3 item 6):ensure_finalized(fqcn)lazily computes and memoizes each class’s ancestor chain on first access viaDashMap<Arc<str>, OnceLock<Arc<[Arc<str>]>>>with thread-local cycle detection.finalize()is now a warm-all wrapper;remove_file_definitions()evicts only the affected entries granularly.
Performance
Section titled “Performance”- Lazy finalization removes the pass barrier (Phase 3 item 7): the eager
finalize()barrier that blocked all of Pass 2 until every ancestor chain was warm is removed.ensure_finalized()is now called at eachall_parentsread site (get_method_inner,get_property_inner,get_class_constant,extends_or_implements,has_unknown_ancestor,collect_members_for_fqcn,ClassAnalyzer::analyze_all,check_trait_constraints,argument_type_satisfies_param). Phase 3 is now complete.
- LSP incremental re-analysis: classes defined in an analyzed file but never referenced during Pass 2 had empty
all_parentsat snapshot time, causingrestore_all_parentsto silently restore empty ancestor chains on the LSP fast path.file_structural_snapshotnow callsensure_finalizedfor each symbol before capturing it.
[0.15.0] - 2026-04-28
Section titled “[0.15.0] - 2026-04-28”- Return type covariance for named-object overrides:
ClassAnalyzernow delegates tonamed_object_return_compatiblewhen checking overriding methods, catching cases where a child class returns an unrelated type instead of the declared parent return type. Mixed scalar+object unions still skip the check to avoid false positives. - Type narrowing after
instanceof $this: when the right-hand side ofinstanceofis$this, it is resolved to the current class FQCN before narrowing, eliminating false-positiveMixedMethodCallandUndefinedPropertydiagnostics onif (!$other instanceof $this)guards. (#144)
Changed
Section titled “Changed”stmt.rssplit intostmt/sub-module (mod.rs,loops.rs,return_type.rs), following the same pattern ascall/. No behavior change.
[0.14.0] - 2026-04-28
Section titled “[0.14.0] - 2026-04-28”- Generic template substitution extended to array shapes (
TKeyedArray,TNonEmptyArray,TNonEmptyList), callable/closure types, conditional types, and intersection types. Variable calls ($fn()) onTClosure/TCallablenow resolve the correct return type instead ofmixed.TIntersectionmethod calls resolve against the part that owns the method. Docblock parser gainsarray{key: T}shape syntax andcallable(T): R/Closure(T): Rparsing. ParsedDocblock::is_inherit_docflag: set when@inheritDoc,@inheritdoc, or{@inheritDoc}is present in a docblock, enabling LSP clients to walk the inheritance chain for hover and completion without implementing resolution in mir itself.
- LSP / incremental re-analysis:
inject_stub_slicenow populatesfile_namespacesandfile_importsin the codebase, fixing false-positiveUndefinedClassdiagnostics foruse-aliased classes after any incremental re-analysis triggered byre_analyze_file.
Changed
Section titled “Changed”Locationtype unified inmir-types; internal codebase storage switched from byte offsets to(line, col_start, col_end). Allmark_*_referenced_at()methods now accept line/column instead of byte offsets. Columns use 0-based Unicode code-point counts (LSP UTF-32 encoding); UTF-16 conversion happens at the LSP boundary for clients that do not advertise UTF-32 support. Existing on-disk caches silently rebuild on the next run.
- Docs deploy now invokes a reusable
workflow_callpath todocs.ymlso the deployment runs under a branch-authorized context instead of directly from a tag, fixing GitHub Pages environment protection failures.
[0.13.0] - 2026-04-28
Section titled “[0.13.0] - 2026-04-28”- Interactive WASM playground embedded in the docs site: select PHP version (8.1–8.5), type PHP code, and see live diagnostics with underline overlays and severity-colored cards. (#287)
Changed
Section titled “Changed”- Docs site logo added to README and top bar; branding updated.
- php-ast and php-rs-parser bumped to 0.9.6.
- Node.js version in docs deploy workflows raised from 20 to 22 (Astro now requires >=22.12.0).
[0.12.0] - 2026-04-27
Section titled “[0.12.0] - 2026-04-27”PossiblyInvalidArgumentissue: emitted when afalse|Tunion value is passed to a parameter that does not acceptfalse, surfacing potential type mismatches that were previously silently widened tomixed.- Backed enum
->valueand->nameaccess now returns a precise inferred type (TLiteralString/TLiteralIntfor->value,TLiteralStringfor->name) instead ofmixed. call_user_funcandcall_user_func_arraystring callables (e.g.'ClassName::methodName') are now tracked as real call references, fixing false-positive stub warnings on those forms.
- Infinite recursion on circular
@mixinreferences: the mixin resolver now carries a seen-set and breaks cycles instead of stack-overflowing. - Benchmark harness: rayon stack size raised to 16 MiB and the global thread pool is initialised explicitly, preventing stack overflows on deeply recursive PHP files during benchmarking.
timeout-minutesadded to all workflow jobs and a concurrency group added to the CI workflow to cancel superseded runs.
[0.11.1] - 2026-04-26
Section titled “[0.11.1] - 2026-04-26”- Release CI: GitHub Release is now created from the CHANGELOG before binaries are uploaded, fixing a race condition where
upload-rust-binary-actionfailed with “release not found”.
[0.11.0] - 2026-04-26
Section titled “[0.11.0] - 2026-04-26”InvalidDocblockissue: emitted when a type annotation in a docblock cannot be parsed (malformed syntax). (#282)- Injectable user stubs:
<stubs><file name="..."/>and<stubs><directory name="..."/>elements inmir.xml/psalm.xmlload additional stub paths before analysis; stub files are not themselves analyzed for errors. (#285) phpVersioncan now be set as an XML attribute on the root<mir>or<psalm>element (e.g.<mir phpVersion="8.2">), matching Psalm’s config syntax, in addition to the existing child-element form. (#285)
Changed
Section titled “Changed”- phpstorm-stubs is now vendored directly in
stubs/(tracked in git) instead of a git submodule. External contributors no longer need to rungit submodule update --init. (#283) - Documentation site migrated from mdBook to Astro Starlight; issue-kind reference pages are now split into individual pages grouped by category.
[0.10.0] - 2026-04-26
Section titled “[0.10.0] - 2026-04-26”- Composer package
miropen/mir-php. Apost-install-cmd/post-update-cmdhook downloads the prebuiltmirbinary matching the installed version and host platform from GitHub Releases, verifies the SHA-256 sidecar, and exposesvendor/bin/mir. Single-entry extraction with strict path-traversal and symlink rejection. Supported targets:x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu,x86_64-apple-darwin,aarch64-apple-darwin,x86_64-pc-windows-msvc. ReleaseGitHub Actions workflow building and uploading per-target archives + sha256 sidecars onv*tags.NullArgumentissue: emitted when a literalnullis passed to a non-nullable parameter (previously subsumed byInvalidArgument). Severity: warning.UnusedFunctionissue: emitted for free functions that are never called whenfind_dead_codeis enabled.InvalidPropertyAssignmentissue: emitted when a value of an incompatible type is assigned to a typed property. Handles class inheritance via the codebase.
cargo install mir-clireferences in README and docs corrected tomir-php(the actual crate name).- Panic in docblock extraction when source text before a declaration contains multibyte characters (e.g.,
→).find_preceding_docblocknow correctly advances past multibyte chars when scanning for word boundaries.
[0.9.1] - 2026-04-26
Section titled “[0.9.1] - 2026-04-26”Location.line_endfield — all issues now carry an end line number, enabling multi-line range highlighting in editors and code scanning tools. (#270)- SARIF output:
region.endLinepopulated fromline_end. (#270) - SARIF output: results now include
rank(Error → 90, Warning → 95, Info → 99) matching Psalm’s scoring range. (#270) - SARIF output: rules now include
properties.tags("security"for taint issues,"maintainability"for all others). (#270) - Psalm docblock parity:
@psalm-assert-if-falsetype narrowing. (#267) - Psalm docblock parity:
@psalm-import-typetype alias imports. (#267) - Psalm docblock parity:
@psalm-paramand@psalm-returntype narrowing annotations. (#267)
- SARIF output:
startColumn/endColumnare now correctly 1-based per SARIF 2.1.0 §3.30.5 (previously off by one). (#270) - SARIF output: rules now include
defaultConfiguration.levelso the GitHub Code Scanning rules panel shows severity. (#270) - SARIF output: results now include
partialFingerprints.primaryLocationLineHash(FNV-1a of rule name + snippet) so GitHub Code Scanning can track findings across commits. (#270) - Static calls now correctly check for
__callStatic(not__call) when suppressingUndefinedMethodon missing static methods. (#271) - Magic method dead-code exclusion now uses lowercase keys matching
own_methodsstorage, so__callStatic,__toString, and__debugInfoare correctly exempted fromUnusedMethodreports. (#271) __unserializeadded toMAGIC_METHODS_WITH_RUNTIME_PARAMS, preventing its$dataparameter from being flagged as unused. (#271)- Trait docblock parsing now falls back to raw-source lookup when php-rs-parser absorbs the trait-level docblock, ensuring
@psalm-require-extendsand@psalm-require-implementsare correctly detected. (#267)
Changed
Section titled “Changed”- Bumped blake3, php-ast, php-lexer, and php-rs-parser to latest. (#272)
[0.9.0] - 2026-04-26
Section titled “[0.9.0] - 2026-04-26”- Trait method bodies are now analyzed in Pass 2; diagnostics (
UndefinedFunction,UndefinedMethod, unused variables, etc.) are emitted for code inside traits. (#264) UnreachableCodeissue — statements following a terminator (return,throw,exit,die) in the same block are now flagged; nested closures are analyzed with a fresh context and are not affected by divergence in the outer block. (#262)
PossiblyUndefinedVariablepromoted toWarningseverity, making it visible at the default error level and matching Psalm’s behavior. (#261)- 10 false-positive
UndefinedMethodreports eliminated: dynamic method calls via variable expressions ($obj->{$var}()) no longer trigger a spurious lookup, and private trait methods are now correctly accessible from classes that use the trait. (#260) - Improved Psalm docblock parity. (#265, #266)
[0.8.0] - 2026-04-25
Section titled “[0.8.0] - 2026-04-25”PhpVersion::LATESTconstant (currently8.5) — used as the default when no explicit version is configured.ProjectAnalyzer::with_php_versionbuilder method to set the target PHP version.@deprecatedtag messages are now included inDeprecatedissue descriptions.php_versionis now propagated throughStatementsAnalyzerandExpressionAnalyzerfor version-gated checks.
UndefinedClassis now detected in 7 previously-silent code paths.- Static method call spans now use the parser span for the method name rather than manual offset arithmetic.
- Windows build:
canonicalize()returns\\?\-prefixed UNC paths on Windows; the build script now strips that prefix before embedding stub paths ininclude_str!.
Changed
Section titled “Changed”ProjectAnalyzer::php_versionfield is nowOption<PhpVersion>(None= usePhpVersion::LATEST); previously it was a barePhpVersiondefaulting to 8.4.- Bumped
php-rs-parser,php-ast, andphp-lexerto 0.9.2.
Performance
Section titled “Performance”IssueBuffer::adddeduplication changed from an O(n) scan to aHashSetlookup.
[0.7.3] - 2026-04-25
Section titled “[0.7.3] - 2026-04-25”- Cross-file
.phptfixture format with===file:Name.php===sections and optionalcomposer.jsonfor PSR-4 lazy-loading scenarios; 21 new cross-file fixtures added. ===config===section in.phptfixtures for per-fixture settings (php_version,find_dead_code); dead-code fixtures now declare this in config instead of relying on a hard-coded category list.- New
stub_behavior/fixtures coveringstdClass,preg_match,sscanf,array_mapnull callback, andarray_keysoptional filter. - Correctness tests for
inject_stub_slicecovering symbol overwrite,symbol_to_fileupdates,global_varscleanup onremove_file_definitions, andStubVfsroundtrip navigability.
Changed
Section titled “Changed”- Switched stubs from generated Rust files (
mir-stubs-gen) to phpstorm-stubs loaded at build time viaCUSTOM_STUB_FILES; themir-stubs-gencrate is removed. - Unified single-file and multi-file
.phptfixture parsers into a singleparse_phptfunction; existing===source===markers renamed to===file===.
UnimplementedAbstractMethodandUnimplementedInterfaceMethoderrors now report the method name with its original declared casing instead of the lowercase-normalized form.
[0.7.2] - 2026-04-24
Section titled “[0.7.2] - 2026-04-24”Changed
Section titled “Changed”- Bumped
php-rs-parser,php-ast, andphp-lexerto 0.9.1.
[0.7.1] - 2026-04-22
Section titled “[0.7.1] - 2026-04-22”StubSlice::fileandStubSlice::global_varsfields so a slice can describe the source file it came from and the@var-annotated globals it declares.CodebaseBuilderandcodebase_from_partsinmir-codebase— compose a finalizedCodebasefrom per-fileStubSlices without mutating shared state during collection.DefinitionCollector::new_for_sliceandDefinitionCollector::collect_slice— a pure-function entry point that returns aStubSliceinstead of writing to aCodebase. Enables downstream consumers (e.g. salsa queries) to treat Pass 1 as a pure computation.
Changed
Section titled “Changed”DefinitionCollectornow builds aStubSliceinternally; the existingnew+collectAPI is preserved as a shim that injects the slice on completion.Codebase::inject_stub_slicenow populatessymbol_to_fileandglobal_varswhen the slice has afileset.
[0.7.0] - 2026-04-21
Section titled “[0.7.0] - 2026-04-21”- PHP-first stub pipeline — stubs are now authored as PHP source files under
stubs/{ext}/withstub.tomlmanifests and transformed into Rust via the newmir-stubs-gencodegen tool, replacing the monolithic hand-writtenstubs.rs. (#243) - First-party stubs for 30 PHP extensions — bundled stubs cover common extensions (curl, pdo, json, mbstring, etc.), loaded into the codebase at startup. (#246)
- 19 additional bundled-with-PHP extensions — calendar, exif, ftp, gd, gettext, opcache, pgsql, phar, readline, shmop, soap, sqlite3, sysvmsg, sysvsem, sysvshm, tidy, xmlreader, xmlwriter, xsl. (#251)
UndefinedConstantissue — the analyzer now emitsUndefinedConstantfor references to undefined global and class constants. (#242)- Target PHP version plumbed into
ProjectAnalyzer— the analyzer accepts a target PHP version to gate version-specific behavior. (#249)
Changed
Section titled “Changed”- Upgraded php-rs-parser and php-ast to 0.9; upgraded toml, quick-xml, and criterion to latest. (#245)
Performance
Section titled “Performance”- BLAKE3 for cache hashing — replaced SHA-256 with BLAKE3 for the incremental cache and deduplicated per-file hashing. (#244)
- Leading backslash in
useimports — fully qualified use-imports (use \Foo\Bar;) now resolve correctly by stripping the leading backslash. (#247) composer.jsondetection from path argument — when invoked with a path argument, mir now walks up from that path to locatecomposer.jsoninstead of only checking the CWD. (#247)
- Jobs are now gated (lint → stubs-up-to-date → test) and a dedicated step verifies that regenerated stubs match the committed generated files. (#250)
[0.6.0] - 2026-04-19
Section titled “[0.6.0] - 2026-04-19”- Recurse into nested function and class bodies — the analyzer now descends into nested function declarations and class definitions inside method/function bodies, catching issues in inner scopes that were previously invisible. (#223)
UndefinedClassforextends/implements— emitUndefinedClasswhen a class extends or implements a type that does not exist in the codebase or stubs. (#224)InvalidScopefor$thisin invalid context — emitInvalidScopewhen$thisis used outside of an object method (e.g., in a static method or free function). (#220)- Real-world Criterion benchmark suite — added a benchmark that runs analysis over a realistic PHP codebase for continuous performance regression tracking. (#219)
- Intersection type hints —
type_from_hintnow correctly resolves intersection types (A&B), fixing false positives in type-narrowing and parameter checks. (#221)
[0.5.2] - 2026-04-19
Section titled “[0.5.2] - 2026-04-19”StaticDynMethodCallsupport — dynamic static dispatch (Foo::$method()) is now handled as a distinct AST variant; evaluates arguments for taint propagation and returnsmixed. (#216)
Changed
Section titled “Changed”- Upgraded php-rs-parser and php-ast to 0.8; migrated
FileParsertoParserContextfor O(1) arena reset on repeated parses. (#216)
Performance
Section titled “Performance”MethodStoragestored asArc—own_methodsin all storage types now holdsArc<MethodStorage>, making method lookups an atomic refcount bump instead of a deep clone. (#213)- Skip re-analysis on unchanged content —
re_analyze_filereturns cached results immediately when the file content hash matches, avoiding all four analysis phases on repeated LSP saves. (#204) - Skip
finalize()on body-only changes —re_analyze_filecaptures a structural snapshot before removal; if inheritance fields are unchanged after Pass 1, restoresall_parentsdirectly and skips the full class-hierarchy walk. (#205)
- Trait-of-trait method resolution —
get_method()now walks the full transitive trait chain with a cycle guard, eliminating falseUnimplementedInterfaceMethoderrors for methods contributed by indirectly used traits. (#209) elseifnarrowing and branch merge — elseif branches now correctly narrow on the parentifcondition being false, and all elseif branches are folded into the post-if merge (previously only the last branch survived). (#211)TKeyedArrayforeach key type —infer_foreach_typesnow derivesTLiteralString/TLiteralIntkeys fromArrayKeyentries instead of always returningTMixed. (#211)- Switch fallthrough contexts — non-diverging case contexts are now collected and merged into the post-switch type environment; chain-fallthrough into a diverging case is correctly propagated. (#212)
[0.5.1] - 2026-04-18
Section titled “[0.5.1] - 2026-04-18”Performance
Section titled “Performance”- Reference index memory reduction — intern reference keys with a lock-free
u32interner, store all references in a flatVec<Ref>, and compact into two CSR index arrays after Pass 2. Expected ~5× reduction in reference index memory. (#202) - Single-pass definition collection — merged the pre-index and definition collection sub-passes into one parallel
par_iter, eliminating the second parse of every file and removing the sequential serialisation barrier. (#196)
- Column offsets in diagnostics now use Unicode character counts consistently throughout mir-core. (#201)
[0.5.0] - 2026-04-17
Section titled “[0.5.0] - 2026-04-17”issues_by_file()onAnalysisResult— group analysis issues by their source file path for easier per-file reporting. (#154)- Symbol reference location tracking —
AnalysisResult::symbol_atresolves the symbol under a given position, enabling LSP go-to-definition and find-references. (#185) ResolvedSymbol::fileandcodebase_key— extended resolved symbol information with the source file and codebase key for cross-file navigation. (#185)
Changed
Section titled “Changed”- Upgraded php-rs-parser and php-ast to 0.7. (#195)
- Property access symbols now use the identifier span and nullsafe accesses (
?->) are tracked. (#189) - Function, method, and static call symbols now use the identifier span rather than the full call expression span. (#192)
$thisis now injected into method context so$this->method()calls are correctly resolved bysymbol_at. (#193)
[0.4.1] - 2026-04-12
Section titled “[0.4.1] - 2026-04-12”- Diagnostic column offsets — fixed
col_endalways being equal tocol_start(resulting in zero-width diagnostic ranges) and column offsets being raw UTF-8 byte positions instead of character counts. Diagnostics now correctly highlight the full variable/expression range with proper multi-byte character handling. (#182)
[0.4.0] - 2026-04-12
Section titled “[0.4.0] - 2026-04-12”- JetBrains phpstorm-stubs integration — mir now uses the authoritative phpstorm-stubs repository as the source for PHP built-in definitions. This provides comprehensive coverage of 500+ functions, 100+ classes, and 200+ constants across 33 PHP extensions. (#181)
- Global variable registry — new
@varannotation support for tracking globally-scoped variables declared outside of function/class scope. Reduces false positives inUndefinedVariablechecks. (#160)
Changed
Section titled “Changed”- Dependency updates — upgraded php-rs-parser and php-ast to v0.6.0 for improved parsing robustness and performance.
is_builtin_functionnow uses the full loaded stubs to properly detect built-in functions across all extensions.
[0.3.0] - 2026-04-10
Section titled “[0.3.0] - 2026-04-10”- Generic type covariance and contravariance — full support for
@templatetype parameter variance annotations in classes and methods. (#109) - Circular inheritance detection — emit
CircularInheritanceerror when classes form circular inheritance chains. (#110) - Test fixture infrastructure — 22 new test fixtures covering previously uncovered rule categories, bringing fixture test count to 119. (#98)
Changed
Section titled “Changed”- AST doc_comment refactor — switched from manual docblock discovery to using AST
doc_commentfields for more reliable comment association. (#107) - Removed
mir-test-utilscrate to eliminate circular dependency structure. (#106)
- Class-level issue reporting — proper source locations (line/column in
storage::Location) and code snippets now emit correctly for class-level issues. (#105) - Magic method parameters —
UnusedParamchecks now exclude magic method parameters (__construct,__get, etc.). (#108)
[0.2.1] - 2026-04-09
Section titled “[0.2.1] - 2026-04-09”Changed
Section titled “Changed”- Upgraded php-ast and php-rs-parser to v0.5.0.
- Proper source mapping threading from
ParseResultthrough the analysis pipeline.
[0.2.0] - 2026-04-08
Section titled “[0.2.0] - 2026-04-08”- SymbolTable adoption — parallel pre-indexing of file imports, namespaces, and known symbols for better scalability.
- SourceMap and CommentMap — adopted from php-ast for reliable line/column resolution and comment association.
- Test fixture infrastructure with 96 fixture-based tests across 10 rule categories.
- Reduced
UnusedVariablefalse positives from 405 to 127 through improved read tracking in closures and assignment contexts.
[0.1.0] - 2026-03-15
Section titled “[0.1.0] - 2026-03-15”- Initial release of mir, a fast incremental PHP static analyzer written in Rust.
- Core features: type system, type inference, call checking, class analysis, dead code detection, taint analysis, incremental caching, parallel analysis.
- Comprehensive built-in PHP function and class coverage.