Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 24 additions & 1 deletion src/Analyser/Generics/TemplateArgumentFrame.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use PHPStan\Reflection\ResolvedFunctionVariant;
use PHPStan\Turbo\ShadowedByTurboExtension;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\UnresolvedTemplateArgumentType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use function array_keys;
Expand Down Expand Up @@ -112,14 +113,23 @@ public function getStaticVariableConditionalExpressions(Node\Stmt\Static_ $stmt)

/**
* The frame with the inferred types of the body's `static` variables; the
* statements holding them are walked again in the second pass.
* statements holding them are walked again in the second pass. The walks
* inferring the types observed, so on a resolved frame the markers of the
* types are replaced by what the frame resolved them to - as the second
* pass replaces them at their sites.
*
* @param array<int, array{Expr\Variable, Type, Type}> $staticVariableTypes
* @param array<int, true> $statementIndexes
* @param array<int, array{Node\Stmt\Static_, array<string, ConditionalExpressionHolder[]>}> $conditionalExpressions
*/
public function withStaticVariableTypes(array $staticVariableTypes, array $statementIndexes, array $conditionalExpressions = []): self
{
if ($this->resolutions !== null) {
foreach ($staticVariableTypes as $id => [$var, $type, $nativeType]) {
$staticVariableTypes[$id] = [$var, $this->resolveMarkers($type), $this->resolveMarkers($nativeType)];
}
}

return new self(
$this->parent,
$this->resolutions,
Expand Down Expand Up @@ -350,6 +360,19 @@ public function getResolutionCacheKeySuffix(): string
return '';
}

private function resolveMarkers(Type $type): Type
{
return TypeTraverser::map($type, function (Type $type, callable $traverse): Type {
if ($type instanceof UnresolvedTemplateArgumentType && !$type->isClosureSignature()) {
$site = $type->getSite();

return $traverse($this->resolve($site, $type->getTemplateName()) ?? $type->getInitialType() ?? $this->resolveOrUnconstrained($site, $type->getTemplate()));
}

return $traverse($type);
});
}

private static function key(Expr $site, string $templateName): string
{
return spl_object_id($site) . '#' . $templateName;
Expand Down
2 changes: 2 additions & 0 deletions src/Analyser/StmtHandler/StaticVariableHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ public function processStmt(
$dependencies[] = $defaultExprResult->getDependencies();
$variableFlows[] = $defaultExprResult->getVariableFlow();
$impurePoints = array_merge($impurePoints, $defaultExprResult->getImpurePoints());
// the default is evaluated by the first call only, but the sites it creates are the variable's
$scope = $scope->addTemplateArgumentConstraints($defaultExprResult->getScope()->getTemplateArgumentConstraints());
}

$scope = $scope->enterExpressionAssign($var->var);
Expand Down
98 changes: 98 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15416.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php // lint >= 8.1

declare(strict_types = 1);

namespace Bug15416;

use function PHPStan\Testing\assertType;

class Repro
{
/** @return Cache<string> */
public static function getCache(): Cache
{
static $cache = new Cache(Cache::strings());
assertType('Bug15416\Cache<string>', $cache);
return $cache;
}

/** @return Cache<string> */
public static function getLazyCache(): Cache
{
static $cache = null;
if ($cache === null) {
$cache = new Cache(Cache::strings());
}
assertType('Bug15416\Cache<string>', $cache);
return $cache;
}

/** @return Cache<string> */
public static function getWrappedCache(): Cache
{
static $cache = wrap(Cache::strings());
assertType('Bug15416\Cache<string>', $cache);
return $cache;
}

/** @return array{Cache<string>, Cache<int>} */
public static function getCaches(): array
{
static $strings = new Cache(Cache::strings());
static $ints = new Cache([1, 2]);
assertType('Bug15416\Cache<string>', $strings);
assertType('Bug15416\Cache<int>', $ints);
return [$strings, $ints];
}

public static function sendCache(): void
{
static $cache = new Cache(Cache::strings());
assertType('Bug15416\Cache<string>', $cache);
self::takeCache($cache);
}

/** @param Cache<string> $cache */
public static function takeCache(Cache $cache): void
{
}

public static function closure(): void
{
$f = /** @return Cache<string> */ function (): Cache {
static $cache = new Cache(Cache::strings());
assertType('Bug15416\Cache<string>', $cache);
return $cache;
};
}
}

/**
* @template T
*/
class Cache
{
/** @param list<T> $x */
public function __construct(public array $x) { }

/** @return list<string> */
public static function strings(): array { return ['a']; }
}

/**
* @template T
* @param list<T> $x
* @return Cache<T>
*/
function wrap(array $x): Cache
{
return new Cache($x);
}

/** @return Cache<string> */
function getCache(): Cache
{
static $cache = new Cache(Cache::strings());
assertType('Bug15416\Cache<string>', $cache);
return $cache;
}
13 changes: 13 additions & 0 deletions turbo-ext/src/StaticVariableHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,19 @@ class StaticVariableHandler
if (UNEXPECTED(variableFlow.isUndef())) return false;
variableFlows.push(std::move(variableFlow));
if (UNEXPECTED(!mergeImpurePoints(impurePoints, defaultExprResult.raw()))) return false;
/* $scope = $scope->addTemplateArgumentConstraints($defaultExprResult->getScope()->getTemplateArgumentConstraints()); */
zv::Val resultScopeHold;
zval *resultScope = pt_expression_result_scope(defaultExprResult.raw(), resultScopeHold);
if (UNEXPECTED(resultScope == NULL)) return false;
if (UNEXPECTED(Z_TYPE_P(resultScope) != IS_OBJECT)) {
zend_throw_error(NULL, "Call to a member function getTemplateArgumentConstraints() on %s", zend_zval_value_name(resultScope));
return false;
}
zv::Val constraints = pt_mutating_scope_get_template_argument_constraints(Z_OBJ_P(resultScope));
if (UNEXPECTED(constraints.isUndef())) return false;
zv::Val constrainedScope = pt_mutating_scope_add_template_argument_constraints(Z_OBJ_P(scope.raw()), constraints.raw());
if (UNEXPECTED(constrainedScope.isUndef())) return false;
scope = std::move(constrainedScope);
}

var = ptsh::readNodeProperty(pt_svh_static_var_var_site, staticVar, PT_LC("var"));
Expand Down
99 changes: 99 additions & 0 deletions turbo-ext/src/TemplateArgumentFrame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ zv::Val call0(zval *object, const char *lcname, size_t len, const char *method)

void resolveUnconstrainedCallback(zval *site, zval *captured, uint32_t argc, zval *argv, zval *return_value);
void resolveCallback(zval *frame, zval *state1, uint32_t argc, zval *argv, zval *return_value);
void resolveMarkersCallback(zval *frame, zval *state1, uint32_t argc, zval *argv, zval *return_value);

} // namespace

