Skip to content

Commit 9a0bcab

Browse files
phpstan-botclaude
authored andcommitted
Keep the template-argument constraints of a static variable's default and resolve the markers in inferred static variable types
- StaticVariableHandler: carry the template-argument constraints of the scope the default expression returned onto the statement's scope, the way CoalesceHandler carries those of its conditionally evaluated right side. Before this, the `new` / generic call sites in `static $x = new Foo(...)` recorded no facts, so their template arguments were never resolved. - TemplateArgumentFrame::withStaticVariableTypes(): on a resolved frame, replace the UnresolvedTemplateArgumentType markers in the inferred types (collected by the observation walks) with the frame's resolution, the initial type, or the unconstrained default/bound - the same order GenericTypeTemplateTraverser uses at the site. Closure-signature markers are left alone. - Ported both changes to the turbo-ext mirrors (StaticVariableHandler.cpp, TemplateArgumentFrame.cpp). - Analogous cases covered by the regression test: a site created in the body (`static $c = null; $c = new ...`), a generic function call as the default, a run of two `static` variables, a send to a typed parameter instead of a return, a function, and a closure body. A closure stored in a `static` variable was probed too: it stays `Closure(mixed): mixed` with no marker leak, so it is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 4ef9ab7 commit 9a0bcab

5 files changed

Lines changed: 236 additions & 1 deletion

File tree

‎src/Analyser/Generics/TemplateArgumentFrame.php‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Reflection\ResolvedFunctionVariant;
1111
use PHPStan\Turbo\ShadowedByTurboExtension;
1212
use PHPStan\Type\Generic\TemplateType;
13+
use PHPStan\Type\Generic\UnresolvedTemplateArgumentType;
1314
use PHPStan\Type\Type;
1415
use PHPStan\Type\TypeTraverser;
1516
use function array_keys;
@@ -112,14 +113,23 @@ public function getStaticVariableConditionalExpressions(Node\Stmt\Static_ $stmt)
112113

