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
32 changes: 24 additions & 8 deletions src/Analyser/ExpressionResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,20 @@ final class ExpressionResult
private ?MutatingScope $falseyScope = null;

/**
* Whether every ExpressionTypeResolverExtension declined this expression.
* One full-null round settles it: a decline is (in practice) a structural
* decision about the expression, and re-running an expensive extension
* (phpstan-doctrine resolves the receiver's method reflection every time)
* on every read of every call-typed result is what it costs to re-ask.
* Whether every ExpressionTypeResolverExtension declined this expression
* on a PHPDoc-flavoured scope. One full-null round settles it: a decline
* is (in practice) a structural decision about the expression, and
* re-running an expensive extension (phpstan-doctrine resolves the
* receiver's method reflection every time) on every read of every
* call-typed result is what it costs to re-ask. The decline is tracked per
* flavour - an extension may answer on one and decline on the other (the
* receiver can be typed by PHPDoc only).
*/
private bool $extensionsDeclined = false;

/** Like $extensionsDeclined, for the native-flavoured scope. */
private bool $nativeExtensionsDeclined = false;

/**
* @param InternalThrowPoint[] $throwPoints
* @param ImpurePoint[] $impurePoints
Expand Down Expand Up @@ -92,6 +98,7 @@ public function __construct(
private ?Type $projectedNativeType = null,
private ?array $readVariableNames = null,
private ?Dependencies $dependencies = null,
bool $nativeExtensionsDeclined = false,
)
{
// A precomputed type and a lazy typeCallback are mutually exclusive, but
Expand All @@ -111,6 +118,7 @@ public function __construct(
$this->specifyTypesCallback = $specifyTypesCallback;
$this->createTypesCallback = $createTypesCallback;
$this->extensionsDeclined = $extensionsDeclined;
$this->nativeExtensionsDeclined = $nativeExtensionsDeclined;
}

/**
Expand Down Expand Up @@ -146,6 +154,7 @@ public function finalize(MutatingScope $scope, bool $hasYield, bool $isAlwaysTer
specifiedTypes: $this->specifiedTypes,
cachedType: $this->cachedType,
extensionsDeclined: $this->extensionsDeclined,
nativeExtensionsDeclined: $this->nativeExtensionsDeclined,
cachedNativeType: $this->cachedNativeType,
resolvedType: $this->resolvedType,
resolvedNativeType: $this->resolvedNativeType,
Expand Down Expand Up @@ -382,7 +391,7 @@ public function getNativeType(): Type

// old-world getNativeType() promoted the scope and re-entered
// resolveType(), extension hook included
$extensionType = $this->extensionsDeclined ? null : $this->consultExpressionTypeResolverExtensions($this->beforeScope->doNotTreatPhpDocTypesAsCertain());
$extensionType = $this->nativeExtensionsDeclined ? null : $this->consultExpressionTypeResolverExtensions($this->beforeScope->doNotTreatPhpDocTypesAsCertain());
if ($extensionType !== null) {
return $this->cachedNativeType = $extensionType;
}
Expand All @@ -403,7 +412,8 @@ public function getNativeType(): Type

private function consultExpressionTypeResolverExtensions(MutatingScope $readScope): ?Type
{
if ($this->extensionsDeclined) {
$native = $readScope->nativeTypesPromoted;
if ($native ? $this->nativeExtensionsDeclined : $this->extensionsDeclined) {
return null;
}

Expand All @@ -414,7 +424,11 @@ private function consultExpressionTypeResolverExtensions(MutatingScope $readScop
}
}

$this->extensionsDeclined = true;
if ($native) {
$this->nativeExtensionsDeclined = true;
} else {
$this->extensionsDeclined = true;
}

return null;
}
Expand Down Expand Up @@ -839,6 +853,7 @@ public function atAskPosition(MutatingScope $scope): self
variableFlow: $this->variableFlow,
specifiedTypes: $this->specifiedTypes,
extensionsDeclined: $this->extensionsDeclined,
nativeExtensionsDeclined: $this->nativeExtensionsDeclined,
resolvedType: $fromScope ? null : $this->resolvedType,
resolvedNativeType: $fromScope ? null : $this->resolvedNativeType,
projectedType: $fromScope ? null : $this->projectedType,
Expand Down Expand Up @@ -882,6 +897,7 @@ public function onNonNullabilityDevicedScopes(MutatingScope $beforeScope, Mutati
variableFlow: $this->variableFlow,
specifiedTypes: $this->specifiedTypes,
extensionsDeclined: $this->extensionsDeclined,
nativeExtensionsDeclined: $this->nativeExtensionsDeclined,
resolvedType: $this->resolvedType,
resolvedNativeType: $this->resolvedNativeType,
projectedType: $this->projectedType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class ExpressionTypeResolverExtensionTest extends TypeInferenceTestCase
public static function dataFileAsserts(): iterable
{
yield from self::gatherAssertTypes(__DIR__ . '/data/expression-type-resolver-extension.php');
yield from self::gatherAssertTypes(__DIR__ . '/data/bug-15496.php');
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Bug15496;

use PhpParser\Node\Expr;
use PhpParser\Node\Identifier;
use PHPStan\Analyser\Scope;
use PHPStan\Type\ArrayType;
use PHPStan\Type\ExpressionTypeResolverExtension;
use PHPStan\Type\IntegerType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;

/**
* Types `Order::$lines` as array<int, Line>, and `$fallbackLines` fetched on
* an object of unknown class as array<int, Line>; declines everything else.
*/
final class Bug15496ExpressionTypeResolverExtension implements ExpressionTypeResolverExtension
{
public function getType(Expr $expr, Scope $scope): ?Type
{
if (!$expr instanceof Expr\PropertyFetch || !$expr->name instanceof Identifier) {
return null;
}
if ($expr->name->name === 'fallbackLines') {
$varType = $scope->getType($expr->var);
if (!$varType->isObject()->yes() || $varType->getObjectClassNames() !== []) {
return null;
}
return new ArrayType(new IntegerType(), new ObjectType(Line::class));
}
if ($expr->name->name !== 'lines') {
return null;
}
if (!(new ObjectType(Order::class))->isSuperTypeOf($scope->getType($expr->var))->yes()) {
return null;
}
return new ArrayType(new IntegerType(), new ObjectType(Line::class));
}
}
42 changes: 42 additions & 0 deletions tests/PHPStan/Analyser/data/bug-15496.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace Bug15496;