Expand Down Expand Up @@ -203,6 +204,12 @@ class TemplateArgumentFrame
if (UNEXPECTED(resolutions == NULL)) return zv::Val();
zval *indexes = pt_typed_slot(self, slots::siteStatementIndexes, self->ce, "siteStatementIndexes");
if (UNEXPECTED(indexes == NULL)) return zv::Val();
zv::Val resolvedStaticVariableTypes;
if (Z_TYPE_P(resolutions) != IS_NULL) {
resolvedStaticVariableTypes = resolveStaticVariableTypeMarkers(staticVariableTypes);
if (UNEXPECTED(resolvedStaticVariableTypes.isUndef())) return zv::Val();
staticVariableTypes = resolvedStaticVariableTypes.raw();
}
zv::Val mergedIndexes = arrayUnion(Z_ARRVAL_P(indexes), Z_ARRVAL_P(statementIndexes));
zval *body = OBJ_PROP_NUM(self, slots::closureSignatureBody);
return create(
Expand All @@ -219,6 +226,50 @@ class TemplateArgumentFrame
);
}

/* withStaticVariableTypes()'s loop on a resolved frame: every entry
* [$var, $type, $nativeType] with both types through resolveMarkers(),
* under its key; UNDEF = pending exception */
zv::Val resolveStaticVariableTypeMarkers(zval *staticVariableTypes) const
{
zv::Arr result = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(staticVariableTypes)));
for (auto entry : zv::ArrRef(staticVariableTypes)) {
zval *types = entry.value().deref().raw();
zval *var = Z_TYPE_P(types) == IS_ARRAY ? zend_hash_index_find(Z_ARRVAL_P(types), 0) : NULL;
zval *type = Z_TYPE_P(types) == IS_ARRAY ? zend_hash_index_find(Z_ARRVAL_P(types), 1) : NULL;
zval *nativeType = Z_TYPE_P(types) == IS_ARRAY ? zend_hash_index_find(Z_ARRVAL_P(types), 2) : NULL;
if (UNEXPECTED(var == NULL || type == NULL || nativeType == NULL)) {
zend_throw_error(NULL, "TemplateArgumentFrame::withStaticVariableTypes(): an entry must be [$var, $type, $nativeType]");
return zv::Val();
}
zv::Val resolvedType = resolveMarkers(type);
if (UNEXPECTED(resolvedType.isUndef())) return zv::Val();
zv::Val resolvedNativeType = resolveMarkers(nativeType);
if (UNEXPECTED(resolvedNativeType.isUndef())) return zv::Val();
zv::Arr triple = zv::Arr::create(3);
triple.push(zv::Ref(var));
triple.push(std::move(resolvedType));
triple.push(std::move(resolvedNativeType));
zend_string *key = entry.stringKeyOrNull();
if (key != NULL) {
result.set(key, zv::Val(std::move(triple)));
} else {
zval tripleZv = zv::Val(std::move(triple)).take();
zend_hash_index_update(result.table(), entry.indexKey(), &tripleZv);
}
}
return zv::Val(std::move(result));
}