113114
/**
114115
* The frame with the inferred types of the body's `static` variables; the
115-
* statements holding them are walked again in the second pass.
116+
* statements holding them are walked again in the second pass. The walks
117+
* inferring the types observed, so on a resolved frame the markers of the
118+
* types are replaced by what the frame resolved them to - as the second
119+
* pass replaces them at their sites.
116120
*
117121
* @param array<int, array{Expr\Variable, Type, Type}> $staticVariableTypes
118122
* @param array<int, true> $statementIndexes
119123
* @param array<int, array{Node\Stmt\Static_, array<string, ConditionalExpressionHolder[]>}> $conditionalExpressions
120124
*/
121125
public function withStaticVariableTypes(array $staticVariableTypes, array $statementIndexes, array $conditionalExpressions = []): self
122126
{
127+
if ($this->resolutions !== null) {
128+
foreach ($staticVariableTypes as $id => [$var, $type, $nativeType]) {
129+
$staticVariableTypes[$id] = [$var, $this->resolveMarkers($type), $this->resolveMarkers($nativeType)];
130+
}
131+
}
132+
123133
return new self(
124134
$this->parent,
125135
$this->resolutions,
@@ -350,6 +360,19 @@ public function getResolutionCacheKeySuffix(): string
350360
return '';
351361
}
352362

363+
private function resolveMarkers(Type $type): Type
364+
{
365+
return TypeTraverser::map($type, function (Type $type, callable $traverse): Type {
366+
if ($type instanceof UnresolvedTemplateArgumentType && !$type->isClosureSignature()) {
367+
$site = $type->getSite();
368+
369+
return $traverse($this->resolve($site, $type->getTemplateName()) ?? $type->getInitialType() ?? $this->resolveOrUnconstrained($site, $type->getTemplate()));
370+
}
371+
372+
return $traverse($type);
373+
});
374+
}
375+
353376
private static function key(Expr $site, string $templateName): string
354377
{
355378
return spl_object_id($site) . '#' . $templateName;

‎src/Analyser/StmtHandler/StaticVariableHandler.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ public function processStmt(
7878
$dependencies[] = $defaultExprResult->getDependencies();
7979
$variableFlows[] = $defaultExprResult->getVariableFlow();
8080
$impurePoints = array_merge($impurePoints, $defaultExprResult->getImpurePoints());
81+
// the default is evaluated by the first call only, but the sites it creates are the variable's
82+
$scope = $scope->addTemplateArgumentConstraints($defaultExprResult->getScope()->getTemplateArgumentConstraints());
8183
}
8284

8385
$scope = $scope->enterExpressionAssign($var->var);
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug15416;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
class Repro
10+
{
11+
/** @return Cache<string> */
12+
public static function getCache(): Cache
13+
{
14+
static $cache = new Cache(Cache::strings());
15+
assertType('Bug15416\Cache<string>', $cache);
16+
return $cache;
17+
}
18+
19+
/** @return Cache<string> */
20+
public static function getLazyCache(): Cache
21+
{
22+
static $cache = null;
23+
if ($cache === null) {
24+
$cache = new Cache(Cache::strings());
25+
}
26+
assertType('Bug15416\Cache<string>', $cache);
27+
return $cache;
28+
}
29+
30+
/** @return Cache<string> */
31+
public static function getWrappedCache(): Cache
32+
{
33+
static $cache = wrap(Cache::strings());
34+
assertType('Bug15416\Cache<string>', $cache);
35+
return $cache;
36+
}
37+
38+
/** @return array{Cache<string>, Cache<int>} */
39+
public static function getCaches(): array
40+
{
41+
static $strings = new Cache(Cache::strings());
42+
static $ints = new Cache([1, 2]);
43+
assertType('Bug15416\Cache<string>', $strings);
44+
assertType('Bug15416\Cache<int>', $ints);
45+
return [$strings, $ints];
46+
}
47+
48+
public static function sendCache(): void
49+
{
50+
static $cache = new Cache(Cache::strings());
51+
assertType('Bug15416\Cache<string>', $cache);
52+
self::takeCache($cache);
53+
}
54+
55+
/** @param Cache<string> $cache */
56+
public static function takeCache(Cache $cache): void
57+
{
58+
}
59+
60+
public static function closure(): void
61+
{
62+
$f = /** @return Cache<string> */ function (): Cache {
63+
static $cache = new Cache(Cache::strings());
64+
assertType('Bug15416\Cache<string>', $cache);
65+
return $cache;
66+
};
67+
}
68+
}
69+
70+
/**
71+
* @template T
72+
*/
73+
class Cache
74+
{
75+
/** @param list<T> $x */
76+
public function __construct(public array $x) { }
77+
78+
/** @return list<string> */
79+
public static function strings(): array { return ['a']; }
80+
}
81+
82+
/**
83+
* @template T
84+
* @param list<T> $x
85+
* @return Cache<T>
86+
*/
87+
function wrap(array $x): Cache
88+
{
89+
return new Cache($x);
90+
}
91+
92+
/** @return Cache<string> */
93+
function getCache(): Cache
94+
{
95+
static $cache = new Cache(Cache::strings());
96+
assertType('Bug15416\Cache<string>', $cache);
97+
return $cache;
98+
}

‎turbo-ext/src/StaticVariableHandler.cpp‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,19 @@ class StaticVariableHandler
188188
if (UNEXPECTED(variableFlow.isUndef())) return false;
189189
variableFlows.push(std::move(variableFlow));
190190
if (UNEXPECTED(!mergeImpurePoints(impurePoints, defaultExprResult.raw()))) return false;
191+
/* $scope = $scope->addTemplateArgumentConstraints($defaultExprResult->getScope()->getTemplateArgumentConstraints()); */
192+
zv::Val resultScopeHold;
193+
zval *resultScope = pt_expression_result_scope(defaultExprResult.raw(), resultScopeHold);
194+
if (UNEXPECTED(resultScope == NULL)) return false;
195+
if (UNEXPECTED(Z_TYPE_P(resultScope) != IS_OBJECT)) {
196+
zend_throw_error(NULL, "Call to a member function getTemplateArgumentConstraints() on %s", zend_zval_value_name(resultScope));
197+
return false;
198+
}
199+
zv::Val constraints = pt_mutating_scope_get_template_argument_constraints(Z_OBJ_P(resultScope));
200+
if (UNEXPECTED(constraints.isUndef())) return false;
201+
zv::Val constrainedScope = pt_mutating_scope_add_template_argument_constraints(Z_OBJ_P(scope.raw()), constraints.raw());
202+
if (UNEXPECTED(constrainedScope.isUndef())) return false;
203+
scope = std::move(constrainedScope);
191204
}
192205

193206
var = ptsh::readNodeProperty(pt_svh_static_var_var_site, staticVar, PT_LC("var"));

‎turbo-ext/src/TemplateArgumentFrame.cpp‎

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ zv::Val call0(zval *object, const char *lcname, size_t len, const char *method)
4949

5050
void resolveUnconstrainedCallback(zval *site, zval *captured, uint32_t argc, zval *argv, zval *return_value);
5151
void resolveCallback(zval *frame, zval *state1, uint32_t argc, zval *argv, zval *return_value);
52+
void resolveMarkersCallback(zval *frame, zval *state1, uint32_t argc, zval *argv, zval *return_value);
5253

5354
} // namespace
5455

