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 phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ parameters:
count: 1
path: src/Analyser/ExprHandler/Helper/IncDecTypeHelper.php

-
rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated.
identifier: phpstanApi.instanceofType
count: 1
path: src/Analyser/Generics/TemplateArgumentObserver.php

-
rawMessage: 'Doing instanceof PHPStan\Type\ObjectShapeType is error-prone and deprecated. Use Type::isObject() and Type::hasProperty() instead.'
identifier: phpstanApi.instanceofType
count: 2
path: src/Analyser/Generics/TemplateArgumentObserver.php

-
rawMessage: Casting to string something that's already string.
identifier: cast.useless
Expand Down
47 changes: 47 additions & 0 deletions src/Analyser/Generics/TemplateArgumentObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PhpParser\Node\Expr;
use PHPStan\Analyser\OutOfClassScope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\MissingPropertyFromReflectionException;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\ResolvedFunctionVariant;
use PHPStan\Turbo\ShadowedByTurboExtension;
Expand All @@ -14,8 +15,10 @@
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\Generic\UnresolvedTemplateArgumentType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectShapeType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeTraverser;
Expand Down Expand Up @@ -451,6 +454,18 @@
// never holds no markers, and is its own iterable key and value type
return $constraints;
}
if ($declared instanceof ObjectShapeType) {
return $this->observeObjectShapeSend($constraints, $declared, $actual, $isCallArgument);
}
if ($declared instanceof IntersectionType) {
// object{...}&Foo - the shape's properties are bounds next to Foo's arguments
foreach ($declared->getTypes() as $member) {
if (!$member instanceof ObjectShapeType) {
continue;
}
$constraints = $this->observeObjectShapeSend($constraints, $member, $actual, $isCallArgument);
}
}

$actualReflections = $actual->getObjectClassReflections();
if (count($actualReflections) === 1) {
Expand Down Expand Up @@ -506,6 +521,38 @@
return $constraints;
}

