Skip to content

Commit 0691e18

Browse files
committed
Fix tracking and overwrite of simple array shapes
1 parent c42c216 commit 0691e18

13 files changed

Lines changed: 1384 additions & 104 deletions

docs/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4545

4646
### Fixed
4747

48+
- **`@var` docblock variable names included in variable name suggestions.** When a `/** @var Type $varName */` docblock declares a variable name, that name now appears in `$`-triggered variable name completions. Previously the variable name scanner only found variables from actual PHP assignments, parameters, foreach bindings, and catch clauses — docblock-only declarations were invisible. The AST statement walker now calls `find_inline_var_docblock` for each statement to extract any preceding `@var` annotation with an explicit variable name. Works in top-level code, inside method bodies, closures, and all control-flow blocks. Docblocks without a variable name (e.g. `/** @var string */`) do not inject phantom variables.
49+
- **`@var` without variable name for array access.** `/** @var array<int, Customer> */` followed by `$thing = []; $thing[0]->` now resolves the element type. Previously `find_iterable_raw_type_in_source` only matched annotations that explicitly named the variable (e.g. `/** @var array<int, Customer> $thing */`). The scanner now also matches no-variable-name `@var` annotations when the line immediately following the docblock is an assignment to the target variable. Works with `array<K, V>`, `list<V>`, `array<V>`, and all other generic iterable forms.
50+
- **Inferred element type from array literals.** `$thing = [new Customer()]; $thing[0]->` now resolves to `Customer`. When an array literal contains positional (non-keyed) entries like `new ClassName()`, the element types are inferred and combined into a `list<Type>` annotation. Previously only string-keyed entries (for array shapes) and push-style assignments (`$var[] = expr`) were tracked; positional entries were silently discarded. Mixed arrays with both keyed and positional entries infer the positional elements as `list<>` when no string keys are present.
51+
- **Inline array literal with index access.** `[Customer::first()][0]->` now resolves to the element type. Subject extraction recognises inline array literals (bracket pairs without a `$var` prefix) and produces a structured subject. The resolver splits the literal from the index segments, resolves each comma-separated element via call-chain or `new` expression resolution, and returns the element classes. The inline-literal handler runs before the enum-case/static-member check so that subjects containing `::` inside brackets are not misinterpreted.
52+
- **Inline array element function calls.** `end($customers)->`, `current($this->users)->`, and similar inline uses of array element-extracting functions (`end`, `current`, `reset`, `next`, `prev`, `array_pop`, `array_shift`) now resolve to the element type. Previously this only worked when the result was assigned to a variable (`$last = end($customers); $last->`). The resolver now detects known array functions in the inline call path, resolves the first argument's iterable type (supporting plain variables with docblock/assignment scanning, call chains like `Customer::get()->all()`, static calls, and property access), extracts the generic element type, and returns the corresponding class.
4853
- **Generator TSend inference inside nested control flow.** `$var = yield $expr` inside `while`, `if`, `foreach`, or other nested blocks now correctly resolves `$var` to the TSend type from the enclosing generator's `@return Generator<TKey, TValue, TSend, TReturn>` annotation. Previously `find_enclosing_return_type` was called with the cursor offset, so its backward brace scan stopped at the innermost block's `{` (e.g. the `if`'s opening brace) instead of reaching the function's opening brace. The call now uses the method/function body's opening brace offset directly from the AST, which is immune to intermediate control-flow braces. Yield-based TValue inference (`yield $var`) was already unaffected because the text-based body scan operates on the full function body.
4954
- **Callable parameter type parsing in union types.** `extract_callable_param_types` now handles callable signatures wrapped in unions like `(Closure(Builder<TModel>): mixed)|null`, `callable(Collection<int, TValue>, int): mixed|null`, and `null|callable(Order): bool`. Previously the parser treated the entire union as one token and failed to find the callable signature. The `split_type_token` tokenizer was also fixed to keep generic suffixes like `Collection<int, User>|null` and parenthesized groups like `(Closure(X): Y)|null` as single tokens instead of splitting at the closing `>`, `}`, or `)`. This enables closure parameter inference for Laravel methods like `Builder::whereHas()`, `BuildsQueries::chunk()`, and `Builder::with()`, where the callback parameter types use these union patterns.
5055
- **Blank lines inside method chains no longer break resolution.** A blank (or whitespace-only) line between segments of a fluent chain (e.g. `Brand::with('english')\n\n ->paginate()`) caused the backward walk in `collapse_continuation_lines` to stop prematurely, treating the empty line as the base expression. Neither completion nor go-to-definition worked in this case. The backward walk now skips blank lines, and the collapsed result omits them, so chains with cosmetic spacing resolve correctly.

docs/todo-laravel.md

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# PHPantom — Laravel Support: Remaining Work
22

3-
> Last updated: 2026-02-27
3+
> Last updated: 2026-02-28
44
55
This document tracks bugs, known gaps, and missing features in
66
PHPantom's Laravel Eloquent support. For the general architecture and
@@ -283,4 +283,90 @@ value type.
283283
**Priority:** Low. This is a convenience syntax and most users use
284284
closures instead. Requires synthesizing virtual properties on
285285
collection classes that return a proxy type parameterised with the
286-
collection's value type.
286+
collection's value type.
287+
288+
#### 11. `collect()` and other helper functions lose generic type info
289+
290+
Laravel's `collect()` helper is annotated with function-level
291+
`@template` parameters:
292+
293+
```php
294+
/**
295+
* @template TKey of array-key
296+
* @template TValue
297+
* @param array<TKey, TValue> $value
298+
* @return \Illuminate\Support\Collection<TKey, TValue>
299+
*/
300+
function collect($value = []) { ... }
301+
```
302+
303+
We correctly resolve the return type as `Collection`, but the
304+
generic arguments `TKey` and `TValue` are lost — the result is an
305+
unparameterised `Collection`, so `$users = collect($array)` followed
306+
by `$users->first()->` produces no completions for the element type.
307+
308+
**Root cause:** `FunctionInfo` has no `template_params` or
309+
`template_bindings` fields (unlike `MethodInfo`, which has both).
310+
The `synthesize_template_conditional` function only handles the
311+
narrow pattern `@return T` where `T` is a bare template param bound
312+
via `@param class-string<T>`. It does **not** handle `@return
313+
Collection<TKey, TValue>` where multiple template params appear
314+
inside a generic return type.
315+
316+
This affects every Laravel helper that uses function-level generics:
317+
`collect()`, `value()`, `retry()`, `tap()`, `with()`, `transform()`,
318+
`data_get()`, plus non-Laravel functions with the same pattern.
319+
320+
**Where to change:** Add `template_params: Vec<String>` and
321+
`template_bindings: Vec<(String, String)>` to `FunctionInfo` (mirror
322+
the existing fields on `MethodInfo`). Populate them in
323+
`parser/functions.rs` from `@template` and `@param` annotations.
324+
In `resolve_rhs_function_call` (in `variable_resolution.rs`), after
325+
loading the `FunctionInfo`, build a substitution map from template
326+
bindings → call-site argument types and apply it to the return type
327+
before passing it to `type_hint_to_classes`. See the general TODO
328+
item (§ PHP Language Feature Gaps, "Function-level `@template`
329+
generic resolution") for the full implementation plan.
330+
331+
**Priority:** Medium-high. `collect()` alone is used in virtually
332+
every Laravel codebase, and the loss of element types breaks
333+
completion chains on the resulting collection.
334+
335+
#### 12. `$this` in inferred callable parameter types resolves to wrong class
336+
337+
When a closure parameter is untyped and the inference system extracts
338+
callable param types from the called method's signature, `$this` in
339+
the extracted type resolves to the **calling class** (the class
340+
containing the user's code) instead of the class that declares the
341+
method.
342+
343+
```php
344+
// Builder::when() signature (from Conditionable trait):
345+
// @param callable($this, mixed): $this $callback
346+
347+
// In a controller:
348+
User::when($active, function ($query) {
349+
$query-> // $query inferred as Controller, not Builder<User>
350+
});
351+
```
352+
353+
The callable param types are extracted as raw strings by
354+
`extract_callable_param_types`. When `$this` appears in these
355+
strings, `resolve_closure_params_with_inferred` passes them to
356+
`type_hint_to_classes`, which resolves `$this` relative to
357+
`ctx.current_class` — the class the user is editing, not the class
358+
that owns the method.
359+
360+
In practice, most users type-hint the closure parameter explicitly
361+
(`function (Builder $query) { ... }`), which bypasses the inference
362+
entirely. The gap only manifests for untyped closure params.
363+
364+
**Where to change:** In `infer_callable_params_from_receiver` (and
365+
the static variant), after extracting callable param types, replace
366+
any literal `$this` or `static` tokens with the FQN of the receiver
367+
class before returning them. This ensures the inferred types
368+
reference the declaring class rather than the calling class.
369+
370+
**Priority:** Low. The explicit type-hint workaround is standard
371+
practice, and most IDE-aware codebases already type their closure
372+
parameters.

0 commit comments

Comments
 (0)