Skip to content

Commit 5eb9e6f

Browse files
authored
Internal improvements 8 (#31)
* Add DoctrineCollection and DoctrineCollectionInterface with type annotations; enhance ConfigTest and ScopeManagerTest for improved type checking and identify edge cases * Refactor DocblockExtractor to prioritize tag extraction for phpstan and psalm tags; enhance tests for parameter, return, and variable tags, and enhance root directory detection * Refactor type error assertions in tests for consistency and clarity - Updated type error assertions in various tests to ensure consistent formatting and clarity. - Added explicit line breaks for better readability in assertions. - Ensured that all type error messages are correctly formatted and aligned with expected types. - Improved the overall structure of test cases related to argument unpacking, by-reference parameters, and generics. * Enhance documentation with tooling annotation priority hierarchies for parameters, returns, and templates across multiple files * Enhance SpecialTypeResolver and StreamWrapper to support class trait use docblocks; add tests for generic traits and vendor isolation * Enhance generics documentation and tests for class, interface, and trait inheritance; add single-line inline trait use example * Fix formatting in GenericTraitsUseAnnotationTest and add newline at end of SingleLineInlineTraitUseService * Refactor User class and add HookedUser class with asymmetric visibility properties; update tests to validate property hooks
1 parent 5d74127 commit 5eb9e6f

87 files changed

Lines changed: 1500 additions & 310 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/advanced/how-it-works.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ Whenever your application loads a PHP file via `require`, `include`, or Composer
3838

3939
---
4040

41+
## Project Root Resolution & Web Server Independence
42+
43+
To ensure consistent configuration loading across CLI commands, test runners (Pest, PHPUnit), and production web servers (where `getcwd()` points to `public/`), `Config::getProjectRoot()` searches upwards from the library's directory to locate `vendor/autoload.php` or `composer.json`.
44+
45+
Once located, the project root path is memoized in static memory. All relative `include`, `exclude`, and `cache_dir` configuration globs resolve reliably against the true application root directory across all PHP SAPIs (`cli`, `fpm`, `frankenphp`, `swoole`).
46+
47+
---
48+
4149
## Stream Interception
4250

4351
TypePHP registers a custom stream wrapper for PHP's native `file://` protocol using `stream_wrapper_register()`.
@@ -83,6 +91,18 @@ If the file is included, TypePHP parses the source code into an AST using `nikic
8391

8492
---
8593

94+
## Tooling Annotation Normalization & Tag Priority Hierarchy
95+
96+
Third-party packages often define both broad IDE docblocks and strict tool-specific contracts on the same signature (such as `@param mixed $element` alongside `@phpstan-param T $element` in Doctrine Collections).
97+
98+
`DocblockExtractor` normalizes and evaluates tag definitions using a **3-Tier Priority System**:
99+
100+
1. **Tool-Specific Annotations Take Precedence:** `@phpstan-param` and `@psalm-param` override `@param`; `@phpstan-return` and `@psalm-return` override `@return`; `@phpstan-var` overrides `@var`.
101+
2. **Inherited Template Extraction:** Collects class, interface, and trait template mappings across all recognized variations (`@extends`, `@template-extends`, `@phpstan-extends`, `@psalm-extends`, `@implements`, `@template-implements`, `@use`, `@template-use`).
102+
3. **Variance Modifiers:** Extracts class-level `@template-covariant` and `@template-contravariant` tags to configure the runtime variance engine.
103+
104+
---
105+
86106
## Zero Line-Drift Formatting and Caching
87107

88108
A common issue with AST code injection is that adding new statements pushes subsequent code down, causing line numbers in error stack traces to drift.
@@ -226,3 +246,4 @@ TypePHP gives you granular control so you can choose where and when to pay the p
226246
* **Selective Path Whitelisting:** Type-check only mission-critical domain logic (`app/Domain/**`) while bypassing non-critical files completely.
227247
* **Granular Toggles:** Turn off array checking (`inline_vars.arrays => false`) or scalar checking (`inline_vars.scalars => false`) on high-frequency internal loops while maintaining strict parameter and return boundaries (`params => true`, `returns => true`).
228248
* **Environment Master Switch:** Disable TypePHP completely in environment builds (`enabled => false`) for 100% un-transformed, native PHP execution speed.
249+
```

docs/core-concepts/function-contracts.md

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,39 @@ registerUser(-5, 'Alice', 'admin');
3737
3838
---
3939

40+
## Tooling Annotation Priority Hierarchy (`@phpstan-*` > `@psalm-*` > `@*`)
41+
42+
Modern PHP packages and frameworks (such as **Doctrine Collections**, **Symfony**, and **Laravel**) frequently declare both broad IDE-fallback annotations and strict static analysis contracts on the exact same method signature:
43+
44+
```php
45+
/**
46+
* @param mixed $element // Broad fallback for standard IDEs
47+
* @phpstan-param positive-int $element // Refined contract for static analyzers
48+
*
49+
* @return mixed
50+
* @phpstan-return list<positive-int>
51+
*/
52+
public function add(mixed $element): mixed;
53+
```
54+
55+
When multiple tool annotations are declared on the same parameter or return value, TypePHP resolves the active contract using a deterministic **3-Tier Priority Hierarchy**:
56+
57+
$$\text{1. } \mathbf{@phpstan\text{-}*} \quad \longrightarrow \quad \text{2. } \mathbf{@psalm\text{-}*} \quad \longrightarrow \quad \text{3. } \mathbf{@* \text{ (Standard)}}$$
58+
59+
### Why Priority Matters in Real-World Codebases
60+
61+
1. **Refined Contracts Take Precedence:** Tool-specific annotations (`@phpstan-param`, `@psalm-return`) contain specific type constraints (such as generic templates, array shapes, or integer bounds) that standard `@param mixed` omits. TypePHP always enforces the tighter, intended contract.
62+
2. **Third-Party Framework Compatibility:** Libraries like Doctrine Collections declare `@phpstan-param T $element` on `Collection::add` alongside native `mixed $element`. TypePHP automatically prioritizes `@phpstan-param`, making generic collections enforce types at runtime without manual wrapper code.
63+
64+
### Tooling Priority Matrix Across Boundary Contracts
65+
66+
| Boundary Type | Priority 1 (Highest) | Priority 2 | Priority 3 (Fallback) |
67+
| :--- | :--- | :--- | :--- |
68+
| **Parameters** | `@phpstan-param` | `@psalm-param` | `@param` |
69+
| **Return Values** | `@phpstan-return` | `@psalm-return` | `@return` |
70+
71+
---
72+
4073
## PHP 8.0+ Named Arguments
4174

4275
TypePHP natively supports PHP 8.0+ Named Arguments. Because parameter contracts are mapped by parameter name rather than argument position index, you can pass named arguments in any order, and TypePHP will accurately validate each parameter:
@@ -257,7 +290,7 @@ TypePHP fully validates class constructor arguments, supporting both standard co
257290

258291
### Promoted Properties (PHP 8.0+)
259292

260-
Annotate promoted properties in the constructor's docblock using standard `@param` tags:
293+
Annotate promoted properties in the constructor's docblock using standard `@param` or `@phpstan-param` tags:
261294

262295
```php
263296
class Order
@@ -284,7 +317,7 @@ new Order(-1, 'SKU-99', 5);
284317

285318
### Property `@var` Fallback for Un-Annotated Constructors
286319

287-
If a constructor parameter is un-annotated (or lacks a `@param` tag), TypePHP automatically inspects the corresponding class property's `@var` docblock to infer the parameter contract:
320+
If a constructor parameter is un-annotated (or lacks a `@param` tag), TypePHP automatically inspects the corresponding class property's `@var` / `@phpstan-var` docblock to infer the parameter contract:
288321

289322
```php
290323
class User
@@ -329,8 +362,6 @@ getUserStatus(-10);
329362
// Throws: TypeError: getUserStatus(): Return value['id'] must be of type positive-int
330363
```
331364

332-
> **PHPStan and Psalm Compatibility:** TypePHP also recognizes `@phpstan-param`, `@phpstan-return`, `@psalm-param`, and `@psalm-return` annotations.
333-
334365
---
335366

336367
## Fluent `$this` Identity Returns

docs/generics/generics-and-bounds.md

Lines changed: 158 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,43 @@ collectSameType(10, 20, 'invalid');
7474

7575
---
7676

77+
## Tooling Template Priority Hierarchy (`@phpstan-template-*` > `@psalm-template-*` > `@template-*`)
78+
79+
When third-party packages or framework classes declare both general IDE template docblocks and strict tool-specific annotations on the same class, TypePHP resolves the active generic contract using a deterministic **3-Tier Priority Hierarchy**:
80+
81+
$$\begin{aligned}
82+
\mathbf{Priority\ 1\ (Highest):} & \quad \text{@phpstan-template-covariant} \ > \ \text{@phpstan-template-contravariant} \ > \ \text{@phpstan-template} \\
83+
\mathbf{Priority\ 2:} & \quad \text{@psalm-template-covariant} \ > \ \text{@psalm-template-contravariant} \ > \ \text{@psalm-template} \\
84+
\mathbf{Priority\ 3\ (Base):} & \quad \text{@template-covariant} \ > \ \text{@template-contravariant} \ > \ \text{@template}
85+
\end{aligned}$$
86+
87+
### Why Priority Matters for Templates
88+
89+
Authors often write a broad `@template T` for generic IDE docblocks, and then declare `@phpstan-template T of Animal` to specify strict upper bounds for static analyzers. TypePHP always extracts the tool-specific annotation so that runtime bound enforcement matches the author's intended contract:
90+
91+
```php
92+
/**
93+
* Standard tag has no bound, but @phpstan-template enforces Animal bound:
94+
*
95+
* @template T
96+
* @phpstan-template T of Animal
97+
* @phpstan-template-covariant T
98+
*/
99+
class BoundedProducer
100+
{
101+
public function __construct(public mixed $item) {}
102+
}
103+
104+
// Valid: Dog extends Animal
105+
new BoundedProducer(new Dog());
106+
107+
// Invalid: Car does not extend Animal
108+
new BoundedProducer(new Car());
109+
// Throws: TypeError: BoundedProducer::__construct(): Argument $item (template T) must be of type Animal, Car given
110+
```
111+
112+
---
113+
77114
## Multiple Generic Templates (`@template T`, `@template U`)
78115

79116
Functions and classes are not limited to a single template parameter. You can declare multiple independent generic templates (such as `T`, `U`, `K`, `V`):
@@ -163,24 +200,24 @@ $users = new Collection();
163200
/** @var Dictionary<string, Product> $catalog */
164201
$catalog = new Dictionary();
165202

166-
// Single-Template Smart Fallback (No template name needed!)
203+
// Single-Template Smart Fallback (No template name needed!)
167204
$userType = TypePHP::getGenericType(object: $users); // Returns 'App\Models\User'
168205

169-
// Multi-Template Explicit Inspection
206+
// Multi-Template Explicit Inspection
170207
$keyType = TypePHP::getGenericType(object: $catalog, template: 'K'); // Returns 'string'
171208
$valueType = TypePHP::getGenericType(object: $catalog, template: 'V'); // Returns 'App\Models\Product'
172209

173210
// Inherited Generic Classes (@extends BaseRepository<User>)
174211
$userRepo = new UserRepository();
175212
$repoType = TypePHP::getGenericType(object: $userRepo); // Returns 'App\Models\User'
176213

177-
// Inspect all bound template parameters as an array
214+
// Inspect all bound template parameters as an array
178215
$types = TypePHP::getGenericTypes(object: $catalog); // Returns ['K' => 'string', 'V' => 'App\Models\Product']
179216

180-
// Inspect Declared Variance ('covariant', 'contravariant', or 'invariant')
217+
// Inspect Declared Variance ('covariant', 'contravariant', or 'invariant')
181218
$variance = TypePHP::getGenericVariance(object: $producer); // Returns 'covariant'
182219

183-
// Inspect All Bound Variances as Arrays
220+
// Inspect All Bound Variances as Arrays
184221
$variances = TypePHP::getGenericVariances(object: $producer); // Returns ['T' => 'covariant']
185222
```
186223

@@ -626,9 +663,17 @@ $producers->add(new Producer(new Car()));
626663

627664
---
628665

629-
## Class Inheritance (`@extends` and `@implements`)
666+
## Class, Interface, & Trait Inheritance (`@extends`, `@implements`, `@use`)
667+
668+
When a child class extends a generic parent class, implements a generic interface, or uses a generic trait, declare the template mapping using any of the recognized inherited template annotations:
669+
670+
| Inheritance Context | Supported Tag Variations |
671+
| :--- | :--- |
672+
| **Class Inheritance** | `@extends`, `@template-extends`, `@phpstan-extends`, `@psalm-extends` |
673+
| **Interface Implementation** | `@implements`, `@template-implements`, `@phpstan-implements`, `@psalm-implements` |
674+
| **Trait Usage** | `@use`, `@template-use`, `@phpstan-use` |
630675

631-
When a child class extends a generic parent class or implements a generic interface, declare the template mapping using `@extends` or `@implements` (also recognized as `@template-extends` and `@template-implements`):
676+
### 1. Interface Implementation (`@implements` / `@template-implements`)
632677

633678
```php
634679
/**
@@ -646,9 +691,9 @@ interface ProcessorInterface
646691
}
647692

648693
/**
649-
* Fulfills T = Cat via @implements
694+
* Fulfills T = Cat via @template-implements
650695
*
651-
* @implements ProcessorInterface<Cat>
696+
* @template-implements ProcessorInterface<Cat>
652697
*/
653698
class CatProcessor implements ProcessorInterface
654699
{
@@ -668,6 +713,106 @@ $processor->process(new Dog());
668713
// Throws: TypeError: CatProcessor::process(): Argument $item (template T = Cat) must be of type Cat
669714
```
670715

716+
### 2. Class Extension (`@extends` / `@template-extends`)
717+
718+
```php
719+
/**
720+
* @template T
721+
*/
722+
abstract class BaseRepository
723+
{
724+
/**
725+
* @param T $entity
726+
*/
727+
public function save(mixed $entity): void
728+
{
729+
// ...
730+
}
731+
}
732+
733+
/**
734+
* Fulfills T = User via @template-extends
735+
*
736+
* @template-extends BaseRepository<User>
737+
*/
738+
class UserRepository extends BaseRepository
739+
{
740+
}
741+
742+
$userRepo = new UserRepository();
743+
744+
// Valid Save
745+
$userRepo->save(new User('Alice'));
746+
747+
// Invalid Save
748+
$userRepo->save(new Product('SKU-100'));
749+
// Throws: TypeError: UserRepository::save(): Argument $entity (template T = User) must be of type User
750+
```
751+
752+
### 3. Generic Traits (`@use` / `@template-use` / `@phpstan-use`)
753+
754+
When a class uses a generic Trait, declare the template binding either at the **class level** or **directly above the inline `use Trait;` statement**:
755+
756+
#### Generic Trait Definition (`ItemLoggerTrait.php`)
757+
758+
```php
759+
/**
760+
* @template T
761+
*/
762+
trait ItemLoggerTrait
763+
{
764+
/**
765+
* @param T $item
766+
*/
767+
public function logItem(mixed $item): bool
768+
{
769+
return true;
770+
}
771+
}
772+
```
773+
774+
#### Option A: Class-Level Trait Annotation (`@use` / `@template-use`)
775+
776+
```php
777+
/**
778+
* Class docblock binds T = Dog for the trait
779+
*
780+
* @use ItemLoggerTrait<Dog>
781+
*/
782+
class ClassLevelLogService
783+
{
784+
use ItemLoggerTrait;
785+
}
786+
787+
$service = new ClassLevelLogService();
788+
789+
$service->logItem(new Dog()); // Valid
790+
791+
$service->logItem(new Car());
792+
// Throws: TypeError: Argument $item (template T = Dog) must be of type Dog, Car given
793+
```
794+
795+
#### Option B: Inline Statement Trait Annotation (`/** @use */ use Trait;`)
796+
797+
```php
798+
class InlineLogService
799+
{
800+
/**
801+
* Inline statement docblock binds T = Dog
802+
*
803+
* @use ItemLoggerTrait<Dog>
804+
*/
805+
use ItemLoggerTrait;
806+
}
807+
808+
$service = new InlineLogService();
809+
810+
$service->logItem(new Dog()); // Valid
811+
812+
$service->logItem(new Car());
813+
// Throws: TypeError: Argument $item (template T = Dog) must be of type Dog, Car given
814+
```
815+
671816
---
672817

673818
## Real-World Example 1: Generic Collections (`Collection<T>`)
@@ -722,7 +867,7 @@ $users->add(new Product('SKU-999'));
722867

723868
## Real-World Example 2: Generic Repositories (`Repository<T>`)
724869

725-
When a class extends a generic parent class (`@extends BaseRepository<User>`), TypePHP automatically resolves and inherits the parent's generic template bindings:
870+
When a class extends a generic parent class (`@extends BaseRepository<User>` or `@template-extends BaseRepository<User>`), TypePHP automatically resolves and inherits the parent's generic template bindings:
726871

727872
```php
728873
namespace App\Repositories;
@@ -744,9 +889,9 @@ abstract class BaseRepository
744889
}
745890

746891
/**
747-
* Fulfills T = User via @extends
892+
* Fulfills T = User via @template-extends
748893
*
749-
* @extends BaseRepository<User>
894+
* @template-extends BaseRepository<User>
750895
*/
751896
class UserRepository extends BaseRepository
752897
{
@@ -951,3 +1096,4 @@ processCovariantConsumer(new Consumer(new Dog()));
9511096
processCovariantConsumer(new Consumer(new Car()));
9521097
// Throws: TypeError: processCovariantConsumer() expects Consumer<covariant Animal>, but Consumer<Car> was given
9531098
```
1099+
```

0 commit comments

Comments
 (0)