@@ -203,6 +204,12 @@ class TemplateArgumentFrame
203204
if (UNEXPECTED(resolutions == NULL)) return zv::Val();
204205
zval *indexes = pt_typed_slot(self, slots::siteStatementIndexes, self->ce, "siteStatementIndexes");
205206
if (UNEXPECTED(indexes == NULL)) return zv::Val();
207+
zv::Val resolvedStaticVariableTypes;
208+
if (Z_TYPE_P(resolutions) != IS_NULL) {
209+
resolvedStaticVariableTypes = resolveStaticVariableTypeMarkers(staticVariableTypes);
210+
if (UNEXPECTED(resolvedStaticVariableTypes.isUndef())) return zv::Val();
211+
staticVariableTypes = resolvedStaticVariableTypes.raw();
212+
}
206213
zv::Val mergedIndexes = arrayUnion(Z_ARRVAL_P(indexes), Z_ARRVAL_P(statementIndexes));
207214
zval *body = OBJ_PROP_NUM(self, slots::closureSignatureBody);
208215
return create(
@@ -219,6 +226,50 @@ class TemplateArgumentFrame
219226
);
220227
}
221228

229+
/* withStaticVariableTypes()'s loop on a resolved frame: every entry
230+
* [$var, $type, $nativeType] with both types through resolveMarkers(),
231+
* under its key; UNDEF = pending exception */
232+
zv::Val resolveStaticVariableTypeMarkers(zval *staticVariableTypes) const
233+
{
234+
zv::Arr result = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(staticVariableTypes)));
235+
for (auto entry : zv::ArrRef(staticVariableTypes)) {
236+
zval *types = entry.value().deref().raw();
237+
zval *var = Z_TYPE_P(types) == IS_ARRAY ? zend_hash_index_find(Z_ARRVAL_P(types), 0) : NULL;
238+
zval *type = Z_TYPE_P(types) == IS_ARRAY ? zend_hash_index_find(Z_ARRVAL_P(types), 1) : NULL;
239+
zval *nativeType = Z_TYPE_P(types) == IS_ARRAY ? zend_hash_index_find(Z_ARRVAL_P(types), 2) : NULL;
240+
if (UNEXPECTED(var == NULL || type == NULL || nativeType == NULL)) {
241+
zend_throw_error(NULL, "TemplateArgumentFrame::withStaticVariableTypes(): an entry must be [$var, $type, $nativeType]");
242+
return zv::Val();
243+
}
244+
zv::Val resolvedType = resolveMarkers(type);
245+
if (UNEXPECTED(resolvedType.isUndef())) return zv::Val();
246+
zv::Val resolvedNativeType = resolveMarkers(nativeType);
247+
if (UNEXPECTED(resolvedNativeType.isUndef())) return zv::Val();
248+
zv::Arr triple = zv::Arr::create(3);
249+
triple.push(zv::Ref(var));
250+
triple.push(std::move(resolvedType));
251+
triple.push(std::move(resolvedNativeType));
252+
zend_string *key = entry.stringKeyOrNull();
253+
if (key != NULL) {
254+
result.set(key, zv::Val(std::move(triple)));
255+
} else {
256+
zval tripleZv = zv::Val(std::move(triple)).take();
257+
zend_hash_index_update(result.table(), entry.indexKey(), &tripleZv);
258+
}
259+
}
260+
return zv::Val(std::move(result));
261+
}
262+
263+
/* Mirrors resolveMarkers(); UNDEF = pending exception */
264+
zv::Val resolveMarkers(zval *type) const
265+
{
266+
zval selfZv;
267+
ZVAL_OBJ(&selfZv, self);
268+
zv::Val callback = pt_type_native_callback(resolveMarkersCallback, &selfZv, NULL);
269+
if (UNEXPECTED(callback.isUndef())) return zv::Val();
270+
return pt_type_traverser_map_of(type, callback.raw());
271+
}
272+
222273
/* Mirrors getByRefSiteMode(): the entry of this frame's byRefSites for
223274
* spl_object_id($site) holding that very node, then the parent's; the
224275
* interned 'local' / 'escaped', or PHP null. UNDEF = pending exception */
@@ -556,6 +607,54 @@ void resolveCallback(zval *frame, zval *state1, uint32_t argc, zval *argv, zval
556607
resolved.intoReturnValue(return_value);
557608
}
558609

