Skip to content

Commit ad8d5f9

Browse files
committed
Handle remaning issues fround by the PHPactor tests
1 parent a3c379d commit ad8d5f9

13 files changed

Lines changed: 357 additions & 195 deletions

File tree

docs/CHANGELOG.md

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

1010
### Added
1111

12+
- **Pipe operator (PHP 8.5).** `$input |> trim(...) |> createDate(...)` resolves through the chain, returning the last callable's return type. Completion works after assigning the pipe result to a variable.
13+
- **Pass-by-reference parameter type inference.** After calling a function that accepts a typed `&$var` parameter (e.g. `function foo(Baz &$bar)`), the variable acquires the parameter's type for subsequent completion.
14+
- **Generic `@phpstan-assert` with `class-string<T>`.** `@phpstan-assert T $value` combined with a `@template T` bound via `class-string<T>` now resolves the narrowed type from the call-site argument. For example, `Assert::assertInstanceOf(Foo::class, $obj)` narrows `$obj` to `Foo`.
15+
- **Interface template inheritance.** When a class implements an interface whose methods use `@template`, `@param class-string<T>`, or `@return T`, the implementing class's overridden methods now inherit the template parameters, bindings, conditional return types, and type assertions. Previously these were lost during interface merging.
1216
- **Invoked closure and arrow function return types.** `(fn(): Foo => new Foo())()` and `(function(): Bar { return new Bar(); })()` now resolve to the return type of the closure or arrow function. Previously these patterns produced no completions.
1317
- **`new $classStringVar` and `$classStringVar::method()`.** When a variable holds a class-string value (e.g. `$f = Foo::class`), `new $f` resolves to `Foo` and `$f::staticMethod()` resolves through `Foo`'s static members.
1418
- **`iterator_to_array()` element type.** `iterator_to_array($iter)` now resolves the element type from the iterator's generic annotation, so `$items[0]->` offers the correct completions.

docs/todo.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ with each step.
2525
| [Laravel](todo/laravel.md) | Model property gaps, relationship methods, type narrowing, custom builders |
2626
| [Blade](todo/blade.md) | Preprocessor, component support, cross-file view intelligence |
2727
| [Bug Fixes](todo/bugs.md) | Incorrect behaviour that should be fixed regardless of feature priority |
28-
| [Testing](todo/testing.md) | Ignored fixture tasks grouped by missing feature, with effort estimates |
2928
| [Configuration](todo/config.md) | Per-project `.phpantom.toml` file, PHP version override, diagnostic tool toggles, prompt-and-remember settings |
3029
| [Refactoring](todo/refactor.md) | Technical debt and cleanup tasks. Gate check between sprints: clear all items before starting the next sprint |
3130

docs/todo/testing.md

Lines changed: 0 additions & 95 deletions
This file was deleted.

docs/todo/type-inference.md

Lines changed: 0 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,6 @@ within the same impact tier.
1515

1616
---
1717

18-
## 1. Pipe operator (PHP 8.5)
19-
**Impact: High · Effort: Low**
20-
21-
PHP 8.5 introduced the pipe operator (`|>`):
22-
23-
```php
24-
$result = $input
25-
|> htmlspecialchars(...)
26-
|> strtoupper(...)
27-
|> fn($s) => "<b>$s</b>";
28-
```
29-
30-
The mago parser already produces `Expression::Pipe` nodes, and the
31-
closure resolution module walks into pipe sub-expressions to find
32-
closures. However, **no type resolution** is performed for the pipe
33-
result — the RHS callable's return type is never resolved, so
34-
`$result->` after a pipe chain produces no completions.
35-
36-
**Fix:** In `resolve_rhs_expression`, add a `Expression::Pipe` arm
37-
that resolves the RHS callable (function reference, closure, or
38-
arrow function) and returns its return type. For first-class
39-
callable syntax (`htmlspecialchars(...)`), reuse the existing
40-
`extract_first_class_callable_return_type` logic.
41-
42-
---
43-
4418
## 3. Parse and resolve `($param is T ? A : B)` return types
4519
**Impact: High · Effort: Medium**
4620