use function PHPStan\Testing\assertNativeType;
use function PHPStan\Testing\assertType;

final class Line
{
public int $quantity = 1;
}

final class Order
{
public array $lines = [];

public array $fallbackLines = [];
}

/** @param list<Order> $orders */
function test(array $orders, Order $native): void
{
$order = $orders[0];
foreach ($order->lines as $line) {
assertType('Bug15496\Line', $line);
assertNativeType('mixed', $line);
}

foreach ($native->lines as $line) {
assertType('Bug15496\Line', $line);
assertNativeType('Bug15496\Line', $line);
}
}

/** @param Order $order */
function testNativeOnly(object $order): void
{
foreach ($order->fallbackLines as $line) {
assertType('mixed', $line);
assertNativeType('Bug15496\Line', $line);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ services:
tags:
- phpstan.broker.expressionTypeResolverExtension

-
class: Bug15496\Bug15496ExpressionTypeResolverExtension
tags:
- phpstan.broker.expressionTypeResolverExtension
20 changes: 16 additions & 4 deletions turbo-ext/src/ExpressionResult.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ struct ConstructorArgs
zval *projectedNativeType = NULL;
zval *readVariableNames = NULL;
zval *dependencies = NULL;
bool nativeExtensionsDeclined = false;
};

/* a nullable slot value as a constructor argument: NULL for PHP null */
Expand Down Expand Up @@ -245,6 +246,7 @@ class ExpressionResult
writeSlot(object, slots::specifyTypesCallback, a.specifyTypesCallback);
writeSlot(object, slots::createTypesCallback, a.createTypesCallback);
writeBoolSlot(object, slots::extensionsDeclined, a.extensionsDeclined);
writeBoolSlot(object, slots::nativeExtensionsDeclined, a.nativeExtensionsDeclined);
return true;
}

