Skip to content

Commit cf02ef3

Browse files
committed
Implement generics
1 parent d6eb3a1 commit cf02ef3

8 files changed

Lines changed: 2299 additions & 14 deletions

File tree

example.php

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,110 @@ public function isSuccess(): bool
328328
}
329329
}
330330

331+
// ─── Generics (@template / @extends) ───────────────────────────────────────
332+
333+
/**
334+
* A generic repository — the base class declares template parameters
335+
* that child classes fill in with concrete types via @extends.
336+
*
337+
* @template T
338+
*/
339+
class Repository
340+
{
341+
/** @var T|null */
342+
protected $cached = null;
343+
344+
/** @return T */
345+
public function find(int $id)
346+
{
347+
return $this->cached;
348+
}
349+
350+
/** @return T|null */
351+
public function findOrNull(int $id)
352+
{
353+
return $this->cached;
354+
}
355+
356+
/** @return T */
357+
public function first()
358+
{
359+
return $this->cached;
360+
}
361+
}
362+
363+
/**
364+
* Concrete repository: @extends tells the engine that T = User.
365+
* All inherited methods now return User instead of the abstract T.
366+
*
367+
* @extends Repository<User>
368+
*/
369+
class UserRepository extends Repository
370+
{
371+
public function findByEmail(string $email): ?User
372+
{
373+
return null;
374+
}
375+
}
376+
377+
/**
378+
* A generic collection with two template parameters.
379+
*
380+
* @template TKey of array-key
381+
* @template-covariant TValue
382+
*/
383+
class TypedCollection
384+
{
385+
/** @var array<TKey, TValue> */
386+
protected array $items = [];
387+
388+
/** @return TValue */
389+
public function first() { return reset($this->items); }
390+
391+
/** @return ?TValue */
392+
public function last() { return end($this->items) ?: null; }
393+
394+
/** @return static */
395+
public function filter(callable $fn): static { return $this; }
396+
397+
/** @return int */
398+
public function count(): int { return count($this->items); }
399+
400+
/** @return array<TKey, TValue> */
401+
public function all(): array { return $this->items; }
402+
}
403+
404+
/**
405+
* A user collection — TKey = int, TValue = User.
406+
*
407+
* @extends TypedCollection<int, User>
408+
*/
409+
class UserCollection extends TypedCollection
410+
{
411+
public function adminsOnly(): self
412+
{
413+
return $this;
414+
}
415+
}
416+
417+
/**
418+
* Chained generics: this extends UserRepository, which extends
419+
* Repository<User>. Grandparent methods resolve through the chain.
420+
*/
421+
class CachingUserRepository extends UserRepository
422+
{
423+
public function clearCache(): void {}
424+
}
425+
426+
/**
427+
* Demonstrates @phpstan-extends (the PHPStan-prefixed variant).
428+
*
429+
* @phpstan-extends TypedCollection<string, Response>
430+
*/
431+
class ResponseCollection extends TypedCollection
432+
{
433+
}
434+
331435
// ─── Container (conditional return types) ───────────────────────────────────
332436

333437
class Container
@@ -693,3 +797,56 @@ function handleIntersection(User&Loggable $entity): void
693797

694798
$user = $users[0];
695799
$user->getEmail(); // assigned from array access → User
800+
801+
802+
// ═══════════════════════════════════════════════════════════════════════════
803+
// Generics — @template / @extends type resolution
804+
// ═══════════════════════════════════════════════════════════════════════════
805+
//
806+
// When a parent class declares @template parameters and a child class
807+
// provides concrete types via @extends, all inherited methods and
808+
// properties have their template types replaced with the real types.
809+
810+
811+
// ── Basic @extends — Repository<User> ───────────────────────────────────────
812+
813+
$repo = new UserRepository();
814+
$repo->find(1)->getEmail(); // find() returns T → User
815+
$repo->first()->getName(); // first() returns T → User
816+
$repo->findOrNull(1)?->getEmail(); // findOrNull() returns ?T → ?User
817+
$repo->findByEmail('a@b.c'); // own method still works
818+
819+
820+
// ── Two Template Parameters — TypedCollection<int, User> ────────────────────
821+
822+
$users = new UserCollection();
823+
$users->first()->getEmail(); // first() returns TValue → User
824+
$users->last()?->getName(); // last() returns ?TValue → ?User
825+
$users->adminsOnly(); // own method returns self
826+
$users->filter(fn($u) => true); // filter() returns static → UserCollection
827+
$users->count(); // count() returns int (non-template, unchanged)
828+
829+
830+
// ── Chained / Grandparent Generics ──────────────────────────────────────────
831+
832+
$cachingRepo = new CachingUserRepository();
833+
$cachingRepo->find(1)->getEmail(); // grandparent Repository<User>::find() → User
834+
$cachingRepo->first()->getName(); // grandparent first() → User
835+
$cachingRepo->clearCache(); // own method
836+
837+
838+
// ── @phpstan-extends Variant ────────────────────────────────────────────────
839+
840+
$responses = new ResponseCollection();
841+
$responses->first()->getStatusCode(); // first() returns TValue → Response
842+
$responses->last()?->getBody(); // last() returns ?TValue → ?Response
843+
844+
845+
// ── Property Type Substitution ──────────────────────────────────────────────
846+
// Inherited properties with template types are also substituted.
847+
848+
class PropertyDemo extends UserRepository {
849+
function test() {
850+
$this->cached->getEmail(); // $cached has type T → User
851+
}
852+
}

src/completion/resolver.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,9 @@ impl Backend {
234234
mixins: vec![],
235235
is_final: false,
236236
is_deprecated: false,
237+
template_params: vec![],
238+
extends_generics: vec![],
239+
implements_generics: vec![],
237240
};
238241
&dummy_class
239242
}
@@ -523,7 +526,12 @@ impl Backend {
523526
all_classes: &[ClassInfo],
524527
class_loader: &dyn Fn(&str) -> Option<ClassInfo>,
525528
) -> Vec<ClassInfo> {
526-
let prop = match class_info.properties.iter().find(|p| p.name == prop_name) {
529+
// Resolve inheritance so that inherited (and generic-substituted)
530+
// properties are visible. For example, if `ConfigWrapper extends
531+
// Wrapper<Config>` and `Wrapper` has `/** @var T */ public $value`,
532+
// the merged class will have `$value` with type `Config`.
533+
let merged = Self::resolve_class_with_inheritance(class_info, class_loader);
534+
let prop = match merged.properties.iter().find(|p| p.name == prop_name) {
527535
Some(p) => p,
528536
None => return vec![],
529537
};

src/docblock/mod.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,11 @@ pub(crate) mod types;
3636

3737
// Tags
3838
pub use tags::{
39-
extract_method_tags, extract_mixin_tags, extract_param_raw_type, extract_property_tags,
40-
extract_return_type, extract_type_assertions, extract_var_type, extract_var_type_with_name,
41-
find_inline_var_docblock, find_iterable_raw_type_in_source, find_var_raw_type_in_source,
42-
get_docblock_text_for_node, has_deprecated_tag, resolve_effective_type, should_override_type,
39+
extract_generics_tag, extract_method_tags, extract_mixin_tags, extract_param_raw_type,
40+
extract_property_tags, extract_return_type, extract_template_params, extract_type_assertions,
41+
extract_var_type, extract_var_type_with_name, find_inline_var_docblock,
42+
find_iterable_raw_type_in_source, find_var_raw_type_in_source, get_docblock_text_for_node,
43+
has_deprecated_tag, resolve_effective_type, should_override_type,
4344
};
4445

4546
// Conditional return types

0 commit comments

Comments
 (0)