Skip to content
Merged
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
2 changes: 1 addition & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1678,7 +1678,7 @@ parameters:
-
rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated.
identifier: phpstanApi.instanceofType
count: 8
count: 7
path: src/Type/TypeCombinator.php

-
Expand Down
71 changes: 64 additions & 7 deletions src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -420,22 +420,27 @@ public static function doUnion(Type ...$types): Type
$enumCaseTypes = [];
$integerRangeTypes = [];
for ($i = 0; $i < $typesCount; $i++) {
// a constant scalar refined by an accessory type ('Foo'&hasMethod(bar))
// stays with the other types so it is compared against the plain
// constant scalars and other refinements of the same value
if (!$types[$i] instanceof IntersectionType && $types[$i]->isConstantScalarValue()->yes()) {
if ($types[$i]->isConstantScalarValue()->yes()) {
$type = $types[$i];
$scalarTypes[get_class($type)][$type->describe(VerbosityLevel::cache())] = $type;
unset($types[$i]);
continue;
}

// only a type containing every value of the scalar (not T of int, 1&T)
// absorbs all the constants of that scalar
if ($types[$i]->isBoolean()->yes()) {
$hasGenericScalarTypes[ConstantBooleanType::class] = true;
if ($types[$i]->isSuperTypeOf(new BooleanType())->yes()) {
$hasGenericScalarTypes[ConstantBooleanType::class] = true;
}
} elseif ($types[$i]->isFloat()->yes()) {
$hasGenericScalarTypes[ConstantFloatType::class] = true;
if ($types[$i]->isSuperTypeOf(new FloatType())->yes()) {
$hasGenericScalarTypes[ConstantFloatType::class] = true;
}
} elseif ($types[$i]->isInteger()->yes() && !$types[$i] instanceof IntegerRangeType) {
$hasGenericScalarTypes[ConstantIntegerType::class] = true;
if ($types[$i]->isSuperTypeOf(new IntegerType())->yes()) {
$hasGenericScalarTypes[ConstantIntegerType::class] = true;
}
} elseif ($types[$i]->isString()->yes() && $types[$i]->isClassString()->no() && TypeUtils::getAccessoryTypes($types[$i]) === []) {
$hasGenericScalarTypes[ConstantStringType::class] = true;
} else {
Expand Down Expand Up @@ -474,6 +479,10 @@ public static function doUnion(Type ...$types): Type
$types = array_merge(array_values($types), $integerRangeTypes);
$typesCount = count($types);

if (isset($scalarTypes[IntersectionType::class])) {
$scalarTypes[IntersectionType::class] = self::reduceRefinedConstantScalars($scalarTypes[IntersectionType::class], $scalarTypes);
}

foreach ($scalarTypes as $classType => $scalarTypeItems) {
if (isset($hasGenericScalarTypes[$classType])) {
unset($scalarTypes[$classType]);
Expand Down Expand Up @@ -703,6 +712,54 @@ private static function isAlreadyNormalized(array $alreadyNormalized, Type $a, T
return false;
}

/**
* Scalar buckets are compared only against the non-scalar union members, which
* is correct for plain constants (distinct values are disjoint) but not for
* constants refined by another type ('Foo'&hasMethod(bar)). Those are absorbed
* here by the plain constant of the same value, or by a less refined
* intersection of the same value. Values never need to be compared across.
*
* @param array<string, Type> $intersections
* @param array<string, array<string, Type>> $scalarTypes
* @return array<string, Type>
*/
private static function reduceRefinedConstantScalars(array $intersections, array $scalarTypes): array
{
$byValue = [];
foreach ($intersections as $key => $intersection) {
$constants = $intersection->getConstantScalarTypes();
if (count($constants) !== 1) {
continue;
}
$valueKey = $constants[0]->describe(VerbosityLevel::cache());
$plain = $scalarTypes[get_class($constants[0])][$valueKey] ?? null;
if ($plain !== null && $plain->isSuperTypeOf($intersection)->yes()) {
unset($intersections[$key]);
continue;
}
$byValue[get_class($constants[0])][$valueKey][] = $key;
}

foreach ($byValue as $valueKeys) {
foreach ($valueKeys as $keys) {
$keysCount = count($keys);
for ($i = 0; $i < $keysCount; $i++) {
for ($j = 0; $j < $keysCount; $j++) {
if ($i === $j || !isset($intersections[$keys[$i]], $intersections[$keys[$j]])) {
continue;
}
if (!$intersections[$keys[$i]]->isSuperTypeOf($intersections[$keys[$j]])->yes()) {
continue;
}
unset($intersections[$keys[$j]]);
}
}
}
}

return $intersections;
}

/**
* @return array{Type, null}|array{null, Type}|null
*/
Expand Down
27 changes: 27 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15591.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,30 @@ function testProperty(): void
}
assertType("'Bug15591\\\\Foo'", $class);
}

/**
* @template T
* @template TString of string
* @template TBool of bool
* @param T $x
* @param TString $s
* @param TBool $b
*/
function testRefinedConstantKeepsOtherConstants($x, $s, $b): void
{
assertType('2|(1&T (function Bug15591\testRefinedConstantKeepsOtherConstants(), argument))', $x === 1 ? $x : 2);
assertType("'b'|('a'&TString of string (function Bug15591\\testRefinedConstantKeepsOtherConstants(), argument))", $s === 'a' ? $s : 'b');
assertType('(TBool of bool (function Bug15591\testRefinedConstantKeepsOtherConstants(), argument)&true)|false', $b === true ? $b : false);
}

function testAlreadyRefined(): void
{
$class = Foo::class;
if (method_exists($class, 'm1')) {
foreach (['m2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9', 'm10', 'm11', 'm12', 'm13'] as $method) {
if (method_exists($class, $method)) {
}
}
assertType("'Bug15591\\\\Foo'&hasMethod(m1)", $class);
}
}
24 changes: 24 additions & 0 deletions tests/PHPStan/Analyser/nsrt/union-template-scalar-constant.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php declare(strict_types = 1);

namespace UnionTemplateScalarConstant;

use function PHPStan\Testing\assertType;

/**
* @template TInt of int
* @template TBool of bool
* @template TFloat of float
* @param TInt $i
* @param TBool $b
* @param TFloat $f
*/
function test($i, $b, $f, bool $c, int $int, bool $bool, float $float): void
{
assertType('5|TInt of int (function UnionTemplateScalarConstant\test(), argument)', $c ? $i : 5);
assertType('TBool of bool (function UnionTemplateScalarConstant\test(), argument)|true', $c ? $b : true);
assertType('1.5|TFloat of float (function UnionTemplateScalarConstant\test(), argument)', $c ? $f : 1.5);

assertType('int', $c ? $int : 5);
assertType('bool', $c ? $bool : true);
assertType('float', $c ? $float : 1.5);
}
23 changes: 23 additions & 0 deletions tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,29 @@ public function testBug11507(): void
$this->analyse([__DIR__ . '/data/bug-11507.php'], []);
}

public function testBug6567(): void
{
$this->checkNullables = true;
$this->checkExplicitMixed = false;
$this->analyse([__DIR__ . '/data/bug-6567.php'], [
[
'Function Bug6567\add() should return T of int but returns int.',
11,
'Type int is not always the same as T. It breaks the contract for some argument types, typically subtypes.',
],
[
'Function Bug6567\testBool() should return T of bool but returns false.',
27,
'Type false is not always the same as T. It breaks the contract for some argument types, typically subtypes.',
],
[
'Function Bug6567\testFloat() should return T of float but returns float.',
36,
'Type float is not always the same as T. It breaks the contract for some argument types, typically subtypes.',
],
]);
}

public function testBug15245(): void
{
$this->checkNullables = true;
Expand Down
37 changes: 37 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-6567.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php declare(strict_types = 1);

namespace Bug6567;

/**
* @template T of int
* @param T $p
* @return T
*/
function add($p) {
return $p + 1000;
}

/**
* @param int<0,100> $p
*/
function test($p): void {
\PHPStan\dumpType(add($p));
}

/**
* @template T of bool
* @param T $p
* @return T
*/
function testBool($p) {
return false;
}

/**
* @template T of float
* @param T $p
* @return T
*/
function testFloat($p) {
return $p + 1;
}
106 changes: 98 additions & 8 deletions turbo-ext/src/TypeCombinator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,12 @@ static zv::Val booleanType()
return adopted(pt_boolean_type_new(&raw), raw);
}

static zv::Val floatType()
{
zval raw;
return adopted(pt_float_type_new(&raw), raw);
}

static zv::Val stringType()
{
zval raw;
Expand Down Expand Up @@ -1038,11 +1044,8 @@ class TypeCombinator
Level cacheLevel(PT_LC("cache"));
for (size_t i = 0; i < typesCount; i++) {
zval *type = types[i].raw();
zend_long isConstantScalarValue = PT_TRI_NO;
if (!isInstance(type, pt_ce_intersection_type)) {
isConstantScalarValue = callTrinary(type, PT_LC("isconstantscalarvalue"));
PT_FAIL_IF_NEG(isConstantScalarValue);
}
zend_long isConstantScalarValue = callTrinary(type, PT_LC("isconstantscalarvalue"));
PT_FAIL_IF_NEG(isConstantScalarValue);
if (isConstantScalarValue == PT_TRI_YES) {
zv::Val description = describe(type, cacheLevel);
zend_string *key = describedKey(description);
Expand All @@ -1054,17 +1057,29 @@ class TypeCombinator
zend_long isBoolean = callTrinary(type, PT_LC("isboolean"));
PT_FAIL_IF_NEG(isBoolean);
if (isBoolean == PT_TRI_YES) {
hasGenericBoolean = true;
zv::Val generic = booleanType();
PT_FAIL_IF_UNDEF(generic);
int covers = isSuperTypeYes(type, generic.raw());
PT_FAIL_IF_NEG(covers);
if (covers) hasGenericBoolean = true;
} else {
zend_long isFloat = callTrinary(type, PT_LC("isfloat"));
PT_FAIL_IF_NEG(isFloat);
if (isFloat == PT_TRI_YES) {
hasGenericFloat = true;
zv::Val generic = floatType();
PT_FAIL_IF_UNDEF(generic);
int covers = isSuperTypeYes(type, generic.raw());
PT_FAIL_IF_NEG(covers);
if (covers) hasGenericFloat = true;
} else {
zend_long isInteger = callTrinary(type, PT_LC("isinteger"));
PT_FAIL_IF_NEG(isInteger);
if (isInteger == PT_TRI_YES && !isInstance(type, pt_ce_integer_range_type)) {
hasGenericInteger = true;
zv::Val generic = integerType();
PT_FAIL_IF_UNDEF(generic);
int covers = isSuperTypeYes(type, generic.raw());
PT_FAIL_IF_NEG(covers);
if (covers) hasGenericInteger = true;
} else {
int plainString = isPlainString(type);
PT_FAIL_IF_NEG(plainString);
Expand Down Expand Up @@ -1109,6 +1124,14 @@ class TypeCombinator
}
typesCount = types.size();

for (ScalarGroup &group : scalarTypes) {
if (group.ce != pt_ce_intersection_type) continue;
zv::Val reduced = reduceRefinedConstantScalars(group.items.raw(), scalarTypes, cacheLevel);
PT_FAIL_IF_UNDEF(reduced);
group.items = zv::Arr::adoptVal(std::move(reduced));
break;
}

for (ScalarGroup &group : scalarTypes) {
zend_class_entry *classType = group.ce;
if ((classType == pt_ce_constant_boolean_type && hasGenericBoolean)
Expand Down Expand Up @@ -1264,6 +1287,73 @@ class TypeCombinator
types.insert(types.begin() + (ptrdiff_t) i, std::make_move_iterator(inner.begin()), std::make_move_iterator(inner.end()));
}

/* Mirrors reduceRefinedConstantScalars(): the constant-scalar
* intersections without those absorbed by the plain constant of the same
* value or by a less refined intersection of the same value */
static zv::Val reduceRefinedConstantScalars(zval *intersections, std::vector<ScalarGroup> &groups, Level &cacheLevel)
{
struct Refined
{
zend_string *key;
zv::Val type;
zend_class_entry *valueCe;
zv::Val valueDescription;
bool removed;
};
std::vector<Refined> entries;
for (zv::ArrayEntry entry : zv::ArrRef(intersections)) {
zval *intersection = entry.value().raw();
entries.push_back({ entry.stringKeyOrNull(), copy(intersection), NULL, zv::Val(), false });
Refined &refined = entries.back();
zv::Val constants = call(intersection, PT_LC("getconstantscalartypes"));
PT_FAIL_IF_UNDEF(constants);
if (countOf(constants.raw()) != 1) continue;
zval *constant = indexOf(constants.raw(), 0);
if (UNEXPECTED(constant == NULL)) {
zend_throw_error(NULL, "Undefined array key 0");
return zv::Val();
}
zv::Val description = describe(constant, cacheLevel);
zend_string *valueKey = describedKey(description);
if (UNEXPECTED(valueKey == NULL)) return zv::Val();
for (ScalarGroup &group : groups) {
if (group.ce != Z_OBJCE_P(constant)) continue;
zv::Ref plain = group.items.arrRef().find(valueKey);
if (plain.raw() == NULL) break;
int absorbed = isSuperTypeYes(plain.raw(), intersection);
PT_FAIL_IF_NEG(absorbed);
refined.removed = absorbed != 0;
break;
}
if (refined.removed) continue;
refined.valueCe = Z_OBJCE_P(constant);
refined.valueDescription = std::move(description);
}

for (size_t i = 0; i < entries.size(); i++) {
if (entries[i].valueCe == NULL) continue;
for (size_t j = 0; j < entries.size(); j++) {
if (i == j || entries[i].removed || entries[j].removed) continue;
if (entries[j].valueCe != entries[i].valueCe) continue;
if (!zend_string_equals(Z_STR_P(entries[i].valueDescription.raw()), Z_STR_P(entries[j].valueDescription.raw()))) continue;
int absorbed = isSuperTypeYes(entries[i].type.raw(), entries[j].type.raw());
PT_FAIL_IF_NEG(absorbed);
if (absorbed) entries[j].removed = true;
}
}

zv::Arr result = zv::Arr::create((uint32_t) entries.size());
for (Refined &refined : entries) {
if (refined.removed) continue;
if (UNEXPECTED(refined.key == NULL)) {
zend_throw_error(NULL, "phpstan_turbo: a scalar group key must be a string");
return zv::Val();
}
result.set(refined.key, std::move(refined.type));
}
return zv::Val(std::move(result));
}

/* $scalarTypes[$ce][$key] = $type */
static void scalarGroupSet(std::vector<ScalarGroup> &groups, zend_class_entry *ce, zend_string *key, zv::Val type)
{
Expand Down
Loading
Loading