Collection<*> (star projection)* as a type argument means the
function accepts a collection of *any* item type, and combines the write
restriction of covariance (add() expects never) with the read
restriction of contravariance (get() is mixed). It is not
Collection<mixed>: an invariant Collection<mixed> still rejects
Collection<int>.
Mago implements it too. Psalm rejects * as InvalidDocblock and
discards the whole annotation (vimeo/psalm#11931).
References:
- PHPStan phpdoc-types: Generics (Collection<*>)
- PHPStan "A guide to call-site generic variance" (star projections)
- psalm#11931| Analyzer | Version | Result | Diagnostics |
|---|---|---|---|
| phan | 6.0.7Phan 6.0.7 php-ast version 1.1.3 PHP version used to run Phan: 8.5.9 | Unrecognized | phpdoc_advanced_phpstan_star_projection.php:78: Saw possibly unextractable annotation for a fragment of comment '* @param Collection<*> $collection': after Collection, did not see an element name (will guess based on comment order) [PhanUnextractableAnnotationElementName] phpdoc_advanced_phpstan_star_projection.php:78: Saw a token Phan may have failed to parse after '* @param Collection<*> $collection': after Collection, saw '<' [PhanUnextractableAnnotationSuffix] phpdoc_advanced_phpstan_star_projection.php:86: Saw possibly unextractable annotation for a fragment of comment '* @param Collection<*> $collection': after Collection, did not see an element name (will guess based on comment order) [PhanUnextractableAnnotationElementName] phpdoc_advanced_phpstan_star_projection.php:86: Saw a token Phan may have failed to parse after '* @param Collection<*> $collection': after Collection, saw '<' [PhanUnextractableAnnotationSuffix]
Notes: Cannot extract the annotation after `Collection` (`PhanUnextractableAnnotationElementName` / `Suffix` on `Collection<*>`), so the spelling is not parsed and add() is unrestricted. |
| phpstan | 2.2.15PHPStan - PHP Static Analysis Tool 2.2.15 | Enforced reported Lv.5+ | phpdoc_advanced_phpstan_star_projection.php:90: Parameter #1 $item of method Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<*>::add() expects never, int given. [reported-from-level=5] phpdoc_advanced_phpstan_star_projection.php:115: Parameter #1 $collection of function Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\takesMixed expects Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<mixed>, Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<int> given. [reported-from-level=9] With strict-rulesphpdoc_advanced_phpstan_star_projection.php:90: Parameter #1 $item of method Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<*>::add() expects never, int given. phpdoc_advanced_phpstan_star_projection.php:115: Parameter #1 $collection of function Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\takesMixed expects Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<mixed>, Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<int> given.
|
| psalm | 6.18.0Psalm 6.18.0@536dd6236b39a5115385b0307b42d6d1ad431a02 next: 7.0.0-beta22 |
Unrecognized (pzoom≠) | phpdoc_advanced_phpstan_star_projection.php:78: Invalid type 'Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\*' in docblock for Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\printSize [InvalidDocblock] phpdoc_advanced_phpstan_star_projection.php:86: Invalid type 'Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\*' in docblock for Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\writeStar [InvalidDocblock] phpdoc_advanced_phpstan_star_projection.php:115: Argument 1 of Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\takesMixed expects Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<mixed>, but Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<int> provided [InvalidArgument] pzoom (Psalm port)phpdoc_advanced_phpstan_star_projection.php:80: Docblock class Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\* does not exist [UndefinedDocblockClass] phpdoc_advanced_phpstan_star_projection.php:88: Docblock class Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\* does not exist [UndefinedDocblockClass] phpdoc_advanced_phpstan_star_projection.php:90: Argument 1 of Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection::add expects Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\*, but 1 provided [InvalidArgument] phpdoc_advanced_phpstan_star_projection.php:108: Argument 1 of printSize expects Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\*>, but Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<int> provided [InvalidArgument] phpdoc_advanced_phpstan_star_projection.php:109: Argument 1 of printSize expects Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\*>, but Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<string> provided [InvalidArgument] phpdoc_advanced_phpstan_star_projection.php:110: Argument 1 of writeStar expects Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\*>, but Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<int> provided [InvalidArgument] phpdoc_advanced_phpstan_star_projection.php:115: Argument 1 of takesMixed expects Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<mixed>, but Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<int> provided [InvalidArgument]
Notes: Rejects `*` as InvalidDocblock and discards the whole `@param`, so Collection is untyped: the valid Collection<int>/Collection<string> calls stay silent and add() is not restricted. The contrast Collection<mixed> line still fires because that annotation does not use `*`. |
| mago | 1.50.0mago 1.50.0 | Enforced | phpdoc_advanced_phpstan_star_projection.php:90: Possible argument type mismatch for argument #1 of `Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection::add`: expected `never`, but possibly received `int(1)`. [possibly-invalid-argument] phpdoc_advanced_phpstan_star_projection.php:115: Argument type mismatch for argument #1 of `Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\takesMixed`: expected `Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<mixed>`, but provided type `Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<int>` is less specific. [less-specific-nested-argument-type]
|
| mir | 0.78.0mir 0.78.0 | Not enforced | No diagnostics reported.
|
| phpantom | 0.10.0phpantom_lsp 0.10.0 | Not enforced | No diagnostics reported.
|
| intelephense | 1.18.5intelephense 1.18.5 | Not enforced | No diagnostics reported.
|
| phpactor | 2026.06.23.0Phpactor 2026.06.23.0 | Not enforced | No diagnostics reported.
|
| phpy | 1.0.19274phpy 1.0.19274 | Not enforced | No diagnostics reported.
|
| qodana | 262.9437.196Qodana 262.9437.196 | Not enforced | No diagnostics reported.
Notes: Silent on Collection<*>: no undefined-class complaint on `*`, and add() is not restricted to never. |
| noverify | 0.5.5NoVerify, version 0.5.5: built on: 2025.04.22 14:36:46 OS: r-kuchinskas arm64 Commit: 4774b82f8adc53fbef38f95e97af4b953b4d1529 | Unrecognized ⚠ 3 false positives | phpdoc_advanced_phpstan_star_projection.php:35: param $item miss matched with phpdoc type <<T>> [funcParamTypeMissMatch] phpdoc_advanced_phpstan_star_projection.php:80: param $collection miss matched with phpdoc type <<Collection<*>>> [funcParamTypeMissMatch] phpdoc_advanced_phpstan_star_projection.php:88: param $collection miss matched with phpdoc type <<Collection<*>>> [funcParamTypeMissMatch] phpdoc_advanced_phpstan_star_projection.php:96: param $collection miss matched with phpdoc type <<Collection<int>>> [funcParamTypeMissMatch] phpdoc_advanced_phpstan_star_projection.php:104: param $collection miss matched with phpdoc type <<Collection<mixed>>> [funcParamTypeMissMatch]
Expectation diffLine 35: Unexpected errors ["param $item miss matched with phpdoc type <<T>> [funcParamTypeMissMatch]"] Line 96: Unexpected errors ["param $collection miss matched with phpdoc type <<Collection<int>>> [funcParamTypeMissMatch]"] Line 104: Unexpected errors ["param $collection miss matched with phpdoc type <<Collection<mixed>>> [funcParamTypeMissMatch]"] |
| steins | 0.1.8steins 0.1.8 (2026-09-21 revision 76306a9) | Not enforced | No diagnostics reported.
|
| phpstan-strict | 2.2.15PHPStan - PHP Static Analysis Tool 2.2.15 | Enforced | phpdoc_advanced_phpstan_star_projection.php:90: Parameter #1 $item of method Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<*>::add() expects never, int given. phpdoc_advanced_phpstan_star_projection.php:115: Parameter #1 $collection of function Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\takesMixed expects Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<mixed>, Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<int> given.
|
| psalm-next | 7.0.0-beta22Psalm 7.0.0-beta22@de27e5724ee5997a1e3baee4d394ca6f4e46eba0 | Unrecognized | phpdoc_advanced_phpstan_star_projection.php:78: Invalid type 'Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\*' in docblock for Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\printSize [InvalidDocblock] phpdoc_advanced_phpstan_star_projection.php:86: Invalid type 'Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\*' in docblock for Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\writeStar [InvalidDocblock] phpdoc_advanced_phpstan_star_projection.php:115: Argument 1 of Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\takesMixed expects Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<mixed>, but Conformance\Tests\PhpdocAdvancedPhpstanStarProjection\Collection<int> provided [InvalidArgument]
Notes: Same as Psalm 6: InvalidDocblock on `*`, the annotation is dropped, add() is unrestricted. Matches vimeo/psalm#11931. |
<?php
declare(strict_types=1);
namespace Conformance\Tests\PhpdocAdvancedPhpstanStarProjection;
/**
* `Collection<*>` (star projection)
*
* A PHPStan call-site variance spelling. `*` as a type argument means the
* function accepts a collection of *any* item type, and combines the write
* restriction of covariance (`add()` expects `never`) with the read
* restriction of contravariance (`get()` is `mixed`). It is not
* `Collection<mixed>`: an invariant `Collection<mixed>` still rejects
* `Collection<int>`.
*
* Mago implements it too. Psalm rejects `*` as `InvalidDocblock` and
* discards the whole annotation (vimeo/psalm#11931).
*
* References:
* - PHPStan phpdoc-types: Generics (`Collection<*>`)
* - PHPStan "A guide to call-site generic variance" (star projections)
* - https://github.com/vimeo/psalm/issues/11931
*/
/**
* @template T
*/
final class Collection
{
/**
* @param T $item
*/
public function __construct(
public mixed $item,
) {
}
/**
* @param T $item
*/
public function add($item): void
{
$this->item = $item;
}
/**
* @return T
*/
public function get(): mixed
{
return $this->item;
}
public function count(): int
{
return 1;
}
}
/**
* @return Collection<int>
*/
function ints(): Collection
{
return new Collection(0);
}
/**
* @return Collection<string>
*/
function strings(): Collection
{
return new Collection('x');
}
/**
* @param Collection<*> $collection
*/
function printSize(Collection $collection): void // T: Collection<*>
{
echo $collection->count(); // V: count() does not mention T
}
/**
* @param Collection<*> $collection
*/
function writeStar(Collection $collection): void // T: Collection<*>
{
$collection->add(1); // E?: add() on a star projection expects never
}
/**
* @param Collection<int> $collection
*/
function writeInt(Collection $collection): void
{
$collection->add(1); // V: the same write is valid on Collection<int>
}
/**
* @param Collection<mixed> $collection
*/
function takesMixed(Collection $collection): void
{
}
printSize(ints()); // V
printSize(strings()); // V
writeStar(ints()); // V
writeInt(ints()); // V
// Contrast: invariant Collection<mixed> does not accept Collection<int>. A
// tool that read `*` as `mixed` rejects the V lines above the same way.
takesMixed(ints()); // E?[noise]: Collection<int> is not Collection<mixed>