/* Mirrors resolveMarkers(); UNDEF = pending exception */
zv::Val resolveMarkers(zval *type) const
{
zval selfZv;
ZVAL_OBJ(&selfZv, self);
zv::Val callback = pt_type_native_callback(resolveMarkersCallback, &selfZv, NULL);
if (UNEXPECTED(callback.isUndef())) return zv::Val();
return pt_type_traverser_map_of(type, callback.raw());
}

/* Mirrors getByRefSiteMode(): the entry of this frame's byRefSites for
* spl_object_id($site) holding that very node, then the parent's; the
* interned 'local' / 'escaped', or PHP null. UNDEF = pending exception */
Expand Down Expand Up @@ -556,6 +607,54 @@ void resolveCallback(zval *frame, zval *state1, uint32_t argc, zval *argv, zval
resolved.intoReturnValue(return_value);
}

/* function (Type $type, callable $traverse): Type of resolveMarkers() — the
* frame in the first state slot */
void resolveMarkersCallback(zval *frame, zval *state1, uint32_t argc, zval *argv, zval *return_value)
{
(void) state1;
if (UNEXPECTED(argc < 2 || Z_TYPE(argv[0]) != IS_OBJECT)) {
zend_type_error("TemplateArgumentFrame traversal: expected (Type $type, callable $traverse)");
return;
}
zval *type = &argv[0];
zval *traverse = &argv[1];
if (instanceof_function(Z_OBJCE_P(type), pt_ce_unresolved_template_argument_type)) {
bool closureSignature = false;
if (UNEXPECTED(!pt_unresolved_template_argument_type_is_closure_signature(type, closureSignature))) return;
if (!closureSignature) {
/* $traverse($this->resolve($site, $type->getTemplateName()) ?? $type->getInitialType() ?? $this->resolveOrUnconstrained($site, $type->getTemplate())) */
zval *site = pt_unresolved_template_argument_type_site(type);
if (UNEXPECTED(site == NULL)) return;
zv::Val siteHold = zv::Val::copyOf(zv::Ref(site));
zv::Val name = pt_unresolved_template_argument_type_get_template_name(type);
if (UNEXPECTED(name.isUndef())) return;
if (UNEXPECTED(Z_TYPE_P(name.raw()) != IS_STRING)) {
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()));
return;
}
zv::Val replacement = TemplateArgumentFrame(Z_OBJ_P(frame)).resolve(siteHold.raw(), Z_STR_P(name.raw()));
if (UNEXPECTED(replacement.isUndef())) return;
if (Z_TYPE_P(replacement.raw()) == IS_NULL) {
replacement = pt_type_call(Z_OBJ_P(type), PT_LC("getinitialtype"), 0, NULL);
if (UNEXPECTED(replacement.isUndef())) return;
}
if (Z_TYPE_P(replacement.raw()) == IS_NULL) {
zv::Val templateType = pt_type_call(Z_OBJ_P(type), PT_LC("gettemplate"), 0, NULL);
if (UNEXPECTED(templateType.isUndef())) return;
replacement = TemplateArgumentFrame(Z_OBJ_P(frame)).resolveOrUnconstrained(siteHold.raw(), templateType.raw());
if (UNEXPECTED(replacement.isUndef())) return;
}
zv::Val traversed = pt_type_call_callable(traverse, 1, replacement.raw());
if (UNEXPECTED(traversed.isUndef())) return;
traversed.intoReturnValue(return_value);
return;
}
}
zv::Val traversed = pt_type_call_callable(traverse, 1, type);
if (UNEXPECTED(traversed.isUndef())) return;
traversed.intoReturnValue(return_value);
}

/* static function (Type $type, callable $traverse) use ($site, $scope, $resolve): Type
* — $site in the first state slot, [$scope, $resolve] in the second */
void resolveUnconstrainedCallback(zval *site, zval *captured, uint32_t argc, zval *argv, zval *return_value)
Expand Down
Loading