/**
* Each property of the object flows into the declared property of the shape
* (`(object) ['x' => new Foo(1)]` returned as `object{x: Foo<int>}`).
*/
private function observeObjectShapeSend(TemplateArgumentConstraints $constraints, ObjectShapeType $declared, Type $actual, bool $isCallArgument): TemplateArgumentConstraints
{
if (!$actual->isObject()->yes()) {

Check warning on line 530 in src/Analyser/Generics/TemplateArgumentObserver.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ */ private function observeObjectShapeSend(TemplateArgumentConstraints $constraints, ObjectShapeType $declared, Type $actual, bool $isCallArgument): TemplateArgumentConstraints { - if (!$actual->isObject()->yes()) { + if ($actual->isObject()->no()) { return $constraints; }

Check warning on line 530 in src/Analyser/Generics/TemplateArgumentObserver.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ */ private function observeObjectShapeSend(TemplateArgumentConstraints $constraints, ObjectShapeType $declared, Type $actual, bool $isCallArgument): TemplateArgumentConstraints { - if (!$actual->isObject()->yes()) { + if ($actual->isObject()->no()) { return $constraints; }
return $constraints;
}

$scope = new OutOfClassScope();
foreach ($declared->getProperties() as $name => $declaredPropertyType) {
$name = (string) $name;
if (!$actual->hasInstanceProperty($name)->yes()) {

Check warning on line 537 in src/Analyser/Generics/TemplateArgumentObserver.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $scope = new OutOfClassScope(); foreach ($declared->getProperties() as $name => $declaredPropertyType) { $name = (string) $name; - if (!$actual->hasInstanceProperty($name)->yes()) { + if ($actual->hasInstanceProperty($name)->no()) { continue; }

Check warning on line 537 in src/Analyser/Generics/TemplateArgumentObserver.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $scope = new OutOfClassScope(); foreach ($declared->getProperties() as $name => $declaredPropertyType) { $name = (string) $name; - if (!$actual->hasInstanceProperty($name)->yes()) { + if ($actual->hasInstanceProperty($name)->no()) { continue; }
continue;
}

try {
$actualProperty = $actual->getInstanceProperty($name, $scope);
} catch (MissingPropertyFromReflectionException) {
continue;
}
if (!$actualProperty->isPublic() || $actualProperty->isStatic()) {
continue;
}

$constraints = $this->observeSend($constraints, $declaredPropertyType, $actualProperty->getReadableType(), $isCallArgument);
}

return $constraints;
}

/**
* A template argument of an object sent to the declared argument at the same
* position. A union of several sites' markers (Box<T|U> of zip(of(1), of(2)))
Expand Down
5 changes: 5 additions & 0 deletions tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,9 @@ public function testYieldOversizedSelfRejection(): void
$this->analyse([__DIR__ . '/data/bug-yield-oversized-self-rejection.php'], []);
}

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

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

namespace Bug15417Generator;

/**
* @template TKey of array-key
* @template-covariant TValue
*/
final class Collection
{
/** @param iterable<TKey, TValue> $a */
public function __construct($a) { }
}

final class Repro
{

/** @return \Generator<int, object{x: Collection<int, string>}> */
public static function gen(): \Generator
{
yield (object) ['x' => new Collection(['a', 'b'])];
}

}
6 changes: 6 additions & 0 deletions tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1156,4 +1156,10 @@ public function testBug11421(): void
]);
}

public function testBug15417(): void
{
$this->checkThisOnly = false;
$this->analyse([__DIR__ . '/data/bug-15417-argument.php'], []);
}

}
17 changes: 17 additions & 0 deletions tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1419,4 +1419,21 @@ public function testBug13325(): void
$this->analyse([__DIR__ . '/data/bug-13325.php'], []);
}

#[RequiresPhp('>= 8.2.0')]
public function testBug15417(): void
{
$this->analyse([__DIR__ . '/data/bug-15417.php'], []);
}

public function testBug15417Shapes(): void
{
$this->analyse([__DIR__ . '/data/bug-15417-shapes.php'], [
[
'Method Bug15417Shapes\\Repro::stillReported() should return object{x: Bug15417Shapes\\Collection<string, string>} but returns object{x: Bug15417Shapes\\Collection<0|1, \'a\'|\'b\'>}&stdClass.',
51,
'Property ($x) type Bug15417Shapes\\Collection<string, string> does not accept type Bug15417Shapes\\Collection<0|1, \'a\'|\'b\'>.',
],
]);
}

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

namespace Bug15417Argument;

/**
* @template TKey of array-key
* @template-covariant TValue
*/
final class Collection
{
/** @param iterable<TKey, TValue> $a */
public function __construct($a) { }
}

final class Repro
{

/** @param object{x: Collection<int, string>} $o */
public static function take(object $o): void
{
}

public static function doFoo(): void
{
self::take((object) ['x' => new Collection(['a', 'b'])]);
}

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

namespace Bug15417Shapes;

/**
* @template TKey of array-key
* @template-covariant TValue
*/
final class Collection
{
/** @param iterable<TKey, TValue> $a */
public function __construct($a) { }
}

final class Repro
{

/** @return object{x: Collection<int, string>} */
public static function shape(): object
{
return (object) ['x' => new Collection(['a', 'b'])];
}

/** @return object{y: object{x: Collection<int, string>}} */
public static function nested(): object
{
return (object) ['y' => (object) ['x' => new Collection(['a', 'b'])]];
}

/** @return object{x: Collection<int, string>}|null */
public static function nullable(bool $b): ?object
{
return $b ? (object) ['x' => new Collection(['a', 'b'])] : null;
}

/** @return object{x: Collection<int, string>}&\stdClass */
public static function intersection(): object
{
return (object) ['x' => new Collection(['a', 'b'])];
}

/** @return list<object{x: Collection<int, string>, y?: Collection<int, int>}> */
public static function optional(): array
{
return [(object) ['x' => new Collection(['a', 'b'])]];
}

/** @return object{x: Collection<string, string>} */
public static function stillReported(): object
{
return (object) ['x' => new Collection(['a', 'b'])];
}

}
33 changes: 33 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-15417.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php // lint >= 8.2

declare(strict_types = 1);

namespace Bug15417;

class Repro
{
/** @return Collection<int, object{ch: string, x: Collection<int, object{t: string}>}> */
public static function get(string $s): Collection
{
$data = [];
foreach (explode(',', $s) as $char) {
$c = array_map(fn($x) => (object)['t' => $x], ['a', 'b', 'c', 'd']);
if ($s !== 'z') {
$data[] = (object)['ch' => $char, 'x' => new Collection((new \Random\Randomizer)->shuffleArray($c))];
}
}

return new Collection($data);
}
}

/**
* @template TKey of array-key
* @template-covariant TValue (THEA fix by FixLarastanStubs.php)
*/
class Collection
{
/** @param iterable<TKey, TValue> $a */
public function __construct($a) { echo gettype($a); }
}

Original file line number Diff line number Diff line change
Expand Up @@ -1145,4 +1145,9 @@ public function testIntersectionWithTemplateMixed(): void
]);
}

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

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

namespace Bug15417Property;

/**
* @template TKey of array-key
* @template-covariant TValue
*/
final class Collection
{
/** @param iterable<TKey, TValue> $a */
public function __construct($a) { }
}

final class Repro
{

/** @var object{x: Collection<int, string>} */
public object $p;

public function doFoo(): void
{
$this->p = (object) ['x' => new Collection(['a', 'b'])];
}

}
61 changes: 61 additions & 0 deletions turbo-ext/src/TemplateArgumentObserver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,19 @@ class TemplateArgumentObserver
}
if (isMarker(actual)) return constraints;
if (isInstance(actual, pt_ce_never_type)) return constraints;
if (isInstance(declared, pt_ce_object_shape_type)) {
return observeObjectShapeSend(std::move(constraints), declared, actual, isCallArgument);
}
if (isInstance(declared, pt_ce_intersection_type)) {
zv::Val types = pt_type_op(Z_OBJ_P(declared), PT_OP_GET_TYPES, 0, NULL);
if (UNEXPECTED(types.isUndef())) return zv::Val();
for (auto entry : zv::ArrRef(types.raw())) {
zval *member = entry.value().deref().raw();
if (!isInstance(member, pt_ce_object_shape_type)) continue;
constraints = observeObjectShapeSend(std::move(constraints), member, actual, isCallArgument);
if (UNEXPECTED(constraints.isUndef())) return zv::Val();
}
}