Expand Down Expand Up @@ -275,6 +277,7 @@ class ExpressionResult
a.specifiedTypes = slot(slots::specifiedTypes);
a.cachedType = argOf(slot(slots::cachedType));
a.extensionsDeclined = boolSlot(slots::extensionsDeclined);
a.nativeExtensionsDeclined = boolSlot(slots::nativeExtensionsDeclined);
a.cachedNativeType = argOf(slot(slots::cachedNativeType));
a.resolvedType = argOf(slot(slots::resolvedType));
a.resolvedNativeType = argOf(slot(slots::resolvedNativeType));
Expand Down Expand Up @@ -426,7 +429,7 @@ class ExpressionResult
// old-world getNativeType() promoted the scope and re-entered
// resolveType(), extension hook included
zv::Val nativeScope;
if (!boolSlot(slots::extensionsDeclined)) {
if (!boolSlot(slots::nativeExtensionsDeclined)) {
nativeScope = scopeNativeView(slot(slots::beforeScope));
if (UNEXPECTED(nativeScope.isUndef())) return zv::Val();
zv::Val extensionType = consultExpressionTypeResolverExtensions(nativeScope.raw());
Expand Down Expand Up @@ -741,6 +744,7 @@ class ExpressionResult
a.variableFlow = argOf(slot(slots::variableFlow));
a.specifiedTypes = slot(slots::specifiedTypes);
a.extensionsDeclined = boolSlot(slots::extensionsDeclined);
a.nativeExtensionsDeclined = boolSlot(slots::nativeExtensionsDeclined);
a.resolvedType = fromScope ? NULL : argOf(slot(slots::resolvedType));
a.resolvedNativeType = fromScope ? NULL : argOf(slot(slots::resolvedNativeType));
a.projectedType = fromScope ? NULL : argOf(slot(slots::projectedType));
Expand Down Expand Up @@ -776,6 +780,7 @@ class ExpressionResult
a.variableFlow = argOf(slot(slots::variableFlow));
a.specifiedTypes = slot(slots::specifiedTypes);
a.extensionsDeclined = boolSlot(slots::extensionsDeclined);
a.nativeExtensionsDeclined = boolSlot(slots::nativeExtensionsDeclined);
a.resolvedType = argOf(slot(slots::resolvedType));
a.resolvedNativeType = argOf(slot(slots::resolvedNativeType));
a.projectedType = argOf(slot(slots::projectedType));
Expand Down Expand Up @@ -888,7 +893,10 @@ class ExpressionResult
* PHP null when every extension declined; UNDEF = pending exception. */
zv::Val consultExpressionTypeResolverExtensions(zval *readScope)
{
if (boolSlot(slots::extensionsDeclined)) return zv::Val::null();
bool native;
if (UNEXPECTED(!scopeNativeTypesPromoted(readScope, native))) return zv::Val();
uint32_t declinedSlot = native ? slots::nativeExtensionsDeclined : slots::extensionsDeclined;
if (boolSlot(declinedSlot)) return zv::Val::null();

zv::Val extensions = pt_extensions_collection_get_all(Z_OBJ_P(slot(slots::expressionTypeResolverExtensions)));
if (UNEXPECTED(extensions.isUndef())) return zv::Val();
Expand All @@ -903,7 +911,7 @@ class ExpressionResult
}
}

writeBoolSlot(self, slots::extensionsDeclined, true);
writeBoolSlot(self, declinedSlot, true);

return zv::Val::null();
}
Expand Down Expand Up @@ -1630,16 +1638,18 @@ PT_MINIT_REGISTRATION(pt_register_expression_result)
reg::withDefault(reg::obj("projectedNativeType", pt_er_type, true), "null"),
reg::withDefault(reg::arrayArg("readVariableNames", true), "null"),
reg::withDefault(reg::obj("dependencies", "PHPStan\\Dependency\\Dependencies", true), "null"),
reg::withDefault(reg::boolArg("nativeExtensionsDeclined"), "false"),
}, [](INTERNAL_FUNCTION_PARAMETERS) {
ConstructorArgs a;
zval *typeCallback, *specifyTypesCallback, *createTypesCallback = NULL;
bool hasYield, isAlwaysTerminating;
bool containsNullsafe = false;
bool extensionsDeclined = false;
bool nativeExtensionsDeclined = false;
zval *issetabilityDescriptor = NULL, *truthyScopeOverrideResult = NULL, *falseyScopeOverrideResult = NULL;
zval *type = NULL, *nativeType = NULL, *argsResult = NULL, *variableFlow = NULL;
zval *specifiedTypes = NULL, *cachedType = NULL, *cachedNativeType = NULL, *resolvedType = NULL, *resolvedNativeType = NULL, *projectedType = NULL, *projectedNativeType = NULL, *readVariableNames = NULL, *dependencies = NULL;
ZEND_PARSE_PARAMETERS_START(11, 30)
ZEND_PARSE_PARAMETERS_START(11, 31)
Z_PARAM_OBJECT(a.expressionTypeResolverExtensions)
Z_PARAM_OBJECT(a.defaultNarrowingHelper)
Z_PARAM_OBJECT(a.scope)
Expand Down Expand Up @@ -1671,6 +1681,7 @@ PT_MINIT_REGISTRATION(pt_register_expression_result)
Z_PARAM_OBJECT_OR_NULL(projectedNativeType)
Z_PARAM_ARRAY_OR_NULL(readVariableNames)
Z_PARAM_OBJECT_OR_NULL(dependencies)
Z_PARAM_BOOL(nativeExtensionsDeclined)
ZEND_PARSE_PARAMETERS_END();
if (!pt_er_callable_arg(typeCallback, 10, true, a.typeCallback)
|| !pt_er_callable_arg(specifyTypesCallback, 11, false, a.specifyTypesCallback)
Expand All @@ -1697,6 +1708,7 @@ PT_MINIT_REGISTRATION(pt_register_expression_result)
a.projectedNativeType = projectedNativeType;
a.readVariableNames = readVariableNames;
a.dependencies = dependencies;
a.nativeExtensionsDeclined = nativeExtensionsDeclined;
if (UNEXPECTED(!ExpressionResult::construct(Z_OBJ_P(ZEND_THIS), a))) RETURN_THROWS();
});

Expand Down
Loading
Loading