@@ -242,35 +216,6 @@ scopes, and parse the set-visibility modifier into a new
242216

243217
---
244218

245-
## 7. Narrow types of `&$var` parameters after function calls
246-
**Impact: Medium · Effort: Medium**
247-
248-
When a function takes a parameter by reference, the variable's type
249-
after the call should reflect what the function writes to it. PHPStan
250-
has `FunctionParameterOutTypeExtension` for this.
251-
252-
Key examples:
253-
254-
| Function | Parameter | Type after call |
255-
|---|---|---|
256-
| `preg_match($pattern, $subject, &$matches)` | `$matches` | Typed array shape based on the regex |
257-
| `preg_match_all($pattern, $subject, &$matches)` | `$matches` | Nested typed array based on the regex |
258-
| `parse_str($string, &$result)` | `$result` | `array<string, string>` |
259-
| `sscanf($string, $format, &...$vars)` | `$vars` | Types based on format specifiers |
260-
261-
The most impactful case is `preg_match` — PHPStan's
262-
`RegexArrayShapeMatcher` parses the regex pattern to produce a precise
263-
array shape for `$matches`, including named capture groups. A simpler
264-
first step would be to just type `$matches` as `array<int|string,
265-
string>` when passed to `preg_match`.
266-
267-
**Implementation:** When resolving a variable's type after a function
268-
call where the variable was passed by reference, look up the
269-
function's parameter annotations for `@param-out` tags (PHPStan/Psalm
270-
extension) or use a built-in map for known functions.
271-
272-
---
273-
274219
## 8. SPL iterator generic stubs
275220
**Impact: Medium · Effort: Medium**
276221

@@ -483,43 +428,6 @@ type:
483428

484429
---
485430

486-
## 20. Generic `@phpstan-assert` with `class-string<T>` parameter inference
487-
**Impact: Medium · Effort: Medium-High**
488-
489-
When a function declares `@phpstan-assert T $value` with a
490-
`@template T` bound via a `class-string<T>` parameter, the narrowed
491-
type should be inferred from the class-string argument at the call
492-
site. For example:
493-
494-
```php
495-
/**
496-
* @template T of object
497-
* @param class-string<T> $class
498-
* @phpstan-assert T $value
499-
*/
500-
function assertInstanceOf(string $class, mixed $value): void {}
501-
502-
assertInstanceOf(Foo::class, $x);
503-
$x->fooMethod(); // $x should be narrowed to Foo
504-
```
505-
506-
**Discovered via:** fixture conversion (phpstan_assert_generic).
507-
508-
---
509-
510-
## 25. `class-string<T>` on interface method not inherited
511-
**Impact: Medium · Effort: Medium**
512-
513-
When an interface method uses `class-string<T>` in its return type
514-
and a class implements that interface, the implementing class's method
515-
does not inherit the generic return type. The `class-string<T>`
516-
pattern works on the interface directly but is lost during
517-
inheritance merging.
518-
519-
**Discovered via:** fixture conversion (class_string_generic_interface).
520-
521-
---
522-
523431
## 28. `__invoke()` return type resolution
524432
**Impact: Low-Medium · Effort: Low**
525433

example.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2140,6 +2140,48 @@ public function store(): void {}
21402140
}
21412141

21422142