610+
/* function (Type $type, callable $traverse): Type of resolveMarkers() — the
611+
* frame in the first state slot */
612+
void resolveMarkersCallback(zval *frame, zval *state1, uint32_t argc, zval *argv, zval *return_value)
613+
{
614+
(void) state1;
615+
if (UNEXPECTED(argc < 2 || Z_TYPE(argv[0]) != IS_OBJECT)) {
616+
zend_type_error("TemplateArgumentFrame traversal: expected (Type $type, callable $traverse)");
617+
return;
618+
}
619+
zval *type = &argv[0];
620+
zval *traverse = &argv[1];
621+
if (instanceof_function(Z_OBJCE_P(type), pt_ce_unresolved_template_argument_type)) {
622+
bool closureSignature = false;
623+
if (UNEXPECTED(!pt_unresolved_template_argument_type_is_closure_signature(type, closureSignature))) return;
624+
if (!closureSignature) {
625+
/* $traverse($this->resolve($site, $type->getTemplateName()) ?? $type->getInitialType() ?? $this->resolveOrUnconstrained($site, $type->getTemplate())) */
626+
zval *site = pt_unresolved_template_argument_type_site(type);
627+
if (UNEXPECTED(site == NULL)) return;
628+
zv::Val siteHold = zv::Val::copyOf(zv::Ref(site));
629+
zv::Val name = pt_unresolved_template_argument_type_get_template_name(type);
630+
if (UNEXPECTED(name.isUndef())) return;
631+
if (UNEXPECTED(Z_TYPE_P(name.raw()) != IS_STRING)) {
632+
zend_type_error("%s::resolve(): Argument #2 ($templateName) must be of type string, %s given", ZSTR_VAL(Z_OBJCE_P(frame)->name), zend_zval_value_name(name.raw()));
633+
return;
634+
}
635+
zv::Val replacement = TemplateArgumentFrame(Z_OBJ_P(frame)).resolve(siteHold.raw(), Z_STR_P(name.raw()));
636+
if (UNEXPECTED(replacement.isUndef())) return;
637+
if (Z_TYPE_P(replacement.raw()) == IS_NULL) {
638+
replacement = pt_type_call(Z_OBJ_P(type), PT_LC("getinitialtype"), 0, NULL);
639+
if (UNEXPECTED(replacement.isUndef())) return;
640+
}
641+
if (Z_TYPE_P(replacement.raw()) == IS_NULL) {
642+
zv::Val templateType = pt_type_call(Z_OBJ_P(type), PT_LC("gettemplate"), 0, NULL);
643+
if (UNEXPECTED(templateType.isUndef())) return;
644+
replacement = TemplateArgumentFrame(Z_OBJ_P(frame)).resolveOrUnconstrained(siteHold.raw(), templateType.raw());
645+
if (UNEXPECTED(replacement.isUndef())) return;
646+
}
647+
zv::Val traversed = pt_type_call_callable(traverse, 1, replacement.raw());
648+
if (UNEXPECTED(traversed.isUndef())) return;
649+
traversed.intoReturnValue(return_value);
650+
return;
651+
}
652+
}
653+
zv::Val traversed = pt_type_call_callable(traverse, 1, type);
654+
if (UNEXPECTED(traversed.isUndef())) return;
655+
traversed.intoReturnValue(return_value);
656+
}
657+
559658
/* static function (Type $type, callable $traverse) use ($site, $scope, $resolve): Type
560659
* — $site in the first state slot, [$scope, $resolve] in the second */
561660
void resolveUnconstrainedCallback(zval *site, zval *captured, uint32_t argc, zval *argv, zval *return_value)

0 commit comments

Comments
 (0)