Skip to content

0.9.0

Latest

Choose a tag to compare

@AJenbo AJenbo released this 19 Jul 23:24
Immutable release. Only release title and notes can be modified.

Added

  • Macro hover shows origin and inferred return types. Hovering on a macro method call now displays a "macro" indicator instead of the generic "virtual" label, distinguishing ::macro() registrations from @method/@mixin synthesized members. When the closure has no explicit return type hint, the return type is inferred from the closure body and shown with an "(inferred)" annotation. Bare $this / self / static returns preserve their keyword form, and method chains like $this->transform(...) use the last method's declared return type directly, preserving $this, static, and generic parameters that the general resolver would flatten to a bare class name. Regular (non-macro) methods with inferred return types also show the "(inferred)" annotation on hover. Contributed by @calebdw.
  • Return type mismatch diagnostics (type_mismatch_return). Functions and methods with a declared return type are now checked against their return statements. Incompatible return values are flagged as errors. Void functions returning a value and bare return; in non-void functions are also flagged. Generators (functions using yield) are skipped. Uses the same conservative is_type_compatible policy as argument type checking to avoid false positives. Contributed by @calebdw.
  • Property type assignment diagnostics (type_mismatch_property). Assignments to typed properties ($this->prop = expr and self::$prop = expr) are checked against the declared property type. Incompatible values are flagged as errors. Only plain = assignments are checked; compound operators (+=, .=, etc.) are skipped. Untyped and mixed properties are not flagged. Contributed by @calebdw.
  • Conditional return types keep an intersection with the matched class. A @return ($x is class-string<T> ? T&SomeInterface : SomeInterface) annotation now resolves the matched branch to the concrete class intersected with the interface, instead of collapsing it to the bare class. Mock factories such as Mockery's mock(Foo::class) and Laravel's $this->mock(Foo::class) therefore resolve to Foo&MockInterface, so their members complete and assigning the result to a Foo-typed property or returning it from a Foo&MockInterface method no longer reports a spurious type mismatch.
  • PSR-4 mismatch diagnostics and rename-based moves. Files now warn when the declared namespace or primary class name does not match the PSR-4 path or filename, with quick fixes to correct them. Renaming a class from its declaration now opens the full FQCN so you can move it between namespaces in one step, and renaming a namespace can rewrite multiple segments at once while moving PSR-4 directories and updating references across the project. Contributed by @calebdw.
  • Case-sensitive autoloading diagnostic. A class reference whose casing differs from the class's actual declaration is now flagged, with a quick fix to correct it. This catches the bug where code loads on a case-insensitive filesystem (macOS, Windows) but fails with a class-not-found error on Linux, because PSR-4 maps the name to a file path and path lookups are case-sensitive there. It covers use imports and inline references to autoloaded classes; built-in classes and same-file references, which never reach the autoloader, are left alone.
  • Completion candidates ranked by dependency provenance. Class, function, and constant completions are now sorted by origin tier: project code first, then core/stub symbols, then explicit Composer dependencies (require / require-dev), then transitive vendor dependencies last. The provenance is inferred from composer.json and installed.json during indexing. Contributed by @calebdw.
  • analyze and fix work without composer.json. Both commands now treat a directory that has no composer.json (a WordPress site, a legacy codebase) as a plain PHP project: classes are indexed by scanning the tree and files are discovered by walking the root, so projects that never adopted Composer can be analysed directly. A note on stderr flags the fallback so a mistyped --project-root is not silently analysed as a bare tree.
  • update command. A new phpantom_lsp update subcommand downloads the latest release from GitHub and replaces the current binary. Supports --check (dry run, exit code 1 if update available) and --no-confirm (for CI). Handles .tar.gz (Unix) and .zip (Windows) archives across all 6 supported platforms. Contributed by @calebdw in #194.
  • array_map infers the output element type from its callback. The result of array_map now reflects what the callback actually returns instead of assuming the input element type is preserved. An explicit return type hint is honoured, including scalars like string or int, so array_map(fn(Item $item): string => $item->id, $items) produces list<string> rather than list<Item>. When the callback has no return type hint, the type is inferred from its body expression, so array_map(fn($item) => $item->id, $items) over a list<Item> also produces list<string>. Fixes #147. (contributed by @calebdw in #195)
  • Static methods complete on instance access. Member completion after -> now offers a class's static methods alongside its instance methods, since PHP lets you call a static method through an instance ($obj->make()). Static properties remain excluded, as they are only reachable via ::. Contributed by @calebdw in #174.
  • Array-callable navigation. Method-name strings in array callables ([Controller::class, 'method'] and [$object, 'method']) now resolve like a real member reference. This makes go-to-definition, find-references, and rename work on Laravel controller actions such as Route::get('/', [IndexPageController::class, 'indexPage']).
  • Array-callable method completion. Typing inside the method-name string of an array callable ([Controller::class, '|']) now offers method name completions from the resolved class, including inherited and trait methods. Works with Class::class constants, $this, and typed variables. (thanks @calebdw)
  • Convert arrow function to closure. A new refactor.rewrite code action converts arrow functions to anonymous closures (fn($x) => $x * 2 to function($x) { return $x * 2; }). Variables from the outer scope are automatically captured via a use() clause. Preserves static and return type hints. Contributed by @calebdw in #191.
  • @phpstan-sealed tag support. The @phpstan-sealed FooClass|BarClass PHPDoc tag is now recognized. Class names in the tag are treated as type references, preventing false "unused import" diagnostics. Docblock completion also offers the tag. (contributed by @calebdw in #190)
  • Magic methods complete when implemented. Magic methods declared on a class (__invoke, __toString, __call, and the rest) are now offered in member completion, so explicit calls like $x->__invoke() autocomplete and support go-to-definition. They are sorted below the regular methods so they never appear at the top of the list.
  • Staleness detection and auto-refresh. The class index, function index, and constant index now stay fresh automatically. When PHP files are created or deleted outside the editor (e.g. git checkout, code generation), the indices update without a restart, and edits made outside the editor are reflected the next time the file is used. When composer.json or composer.lock changes (e.g. after composer install), vendor packages are rescanned automatically.
  • #[ArrayShape] attribute support. Functions and methods annotated with #[ArrayShape(["key" => "type", ...])] (used by ~84 phpstorm-stubs entries) now produce array shape key completions, hover type info, and correct type resolution. Affects commonly used functions like parse_url, stat, pathinfo, gc_status, getimagesize, and session_get_cookie_params.
  • Convert to arrow function. A new refactor.rewrite code action converts single-expression closures to arrow functions (function($x) { return $x * 2; } to fn($x) => $x * 2). The action is only offered when the conversion is safe: single return statement, no by-reference use captures, no void/never return type, and PHP >= 7.4.
  • Convert switch to match. A new refactor.rewrite code action converts switch statements to match expressions when all arms are single-expression returns or assignments to the same variable. Handles fall-through cases (merged with commas), trailing break removal, and throw arms. Requires PHP >= 8.0.
  • Extract interface. A new refactor.extract code action generates an interface from a concrete class. All public method signatures (excluding the constructor) are extracted into a new {ClassName}Interface.php file in the same directory, and the class is updated with implements {ClassName}Interface. Class-level and method-level @template tags are preserved when referenced by extracted methods.
  • @template on @method tags. Virtual methods declared via @method PHPDoc tags can now define their own template parameters using the <T of Bound> syntax (e.g. @method TVal get<TVal of mixed>(TVal $default)). Template inference at call sites works the same as for real methods.
  • Laravel custom Eloquent builder support. Models using the #[UseEloquentBuilder] attribute now have their custom builder's methods forwarded as static methods on the model. query(), newQuery(), and newModelQuery() return the custom builder type with correct generic model substitution. Contributed by @MingJen in #118.
  • Eloquent relation and column string completion. Typing inside string arguments to with(), load(), whereHas(), and other Eloquent methods that accept relation names now offers relationship method names as completions, with dot-notation traversal for nested relations. Similarly, where(), orderBy(), select(), pluck(), and other column-accepting methods offer model column names (from $casts, $fillable, @property tags, timestamps, etc.).
  • Authenticated user resolves to the configured model. $request->user(), auth()->user(), and Auth::user() now resolve to the Eloquent model declared in config/auth.php instead of only the bare Authenticatable contract, so completion, hover, and member access work on the concrete model ($request->user()->email). Naming a guard selects that guard's model, so auth('admin')->user(), Auth::guard('admin')->user(), and $request->user('admin') resolve to the model configured for the admin guard rather than the default one. The config is read statically: only the literal default of env('AUTH_MODEL', User::class) is used, never the runtime environment. When a guard, provider, or model could vary at runtime, the result widens to a union of every candidate, and the floor is raised from the abstract contract to the project's own classes that implement it, so a single-model app resolves to just that model while a multi-model app offers each. Members that exist on some candidate resolve; genuinely unknown members still report.
  • Laravel macros are recognized as real methods. A method registered with SomeClass::macro('name', fn (...) => ...), whether in your own service providers or in an installed package's, now appears in completion on that class, shows the closure's parameters and return type on hover and in signature help, and resolves for member access and chaining. Go-to-definition on a macro call jumps to its ::macro(...) registration site, landing on the first character of the macro name string. Both instance ($collection->name()) and static (SomeClass::name()) calls work. A macro registered through a facade (View::macro('extends', ...)) also attaches to the concrete class the facade resolves to, so an instance call on that class ($factory->extends()) resolves as well as the static facade call. Discovery now follows provider-rooted helper classes in both app code and installed packages, whether the provider references the helper through a static call, Foo::class, or new Foo(), and also recognizes typed variable registrations like Builder $query followed by $query->macro(...), including inside callbacks such as function (Builder $builder) { $builder->macro(...); }. Find-references and rename now link the registration string with macro call sites in both directions, including chained collection-style calls such as ->pluck(...)->macroName(), and workspace symbol maps are warmed in the background so repeated workspace-wide rename/reference requests avoid reparsing unopened files.
  • Container string aliases and global facades resolve. resolve('blade.compiler') and app('cache') resolve to the concrete class Laravel binds the string to, so member access on the result completes, navigates, and type-checks, whether the call is chained directly or its result is first assigned to a variable. Bare global facade aliases such as \App and \DB resolve to their facade class without an explicit import. Both alias tables are read by parsing the framework the project actually has installed (never a version-specific list baked into PHPantom), so a name that only a service provider registers stays unresolved rather than being guessed. A project class whose short name collides with a facade alias (e.g. an app's own Request in the current namespace) still wins, since the alias table is only consulted after namespace-aware resolution misses.
  • model-property<T> pseudo-type recognition. The Larastan model-property<Model> type no longer triggers "unknown class" diagnostics. It is treated as a string subtype.
  • compact() strings are linked to local variables. A string argument to compact('user') is now treated as a reference to the matching local variable. Renaming the variable updates the string (and renaming from the string updates the variable and its other uses), find-references includes the string, and go-to-definition on the string jumps to the variable's assignment. Contributed by @calebdw in #159.
  • Imported and same-namespace symbols rank first in completion. Classes, functions, and constants that are already imported via a use statement or live in the same namespace now always appear above non-imported symbols in the completion list, regardless of dependency provenance. Previously a non-imported project class could outrank an already-imported vendor class, forcing users to scroll past irrelevant results. Contributed by @calebdw.
  • Laravel route controller method navigation and completion. Method-name strings inside Route::controller(X::class)->group(fn(){…}) closures now resolve as references to the controller's methods. Go-to-definition, find-references, rename, hover, and diagnostics all work on the action string (e.g. Route::patch('cancel', 'cancel') resolves 'cancel' to WorkItemController::cancel()). Autocompletion inside the action string offers the controller's methods. Handles ->controller() anywhere in the fluent chain, chained route calls (->name(), etc.), and nested groups where an inner ->controller() shadows the outer one. Contributed by @calebdw.
  • Package provenance displayed in hover. Hovering over a class, method, property, constant, or function now shows a colored badge indicating where the symbol comes from: 🟢 for direct Composer dependencies (e.g. laravel/framework), 🟠 for transitive dependencies with an italic (transitive) marker, and 🟣 for PHP core/extension symbols. Project-local symbols show no badge. The package name is resolved from vendor/composer/installed.json. Closes #228. Contributed by @calebdw.
  • Diagnostic ignore rules in .phpantom.toml. A new [[diagnostics.ignore]] config section suppresses matching diagnostics project-wide, similar to PHPStan's ignoreErrors. Each rule can constrain by file path (glob), message (regex), and/or diagnostic code, so a project can silence known-noisy paths (test fixtures, vendored code with unavailable stubs) without editor-only @phpantom-ignore comments scattered through the codebase.
  • Built-in formatter respects mago.toml. When formatting falls back to the embedded formatter, a mago.toml at the workspace root is now honoured, applying its [formatter] preset and settings instead of the PER-CS 2.0 defaults. Contributed by @enwi in #233.
  • Rename updates $param in conditional return types. Renaming a function parameter now also renames references to that parameter inside PHPDoc conditional return type annotations (@return ($param is true ? T : U)), including nested conditionals. Previously the @param tag and function body were updated but the @return conditional was left stale. Contributed by @calebdw.
  • @param-closure-this support in hover, go-to-definition, and go-to-type-definition. Hovering on $this inside a closure whose enclosing call site declares @param-closure-this now shows the overridden type instead of the lexically enclosing class. Go-to-definition and go-to-type-definition on $this likewise jump to the overridden class declaration. Previously only completion resolved the override. Contributed by @calebdw.
  • Path-repository packages included in PSR-4 mappings. Local Composer packages installed via path repositories (e.g. internachi/modular modules) are now discovered from vendor/composer/installed.json and their PSR-4 autoload entries are included in the project's namespace mappings. This fixes macro scanning, class resolution, and future namespace validation for modular Laravel projects where application code lives outside the root composer.json's own PSR-4 directories. Only packages whose files live outside vendor/ (symlinked in from a module directory such as app-modules/) count as project source; a path repository that resolves back inside vendor/ is treated as an ordinary dependency, so it is indexed for type resolution but not analyzed as your own code. Contributed by @calebdw.
  • Provenance badges for external path-repository packages. Symlinked path-repository packages whose resolved path is outside the workspace root now correctly show the package name badge in hover instead of being silently treated as project code. Path-repo packages inside the workspace (e.g. modular app modules) continue to show no badge. Contributed by @calebdw.

Changed

  • Laravel analysis runs only in Laravel projects. Eloquent model member synthesis, query-builder forwarding, the contract-to-concrete bindings, and the framework class patches are now gated on the project depending on Laravel or a standalone Illuminate component. Projects that use neither skip that work entirely during indexing and type resolution, so they index faster and never pay for Laravel-specific scanning.
  • More responsive editing. File parsing and diagnostics run in the background, so completion and hover no longer stall behind a full-file parse while you type. Contributed by @MingJen in #118.
  • Faster repeat completions. Member completion results are reused between keystrokes, so refining a completion by typing more characters returns instantly. Contributed by @MingJen in #118.
  • No first-access delay on Eloquent completions. Common Laravel builder types are prepared at startup, eliminating the pause the first time you complete on a query builder. Contributed by @MingJen in #118.
  • Faster code actions. The lightbulb menu now appears more quickly, since all refactorings share a single parse of the file instead of re-parsing it for each one.
  • Lower memory use while indexing. Scanning a workspace for classes, functions, and constants now reads files through the operating system's page cache instead of copying each one into memory, reducing peak memory when indexing large projects and their vendor trees.

Fixed

  • Deeply nested code no longer crashes the analyzer or editor. Files with very deeply nested expressions, such as the codec tables in WordPress' bundled getID3 library, could abort the whole analyze run (or the language server) with a stack overflow while parsing. Parsing and analysis threads now run with enough stack headroom to handle them.
  • Large procedural files no longer stall analysis. Analyzing a large legacy class that builds up array state across hundreds of conditional branches took minutes per file, long enough to look like a hang, and the same blowup could stall hover and completion in the editor. Methods without a declared return type now have their return type inferred from the body once per request instead of once per call site. The worst observed file went from over two minutes to under a second.
  • Array keys written in only one branch survive the merge. When an if/else writes different keys into the same array variable, the branches now merge into a single shape that keeps every key, marking keys set in only one branch as optional (array{a: int, b?: string}). Previously later writes continued from just one branch's shape, silently dropping the keys tracked in the other, and each branch carried its own shape variant, which made merges increasingly expensive in branch-heavy methods.
  • Laravel date helpers respect Date::use() / Date::useClass(). The configured date class is discovered from project service providers through the Date facade or DateFactory, so now(), today(), Date facade calls, and DateFactory calls resolve to the actual generated type (for example Carbon\CarbonImmutable) rather than the framework's broad CarbonInterface declaration or default Illuminate\Support\Carbon. Variable inference, return diagnostics, and inferred hover returns preserve nullable results such as CarbonImmutable|null; the native Laravel declaration remains visible in hover. Early requests wait for date discovery rather than inferring a stale default class. Adding, changing, or removing the Date::use() call in a provider updates the resolution during the same editing session, and a Date::use() call in a file that is not a registered provider never overrides the project's real configuration.
  • A conditional @return type is evaluated at the call site even when it is declared on an interface. A method whose PHPStan conditional return type narrows a literal-array argument (as in Spatie LaravelData's Data::collect([...]), which yields array<static> for an array) now resolves to that narrowed branch instead of the method's broad declared union. This holds when the conditional is declared on an interface while the concrete method comes from a trait, and the narrowed branch now supersedes the union rather than being discarded (which previously also dropped the union's array member). Member access, hover, and the return- and property-type mismatch diagnostics all see the precise type, eliminating false "incompatible with declared type array<Foo>" reports.
  • new ReflectionClass($class) resolves instances to the reflected type. When the argument is a class-string<T>, newInstance() and newInstanceArgs() now resolve to the object type T (nullable for newInstanceArgs) instead of the class-string, so returning $reflection->newInstanceArgs(...) from a method declared to return that object type no longer reports a false return-type mismatch. More generally, a docblock type now refines a native union that mixes object with a scalar (such as the object|string hint many reflection stubs carry), where it was previously discarded.
  • A property assigned from a function of itself no longer crashes analysis. A self-referencing assignment such as $this->items = array_unique(array_merge($this->items, $more)), where the property is read on its own right-hand side, previously sent type resolution into unbounded recursion that overflowed the stack and aborted the whole analyze run. The property now resolves to its declared type instead.
  • Method-call chains no longer report a spurious unresolved type on one branch but not an adjacent identical one. A variable assigned from a call whose return type is inferred from the callee's body (for example an Eloquent query builder returned by an un-annotated query() method) kept its type across the whole method, so every $query->whereBetween(...)->pluck(...) chain resolves consistently. Previously the receiver's type could be dropped for one branch while an identically shaped adjacent branch resolved cleanly, producing an intermittent "type could not be resolved" warning.
  • A variable destructured from an untyped array can be narrowed by a later assertion. When list-destructuring pulls variables out of a value whose type is unknown ([$type, $variable] = $declarations[0] where $declarations is a bare array), a following assertInstanceOf(Wanted::class, $type) now narrows $type to the asserted class, so member access on it resolves instead of reporting the type as unresolvable. A plain assignment from the same untyped value already worked; only the destructuring form left the variables unnarrowable.
  • assertInstanceOf narrows when the expected class is held in a variable. Passing a variable that holds a ::class value as the first argument ($cls = Wanted::class; assertInstanceOf($cls, $subject)) now narrows the subject the same way the inlined Wanted::class literal does, including when the variable is assigned inside a loop or other braced block or list-destructured out of the array a foreach iterates ([$a, $b, $cls] = $expected;). Previously only the inlined literal narrowed, so the loop-based PHPUnit data-provider pattern left the subject's type unresolved.
  • Narrowing guards apply to array-index subjects. An assertInstanceOf(Wanted::class, $arr['key']) assertion and an is_a($arr['key'], Foo::class, true) class-string guard now narrow the indexed element ($arr['key'], $arr[0]) just like they do for a plain variable, so a following member access resolves and the narrowed class-string satisfies a class-string<Foo> parameter. Previously the narrowing was silently dropped when the subject was an array-index expression, leaving the element's type unresolved.
  • A for loop's init-clause variable resolves in the condition and update clauses. A variable assigned in the init clause of a for loop (for ($p = $e->getPrevious(); $p; $p = $p->getPrevious())) now has its type available in the condition and update expressions on the same for line, so member access there resolves instead of reporting the type as unresolvable. Previously only the loop body saw the variable.
  • A closure parameter's declared type is kept when the collection's element type is a partial union. Passing a closure to a method like filter() on a subject that is a union of differently parameterized collections (Collection<CanApply>|Collection<ViewModel>|Collection<stdClass>) no longer collapses the closure parameter to the first collection's element type. When the parameter declares its own union (function (CanApply|ViewModel|stdClass $item)), that declared type is preserved, so member access inside the closure body resolves against every arm instead of falsely reporting a property missing on the first one.
  • Callable return templates bind from an unannotated closure's typed parameters. A template parameter that appears only in a callable argument's return position (as in a collection's reduce(), declared @param callable(TCarry, TValue): TReturn with @return TReturn) now resolves when the closure has no explicit return type but its body's type follows from its own parameter hints. $items->reduce(fn(Decimal $carry, $op) => $carry->add($op->getPrice()), new Decimal('0')) resolves to Decimal, so member access on the result completes, navigates, and type-checks.
  • Indexing an array with a dynamic key resolves the element type. Accessing an array shape with a variable key ($prices[$priceToUse]) now resolves to the union of the shape's value types, so member access on the result completes, navigates, and type-checks. Writes through a dynamic key are tracked too: a map built in a loop ($sums[$id] = $this->getStructure()) reads back element-by-element, and a nested write mixing literal and dynamic keys ($return['data'][$count]['earnings'] = $price) can be read back through the same key path.
  • Analysis results no longer vary between runs of the same project. Two timing-dependent flaws could make type resolution silently fail depending on which files were analyzed together: a thread waiting for another thread's in-progress parse of the same file gave up after a fixed timeout and cached the class as nonexistent for the rest of the session, and looking up a built-in constant re-registered the built-in functions defined alongside it without their signature corrections, losing array_map's template parameters. Both mainly surfaced as closure parameters that wouldn't resolve (array_map(fn($e) => $e->value, ...) reporting the type of $e as unresolvable) in full-project analyze runs and in long editor sessions, while analyzing the same file alone worked. Full-project diagnostics are now identical across repeated runs.
  • An array<T>|false return keeps its element type after a false check. A function typed array<int, User>|false (native array|false refined by a docblock) now retains the array's element type, so after if (!is_array($result)) return; or if ($result === false) return; the surviving array iterates to the declared element instead of losing it. Previously only the |null variant worked; the |false union dropped the docblock entirely, leaving the foreach value unresolved.
  • Leading-backslash global function calls resolve in member chains. A helper call written with an explicit global-namespace prefix, such as \response()->json(...), now resolves its return type the same way the unqualified response()->json(...) does, so member access, completion, and navigation on the result work.
  • A guard that reassigns one path keeps the narrowed type on the other. When a variable holds a partially-known type (a class or enum combined with an unresolved value) and a guard like if (!$type instanceof Country) { $type = Country::ADMIN; } reassigns only the failing path, the variable now correctly resolves to the narrowed type after the guard. Previously the unresolved component caused the narrowed fall-through type to be dropped, leaving the variable with no type, so member access on it reported the type as unresolvable.
  • Conditional return types with generic static<...> branches keep their type arguments. A method whose @return is a PHPStan conditional (($flag is true ? static<int, static> : static<int, static<int, TValue>>)) now resolves through to the fully substituted type instead of collapsing to a bare class name. This most visibly affected Laravel's Collection::chunk(), where iterating the result (foreach ($items->chunk(500) as $batch)) gave $batch no resolvable type; the nested collection element now resolves so member access on it completes, navigates, and type-checks.
  • Conditional return types whose selected branch is mixed stay usable. A call whose conditional @return resolves to mixed (such as Laravel's session($key) with ($key is string ? mixed : null)) now gives the value the type mixed instead of leaving it untyped. The value can then be narrowed as usual, so a later is_string()/instanceof guard refines it rather than being ignored, which removes a false-positive "expects string, got null" argument error after such a guard. Out-of-order named arguments in a conditional call (map(source: $x, signature: Foo::class)) also now bind to the parameter they name, so the branch keyed on that parameter resolves correctly regardless of argument order.
  • Fluent chains through a trait's return $this keep the using class. A trait method that returns $this without a declared return type now resolves to the class that uses the trait, so a chained call continues with that class's own members (its other traits, properties, and methods) instead of narrowing to the trait after the first call. This most visibly affected fluent test-assertion helpers, where every step past the first ->assert...() call reported unknown members.
  • Inline test fixtures no longer trigger PSR-4 mismatch warnings. Files that mix a top-level test call (e.g. Pest's it(...)/describe(...)) with an inline enum, trait, class, or interface now skip the namespace and filename mismatch diagnostics, so helper fixtures embedded in test files no longer produce noisy PSR-4 warnings. Regular single-class PSR-4 files, including ones with ordinary top-level statements like if guards, still report normally. Contributed by @calebdw.
  • Pull-diagnostic editors no longer show native diagnostics twice. When an editor supports pull diagnostics, PHPantom now delivers native diagnostics through the pull path only, while still refreshing quickly after the fast pass. This keeps namespace, class-name, and other native diagnostics responsive without duplicating them in clients that keep pushed and pulled diagnostics separate. Contributed by @calebdw.
  • Extract Variable no longer appears on declarations. The Extract variable refactor is now only offered inside executable function and method bodies, so selecting a class or trait name no longer suggests introducing a meaningless local variable. Contributed by @calebdw.
  • @phpstan-require-extends gives $this the base class's members inside a trait. A trait annotated @phpstan-require-extends Base can now use Base's methods, properties, and constants on $this when the trait is analyzed on its own, so completion, hover, member access, and go-to-definition work on those members instead of reporting the type as unresolvable. Previously this only worked when viewing the code through a concrete class that used the trait.
  • Parenthesized return types resolve instead of being dropped. A method annotated with a grouped @return type such as (Foo&object{pivot: Bar})|null now resolves correctly. Previously any @return starting with ( was mistaken for a conditional return type and discarded, so the method silently inherited an ancestor's raw template parameter. This most visibly affected Eloquent relations: chaining off $model->relation()->first() now resolves to the related model rather than an unresolvable type.
  • Indexing a call result inline resolves the element type. Chaining off an indexed method or function call ($node->findChildrenOfType(Attr::class)[0]->getParent()) now resolves the element type instead of breaking the chain, so member access on it completes, navigates, and type-checks. When the call is declared @return T[] with a class-string<T> argument, the element is inferred from the argument at the call site. Enum cases() results resolve too: Status::cases()[0]->value knows the element is the enum, since cases() returns a list of the enum's own instances.
  • Member-existence guards prove the member exists. Accessing a member inside a branch guarded by property_exists($obj, 'name'), method_exists($obj, 'name'), or isset($obj->name) is no longer reported as an unknown member, matching how PHPStan treats the guard as proof for the rest of that branch. The proof holds in if statements and ternary conditions alike, through && chains and negated guard clauses that return early, and is confined to the guarded branch, so the same access elsewhere still reports.
  • assertTrue/assertFalse prove their wrapped condition. A check wrapped in assertTrue(...) or assertFalse(...) (any method carrying @phpstan-assert true/false $condition, as PHPUnit's do) now narrows exactly like the equivalent if guard, because the assertion re-exports its inner condition. assertTrue(property_exists($model, 'value')) proves the property for the rest of the scope, and assertFalse($x instanceof Foo) excludes Foo from $x's type.
  • PHPUnit's assertIs* / assertIsNot* narrow to the asserted type. An assertion of a scalar or pseudo-type (@phpstan-assert string/int/float/bool/object/array/callable/numeric/scalar $x, as assertIsString, assertIsObject, assertIsArray, and the rest carry) now narrows the value like the matching is_*() guard, and the assertIsNot* negations exclude that type from a union. In particular, asserting a value is an object lets subsequent member access resolve instead of being flagged as unresolved.
  • A class_exists() guard keeps a variable's concrete class-string type. A variable typed class-string<Foo> (via @var or @param) that then passes through a class_exists($var) guard clause (if (!class_exists($var)) { throw; }) now keeps its <Foo> type argument instead of being widened to a bare class-string. As a result new $var() still resolves to Foo, so member access on the resulting object continues to work.
  • Each if/elseif branch narrows a property path to its own type. A property or array-element path ($args[0]->value) narrowed by instanceof in one branch no longer leaks that type into a later elseif branch that narrows the same path to a different type. Member access in the second branch now resolves against the second branch's type instead of falsely reporting a missing member from the first branch's type.
  • instanceof narrows a parameter inside an arrow-function body. In fn($x) => $x instanceof Foo && $x->method(), the parameter $x narrowed by the first && conjunct is now visible to the member access in a later conjunct, so completion, hover, and member access resolve against Foo instead of reporting the type as unresolvable. This matches how the same && narrowing already worked outside arrow functions.
  • Generic inference binds through call-expression arguments. A @template parameter constrained by array<T> now binds when the argument is a method or function call whose return type is an array (first(self::getEmailConfigs())), not only when it is a variable or an array literal. Closure parameters that share a name with an outer variable now shadow it unconditionally, so they no longer silently borrow the outer variable's type.
  • array_map and array_filter type their callback parameter. A closure passed to array_map or array_filter now has its parameter inferred from the array's element type, including when the array is itself a method or function call (array_map(fn($node) => $node->getImage(), $obj->getChildren())), so member access inside the callback resolves.
  • A templated helper with a class-string default resolves when called with no arguments. A container-style accessor declared @template T of object, @param class-string<T> $name, @return T with a Foo::class parameter default now binds T from that default when called with no arguments, so $app = app() resolves to the default class exactly as app(Foo::class) binds to Foo. Member access on the result completes, navigates, and type-checks instead of reporting the type as unresolvable.
  • Class-string unions carry through a foreach over an array-literal variable. Iterating a variable assigned a list of ::class constants ($repos = [A::class, B::class]; foreach ($repos as $r)) now resolves each element to its class, so a call like app()->make($r) binds its class-string<T> template to the union and the chained call resolves.
  • foreach over SPL iterators resolves the element type. Iterating an SPL iterator whose generics carry a third inner-iterator argument (@extends FilterIterator<int, SplFileInfo, ...> or @var AppendIterator<int, SplFileInfo, ...>) now types the value variable as the middle value type (SplFileInfo) instead of the inner iterator. Iterating a directly-constructed SPL iterator (foreach (new DirectoryIterator($dir) as $file)) also resolves the value type through the class's current() method, so members like $file->isFile() and $file->getRealPath() complete, navigate, and type-check.
  • An inline @var before a foreach refines a broad iterable variable. A /** @var iterable<Foo> $items */ placed just before foreach ($items as $item) now types the loop variable even when $items already carried a broad type such as mixed (common for mixed closure or function parameters) or a bare array. Previously the broad type occupied the variable and the annotation was ignored, so member access on the loop variable reported the type as unresolvable. The same works when the iterable is a method chain (foreach ($users->active() as $u)): a @var naming the base variable types it for the loop, whether the variable was previously untyped, broadly typed, or deliberately overridden.
  • compact() with an array argument counts its variables as used. Variables named inside an array passed to compact() (compact(['a', 'b']), including nested arrays) are no longer falsely reported as unused or undefined. Rename, find-references, and go-to-definition also work on the names inside the array, matching the existing behaviour for direct string arguments.
  • A variable used only as a dynamic member name is no longer reported unused. A variable read solely as the method or property selector in a dynamic access ($obj->{$name}(), $obj?->{$name}, Cls::{$name}()) now counts as used, so it is no longer flagged by the unused-variable diagnostic.
  • Values typed as a Laravel contract resolve through their concrete class. Calling a method on a value type-hinted as a core Illuminate contract (such as the view contract) no longer reports a false "method not found" for methods the framework's default concrete handles dynamically (macros and other __call-dispatched calls). The concrete is bound to the contract, so its members are visible for completion and hover and its magic-method dispatch suppresses the spurious diagnostic.
  • Eloquent relations resolve regardless of the case used to access them. Accessing a relationship as a property with different casing than the method declaration ($order->orderproducts for an orderProducts() relation) now resolves the same relationship for hover, completion, chaining, and diagnostics. This matches Laravel's runtime behaviour, where the magic accessor resolves relations through a case-insensitive method lookup, so a differently-cased access is no longer reported as an unknown member.
  • $this inside an anonymous class resolves to that class. Members accessed on $this within an anonymous class's own methods (return new class { function get() { return $this->value; } }) now resolve against the anonymous class instead of the class whose method contains the new class { ... } expression, so its properties and methods no longer report as unknown.
  • Array literals that look like callables are no longer flagged as bad method calls. A two-element array such as [Foo::class, 'name'] or [$object, 'name'] is only a callable when it is actually used as one, but plain data often takes the same shape (a list of [class, label] pairs, or an array passed as data to array_filter). Diagnostics no longer report the second element as a missing method in these cases, eliminating false "method not found" errors on ordinary data. Go-to-definition, find-references, and rename on genuine array callables still work.
  • A class named after a built-in resolves to the project's version. Inside a namespace, new Iterator() (and other unqualified class references) now resolves to a same-namespace class of that name before falling back to the global PHP class of the same short name, matching how PHP itself resolves names. Previously a project class such as App\Input\Iterator lost to the global SPL \Iterator, so every member on the instance was reported as unknown. An explicit use import still takes precedence.
  • Conditional return types are evaluated at call sites, even nested in a generic. A PHPStan conditional type embedded in a method's generic return (as on Laravel's Collection::groupBy/keyBy) is now collapsed against the call arguments, so the resulting collection carries a concrete key type. Calling a method on that result ($grouped->get('id')) no longer reports a spurious argument-type error printing the raw conditional. When a conditional's subject is an expression rather than a literal (such as $subject in Str::replace(..., $obj->toHtml())), the argument's resolved type selects the branch; when the type genuinely cannot be determined, both branches are kept as a union instead of committing to the wrong one.
  • isset() and empty() guard their own access. Checking isset($obj->prop) or empty($obj->prop) no longer reports the property as unknown or unresolved, even when the subject's type is a union that includes stdClass. Neither construct ever errors at runtime when the member doesn't exist, so flagging them was always a false positive.
  • A method returning object or ?object allows member access on its result. Accessing a property or method on the result of a call whose return type is object (or the nullable ?object) is now treated as the "any object" escape hatch it is, so $repo->all()->projects no longer reports the subject type as unresolvable. Nullability no longer discards the object type.
  • Assigning an object to a property tracks that property's type. After $settings->cache = new stdClass(), reading $settings->cache resolves to stdClass, so a further access like $settings->cache->ttl no longer reports the subject as unresolvable. This makes nested object graphs built up field by field (a common stdClass configuration pattern) resolve for hover, completion, and diagnostics. Assigning null to a property tracks it as null too, but a later not-null assertion (assertNotNull($obj->prop), @phpstan-assert !null) now clears that tracked null, so member access after the assertion is not falsely flagged as access on null.
  • A type guard trusts the runtime check over an incomplete static type. When is_object($x) (or is_string(), is_array(), and similar checks) succeeds but $x's inferred type didn't account for that possibility (for example a foreach element under-inferred from a custom iterator), the guarded branch now takes the guard's asserted type instead of keeping the stale one. This clears spurious "cannot access property/method on scalar" warnings inside these guards.
  • is_a($value, Class::class, true) and class_exists($value) narrow a string to class-string. With the allow_string argument, is_a() accepts a class-name string as well as an object, and now narrows accordingly to class-string<Class> rather than an object instance. class_exists(), interface_exists(), enum_exists(), and trait_exists() narrow to the generic class-string. This also narrows through guard clauses (if (!is_a(...)) { throw ...; }).
  • is_numeric() on a string narrows to numeric-string, not a bare number. The narrowed type previously dropped the string possibility entirely, so passing the checked value on to a string parameter reported a spurious mismatch.
  • A bare truthy check strips null from the checked variable. if ($value) { ... } now removes null (and false) from a nullable type inside the branch, matching the existing behavior of isset() and !== null checks.
  • Type-guard narrowing survives compound conditions and non-variable subjects. instanceof and assert narrowing now holds beyond a single negated-variable guard. It carries across && chains (a later conjunct and the body see an earlier conjunct's narrowing), through || guard clauses that narrow several distinct subjects at once, and applies to property paths, array-indexed elements ($stmts[0], $args[0]->value, $config['key']), and inline assignments in the condition (if (($node = expr()) instanceof Foo)). A @phpstan-assert on a property or indexed argument narrows later accesses to the same expression. This clears a large class of false "property/method not found" warnings in code that guards nested expressions, and the same narrowing now feeds completion and hover.
  • An assertInstanceOf with a variable class keeps the subject's type. When the asserted class is a runtime variable that cannot be resolved to a concrete class (static::assertInstanceOf($expectedClass, $node)), the assertion no longer erases the subject's type. It narrows to object intersected with the prior type, dropping null while keeping the class the subject already had, so a following member access such as $node->getImage() resolves instead of reporting a spurious unresolved-type warning.
  • array-key satisfies an int|string parameter. Passing a value typed array-key to a parameter expecting int|string no longer reports a spurious type mismatch. The two are equivalent, and the subtype check now treats them as such in both directions.
  • A class-string<A|B> value satisfies a class-string<T> template parameter. Passing a value typed class-string<A|B> to a generic parameter typed class-string<T> no longer reports a spurious mismatch. The whole union now binds the template rather than collapsing to its first member, and the value keeps its class-string wrapper.
  • A namespaced class name passed as a string literal resolves. A single-quoted class-string argument such as $repo->find('App\\Models\\User') now names the class App\Models\User, with the source backslash escape collapsed before lookup. Previously the doubled backslash was kept verbatim, so the generic result stayed unresolved and member access on it reported spurious unresolved-type warnings. Container lookups by class name (app('App\\Services\\Foo')) resolve the same way.
  • @see Class#method docblock references resolve the class. Legacy phpDocumentor fragment syntax (@see ASTNode#getMetadataSize) previously looked up the whole Class#method string as a single class name and reported it as unknown. The class and member are now split and validated independently, the same as the Class::method form.
  • @mixin of an Eloquent model exposes the model's synthesized members. A plain class annotated @mixin SomeModel now receives the model's virtual members (relationship properties, scope methods, cast-typed attributes, accessors), not just its declared ones. Accessing a relationship such as $cart->linkCampaign or $cart->items through the mixin resolves the same as it does on the model itself, so completion, hover, and member access work and no longer report spurious unresolved-member warnings.
  • @mixin of a template parameter resolves through its bound. A class annotated @mixin T where T is a @template T of SomeType parameter now exposes SomeType's public members, so member access, completion, and hover work on the wrapper class itself even when no concrete type is bound. When the mixin lives on a base class and a subclass tightens the constraint (AbstractNode<T of Node> extended by CallableNode<T of Callable>), members resolve through the most specific bound in the chain.
  • A method-level template bound to an array type resolves inside the method's own body. A @template T of SomeType[] parameter used as a pass-through (@param T $items / @return T) left T unresolved when accessed inside the method itself, so calls like end($items)->method() reported the member as unknown. Member access, hover, and completion on the parameter now resolve through the declared bound.
  • $this narrowed by assert() resolves inside closures with no enclosing class. In a top-level test closure (such as a Pest it(...) body), assert($this instanceof TestCase) now makes $this resolve to that class, so a value assigned from $this->method() carries the method's return type into the rest of the closure. Member access on those variables no longer reports spurious unresolved-type warnings. instanceof narrowing of $this to a subclass inside a regular method now resolves the subclass's members as well.
  • Values returned from a callback passed to a generic helper now resolve. When a method or function binds a template parameter from a closure's return type (@param \Closure(): T $callback, @return T), the result resolves even when the closure is an unannotated arrow function or block closure, inferring the type from the closure body. Laravel's Cache::remember($key, $ttl, fn() => new Order()) resolves to Order (as do rememberForever, sear, flexible, and withoutOverlapping), so property and method access on the cached value no longer reports spurious unresolved-member warnings.
  • Paginated Eloquent results carry their model type. Iterating Model::paginate(), simplePaginate(), or cursorPaginate() now resolves the loop variable to the model, so foreach (User::paginate() as $user) gives $user the concrete model type and member access on it resolves.
  • Storage::fake() resolves to the concrete filesystem adapter. Storage::fake() and Storage::persistentFake() now resolve to the FilesystemAdapter they actually return rather than the bare filesystem contract, so the assertion helpers used in tests (assertExists, assertMissing, and the rest) complete and resolve on the faked disk.
  • Static method calls see inherited and framework-corrected return types. A Class::method() call now resolves its return type through the class's full inheritance and interface chain, matching how instance calls already behaved. This clears cases where a static call to a method whose precise return type comes from a parent, an interface, or a framework type correction previously resolved to an imprecise type.
  • A project class sharing a global interface's short name no longer breaks subtype checks. When a file imports a project class whose short name matches a global interface (e.g. use App\Input\Iterator;), subtype checks against the global \Iterator or \Traversable kept working. Previously the import shadowed the global interface everywhere in the file, so passing an SPL iterator (RecursiveDirectoryIterator, GlobIterator, RecursiveIteratorIterator) to a parameter typed against the global interface reported a spurious argument type mismatch.
  • Indexing a positional array shape resolves the element type. Given /** @var array{Foo, Bar} $pair */, accessing $pair[0] now resolves to Foo and $pair[1] to Bar. Previously only string-keyed shapes (array{name: string}) resolved through bracket access; positional (tuple-style) shapes indexed with an integer literal reported an unresolved type. Shapes written across multiple lines resolve too, so a @var array{...} block whose entries are listed one per line works the same as a single-line one.
  • Class::class resolves to a class-string. The magic ::class constant now resolves to class-string<Class> instead of a plain string, so the class identity survives through assignments, array elements, and class-string<object> parameters.
  • Indexing an inferred tuple with a class-string fallback no longer widens to string. When a nested array literal is used as a fixed tuple (e.g. iterating [['int', $id], ['array', $list, Type::class]]), integer-literal indexing resolves the element at that position, and a $row[2] ?? Fallback::class expression keeps the value a class-string rather than collapsing to string. Passing the result to a class-string<object> parameter no longer reports a spurious type mismatch.
  • Method calls handled by __call / __callStatic are no longer flagged as unknown. When a class (or any branch of a union type) defines a magic call handler, an unrecognized method call is dispatched to it at runtime and is valid PHP, so it no longer produces a warning. This removes false positives on mock and fluent APIs (Mockery higher-order messages), dynamic query builders, and proxy objects. The call's chain type is still recovered from the magic method's return type, so subsequent links keep resolving.
  • A mock built with a test helper keeps the mocked class. $this->mock(Foo::class), partialMock(), and spy() now resolve to the intersection of Foo and the Mockery mock contract, matching Mockery::mock(). The mock therefore satisfies a parameter or array element typed Foo (so new Result([$this->mock(Rule::class)]) against an array<Rule> no longer reports a spurious mismatch), still passes to a method expecting the mocked class, and keeps resolving mock-expectation chains such as shouldReceive()->with().
  • Variables captured by reference in a closure are no longer flagged as unused. A local variable captured with use (&$var) and written inside the closure (e.g. an accumulator passed to array_walk) is now recognized as used, since the write propagates back to the outer scope through the reference.
  • Passing null to an implicitly-nullable parameter is no longer flagged. A parameter keeps its ability to accept null in two cases the type checker previously lost: when a docblock @param narrows a nullable native hint (a @param Foo[] over a native ?array still accepts null), and when the parameter has a literal null default (Type $x = null, the pre-8.4 implicit-nullable form). Calls passing null to such parameters no longer report a spurious "expects ..., got null" mismatch.
  • A string literal naming a class satisfies a class-string<Bound> parameter. Passing a quoted class name, such as $this->expectException('RuntimeException'), no longer reports a type mismatch when the named class satisfies the expected bound. The diagnostic still fires when the literal names a class that is provably unrelated to the bound.
  • Passing a class constant to a generic parameter infers the constant's value type. A call like static::assertSame(Command::INVALID, $exitCode) where INVALID is an untyped int constant now binds the template parameter to int instead of the constant's owning class, so it no longer reports a spurious "expects Command, got int" mismatch on the second argument.
  • Passing a class name to a class-string<T> generic parameter infers the class, not the string type. Calls like $this->assertInstanceOf('Iterator', $value) bind the template to the class the argument names, so the parameter no longer resolves to the nonsensical class-string<string>. A bare class-string value is likewise accepted, resolving the parameter to class-string<object> rather than reporting a spurious mismatch.
  • A union of class names passed to a generic class-string<T> parameter binds each member. Iterating a class-constant array (foreach ([Page::class, CustomPage::class] as $c)) and passing the loop variable to a @template T of Bound parameter typed class-string<T> now binds T to the union of the concrete classes, checking each against the bound through its full inheritance chain. The call no longer reports a spurious mismatch against the declared bound, and a @return T[] resolves to the union of the concrete element types rather than collapsing to the bound.
  • A ::class argument bound to a bare template parameter no longer reports a spurious mismatch. When a template is bound directly from the call-site argument (@param T $x with SomeClass::class), it now infers the argument's actual class-string<SomeClass> type instead of the bare class name, so the parameter is no longer compared as SomeClass against the very class-string<SomeClass> argument that bound it. This clears false positives on the common Mockery::type(SomeClass::class) pattern. Parameters that accept either a class name or an instance via a class-string<T>|T union, including the variadic array shape used by Mockery::mock(SomeClass::class), still bind T to the named class itself, so the returned value satisfies parameters typed with that class.
  • A generic helper call no longer borrows a type from an unrelated call site. When the same call text appears in two places with a differently-typed argument (e.g. $this->parse($stmt) in two methods where $stmt holds a different subtype), each call now resolves independently. Previously the type inferred at the first call site could leak to the second, producing a spurious argument type mismatch such as "expects ForStatement, got WhileStatement".
  • Iterating an object that implements Iterator directly now resolves the loop variable's type. Previously only IteratorAggregate and classes with an explicit generic annotation (@implements Iterator<Key, Value>) resolved a foreach loop variable's type; a class implementing Iterator itself fell through to unresolved. This most commonly affected SimpleXMLElement: foreach ($xml->children() as $child) now resolves $child to SimpleXMLElement, so $child->getName() and friends no longer report an unknown member.
  • The error-suppression operator (@) no longer blocks type resolution. A variable assigned from a suppressed expression, such as $xml = @simplexml_load_string($content);, now resolves to the underlying call's return type instead of being left unresolved. Member access on the variable no longer reports a false unresolved_member_access.
  • Assignments written inside a condition are now tracked. A variable assigned in an if or while condition is recognized as a definition, including the bare negated guard if (!$item = find()) { return; } and the call-wrapped form while (is_object($token = $iter->next())). The variable resolves in the guarded code and loop body instead of being reported as unresolved, clearing a bucket of false positives on $token-style tokenizer loops and early-return guards.
  • Short-circuit conditions narrow their later operands. Within a single || or && condition, an instanceof (or other guard) in one operand now narrows the variable for the operands that follow it. Because the right side of || runs only when the left is false, the common guard idiom if (!$x instanceof Foo || !$x->method()) { continue; } resolves $x->method() against Foo instead of reporting the method as unknown, and the && mirror ($x instanceof Foo && $x->method()) narrows the same way. Nested chains such as ... || ($x instanceof Foo && $x->method()) narrow correctly too. This clears a bucket of false positives on defensive guard code.
  • Assertion methods narrow types through inheritance. @phpstan-assert and @psalm-assert annotations now narrow the asserted variable no matter how the method is reached: through $this->, self::, static::, parent::, or a subclass name, not only when the call names the declaring class directly. This is the PHPUnit shape ($this->assertInstanceOf(Foo::class, $value), static::assertNotNull($value)), so member access and completion after an assertion in a test method now resolve to the asserted type instead of reporting the variable as unresolved. The exact-type prefix these annotations use (@phpstan-assert =Foo) is also parsed correctly, both for narrowing and so it no longer produces a bogus "unknown class" warning on the docblock. This clears a large bucket of false positives across PHPUnit-based test suites.
  • Symfony polyfill packages now classified as PHP core. Packages like symfony/polyfill-php83 that backport PHP core classes and extension functions (e.g. \Override) are now treated as core stubs instead of transitive vendor dependencies. This gives them the correct sort priority in completion and the correct provenance in hover. Contributed by @calebdw.
  • Misspelled members are no longer colored as valid code. Semantic highlighting now verifies that a member exists on the resolved class before coloring it as a method or property. A method-name string in an array callable like [Controller::class, 'sort'] keeps its plain string coloring when the method does not exist, and the same applies to static calls and $this accesses on unknown members. Classes with __call, __callStatic, or __get catch-alls keep their coloring, and members that do exist now also carry deprecated and static styling. Fixes #187.
  • Semantic highlighting no longer goes stale while typing. After an edit finishes parsing in the background, the server asks the editor to re-pull semantic tokens, so coloring reflects the current code instead of the state from before the edit.
  • namespace and use declarations keep the editor's own coloring. PHPantom no longer paints single tokens across the full import paths in regular PHP files, which visibly overrode the per-segment coloring of the editor's syntax grammar. Blade files still receive these tokens since no PHP grammar is active there.
  • Overloaded PHP functions no longer trigger false type_mismatch_argument diagnostics. Functions with multiple signatures like strtr(string, string, string) and strtr(string, array) now store alternate parameter lists. The type checker tries all overloads and only flags a mismatch when the call is incompatible with ALL signatures. Contributed by @calebdw.
  • iterator_to_array() now correctly returns an array type. Previously iterator_to_array($iter) where $iter was Iterator<Foo> resolved to Iterator<Foo> instead of array<Foo>, producing false type_mismatch_argument diagnostics when passed to array-typed parameters. Both key-value (Iterator<int, Foo> to array<int, Foo>) and value-only (Iterator<Foo> to list<Foo>) generic params are preserved. Contributed by @calebdw.
  • Reassigning a variable from its own array offset now updates the type. $value = $value[0] after $value held list<string>|false now correctly narrows $value to string. Previously the scalar element type was skipped during array-access resolution, leaving the variable with its old array type and producing false type_mismatch_argument diagnostics. Contributed by @calebdw.
  • @phpstan-type / @psalm-type aliases no longer trigger false type_mismatch_argument diagnostics. Local type aliases are expanded to their underlying types before argument compatibility checking, and an alias imported from another file is no longer mistaken for an unknown class. Contributed by @calebdw.
  • Reassigning a variable inside an if branch no longer leaks into later elseif conditions or the else branch. A variable changed in one branch is resolved against its pre-branch type in a following elseif condition, elseif body, or else body, so member access and argument checks there no longer report false diagnostics. Contributed by @calebdw.
  • Type narrowing works through the alternate if:/endif; syntax. A failed condition now narrows types in later elseif/else branches and in the scope after the block, matching the brace syntax. For example, after if ($x === null): $x = new Foo(); endif; written with colons, $x is now known to be non-null past endif;.
  • Ternary conditions narrow property and method-call subjects. An instanceof check in a ternary condition now narrows a property or method-call subject inside the branch, so $this->node instanceof Artifact ? $this->node->getCompilationUnit() : null resolves the then-branch instead of reporting the method as unknown. Because the branch resolves, the ternary's type is the union of both branches rather than collapsing to the else-branch, which also clears the cascading false positives that followed (member access reported on null). Previously only plain variable subjects narrowed in ternaries.
  • int<0,max> no longer triggers a false type_mismatch_argument against non-negative-int. Integer range types (int<min,max>) are now checked for subtype compatibility with refined-int pseudo-types (positive-int, negative-int, non-negative-int, non-positive-int) and vice versa. Range-to-range (int<1,50> <: int<0,100>) and cross-refined (positive-int <: non-negative-int) subtyping also work. Contributed by @calebdw.
  • Intersection types with extra members are no longer falsely flagged as type mismatches. A value whose type carries more intersection members than required now satisfies a narrower intersection, so argument type checks accept it. This clears false positives on the common mock pattern where Mockery::mock(Foo::class) (typed Foo&MockInterface&LegacyMockInterface) is passed to a parameter typed Foo&MockInterface.
  • A class named after a pseudo-type is no longer shadowed by it. A class whose name collides with a PHPDoc pseudo-type, most importantly PHP 8.4's BcMath\Number, resolves to the real class instead of the number pseudo-type. Members on such a value resolve, and passing it to a parameter of the same class type no longer raises a false type_mismatch_argument. Fixes #170.
  • Resource-to-object migrated handles no longer trigger false argument type errors. Functions whose handles became objects in PHP 8.1+ (finfo_open, imap_open, ftp_connect, ldap_connect, pg_connect, pg_query, pspell_new, and similar) now return the object type matching your configured PHP version instead of the legacy resource|false. Passing the handle on to finfo_file, imap_close, and the like no longer reports a spurious type_mismatch_argument. Fixes #164.
  • stream_bucket_make_writeable() results resolve on PHP versions before 8.4. The bucket object it returns accepts arbitrary properties at runtime (its class only formally exists from PHP 8.4 onward), so member access like $bucket->data inside a stream filter's filter() method no longer reports an unresolved-member warning on older configured PHP versions.
  • External formatters no longer corrupt the connection. Running php-cs-fixer or PHP_CodeSniffer for document formatting could kill the language server: the tool inherited the editor's input channel and consumed bytes meant for the server, dropping the connection and forcing a restart. Formatters now run with their input isolated. A timeout also names the tool that was too slow (php-cs-fixer timed out after 10000ms) so the culprit is clear, and the timeout can be raised with [formatting] timeout in .phpantom.toml. Fixes #149.
  • "Go to Declaration or Usages" from a declaration now lists usages. Invoking go-to-definition while the cursor is on a class, interface, trait, enum, or member declaration returns the symbol's usages instead of the declaration's own location. Editors that navigate straight to the definition result (such as PHPStorm) previously did nothing but move the cursor onto the name; they now jump to the usage or show the usage list. Fixes #125.
  • Generator closures now propagate template params through make()-style methods. When a closure containing yield expressions is passed to a method with a union param type like iterable<TKey, TValue>|(Closure(): Generator<TKey, TValue>), the yielded value and key types are inferred from the closure body (casts, literals) and used to bind the method's template parameters. This fixes LazyCollection::make(function() { yield (string) $x; }) resolving as LazyCollection<Closure, Closure> instead of LazyCollection<int, string>. Contributed by @calebdw.
  • Foreach key type resolves through a generic IteratorAggregate. When iterating a class that implements IteratorAggregate<non-empty-string, SplFileInfo>, the loop's key variable previously fell back to int|string; it now resolves to the declared key type, matching how the value type already resolved. Contributed by @calebdw in #216.
  • Generic<T>[] docblock types now correctly resolve to arrays. The [] array suffix was dropped when it followed a generic type (e.g. ReflectionAttribute<T>[]), a brace-delimited shape (e.g. array{id: int}[]), or a parenthesized group. The type tokenizer now consumes trailing [] suffixes before splitting on union/intersection operators, so these types parse correctly. Contributed by @calebdw in #215.
  • Edited functions and constants no longer go stale. Deleting or renaming a standalone function, or changing a define()/const value, is now reflected immediately in completion, hover, and go-to-definition. Previously a removed function kept being offered and jumped to a stale location, and editing a constant's value kept showing the old value, for the rest of the editing session.
  • Reloaded files no longer leave ghost classes. When a file loaded outside the editor (a vendor file, a bundled stub, or a file re-opened after being closed) is parsed again after its contents changed, a class that was renamed or removed no longer keeps resolving from its old definition. Go-to-implementation and type hierarchy stop listing classes that no longer extend a parent, and completion no longer surfaces the deleted class.
  • Integer literals now satisfy integer range parameter types. Argument diagnostics now treat literal integers as valid for int<min, max> and int<min..max> constraints when the value falls within the declared bounds. This fixes false positives like usleep(10_000) against int<0, max> and Laravel-style calls such as repeatEvery(1) against int<1, 59>. Contributed by @calebdw.
  • += on arrays now infers array instead of int|float. The compound assignment operator += is overloaded in PHP: it performs array union when both operands are arrays, but PHPantom was unconditionally treating it as numeric addition. The binary + operator already handled this correctly; the += path now mirrors that logic. Contributed by @calebdw in #214.
  • Diagnostics now update after function signature changes. Editing a standalone function's parameter or return type in one file (e.g. changing bar(null $x) to bar(string $x)) did not refresh diagnostics in other open files that call that function, so stale errors persisted until the editor was restarted. The server now tracks function signature changes (not just class signatures) and refreshes affected open files on save, without flashing false errors into unrelated buffers during editing. Same-file diagnostics continue to update on every keystroke. A textDocument/didSave handler was also added as a reliable refresh point for editors like Neovim. Fixes #123. Contributed by @calebdw in #196.
  • External tool diagnostics (PHPStan, PHPCS, Mago) now run on save only. Previously these expensive tools were scheduled on every keystroke with a debounce timer, which could block save-triggered runs and delay results by seconds. They now fire immediately when a file is opened or saved, with no debounce. External tool workers also send workspace/diagnostic/refresh in pull mode so editors see results without requiring a didChange event. Contributed by @calebdw in #196.
  • @phpstan-ignore with reasons now suppresses diagnostics immediately. Adding a @phpstan-ignore return.type (reason) comment did not clear the cached PHPStan diagnostic until PHPStan re-ran (~10 seconds). The stale diagnostic filter treated everything between @phpstan-ignore and */ as the identifier list, so the parenthesized reason text caused the match to fail. The parser now strips (reason) from each comma-separated entry before matching, correctly handling per-identifier reasons, multiple identifiers, and reason text containing commas. Contributed by @calebdw in #196.
  • Literal type matching in argument diagnostics. String, integer, and float literal arguments now match PHPDoc literal-union parameter types. For example, orderBy('id', 'desc') no longer produces a bogus error when the parameter is typed as 'asc'|'desc'. Conversely, passing a provably wrong literal (e.g. 'invalid' to 'asc'|'desc', or 'hello' to numeric-string) is now correctly flagged. Fixes #180. Contributed by @calebdw in #191.
  • String indexed assignment no longer widens type to array. Bracket-indexed assignment on a string variable ($str[0] = 'z') no longer changes the variable's type from string to array<int, string>. In PHP this operation modifies the string in-place, so the type is now correctly preserved. Contributed by @calebdw in #209.
  • mixed no longer behaves like a scalar in array access and member diagnostics. Accessing a key on an array<string, mixed> parameter (e.g. $body['key']) was incorrectly returning an empty type because mixed was treated like a scalar and skipped by the element-type extractor. This caused ternary expressions like true ? $body['key'] : null to resolve as null instead of mixed|null, producing false type-mismatch diagnostics. The same misclassification could also surface unverifiable-member warnings on values typed as mixed. Contributed by @calebdw in #210.
  • @see self::member() references in class docblocks now navigate correctly. Docblock @see tags already supported ClassName::member() references, but self::member() was being dropped during docblock symbol extraction, so go-to-definition could not follow it. Class docblocks can now refer to their own methods and members with self::... just like normal PHP code. Contributed by @calebdw in #212.
  • Grouped imports resolve correctly across diagnostics and navigation. Grouped use imports now work the same whether they are written on one line or split across multiple lines. Previously, multiline grouped imports could produce false unknown-class diagnostics because only single-line import declarations were skipped by the diagnostic walker, and go-to-definition on a class name inside a grouped use declaration could fail because the grouped item was recorded without its namespace prefix. Imported names inside grouped use declarations are now handled correctly for both unknown-class diagnostics and go-to-definition. Contributed by @calebdw in #213.
  • Laravel Conditionable::when() template inference no longer falls back to missing null defaults. Method template binding now avoids inferring template parameters from omitted null defaults except in the few cases where defaults are actually meaningful for template resolution. This fixes false type_mismatch_argument diagnostics on calls like when($request->integer(...), fn ($q, $id) => ...), where the callback parameter type was being collapsed to null instead of the concrete argument type. Contributed by @calebdw in #205.
  • declare(strict_types=1) detection. The LSP now reads the declare(strict_types=1) directive from the calling file and tightens argument type checking accordingly. Under strict types, implicit coercions that PHP normally allows in function calls, such as int/float to string and numeric-string to int/float, are flagged as type errors. The int-to-float exception is preserved, concatenation is unaffected, and literal numeric forms now retain their kind during checking instead of being flattened to plain strings. Contributed by @calebdw in #193.
  • Type Hierarchy works in more clients. The textDocument/prepareTypeHierarchy capability was registered without registration options, so some clients (notably Zed) did not reliably expose the Type Hierarchy action. The dynamic registration now carries proper TypeHierarchyRegistrationOptions with a PHP document selector, so those clients recognise that the feature applies to PHP files. Contributed by @sidux in #179.
  • Extract method generates correct code for more selections. A variable that the selection reads before it first assigns (for example a parameter the extracted code both consults and updates) is now passed in as an argument as well as returned, instead of being left undefined inside the new method. And an early return whose value references a variable defined inside the selection is now kept inside the extracted method and propagated to the caller, instead of being copied to the call site where that variable does not exist.
  • The editor stays responsive during fast typing in large files. Editors send a burst of requests on every keystroke (completion, a documentation lookup for each suggestion, diagnostics, code lens, semantic highlighting, and more). The server processed only a few at a time, so during continuous typing the burst backed up until the server stopped answering anything at all, including the completion the user was waiting on, and it only recovered after a restart. Now the burst is processed concurrently and every expensive request runs off the main loop: diagnostics (which re-analyze the whole file on each edit) compute in the background instead of on the request that asked for them, so a diagnostic pull returns immediately and never blocks the threads that deliver completion and hover, and repeated whole-file requests (semantic highlighting, code lens, the document outline, folding, document links) are collapsed so a fast typist's superseded requests no longer pile up and monopolize the CPU. Completion and other requests keep coming back while you type.
  • Typing in a large file no longer pegs the CPU and stalls completion. Semantic highlighting recomputed every token's position by rescanning the file from the beginning, so a large file took many seconds at full CPU to highlight. Editors request highlighting on every keystroke, so this ran continuously while typing and starved completion, hover, and other requests until they appeared to hang. Highlighting a large file is now effectively instant, and the same speedup applies to the document outline and code folding, which used the same per-position rescan.
  • The first use of a global helper function no longer stalls. Functions defined in Composer "files" autoload entries and guarded by if (! function_exists(...)) (such as Laravel's app(), session(), and route()) were parsed on demand the first time one was used, which meant the first completion, hover, or go-to-definition involving such a helper blocked while the server parsed every autoload file in turn. These files are now parsed up front during indexing, so the first lookup is instant.
  • Framework global helpers loaded outside Composer autoload are now indexed. Some frameworks ship their global function aliases in a *_global.php file that sits beside an autoloaded helper file but is pulled in by the framework's own bootstrap rather than Composer's files autoload, so it never appears in the autoload manifest. CakePHP is the canonical case: helpers like __(), h(), and env() live in such a sibling and were reported as unknown functions on every call. These sibling helper files are now indexed too, so the globals resolve. Contributed by @dereuromark in #175.
  • Classes defined inside conditional blocks are now fully resolved. A class declared inside an if/else version guard (the Doctrine ServiceEntityRepository pattern, where a base class is defined differently per ORM version) was previously discovered by name only, so its parent and @extends generics were dropped. Such classes now carry their full inheritance, so member completion, hover, go-to-definition, and generic type resolution work both on them and inside their own methods. When the same class name appears in more than one branch, the first declaration wins. Contributed by @MrSrsen in #154.
  • Editing a base class stays responsive in large projects. Changing a class that many others extend used to invalidate the resolved-class cache by rescanning every cached class on each edit, which briefly stalled large projects with deep class hierarchies. Invalidation now touches only the classes that actually depend on the edited one.
  • Completion latency stays flat during sustained fast typing. Concurrent requests resolving the same classes contended on a single lock guarding the resolved-class cache, so completion latency crept upward for as long as a typing burst continued. The cache now allows parallel reads, so the many lookups in flight at once no longer serialize behind one another, and when a request first loads a vendor class the work to record it in the shared index is prepared before the index is locked, so other requests no longer wait on it.
  • The server no longer freezes and stops responding. Editors cancel in-flight requests constantly (every cursor move supersedes the previous hover and highlight), and a burst of cancellations, such as when the editor regains focus after being in the background, could wedge the server so that it went completely silent and had to be restarted. Cancelled requests are now handled cleanly.
  • No hang on cyclic class inheritance. Editing a Laravel model that uses a custom Eloquent builder into a temporary state where two classes extend each other (which happens mid-refactor) no longer freezes completion, hover, and diagnostics for that file.
  • Returning to a backgrounded editor stays responsive. When an editor regains focus it re-reports every file in the workspace as changed in one large batch. Processing that batch could stall the server while it re-read thousands of files from disk. The batch is now handled off the main loop and skips files that were never loaded, so the editor stays responsive.
  • A rare internal parser error no longer permanently breaks a file. If analysis of a file hit an unexpected internal error, that file could become unresolvable for the rest of the session, with completion, hover, and go-to-definition silently returning nothing and each attempt stalling briefly. Such errors are now contained and the file recovers the next time it is used.
  • Inherited members no longer briefly flagged as unknown after opening a project. A method or property inherited from a vendor base class (for example the base methods of a framework controller) could be reported as an unknown member right after a file opened, even though hover resolved it correctly, and the error went away when the file was closed and reopened. Such members now resolve as soon as indexing finishes.
  • Named arguments are matched to parameters by name. Calls that pass arguments by name (f(c: 3)) are now bound to the parameters they actually target instead of by their position in the call. Conditional return types resolve correctly when the deciding argument is passed by name out of order, a "missing required argument" error is now reported when a named argument fills an optional parameter but leaves a required one unsupplied, and pass-by-reference type inference seeds the right variable.
  • Argument-count false positives. Extra arguments to a class with no constructor are no longer flagged (PHP accepts them), and namespaced calls to overloaded built-ins written with a leading backslash (\mt_rand()) are no longer measured against the wrong minimum. Immediately invoking the callable returned by a function or method (makeHandler($a, $b)($request)) now checks the inner call's arguments against the returned callable's own signature instead of the outer call's, fixing both false argument-count errors and wrong inlay hint parameter names on the invocation. Contributed by @calebdw in #191.
  • @var annotations no longer leak between functions. A /** @var T $x */ annotation in one function used to suppress "undefined variable" warnings for that name everywhere in the file; it is now scoped to the function it appears in.
  • PDO fetch methods reflect the fetch mode. PDOStatement::fetch() and fetchAll() now resolve to the type produced by the fetch-mode constant passed to them, so fetch(PDO::FETCH_OBJ) is an object, fetch(PDO::FETCH_ASSOC) is an associative array, and iterating fetchAll(PDO::FETCH_OBJ) yields objects. More generally, conditional return types keyed on a class constant (@return ($mode is Foo::BAR ? ... : ...)) are now evaluated at the call site.
  • Type resolution through chained and untyped access. Null-safe call chains such as $a->b?->c() resolve through the full receiver. Array access on a value of unknown type resolves to mixed, so $x = $arr['key'] ?? 5 no longer produces spurious type errors. foreach element types resolve through interfaces that reach a known iterable several hops away. Nested array-shape narrowing ($a["x"]["y"]) no longer targets the wrong key.
  • self references inside class-level attributes resolve. A self::, static::, or parent:: reference inside an attribute attached to a class (for example #[Route(name: self::ROUTE)]) is now resolved against the class it decorates, so the referenced constant or member is no longer reported as unresolvable.
  • @method tags override inherited methods of the same name. A @method annotation on a class now takes precedence over a method inherited from a more distant ancestor. The common repository pattern, where a base repository declares @method Entity|null findOneBy(...) while its vendor parent returns a generic object, now resolves to the concrete entity type, so members accessed on the result are no longer flagged as unverifiable.
  • ??= keeps the resolved type. After $x ??= new Foo(), the variable resolves to Foo (or the union of its existing non-null type and the assigned value), so property and method access on $x is no longer reported as unresolvable.
  • Generics with fewer arguments than parameters. @extends Collection<User> against Collection<TKey, TValue> now binds User to the value parameter, so inherited element types resolve correctly.
  • Nullable generic return types resolve through inheritance. A method whose native return hint is nullable (object|null) and whose docblock returns a template (@return ?T) now resolves to the bound type, so a repository's find() returns Entity|null instead of the bare object|null. Contributed by @MrSrsen in #152.
  • Conditional is null return types resolve consistently regardless of how the call site is parsed, and an explicitly passed null now selects the null branch.
  • Go-to-definition, rename, and highlight accuracy. References in @see tags to qualified names like App\Foo::bar() now land on the correct location, and renaming a property selects the whole $name instead of $nam.
  • @phpstan-require-extends and @phpstan-require-implements navigation. Class and interface names in these trait constraint tags now support go-to-definition and hover, and an import used only by such a tag is no longer flagged as unused. Contributed by @calebdw in #172.
  • Renaming variables captured by nested closures and arrow functions. Renaming or finding references to a variable used inside deeply nested arrow functions (fn () => fn () => $var) or closures with use ($var) now updates every occurrence, whether the rename is triggered on the declaration or from deep inside the nesting. Contributed by @calebdw in #145.
  • Variables inside dynamic property accesses are tracked. A variable used as a dynamic property selector ($message->{$attribute}) now counts as a use, so it is no longer wrongly reported as unused, find-references includes it, and renaming the variable updates the selector along with its other occurrences. Contributed by @calebdw in #174.
  • Member rename stays scoped to the declaration it targets. Renaming a method or property no longer touches same-named members on unrelated classes. A private method rename updates only that method and its real usages, calls on a receiver whose type cannot be resolved are left alone, renaming one implementation of an interface no longer renames sibling implementations, and renaming a child override stays on the child branch. Renaming a parent or interface declaration still updates the inherited overrides and implemented usages. Contributed by @calebdw in #160.
  • Find references on a constructor lists every call site. Finding references to a __construct declaration now reports the new ClassName(...) instantiations, #[ClassName(...)] attribute usages, and explicit delegation calls written as parent::__construct(), self::__construct(), or Class::__construct(), including for subclasses that inherit the constructor (and excluding subclasses that override it). Attribute classes that are never written as new are now found. Contributed by @RemcoSmitsDev in #155.
  • Positions on lines with multibyte characters. Signature help, go-to-definition on virtual properties, named-argument completion, unused-import removal, and the @phpstan-ignore quickfix placed cursors and edits at the wrong column on lines containing multibyte characters; they now use the correct UTF-16 columns. Type strings containing * wildcards or variance annotations are also no longer mangled.
  • Unused-import hint location. When two imports share a name prefix (use App\Foo; and use App\FooBar;), the "unused import" dimming now lands on the correct statement.
  • Document outline ranges. Methods, properties, constants, and functions in the outline and breadcrumbs now report a range covering the whole declaration, with the name nested inside, as editors expect for folding and breadcrumb extent.
  • Stale vendor symbols after composer update. Functions and constants removed from the vendor tree are now purged from the indexes, so completion and go-to-definition stop offering symbols that no longer exist.
  • Type hierarchy locates the class name even when the class keyword and the name are on separate lines.
  • Edits on Windows (CRLF) files land correctly. Rename, remove-unused-import, and the PHPStan return-type quickfix computed line offsets assuming single-byte line endings, so on files with \r\n terminators the edits drifted one byte per preceding line and could corrupt the file. Offsets now account for the real terminator.
  • Malformed @method tags no longer crash requests. A docblock with a degenerate @method signature (such as @method >()) could panic completion, hover, and go-to-definition. Such tags are now parsed gracefully and simply produce no virtual method.
  • Code lens navigation. Code lenses now work in Zed, Neovim, Emacs, and other editors. Previously the click command used a VS Code-specific API that other editors ignored.
  • @mixin with union types. @mixin Foo|Bar now correctly exposes members from all classes in the union. Previously only single-class mixins were recognized.
  • throw new and catch completion behave like new. Interfaces, abstract classes, traits, and enums are filtered out of throw new completion, which now offers only Throwable descendants, matching new. Completion inside catch() and @throws now applies the same ranking, FQN shortening via use statements, namespace drill-down, and deprecation styling as the other class-name completion contexts.
  • Analysis deadlock. Lazily-parsed vendor files acquired two internal locks in the opposite order from the editor's file-change handler, causing a deadlock when both ran concurrently.
  • External tool diagnostics on large files. PHPStan, Mago, and PHPCS diagnostics no longer time out on files that produce a large report. Their output is now read while the tool is still running, so a report bigger than the operating system's pipe buffer can no longer stall the tool and force a timeout.
  • Promote to constructor property. Promoting a parameter whose property is declared together with others on one line (private int $a, $b;) no longer deletes the sibling properties. The action is now offered only when the property is declared on its own.
  • get_defined_vars() counts as using every variable in scope. A function or method that calls get_defined_vars() (for example to build a debug dump) no longer reports its local variables as unused, since the call reads all of them. Variables local to a nested closure or arrow function are still checked. Contributed by @calebdw in #158.
  • Integer literals now satisfy named refined-int parameter types. A literal like 1 passed to a positive-int or non-negative-int parameter no longer produces a false type_mismatch_argument, matching the existing behaviour for int<min,max> ranges. Passing a literal that genuinely violates the refinement (e.g. 0 to positive-int, or a negative literal to non-negative-int) is now correctly flagged. non-zero-int and callable-string-family PHPDoc types, which previously failed to parse and were silently ignored by these checks, are now recognized as well.
  • PHPStan's __benevolent<T> wrapper type is recognized. A docblock type like @var __benevolent<Foo|null> now resolves as its inner type instead of reporting a false "class not found" on the wrapper.
  • Indexing an object implementing ArrayAccess resolves through offsetGet. $obj[$key] on a class implementing ArrayAccess now resolves to the value type declared in a generic annotation (@implements ArrayAccess<TKey, TValue>), falling back to offsetGet()'s own declared return type when no annotation is present, mirroring how foreach already fell back to Iterator::current(). This also fixes a class's own @template parameter resolving to its declared bound instead of leaking through as an unrelated type name when referenced directly in that same class's @implements/@extends annotations.
  • Reassigning a variable using its own previous value resolves the reference correctly. In $x = f(fn() => ..., $x), the $x read inside the right-hand side now resolves to its type before the reassignment rather than the reassignment's result, so a self-referencing statement like $items = implode(', ', array_map($fn, $items)) no longer reports a spurious argument type mismatch on the reused variable.
  • PHPDoc tags indented with extra spaces after the asterisk are honored. A tag written as * @param (two or more spaces between the asterisk and the tag, a common style in vendor code) was previously ignored entirely. Every such tag now parses the same as the single-space form, so @phpstan-type and @phpstan-import-type aliases are recognized rather than treated as class names, and @param, @return, and @var types written this way take effect. A parameter typed with an imported type alias no longer reports a spurious argument mismatch against the passed value, and the alias name is no longer flagged as an unknown class.
  • Mockery shouldHaveReceived() / shouldHaveBeenCalled() verification chains resolve. These are declared as returning self, but Mockery actually returns a verification director object that exposes with(), withArgs(), once(), and similar chained assertions. Chaining onto the result ($mock->shouldHaveReceived('store')->with(...)->once()) no longer reports the chained call as missing.
  • A leading-backslash type resolves to the global class even when a same-named class is imported. A variable typed \Redis (via @var or elsewhere) now resolves to the global \Redis class regardless of a use SomeNamespace\Redis; import that shares the short name, so its members complete, navigate, and type-check instead of resolving to the imported class.
  • HTML lists in docblock descriptions render on hover. Descriptions written with HTML markup, including bulleted and numbered lists, now appear as formatted Markdown in hover popups instead of showing raw tags or losing their structure entirely. Contributed by @calebdw.
  • Memory no longer grows for the whole session as files are closed. Closing a file now releases the parse errors held for it, so a long editing session that opens and closes many files no longer accumulates their state until restart.
  • Method completion no longer inserts a duplicate pair of parentheses. Typing a method name to completion and then typing ( yourself, instead of accepting the suggestion with Enter or Tab, no longer leaves behind an extra (). The suggestion already inserts the call's parentheses (and argument placeholders), so treating ( as a separate auto-accept trigger produced two pairs.

New Contributors

Full Changelog: 0.8.0...0.9.0