Skip to content

Commit 08e5b94

Browse files
committed
Add support for @template on @method tags
Virtual methods declared via @method PHPDoc tags can now define their own template parameters using the <T of Bound> syntax (e.g. @method TVal get<TVal of mixed>(TVal $default)). Template inference at call sites works the same as for real methods. Includes tests and demo. Removes completed todo item T19.
1 parent f09af9c commit 08e5b94

8 files changed

Lines changed: 312 additions & 64 deletions

File tree

docs/CHANGELOG.md

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

1010
### Added
1111

12+
- **`@template` on `@method` tags.** Virtual methods declared via `@method` PHPDoc tags can now define their own template parameters using the `<T of Bound>` syntax (e.g. `@method TVal get<TVal of mixed>(TVal $default)`). Template inference at call sites works the same as for real methods.
1213
- **Laravel custom Eloquent builder support.** Models using the `#[UseEloquentBuilder]` attribute now have their custom builder's methods forwarded as static methods on the model. `query()`, `newQuery()`, and `newModelQuery()` return the custom builder type with correct generic model substitution. Contributed by @MingJen in https://github.com/AJenbo/phpantom_lsp/pull/118.
1314

1415
### Fixed

docs/todo.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ within the same impact tier.
2525

2626
| # | Item | Impact | Effort |
2727
| --- | --------------------------------------------------------------------------------------------------------------------- | ---------- | ------ |
28-
| T19 | [`@template` on `@method` tags](todo/type-inference.md#t19-template-on-method-tags) | High | Medium |
2928
| D10 | [PHPMD diagnostic proxy](todo/diagnostics.md#d10-phpmd-diagnostic-proxy) | Low | Medium |
3029
| A35 | [Convert to arrow function](todo/actions.md#a35-convert-to-arrow-function) (only non-void single-expression closures) | Low-Medium | Low |
3130
| C2 | [`#[ArrayShape]` return shapes on stub functions](todo/completion.md#c2-arrayshape-return-shapes-on-stub-functions) | Medium | Medium |

docs/todo/type-inference.md

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -686,40 +686,5 @@ blowup.
686686
`references/psalm/src/Psalm/Type.php`
687687

688688

689-
## T19. `@template` on `@method` tags
690-
691-
Virtual methods declared via `@method` PHPDoc tags cannot currently
692-
define their own template parameters. The `extract_method_tags`
693-
function in `src/docblock/virtual_members.rs` always sets
694-
`template_params: Vec::new()`.
695-
696-
This is important for Laravel Facade support, where the underlying
697-
accessor class has generic methods that are proxied through the
698-
Facade's `@method` tags:
699-
700-
```php
701-
/**
702-
* @method static TValue get<TValue>(string $key, TValue $default)
703-
*/
704-
class Config extends Facade {}
705-
```
706-
707-
PHPStan syntax: `@method <T> T get(class-string<T> $class)`
708-
709-
### Implementation
710-
711-
1. Parse method-level template params from `@method` tag syntax in
712-
`extract_method_tags` (`src/docblock/virtual_members.rs`).
713-
2. Store them in `MethodInfo::template_params` on the synthesized
714-
virtual method.
715-
3. Ensure call-site template inference (`resolve_call_return_types`)
716-
works with virtual methods the same way it does with real methods.
717-
718-
### References
719-
720-
- PHPStan: `MethodTagParser` at
721-
`references/phpstan-src/src/PhpDoc/Tag/MethodTagParser.php`
722-
- PHPStan rule tests:
723-
`references/phpstan-src/tests/PHPStan/Rules/Generics/MethodTagTemplateTypeRuleTest.php`
724689

725690

examples/demo.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3540,10 +3540,40 @@ function globalKeywordDemo(): void {
35403540
$globalPen->write(); // Pen — resolved from top-level scope via `global`
35413541
}
35423542

3543+
// ── Method-Tag Template ─────────────────────────────────────────────────────
3544+
3545+
class MethodTagTemplateDemo
3546+
{
3547+
public function demo(): void
3548+
{
3549+
// @method tags with <T of Bound> template params resolve at call sites.
3550+
$registry = new ScaffoldingMethodTagTemplate();
3551+
3552+
// TVal inferred from argument type
3553+
$pen = new Pen('demo');
3554+
$result = $registry->get($pen);
3555+
$result->write(); // TVal = Pen
3556+
3557+
// Inline chain
3558+
$registry->get(new Pencil())->sketch(); // TVal = Pencil
3559+
}
3560+
}
3561+
35433562
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
35443563
// ┃ SCAFFOLDING — Supporting definitions below this line. ┃
35453564

3546-
// ── Template-param @mixin scaffolding ───────────────────────────────────────
3565+
// ── Method-Tag Template scaffolding ────────────────────────────────────────────
3566+
3567+
/**
3568+
* @method TVal get<TVal of mixed>(TVal $default)
3569+
*/
3570+
class ScaffoldingMethodTagTemplate
3571+
{
3572+
/** @return mixed */
3573+
public function __call(string $name, array $args): mixed { return $args[0] ?? null; }
3574+
}
3575+
3576+
// ── Template-param @mixin scaffolding ─────────────────────────────────────────
35473577
interface ScaffoldingAstNodeInterface {
35483578
public function getStartColumn(): int;
35493579
public function getEndColumn(): int;

src/docblock/templates.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ pub fn extract_generics_tag(docblock: &str, tag: &str) -> Vec<(String, Vec<PhpTy
277277

278278
/// Recursively walk a [`PhpType`] tree and collect `(template_name, param_name)` pairs
279279
/// for every template parameter reference found anywhere in the type.
280-
fn collect_template_bindings(
280+
pub(crate) fn collect_template_bindings(
281281
ty: &PhpType,
282282
template_params: &[String],
283283
param_name: &str,

0 commit comments

Comments
 (0)