2143+
// ── Pass-by-Reference Parameter Type ────────────────────────────────────────
2144+
2145+
class PassByReferenceDemo
2146+
{
2147+
public function demo(): void
2148+
{
2149+
// When a function takes a typed &$var parameter, the variable
2150+
// acquires that type after the call.
2151+
initPen($pen);
2152+
$pen->write(); // $pen is now Pen
2153+
}
2154+
}
2155+
2156+
2157+
// ── Interface Template Inheritance ──────────────────────────────────────────
2158+
2159+
class InterfaceTemplateDemo
2160+
{
2161+
public function demo(): void
2162+
{
2163+
// When a class implements an interface with @template + class-string<T>,
2164+
// the implementing class inherits the template machinery.
2165+
$locator = new ScaffoldingEntityLocator();
2166+
$locator->find(Pen::class)->write(); // T resolves to Pen via class-string<T>
2167+
}
2168+
}
2169+
2170+
2171+
// ── Generic @phpstan-assert Narrowing ───────────────────────────────────────
2172+
2173+
class GenericAssertNarrowingDemo
2174+
{
2175+
public function demo(object $obj): void
2176+
{
2177+
// @phpstan-assert with @template + class-string<T> resolves
2178+
// the narrowed type from the call-site argument.
2179+
ScaffoldingAssert::assertInstanceOf(Pen::class, $obj);
2180+
$obj->write(); // $obj narrowed to Pen
2181+
}
2182+
}
2183+
2184+
21432185
// ═══════════════════════════════════════════════════════════════════════════
21442186
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
21452187
// ┃ SCAFFOLDING — Supporting definitions below this line. ┃
@@ -3333,6 +3375,51 @@ public static function isNotRock(mixed $value): bool
33333375
}
33343376
}
33353377

3378+
// ─── Pipe Operator / Pass-by-Reference / Interface Template / Generic Assert ─
3379+
3380+
function createPenFromString(string $input): Pen
3381+
{
3382+
return new Pen();
3383+
}
3384+
3385+
function initPen(?Pen &$pen): void
3386+
{
3387+
$pen = new Pen();
3388+
}
3389+
3390+
interface ScaffoldingEntityFinder
3391+
{
3392+
/**
3393+
* @template T
3394+
* @param class-string<T> $class
3395+
* @return T
3396+
*/
3397+
public function find(string $class): object;
3398+
}
3399+
3400+
class ScaffoldingEntityLocator implements ScaffoldingEntityFinder
3401+
{
3402+
public function find(string $class): object
3403+
{
3404+
return new $class();
3405+
}
3406+
}
3407+
3408+
class ScaffoldingAssert
3409+
{
3410+
/**
3411+
* @template ExpectedType of object
3412+
* @param class-string<ExpectedType> $expected
3413+
* @phpstan-assert ExpectedType $actual
3414+
*/
3415+
public static function assertInstanceOf(string $expected, object $actual): void
3416+
{
3417+
if (!$actual instanceof $expected) {
3418+
throw new \InvalidArgumentException('Type mismatch');
3419+
}
3420+
}
3421+
}
3422+
33363423
// ─── Multi-line @return & Broken Docblock Recovery ──────────────────────────
33373424

33383425
/**
@@ -4247,6 +4334,21 @@ function runDemoAssertions(): void
42474334
$children = $astNode->getChildren();
42484335
assert(is_array($children), 'AstNode::getChildren() must return array');
42494336

4337+
// ── Pass-by-reference parameter type ────────────────────────────────
4338+
$refPen = null;
4339+
initPen($refPen);
4340+
assert($refPen instanceof Pen, 'initPen(&$pen) must give $pen type Pen');
4341+
4342+
// ── Interface template inheritance (class-string<T>) ────────────────
4343+
$locator = new ScaffoldingEntityLocator();
4344+
$locatorResult = $locator->find(Pen::class);
4345+
assert($locatorResult instanceof Pen, 'ScaffoldingEntityLocator::find(Pen::class) must return Pen');
4346+
4347+
// ── Generic @phpstan-assert narrowing ────────────────────────────────
4348+
$assertObj = new Pen();
4349+
ScaffoldingAssert::assertInstanceOf(Pen::class, $assertObj);
4350+
assert($assertObj instanceof Pen, 'ScaffoldingAssert::assertInstanceOf(Pen::class, $obj) must narrow to Pen');
4351+
42504352
echo "All assertions passed.\n";
42514353
}
42524354

0 commit comments

Comments
 (0)