Skip to content
8 changes: 7 additions & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,12 @@ parameters:
count: 1
path: src/Type/Accessory/AccessoryArrayListType.php

-
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.'
identifier: phpstanApi.instanceofType
count: 1
path: src/Type/Accessory/AccessoryDecimalIntegerStringType.php

-
rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated.
identifier: phpstanApi.instanceofType
Expand Down Expand Up @@ -1662,7 +1668,7 @@ parameters:
-
rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated.
identifier: phpstanApi.instanceofType
count: 6
count: 7
path: src/Type/TypeCombinator.php

-
Expand Down
16 changes: 14 additions & 2 deletions src/Type/Accessory/AccessoryDecimalIntegerStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\ErrorType;
use PHPStan\Type\FloatType;
use PHPStan\Type\GeneralizePrecision;
Expand All @@ -27,7 +28,6 @@
use PHPStan\Type\Traits\NonGenericTypeTrait;
use PHPStan\Type\Traits\NonIterableTypeTrait;
use PHPStan\Type\Traits\NonObjectTypeTrait;
use PHPStan\Type\Traits\NonRemoveableTypeTrait;
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
Expand Down Expand Up @@ -55,7 +55,6 @@ class AccessoryDecimalIntegerStringType implements CompoundType, AccessoryType
use NonIterableTypeTrait;
use UndecidedComparisonCompoundTypeTrait;
use NonGenericTypeTrait;
use NonRemoveableTypeTrait;

/** @api */
public function __construct(private bool $inverse = false)
Expand Down Expand Up @@ -203,6 +202,19 @@ public function unsetOffset(Type $offsetType): Type
return new ErrorType();
}

public function tryRemove(Type $typeToRemove): ?Type
{
if ($this->inverse) {
return null;
}

if ($typeToRemove instanceof ConstantStringType && $typeToRemove->getValue() === '0') {
return new IntersectionType([new StringType(), $this, new AccessoryNonFalsyStringType()]);
}

return null;
}

public function toNumber(): Type
{
if ($this->inverse) {
Expand Down
58 changes: 44 additions & 14 deletions src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType;
use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
use PHPStan\Type\Accessory\AccessoryType;
use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
use PHPStan\Type\Accessory\HasOffsetType;
Expand Down Expand Up @@ -600,13 +601,9 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array
}

if ($a->getValue() === '0') {
$description = $b->describe(VerbosityLevel::value());
if ($description === 'non-falsy-string') {
return [null, new IntersectionType([
new StringType(),
new AccessoryNonEmptyStringType(),
...self::getAccessoryCaseStringTypes($b),
])];
$nonEmpty = self::downgradeNonFalsyStringToNonEmpty($b);
if ($nonEmpty !== null) {
return [null, $nonEmpty];
}
}
}
Expand All @@ -625,13 +622,9 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array
}

if ($b->getValue() === '0') {
$description = $a->describe(VerbosityLevel::value());
if ($description === 'non-falsy-string') {
return [new IntersectionType([
new StringType(),
new AccessoryNonEmptyStringType(),
...self::getAccessoryCaseStringTypes($a),
]), null];
$nonEmpty = self::downgradeNonFalsyStringToNonEmpty($a);
if ($nonEmpty !== null) {
return [$nonEmpty, null];
}
}
}
Expand Down Expand Up @@ -673,6 +666,43 @@ private static function getAccessoryCaseStringTypes(Type $type): array
return $accessory;
}

/**
* Turns a non-falsy-string type into its non-empty-string counterpart by
* downgrading the non-falsy accessory while preserving every other accessory
* (numeric-string, decimal-int-string, lowercase-string, …). Used to simplify
* `'0' | non-falsy-string-X` back to `non-empty-string-X`, since `"0"` is the
* only value that separates the two. Returns null when $type is not a
* non-constant non-falsy-string built from an intersection.
*/
private static function downgradeNonFalsyStringToNonEmpty(Type $type): ?Type
{
if (!$type instanceof IntersectionType || $type->isNonFalsyString()->no()) {
return null;
}

$newTypes = [];
$found = false;
foreach ($type->getTypes() as $innerType) {
if ($innerType instanceof AccessoryNonFalsyStringType) {
$found = true;
continue;
}

$newTypes[] = $innerType;
}

if (!$found) {
return null;
}

$withoutNonFalsy = self::intersect(...$newTypes);
if ($withoutNonFalsy->isNonEmptyString()->yes()) {
return $withoutNonFalsy;
}

return self::intersect($withoutNonFalsy, new AccessoryNonEmptyStringType());
}

private static function removeDecimalIntStringAccessory(Type $type): Type
{
if (!$type instanceof IntersectionType) {
Expand Down
114 changes: 114 additions & 0 deletions tests/PHPStan/Analyser/nsrt/decimal-int-string.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,118 @@ public function emptyStringIsNonDecimal(string $s): void
}
}

/**
* @param decimal-int-string $s
*/
public function removingZeroMakesNonFalsy(string $s): void
{
if ($s !== '0') {
assertType('decimal-int-string&non-falsy-string', $s);
} else {
assertType("'0'", $s);
}

if ($s != '0') {
assertType('decimal-int-string&non-falsy-string', $s);
}

if ($s) {
assertType('decimal-int-string&non-falsy-string', $s);
} else {
assertType("'0'", $s);
}
}

/**
* @param numeric-string $s
*/
public function removingZeroMakesNumericNonFalsy(string $s): void
{
if ($s !== '0') {
assertType('non-falsy-string&numeric-string', $s);
} else {
assertType("'0'", $s);
}
}

/**
* @param non-decimal-int-string $s
*/
public function removingZeroFromNonDecimal(string $s): void
{
// '0' is a decimal-int-string, so it is not part of non-decimal-int-string
if ($s !== '0') {
assertType('non-decimal-int-string', $s);
}
}

/**
* @param decimal-int-string $s
*/
public function unionWithZeroRoundTrips(string $s): void
{
$t = $s !== '0' ? $s : '0';
assertType('decimal-int-string', $t);
}
Comment thread
staabm marked this conversation as resolved.

/**
* @param numeric-string $s
*/
public function numericUnionWithZeroRoundTrips(string $s): void
{
$t = $s !== '0' ? $s : '0';
assertType('numeric-string', $t);
Comment thread
staabm marked this conversation as resolved.
}

/**
* @param non-empty-string $s
*/
public function nonEmptyUnionWithZeroRoundTrips(string $s): void
{
$t = $s !== '0' ? $s : '0';
assertType('non-empty-string', $t);
}

/**
* @param uppercase-string $s
*/
public function removingZeroFromUppercase(string $s): void
{
// '' is a falsy uppercase-string too, so removing '0' alone does not
// make an uppercase-string non-falsy.
if ($s !== '0') {
assertType('uppercase-string', $s);
}
}

/**
* @param uppercase-string $s
*/
public function uppercaseUnionWithZeroRoundTrips(string $s): void
{
$t = $s !== '0' ? $s : '0';
assertType('uppercase-string', $t);
}

/**
* @param lowercase-string $s
*/
public function removingZeroFromLowercase(string $s): void
{
// '' is a falsy lowercase-string too, so removing '0' alone does not
// make a lowercase-string non-falsy.
if ($s !== '0') {
assertType('lowercase-string', $s);
}
}

/**
* @param lowercase-string $s
*/
public function lowercaseUnionWithZeroRoundTrips(string $s): void
{
$t = $s !== '0' ? $s : '0';
assertType('lowercase-string', $t);
}

}
Loading