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
12 changes: 12 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -3423,6 +3423,18 @@ private function getScopeStateNativeType(Expr $expr): Type
*/
private function resolveScopeStateType(Expr $expr, bool $native): Type
{
if (!$expr instanceof Variable) {
// mirror resolveType(): an extension's answer wins over both the
// tracked holder and the reflection-derived read below
$askScope = $native ? $this->doNotTreatPhpDocTypesAsCertain() : $this;
foreach ($this->services->expressionTypeResolverExtensions->getAll() as $extension) {
$type = $extension->getType($expr, $askScope);
if ($type !== null) {
return $type;
}
}
}

if (!$expr instanceof Variable && $this->hasExpressionType($expr)->yes()) {
// mirror resolveType()'s tracked-holder lookup without pricing the
// node - the tracked type IS scope state (the extension hook is
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-15421.php');
}

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

namespace ExpressionTypeResolverExtension;

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

class Bug15421ExpressionTypeResolverExtension implements ExpressionTypeResolverExtension
{

public function getType(Expr $expr, Scope $scope): ?Type
{
if ($expr instanceof PropertyFetch
&& $expr->name instanceof Identifier
&& $expr->name->name === 'holder'
&& (new ObjectType('Bug15421\Crate'))->isSuperTypeOf($scope->getType($expr->var))->yes()) {
return new ObjectType('Bug15421\Holder');
}

return null;
}

}
44 changes: 44 additions & 0 deletions tests/PHPStan/Analyser/data/bug-15421.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php declare(strict_types = 1);

namespace Bug15421;

use function PHPStan\Testing\assertType;

final class Day {
public string $date = 'x';
public function toDateString(): string { return 'x'; }
public function next(): ?Day { return null; }
}
final class Holder {
public ?Day $day = null;
/** @var array{day?: Day|null} */
public array $days = [];
public function getDay(): ?Day { return $this->day; }
}
final class Crate {}

function test(Crate $crate): void
{
$a = $crate->holder->day?->toDateString(); // reported on 2.3.0
$h = $crate->holder;
$b = $h->day?->toDateString(); // not reported
}

function analogous(Crate $crate): void
{
assertType('Bug15421\Holder', $crate->holder);
assertType('Bug15421\Day|null', $crate->holder->day);
assertType('string|null', $crate->holder->day?->toDateString());
assertType('string|null', $crate->holder->day?->date);
assertType('Bug15421\Day|null', $crate->holder->day?->next()?->next());
assertType('string|null', $crate->holder->getDay()?->toDateString());
assertType('string|null', $crate->holder->getDay()?->date);
assertType('string|null', $crate->holder->day->date ?? null);
assertType('string|null', $crate->holder->days['day']?->date);
assertType('bool', isset($crate->holder->day->date));
assertType('bool', empty($crate->holder->day->date));
if ($crate->holder->day !== null) {
assertType('Bug15421\Day', $crate->holder->day);
assertType('string', $crate->holder->day->toDateString());
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
# config for ExpressionTypeResolverExtensionTest
parameters:
universalObjectCratesClasses:
- Bug15421\Crate

services:
-
class: ExpressionTypeResolverExtension\MethodCallReturnsBoolExpressionTypeResolverExtension
tags:
- phpstan.broker.expressionTypeResolverExtension

-
class: ExpressionTypeResolverExtension\Bug15421ExpressionTypeResolverExtension
tags:
- phpstan.broker.expressionTypeResolverExtension
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Methods;

use PHPStan\Rules\FunctionCallParametersCheck;
use PHPStan\Rules\NonStringableDynamicAccessCheck;
use PHPStan\Rules\NullsafeCheck;
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
use PHPStan\Rules\Properties\PropertyReflectionFinder;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleLevelHelper;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<CallMethodsRule>
*/
class CallMethodsRuleExpressionTypeResolverExtensionTest extends RuleTestCase
{

protected function getRule(): Rule
{
$reflectionProvider = self::createReflectionProvider();
$ruleLevelHelper = new RuleLevelHelper(
$reflectionProvider,
checkNullables: true,
checkThisOnly: false,
checkUnionTypes: true,
checkExplicitMixed: false,
checkImplicitMixed: false,
checkBenevolentUnionTypes: false,
discoveringSymbolsTip: true,
);
return new CallMethodsRule(
new MethodCallCheck(
$reflectionProvider,
$ruleLevelHelper,
checkFunctionNameCase: true,
reportMagicMethods: true,
),
new FunctionCallParametersCheck(
$ruleLevelHelper,
new NullsafeCheck(),
new UnresolvableTypeHelper(),
new PropertyReflectionFinder(),
$reflectionProvider,
checkArgumentTypes: true,
checkArgumentsPassedByReference: true,
checkExtraArguments: true,
checkMissingTypehints: true,
),
new NonStringableDynamicAccessCheck($ruleLevelHelper, true),
);
}

public function testBug15421(): void
{
$this->analyse([__DIR__ . '/../../Analyser/data/bug-15421.php'], []);
}

public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/../../Analyser/expression-type-resolver-extension.neon',
];
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Properties;

use PHPStan\Php\PhpVersion;
use PHPStan\Rules\NonStringableDynamicAccessCheck;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleLevelHelper;
use PHPStan\Testing\RuleTestCase;
use const PHP_VERSION_ID;

/**
* @extends RuleTestCase<AccessPropertiesRule>
*/
class AccessPropertiesRuleExpressionTypeResolverExtensionTest extends RuleTestCase
{

protected function getRule(): Rule
{
$reflectionProvider = self::createReflectionProvider();
$ruleLevelHelper = new RuleLevelHelper(
$reflectionProvider,
checkNullables: true,
checkThisOnly: false,
checkUnionTypes: true,
checkExplicitMixed: false,
checkImplicitMixed: false,
checkBenevolentUnionTypes: false,
discoveringSymbolsTip: true,
);
return new AccessPropertiesRule(
new AccessPropertiesCheck(
$reflectionProvider,
$ruleLevelHelper,
new PhpVersion(PHP_VERSION_ID),
new NonStringableDynamicAccessCheck($ruleLevelHelper, true),
reportMagicProperties: true,
checkDynamicProperties: false,
),
);
}

public function testBug15421(): void
{
$this->analyse([__DIR__ . '/../../Analyser/data/bug-15421.php'], []);
}

public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/../../Analyser/expression-type-resolver-extension.neon',
];
}

}
26 changes: 26 additions & 0 deletions turbo-ext/src/MutatingScope.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10549,6 +10549,32 @@ class MutatingScope
ZVAL_OBJ(&exprZv, expr);
bool isVariable;
if (UNEXPECTED(!isInstance(zv::Ref(&exprZv), PT_CLASS_VARIABLE, isVariable))) return zv::Val();
if (!isVariable) {
/* mirror resolveType(): an extension's answer wins over both the
* tracked holder and the reflection-derived read below */
zv::Val askScope = native ? thisDoNotTreatPhpDocTypesAsCertain() : self_();
if (UNEXPECTED(askScope.isUndef())) return zv::Val();
zv::Ref extensions = service(PT_MSS_EXPRESSION_TYPE_RESOLVER_EXTENSIONS);
if (UNEXPECTED(!extensions.isObject())) return uninitializedProperty("expressionTypeResolverExtensions");
zv::Val all = pt_extensions_collection_get_all(extensions.asObject());
if (UNEXPECTED(all.isUndef())) return zv::Val();
if (EXPECTED(Z_TYPE_P(all.raw()) == IS_ARRAY)) {
zv::Args args{expr, askScope.raw()};
for (auto entry : zv::ArrRef(all.raw())) {
zv::Ref extension = entry.value().deref();
if (UNEXPECTED(!extension.isObject())) {
zend_throw_error(NULL, "Call to a member function getType() on %s", zend_zval_value_name(extension.raw()));
return zv::Val();
}
zv::Val type = pt_type_call(extension.asObject(), PT_LC("gettype"), 2, args);
if (UNEXPECTED(type.isUndef())) return zv::Val();
if (!type.isNull()) return type;
}
} else {
zend_error(E_WARNING, "foreach() argument must be of type array|object, %s given", zend_zval_value_name(all.raw()));
if (UNEXPECTED(EG(exception))) return zv::Val();
}
}
if (!isVariable) {
zv::Val has = thisHasExpressionType(&exprZv);
if (UNEXPECTED(has.isUndef())) return zv::Val();
Expand Down
Loading