diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 40895297fc8..74a0dfc4baf 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -41,6 +41,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; @@ -242,6 +243,12 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco new AccessoryNonEmptyStringType(), ]); + case 'non-falsy-string': + return new IntersectionType([ + new StringType(), + new AccessoryNonFalsyStringType(), + ]); + case 'bool': return new BooleanType(); diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 584c9570957..bfd1cf53218 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Accessory; use PHPStan\TrinaryLogic; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -144,6 +145,11 @@ public function toFloat(): Type return new FloatType(); } + public function toBoolean(): BooleanType + { + return new BooleanType(); + } + public function toString(): Type { return $this; diff --git a/src/Type/Accessory/AccessoryNonFalsyStringType.php b/src/Type/Accessory/AccessoryNonFalsyStringType.php new file mode 100644 index 00000000000..7637ce3595f --- /dev/null +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -0,0 +1,203 @@ +isAcceptedBy($this, $strictTypes); + } + + return $type->isNonEmptyString(); + } + + public function isSuperTypeOf(Type $type): TrinaryLogic + { + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + if ($this->equals($type)) { + return TrinaryLogic::createYes(); + } + + return $type->isNonEmptyString(); + } + + public function isSubTypeOf(Type $otherType): TrinaryLogic + { + if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOf($this); + } + + return $otherType->isNonEmptyString() + ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + { + return $this->isSubTypeOf($acceptingType); + } + + public function equals(Type $type): bool + { + return $type instanceof self; + } + + public function describe(VerbosityLevel $level): string + { + return 'non-falsy-string'; + } + + public function isOffsetAccessible(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function hasOffsetValueType(Type $offsetType): TrinaryLogic + { + return (new IntegerType())->isSuperTypeOf($offsetType)->and(TrinaryLogic::createMaybe()); + } + + public function getOffsetValueType(Type $offsetType): Type + { + if ($this->hasOffsetValueType($offsetType)->no()) { + return new ErrorType(); + } + + if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()) { + return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); + } + + return new StringType(); + } + + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); + } + + public function isArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function toNumber(): Type + { + return new ErrorType(); + } + + public function toInteger(): Type + { + return new IntegerType(); + } + + public function toFloat(): Type + { + return new FloatType(); + } + + public function toBoolean(): BooleanType + { + return new ConstantBooleanType(true); + } + + public function toString(): Type + { + return $this; + } + + public function toArray(): Type + { + return new ConstantArrayType( + [new ConstantIntegerType(0)], + [$this], + [1], + ); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNumericString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function traverse(callable $cb): Type + { + return $this; + } + + public function generalize(GeneralizePrecision $precision): Type + { + return new StringType(); + } + + public static function __set_state(array $properties): Type + { + return new self(); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php index 94c373f7ea8..6a24d7ea3a7 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php @@ -124,4 +124,16 @@ public function testBug6473(): void $this->analyse([__DIR__ . '/data/bug-6473.php'], []); } + public function testBug5317(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-5317.php'], [ + [ + 'Negated boolean expression is always false.', + 18, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5317.php b/tests/PHPStan/Rules/Comparison/data/bug-5317.php new file mode 100644 index 00000000000..3fb92724981 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5317.php @@ -0,0 +1,19 @@ +