Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d81067d
Bidirectional type narrowing initial implementation
ondrejmirtes Sep 1, 2026
2edf14d
Add regression tests for issues fixed by unresolved template arguments
ondrejmirtes Sep 1, 2026
168c594
Add regression test for #8031
ondrejmirtes Sep 1, 2026
20c3947
Pin bug-15169-calls to the unresolved template argument semantics
ondrejmirtes Sep 6, 2026
8fd2168
Pin the native flavour of template arguments (currently failing)
ondrejmirtes Sep 6, 2026
5fad23a
Avoid retaining call ASTs in template argument caches
ondrejmirtes Sep 6, 2026
3b1a688
Preserve inference constraints from arrow function arguments
ondrejmirtes Sep 6, 2026
a87e6be
Preserve inference constraints from terminating expression branches
ondrejmirtes Sep 6, 2026
1f5f1e0
Preserve inference constraints when foreach scope does not escape
ondrejmirtes Sep 6, 2026
fba5263
Preserve inference constraints from unreachable for loop bodies
ondrejmirtes Sep 6, 2026
c359956
Resolve dependent template bounds and defaults completely
ondrejmirtes Sep 6, 2026
dfd955c
Preserve unresolved template identity in finite type comparisons
ondrejmirtes Sep 6, 2026
6fd7b53
Keep PHPDoc template inference out of native construction types
ondrejmirtes Sep 6, 2026
90fb324
Ignore diagnostic call markers when deciding statement replay
ondrejmirtes Sep 6, 2026
8906711
Reuse analysed bodies for array_map and immediately invoked closures
ondrejmirtes Sep 7, 2026
95ed4fe
Carry template resolution policy in immutable analysis contexts
ondrejmirtes Sep 7, 2026
186e52f
Keep template argument inference compatible with PHP 7.4
ondrejmirtes Sep 7, 2026
d59d8c4
Respect template bounds when inferring from later arguments
ondrejmirtes Sep 7, 2026
8da57e4
Use template bounds for unconstrained generic call arguments
ondrejmirtes Sep 7, 2026
2c6d582
Preserve template defaults when generic calls add no constraints
ondrejmirtes Sep 7, 2026
952645b
Use generic parameter defaults to constrain empty nested collections
ondrejmirtes Sep 7, 2026
194bd0e
Use template bounds for objects passed to impure mixed parameters
ondrejmirtes Sep 7, 2026
5e5139b
Preserve unknown ArrayObject values when constructed from an object
ondrejmirtes Sep 7, 2026
87a4b79
Preserve inferred arguments when return types omit generic arguments
ondrejmirtes Sep 7, 2026
2416411
Use a complete PHP version requirement in Bug14396Test
ondrejmirtes Sep 7, 2026
b379e3d
Update levels expectations for inferred SplObjectStorage values
ondrejmirtes Sep 7, 2026
a82b7e9
Preserve bounds for unconstrained template arguments
ondrejmirtes Sep 7, 2026
66ec5e6
Solve linked invariant template arguments together
ondrejmirtes Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Use generic parameter defaults to constrain empty nested collections
  • Loading branch information
ondrejmirtes committed Sep 7, 2026
commit 952645bbb33a134a827a4b9d1982cbf13a61f629
8 changes: 8 additions & 0 deletions src/Analyser/Generics/TemplateArgumentObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeHelper;
use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\Generic\UnresolvedTemplateArgumentType;
use PHPStan\Type\MixedType;
Expand Down Expand Up @@ -122,6 +123,13 @@ private function observeSend(TemplateArgumentConstraints $constraints, Type $dec
$constraints = $this->observeSend($constraints, $declaredArgument, $argument, $isCallArgument);
continue;
}
if (
$isCallArgument
&& ($argument->getInitialType() === null || $argument->getInitialType() instanceof NeverType)
&& self::hasOnlyInferableTemplates($declaredArgument)
) {
$declaredArgument = TemplateTypeHelper::resolveToDefaults($declaredArgument);
}
if (self::isUninformativeSendTarget($declaredArgument)) {
// An unresolved call parameter, like mixed, uses the object without
// constraining it. Return/property templates are fixed by their
Expand Down
10 changes: 10 additions & 0 deletions tests/PHPStan/Rules/Classes/InstantiationRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ public function testTemplateArgumentArrow(): void
$this->analyse([__DIR__ . '/data/template-argument-arrow.php'], []);
}

public function testTemplateArgumentNestedCollection(): void
{
$this->analyse([__DIR__ . '/data/template-argument-nested-collection.php'], [
[
'Parameter #1 $events of class TemplateArgumentNestedCollection\ContainerEvent constructor expects TemplateArgumentNestedCollection\EventCollection<TemplateArgumentNestedCollection\WrittenEvent<ID of array<string, string>|string = string>>, TemplateArgumentNestedCollection\EventCollection<TemplateArgumentNestedCollection\Event> given.',
62,
],
]);
}

public function testInstantiation(): void
{
$this->analyse(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace TemplateArgumentNestedCollection;

class Event
{
}

/** @template T */
class Collection
{

/** @param iterable<T> $items */
public function __construct(iterable $items = [])
{
}

}

/**
* @template T of Event = Event
* @extends Collection<T>
*/
class EventCollection extends Collection
{
}

/** @template ID of string|array<string, string> = string */
class WrittenEvent extends Event
{

/** @param ID $id */
public function __construct($id)
{
}

}

/** @template ID of string|array<string, string> = string */
class ContainerEvent
{

/** @param EventCollection<WrittenEvent<ID>> $events */
public function __construct(EventCollection $events)
{
}

}

function emptyCollection(): void
{
new ContainerEvent(new EventCollection());
}

function populatedCollection(): void
{
new ContainerEvent(new EventCollection([new WrittenEvent('foo')]));
}

function incompatibleCollection(): void
{
new ContainerEvent(new EventCollection([new Event()]));
}