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/@mixinsynthesized 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/staticreturns 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 theirreturnstatements. Incompatible return values are flagged as errors. Void functions returning a value and barereturn;in non-void functions are also flagged. Generators (functions usingyield) are skipped. Uses the same conservativeis_type_compatiblepolicy as argument type checking to avoid false positives. Contributed by @calebdw. - Property type assignment diagnostics (
type_mismatch_property). Assignments to typed properties ($this->prop = exprandself::$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 andmixedproperties 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'smock(Foo::class)and Laravel's$this->mock(Foo::class)therefore resolve toFoo&MockInterface, so their members complete and assigning the result to aFoo-typed property or returning it from aFoo&MockInterfacemethod 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
useimports 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 fromcomposer.jsonandinstalled.jsonduring indexing. Contributed by @calebdw. analyzeandfixwork 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-rootis not silently analysed as a bare tree.updatecommand. A newphpantom_lsp updatesubcommand 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_mapinfers the output element type from its callback. The result ofarray_mapnow reflects what the callback actually returns instead of assuming the input element type is preserved. An explicit return type hint is honoured, including scalars likestringorint, soarray_map(fn(Item $item): string => $item->id, $items)produceslist<string>rather thanlist<Item>. When the callback has no return type hint, the type is inferred from its body expression, soarray_map(fn($item) => $item->id, $items)over alist<Item>also produceslist<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 asRoute::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 withClass::classconstants,$this, and typed variables. (thanks @calebdw) - Convert arrow function to closure. A new
refactor.rewritecode action converts arrow functions to anonymous closures (fn($x) => $x * 2tofunction($x) { return $x * 2; }). Variables from the outer scope are automatically captured via ause()clause. Preservesstaticand return type hints. Contributed by @calebdw in #191. @phpstan-sealedtag support. The@phpstan-sealed FooClass|BarClassPHPDoc 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. Whencomposer.jsonorcomposer.lockchanges (e.g. aftercomposer 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 likeparse_url,stat,pathinfo,gc_status,getimagesize, andsession_get_cookie_params.- Convert to arrow function. A new
refactor.rewritecode action converts single-expression closures to arrow functions (function($x) { return $x * 2; }tofn($x) => $x * 2). The action is only offered when the conversion is safe: single return statement, no by-referenceusecaptures, novoid/neverreturn type, and PHP >= 7.4. - Convert switch to match. A new
refactor.rewritecode action convertsswitchstatements tomatchexpressions when all arms are single-expression returns or assignments to the same variable. Handles fall-through cases (merged with commas), trailingbreakremoval, andthrowarms. Requires PHP >= 8.0. - Extract interface. A new
refactor.extractcode action generates an interface from a concrete class. All public method signatures (excluding the constructor) are extracted into a new{ClassName}Interface.phpfile in the same directory, and the class is updated withimplements {ClassName}Interface. Class-level and method-level@templatetags are preserved when referenced by extracted methods. @templateon@methodtags. Virtual methods declared via@methodPHPDoc 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(), andnewModelQuery()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,@propertytags, timestamps, etc.). - Authenticated user resolves to the configured model.
$request->user(),auth()->user(), andAuth::user()now resolve to the Eloquent model declared inconfig/auth.phpinstead of only the bareAuthenticatablecontract, so completion, hover, and member access work on the concrete model ($request->user()->email). Naming a guard selects that guard's model, soauth('admin')->user(),Auth::guard('admin')->user(), and$request->user('admin')resolve to the model configured for theadminguard rather than the default one. The config is read statically: only the literal default ofenv('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, ornew Foo(), and also recognizes typed variable registrations likeBuilder $queryfollowed by$query->macro(...), including inside callbacks such asfunction (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')andapp('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\Appand\DBresolve 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 ownRequestin the current namespace) still wins, since the alias table is only consulted after namespace-aware resolution misses. model-property<T>pseudo-type recognition. The Larastanmodel-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 tocompact('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
usestatement 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'toWorkItemController::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 fromvendor/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'signoreErrors. 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-ignorecomments scattered through the codebase. - Built-in formatter respects
mago.toml. When formatting falls back to the embedded formatter, amago.tomlat 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
$paramin 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@paramtag and function body were updated but the@returnconditional was left stale. Contributed by @calebdw. @param-closure-thissupport in hover, go-to-definition, and go-to-type-definition. Hovering on$thisinside a closure whose enclosing call site declares@param-closure-thisnow shows the overridden type instead of the lexically enclosing class. Go-to-definition and go-to-type-definition on$thislikewise 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/modularmodules) are now discovered fromvendor/composer/installed.jsonand 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 rootcomposer.json's own PSR-4 directories. Only packages whose files live outsidevendor/(symlinked in from a module directory such asapp-modules/) count as project source; a path repository that resolves back insidevendor/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
analyzerun (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/elsewrites 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 theDatefacade orDateFactory, sonow(),today(), Date facade calls, and DateFactory calls resolve to the actual generated type (for exampleCarbon\CarbonImmutable) rather than the framework's broadCarbonInterfacedeclaration or defaultIlluminate\Support\Carbon. Variable inference, return diagnostics, and inferred hover returns preserve nullable results such asCarbonImmutable|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 theDate::use()call in a provider updates the resolution during the same editing session, and aDate::use()call in a file that is not a registered provider never overrides the project's real configuration. - A conditional
@returntype 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'sData::collect([...]), which yieldsarray<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'sarraymember). Member access, hover, and the return- and property-type mismatch diagnostics all see the precise type, eliminating false "incompatible with declared typearray<Foo>" reports. new ReflectionClass($class)resolves instances to the reflected type. When the argument is aclass-string<T>,newInstance()andnewInstanceArgs()now resolve to the object typeT(nullable fornewInstanceArgs) 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 mixesobjectwith a scalar (such as theobject|stringhint 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 wholeanalyzerun. 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$declarationsis a barearray), a followingassertInstanceOf(Wanted::class, $type)now narrows$typeto 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. assertInstanceOfnarrows when the expected class is held in a variable. Passing a variable that holds a::classvalue as the first argument ($cls = Wanted::class; assertInstanceOf($cls, $subject)) now narrows the subject the same way the inlinedWanted::classliteral does, including when the variable is assigned inside a loop or other braced block or list-destructured out of the array aforeachiterates ([$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 anis_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 aclass-string<Foo>parameter. Previously the narrowing was silently dropped when the subject was an array-index expression, leaving the element's type unresolved. - A
forloop's init-clause variable resolves in the condition and update clauses. A variable assigned in the init clause of aforloop (for ($p = $e->getPrevious(); $p; $p = $p->getPrevious())) now has its type available in the condition and update expressions on the sameforline, 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): TReturnwith@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 toDecimal, 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$eas 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>|falsereturn keeps its element type after afalsecheck. A function typedarray<int, User>|false(nativearray|falserefined by a docblock) now retains the array's element type, so afterif (!is_array($result)) return;orif ($result === false) return;the surviving array iterates to the declared element instead of losing it. Previously only the|nullvariant worked; the|falseunion 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 unqualifiedresponse()->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@returnis 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'sCollection::chunk(), where iterating the result (foreach ($items->chunk(500) as $batch)) gave$batchno 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
mixedstay usable. A call whose conditional@returnresolves tomixed(such as Laravel'ssession($key)with($key is string ? mixed : null)) now gives the value the typemixedinstead of leaving it untyped. The value can then be narrowed as usual, so a lateris_string()/instanceofguard 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 $thiskeep the using class. A trait method that returns$thiswithout 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 likeifguards, 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 variablerefactor 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-extendsgives$thisthe base class's members inside a trait. A trait annotated@phpstan-require-extends Basecan now useBase's methods, properties, and constants on$thiswhen 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
@returntype such as(Foo&object{pivot: Bar})|nullnow resolves correctly. Previously any@returnstarting 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 aclass-string<T>argument, the element is inferred from the argument at the call site. Enumcases()results resolve too:Status::cases()[0]->valueknows the element is the enum, sincecases()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'), orisset($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 inifstatements 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/assertFalseprove their wrapped condition. A check wrapped inassertTrue(...)orassertFalse(...)(any method carrying@phpstan-assert true/false $condition, as PHPUnit's do) now narrows exactly like the equivalentifguard, because the assertion re-exports its inner condition.assertTrue(property_exists($model, 'value'))proves the property for the rest of the scope, andassertFalse($x instanceof Foo)excludesFoofrom$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, asassertIsString,assertIsObject,assertIsArray, and the rest carry) now narrows the value like the matchingis_*()guard, and theassertIsNot*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 typedclass-string<Foo>(via@varor@param) that then passes through aclass_exists($var)guard clause (if (!class_exists($var)) { throw; }) now keeps its<Foo>type argument instead of being widened to a bareclass-string. As a resultnew $var()still resolves toFoo, so member access on the resulting object continues to work. - Each
if/elseifbranch narrows a property path to its own type. A property or array-element path ($args[0]->value) narrowed byinstanceofin one branch no longer leaks that type into a laterelseifbranch 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. instanceofnarrows a parameter inside an arrow-function body. Infn($x) => $x instanceof Foo && $x->method(), the parameter$xnarrowed by the first&&conjunct is now visible to the member access in a later conjunct, so completion, hover, and member access resolve againstFooinstead 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
@templateparameter constrained byarray<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_mapandarray_filtertype their callback parameter. A closure passed toarray_maporarray_filternow 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-stringdefault resolves when called with no arguments. A container-style accessor declared@template T of object,@param class-string<T> $name,@return Twith aFoo::classparameter default now bindsTfrom that default when called with no arguments, so$app = app()resolves to the default class exactly asapp(Foo::class)binds toFoo. Member access on the result completes, navigates, and type-checks instead of reporting the type as unresolvable. - Class-string unions carry through a
foreachover an array-literal variable. Iterating a variable assigned a list of::classconstants ($repos = [A::class, B::class]; foreach ($repos as $r)) now resolves each element to its class, so a call likeapp()->make($r)binds itsclass-string<T>template to the union and the chained call resolves. foreachover 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'scurrent()method, so members like$file->isFile()and$file->getRealPath()complete, navigate, and type-check.- An inline
@varbefore aforeachrefines a broad iterable variable. A/** @var iterable<Foo> $items */placed just beforeforeach ($items as $item)now types the loop variable even when$itemsalready carried a broad type such asmixed(common formixedclosure or function parameters) or a barearray. 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@varnaming 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 tocompact()(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->orderproductsfor anorderProducts()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. $thisinside an anonymous class resolves to that class. Members accessed on$thiswithin 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 thenew 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 toarray_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 asApp\Input\Iteratorlost to the global SPL\Iterator, so every member on the instance was reported as unknown. An explicituseimport 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$subjectinStr::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()andempty()guard their own access. Checkingisset($obj->prop)orempty($obj->prop)no longer reports the property as unknown or unresolved, even when the subject's type is a union that includesstdClass. Neither construct ever errors at runtime when the member doesn't exist, so flagging them was always a false positive.- A method returning
objector?objectallows member access on its result. Accessing a property or method on the result of a call whose return type isobject(or the nullable?object) is now treated as the "any object" escape hatch it is, so$repo->all()->projectsno longer reports the subject type as unresolvable. Nullability no longer discards theobjecttype. - Assigning an object to a property tracks that property's type. After
$settings->cache = new stdClass(), reading$settings->cacheresolves tostdClass, so a further access like$settings->cache->ttlno longer reports the subject as unresolvable. This makes nested object graphs built up field by field (a commonstdClassconfiguration pattern) resolve for hover, completion, and diagnostics. Assigningnullto 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 onnull. - A type guard trusts the runtime check over an incomplete static type. When
is_object($x)(oris_string(),is_array(), and similar checks) succeeds but$x's inferred type didn't account for that possibility (for example aforeachelement 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)andclass_exists($value)narrow a string toclass-string. With theallow_stringargument,is_a()accepts a class-name string as well as an object, and now narrows accordingly toclass-string<Class>rather than an object instance.class_exists(),interface_exists(),enum_exists(), andtrait_exists()narrow to the genericclass-string. This also narrows through guard clauses (if (!is_a(...)) { throw ...; }).is_numeric()on a string narrows tonumeric-string, not a bare number. The narrowed type previously dropped thestringpossibility entirely, so passing the checked value on to astringparameter reported a spurious mismatch.- A bare truthy check strips
nullfrom the checked variable.if ($value) { ... }now removesnull(andfalse) from a nullable type inside the branch, matching the existing behavior ofisset()and!== nullchecks. - Type-guard narrowing survives compound conditions and non-variable subjects.
instanceofandassertnarrowing 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-asserton 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
assertInstanceOfwith 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 toobjectintersected with the prior type, droppingnullwhile 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-keysatisfies anint|stringparameter. Passing a value typedarray-keyto a parameter expectingint|stringno 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 aclass-string<T>template parameter. Passing a value typedclass-string<A|B>to a generic parameter typedclass-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 itsclass-stringwrapper. - 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 classApp\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#methoddocblock references resolve the class. Legacy phpDocumentor fragment syntax (@see ASTNode#getMetadataSize) previously looked up the wholeClass#methodstring as a single class name and reported it as unknown. The class and member are now split and validated independently, the same as theClass::methodform.@mixinof an Eloquent model exposes the model's synthesized members. A plain class annotated@mixin SomeModelnow 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->linkCampaignor$cart->itemsthrough 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.@mixinof a template parameter resolves through its bound. A class annotated@mixin TwhereTis a@template T of SomeTypeparameter now exposesSomeType'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 byCallableNode<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) leftTunresolved when accessed inside the method itself, so calls likeend($items)->method()reported the member as unknown. Member access, hover, and completion on the parameter now resolve through the declared bound. $thisnarrowed byassert()resolves inside closures with no enclosing class. In a top-level test closure (such as a Pestit(...)body),assert($this instanceof TestCase)now makes$thisresolve 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.instanceofnarrowing of$thisto 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'sCache::remember($key, $ttl, fn() => new Order())resolves toOrder(as dorememberForever,sear,flexible, andwithoutOverlapping), 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(), orcursorPaginate()now resolves the loop variable to the model, soforeach (User::paginate() as $user)gives$userthe concrete model type and member access on it resolves. Storage::fake()resolves to the concrete filesystem adapter.Storage::fake()andStorage::persistentFake()now resolve to theFilesystemAdapterthey 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\Iteratoror\Traversablekept 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 toFooand$pair[1]toBar. 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::classresolves to a class-string. The magic::classconstant now resolves toclass-string<Class>instead of a plainstring, so the class identity survives through assignments, array elements, andclass-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::classexpression keeps the value aclass-stringrather than collapsing tostring. Passing the result to aclass-string<object>parameter no longer reports a spurious type mismatch. - Method calls handled by
__call/__callStaticare 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(), andspy()now resolve to the intersection ofFooand the Mockery mock contract, matchingMockery::mock(). The mock therefore satisfies a parameter or array element typedFoo(sonew Result([$this->mock(Rule::class)])against anarray<Rule>no longer reports a spurious mismatch), still passes to a method expecting the mocked class, and keeps resolving mock-expectation chains such asshouldReceive()->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 toarray_walk) is now recognized as used, since the write propagates back to the outer scope through the reference. - Passing
nullto an implicitly-nullable parameter is no longer flagged. A parameter keeps its ability to acceptnullin two cases the type checker previously lost: when a docblock@paramnarrows a nullable native hint (a@param Foo[]over a native?arraystill acceptsnull), and when the parameter has a literalnulldefault (Type $x = null, the pre-8.4 implicit-nullable form). Calls passingnullto 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)whereINVALIDis an untypedintconstant now binds the template parameter tointinstead 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 nonsensicalclass-string<string>. A bareclass-stringvalue is likewise accepted, resolving the parameter toclass-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 Boundparameter typedclass-string<T>now bindsTto 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
::classargument 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 $xwithSomeClass::class), it now infers the argument's actualclass-string<SomeClass>type instead of the bare class name, so the parameter is no longer compared asSomeClassagainst the veryclass-string<SomeClass>argument that bound it. This clears false positives on the commonMockery::type(SomeClass::class)pattern. Parameters that accept either a class name or an instance via aclass-string<T>|Tunion, including the variadic array shape used byMockery::mock(SomeClass::class), still bindTto 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$stmtholds 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
Iteratordirectly now resolves the loop variable's type. Previously onlyIteratorAggregateand classes with an explicit generic annotation (@implements Iterator<Key, Value>) resolved aforeachloop variable's type; a class implementingIteratoritself fell through to unresolved. This most commonly affectedSimpleXMLElement:foreach ($xml->children() as $child)now resolves$childtoSimpleXMLElement, 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 falseunresolved_member_access. - Assignments written inside a condition are now tracked. A variable assigned in an
iforwhilecondition is recognized as a definition, including the bare negated guardif (!$item = find()) { return; }and the call-wrapped formwhile (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, aninstanceof(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 idiomif (!$x instanceof Foo || !$x->method()) { continue; }resolves$x->method()againstFooinstead 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-assertand@psalm-assertannotations 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-php83that 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$thisaccesses on unknown members. Classes with__call,__callStatic, or__getcatch-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.
namespaceandusedeclarations 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_argumentdiagnostics. Functions with multiple signatures likestrtr(string, string, string)andstrtr(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. Previouslyiterator_to_array($iter)where$iterwasIterator<Foo>resolved toIterator<Foo>instead ofarray<Foo>, producing falsetype_mismatch_argumentdiagnostics when passed to array-typed parameters. Both key-value (Iterator<int, Foo>toarray<int, Foo>) and value-only (Iterator<Foo>tolist<Foo>) generic params are preserved. Contributed by @calebdw.- Reassigning a variable from its own array offset now updates the type.
$value = $value[0]after$valueheldlist<string>|falsenow correctly narrows$valuetostring. Previously the scalar element type was skipped during array-access resolution, leaving the variable with its old array type and producing falsetype_mismatch_argumentdiagnostics. Contributed by @calebdw. @phpstan-type/@psalm-typealiases no longer trigger falsetype_mismatch_argumentdiagnostics. 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
ifbranch no longer leaks into laterelseifconditions or theelsebranch. A variable changed in one branch is resolved against its pre-branch type in a followingelseifcondition,elseifbody, orelsebody, 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 laterelseif/elsebranches and in the scope after the block, matching the brace syntax. For example, afterif ($x === null): $x = new Foo(); endif;written with colons,$xis now known to be non-null pastendif;. - Ternary conditions narrow property and method-call subjects. An
instanceofcheck in a ternary condition now narrows a property or method-call subject inside the branch, so$this->node instanceof Artifact ? $this->node->getCompilationUnit() : nullresolves 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 onnull). Previously only plain variable subjects narrowed in ternaries. int<0,max>no longer triggers a falsetype_mismatch_argumentagainstnon-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)(typedFoo&MockInterface&LegacyMockInterface) is passed to a parameter typedFoo&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 thenumberpseudo-type. Members on such a value resolve, and passing it to a parameter of the same class type no longer raises a falsetype_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 legacyresource|false. Passing the handle on tofinfo_file,imap_close, and the like no longer reports a spurioustype_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->datainside a stream filter'sfilter()method no longer reports an unresolved-member warning on older configured PHP versions.- External formatters no longer corrupt the connection. Running
php-cs-fixeror 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] timeoutin.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 containingyieldexpressions is passed to a method with a union param type likeiterable<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 fixesLazyCollection::make(function() { yield (string) $x; })resolving asLazyCollection<Closure, Closure>instead ofLazyCollection<int, string>. Contributed by @calebdw. - Foreach key type resolves through a generic
IteratorAggregate. When iterating a class that implementsIteratorAggregate<non-empty-string, SplFileInfo>, the loop's key variable previously fell back toint|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()/constvalue, 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>andint<min..max>constraints when the value falls within the declared bounds. This fixes false positives likeusleep(10_000)againstint<0, max>and Laravel-style calls such asrepeatEvery(1)againstint<1, 59>. Contributed by @calebdw. +=on arrays now infersarrayinstead ofint|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)tobar(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. AtextDocument/didSavehandler 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/refreshin pull mode so editors see results without requiring adidChangeevent. Contributed by @calebdw in #196. @phpstan-ignorewith 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-ignoreand*/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'tonumeric-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 fromstringtoarray<int, string>. In PHP this operation modifies the string in-place, so the type is now correctly preserved. Contributed by @calebdw in #209. mixedno longer behaves like a scalar in array access and member diagnostics. Accessing a key on anarray<string, mixed>parameter (e.g.$body['key']) was incorrectly returning an empty type becausemixedwas treated like a scalar and skipped by the element-type extractor. This caused ternary expressions liketrue ? $body['key'] : nullto resolve asnullinstead ofmixed|null, producing false type-mismatch diagnostics. The same misclassification could also surface unverifiable-member warnings on values typed asmixed. Contributed by @calebdw in #210.@see self::member()references in class docblocks now navigate correctly. Docblock@seetags already supportedClassName::member()references, butself::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 withself::...just like normal PHP code. Contributed by @calebdw in #212.- Grouped imports resolve correctly across diagnostics and navigation. Grouped
useimports 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 groupedusedeclaration could fail because the grouped item was recorded without its namespace prefix. Imported names inside groupedusedeclarations 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 missingnulldefaults. Method template binding now avoids inferring template parameters from omittednulldefaults except in the few cases where defaults are actually meaningful for template resolution. This fixes falsetype_mismatch_argumentdiagnostics on calls likewhen($request->integer(...), fn ($q, $id) => ...), where the callback parameter type was being collapsed tonullinstead of the concrete argument type. Contributed by @calebdw in #205. declare(strict_types=1)detection. The LSP now reads thedeclare(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/prepareTypeHierarchycapability was registered without registration options, so some clients (notably Zed) did not reliably expose the Type Hierarchy action. The dynamic registration now carries properTypeHierarchyRegistrationOptionswith 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
returnwhose 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'sapp(),session(), androute()) 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.phpfile that sits beside an autoloaded helper file but is pulled in by the framework's own bootstrap rather than Composer'sfilesautoload, so it never appears in the autoload manifest. CakePHP is the canonical case: helpers like__(),h(), andenv()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/elseversion guard (the DoctrineServiceEntityRepositorypattern, where a base class is defined differently per ORM version) was previously discovered by name only, so its parent and@extendsgenerics 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. @varannotations 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()andfetchAll()now resolve to the type produced by the fetch-mode constant passed to them, sofetch(PDO::FETCH_OBJ)is an object,fetch(PDO::FETCH_ASSOC)is an associative array, and iteratingfetchAll(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 tomixed, so$x = $arr['key'] ?? 5no longer produces spurious type errors.foreachelement 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. selfreferences inside class-level attributes resolve. Aself::,static::, orparent::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.@methodtags override inherited methods of the same name. A@methodannotation 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 genericobject, 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 toFoo(or the union of its existing non-null type and the assigned value), so property and method access on$xis no longer reported as unresolvable.- Generics with fewer arguments than parameters.
@extends Collection<User>againstCollection<TKey, TValue>now bindsUserto 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'sfind()returnsEntity|nullinstead of the bareobject|null. Contributed by @MrSrsen in #152. - Conditional
is nullreturn types resolve consistently regardless of how the call site is parsed, and an explicitly passednullnow selects the null branch. - Go-to-definition, rename, and highlight accuracy. References in
@seetags to qualified names likeApp\Foo::bar()now land on the correct location, and renaming a property selects the whole$nameinstead of$nam. @phpstan-require-extendsand@phpstan-require-implementsnavigation. 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 withuse ($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
__constructdeclaration now reports thenew ClassName(...)instantiations,#[ClassName(...)]attribute usages, and explicit delegation calls written asparent::__construct(),self::__construct(), orClass::__construct(), including for subclasses that inherit the constructor (and excluding subclasses that override it). Attribute classes that are never written asneware 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-ignorequickfix 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;anduse 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
classkeyword 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\nterminators the edits drifted one byte per preceding line and could corrupt the file. Offsets now account for the real terminator. - Malformed
@methodtags no longer crash requests. A docblock with a degenerate@methodsignature (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.
@mixinwith union types.@mixin Foo|Barnow correctly exposes members from all classes in the union. Previously only single-class mixins were recognized.throw newandcatchcompletion behave likenew. Interfaces, abstract classes, traits, and enums are filtered out ofthrow newcompletion, which now offers only Throwable descendants, matchingnew. Completion insidecatch()and@throwsnow 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 callsget_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
1passed to apositive-intornon-negative-intparameter no longer produces a falsetype_mismatch_argument, matching the existing behaviour forint<min,max>ranges. Passing a literal that genuinely violates the refinement (e.g.0topositive-int, or a negative literal tonon-negative-int) is now correctly flagged.non-zero-intandcallable-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
ArrayAccessresolves throughoffsetGet.$obj[$key]on a class implementingArrayAccessnow resolves to the value type declared in a generic annotation (@implements ArrayAccess<TKey, TValue>), falling back tooffsetGet()'s own declared return type when no annotation is present, mirroring howforeachalready fell back toIterator::current(). This also fixes a class's own@templateparameter resolving to its declared bound instead of leaking through as an unrelated type name when referenced directly in that same class's@implements/@extendsannotations. - Reassigning a variable using its own previous value resolves the reference correctly. In
$x = f(fn() => ..., $x), the$xread 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-typeand@phpstan-import-typealiases are recognized rather than treated as class names, and@param,@return, and@vartypes 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 returningself, but Mockery actually returns a verification director object that exposeswith(),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@varor elsewhere) now resolves to the global\Redisclass regardless of ause 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
- @delu999 made their first contribution in #136
- @tolik518 made their first contribution in #139
- @MrSrsen made their first contribution in #152
- @dereuromark made their first contribution in #175
- @sidux made their first contribution in #179
- @enwi made their first contribution in #233
- @carnage made their first contribution in #243
Full Changelog: 0.8.0...0.9.0