zv::Val actualReflections = objectClassReflections(actual);
if (UNEXPECTED(actualReflections.isUndef())) return zv::Val();
Expand Down Expand Up @@ -842,6 +855,54 @@ class TemplateArgumentObserver
return observeSend(std::move(constraints), declaredValue.raw(), actualValue.raw(), isCallArgument);
}

/* Mirrors observeObjectShapeSend(). */
static zv::Val observeObjectShapeSend(zv::Val constraints, zval *declared, zval *actual, bool isCallArgument)
{
bool yes;
if (UNEXPECTED(!trinaryYes(actual, PT_LC("isobject"), yes))) return zv::Val();
if (!yes) return constraints;

zv::Val scope = pt_type_new(PT_CLASS_OUT_OF_CLASS_SCOPE, 0, NULL);
if (UNEXPECTED(scope.isUndef())) return zv::Val();
zv::Val properties = call0(declared, PT_LC("getproperties"), "getProperties");
if (UNEXPECTED(properties.isUndef())) return zv::Val();
if (UNEXPECTED(Z_TYPE_P(properties.raw()) != IS_ARRAY)) {
zend_type_error("phpstan_turbo: getProperties() must return array");
return zv::Val();
}
for (auto entry : zv::ArrRef(properties.raw())) {
/* (string) $name */
zv::Val name = entry.hasStringKey()
? zv::Val::string(entry.stringKey())
: zv::Val::adoptString(zend_long_to_str((zend_long) entry.indexKey()));
zend_long hasProperty = pt_type_call_trinary(Z_OBJ_P(actual), PT_LC("hasinstanceproperty"), 1, name.raw());
if (UNEXPECTED(hasProperty < 0)) return zv::Val();
if (hasProperty != PT_TRI_YES) continue;

zv::Args args{name.raw(), scope.raw()};
zv::Val actualProperty = pt_type_call(Z_OBJ_P(actual), PT_LC("getinstanceproperty"), 2, args);
if (UNEXPECTED(actualProperty.isUndef())) {
zend_class_entry *missingCe = pt_class(PT_CLASS_MISSING_PROPERTY_FROM_REFLECTION_EXCEPTION);
if (EG(exception) == NULL || missingCe == NULL || !instanceof_function(EG(exception)->ce, missingCe)) return zv::Val();
zend_clear_exception();
continue;
}
zv::Val flag = call0(actualProperty.raw(), PT_LC("ispublic"), "isPublic");
if (UNEXPECTED(flag.isUndef())) return zv::Val();
if (!zend_is_true(flag.raw())) continue;
flag = call0(actualProperty.raw(), PT_LC("isstatic"), "isStatic");
if (UNEXPECTED(flag.isUndef())) return zv::Val();
if (zend_is_true(flag.raw())) continue;

zv::Val actualPropertyType = call0(actualProperty.raw(), PT_LC("getreadabletype"), "getReadableType");
if (UNEXPECTED(actualPropertyType.isUndef())) return zv::Val();
constraints = observeSend(std::move(constraints), entry.value().deref().raw(), actualPropertyType.raw(), isCallArgument);
if (UNEXPECTED(constraints.isUndef())) return zv::Val();
}

return constraints;
}

/* Mirrors observeArgumentSend(). */
static zv::Val observeArgumentSend(zv::Val constraints, zval *declaredArgument, zval *argument, zval *variance, bool isCallArgument)
{
Expand Down
Loading
Loading