From 53c29e9261cce3b8401a74823b571b6a32e065e8 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 12 Sep 2024 18:40:21 +0200 Subject: [PATCH 01/26] Introduce lowercase-string --- src/PhpDoc/TypeNodeResolver.php | 11 +- src/Type/Accessory/AccessoryArrayListType.php | 5 + .../Accessory/AccessoryLiteralStringType.php | 5 + .../AccessoryLowercaseStringType.php | 378 ++++++++++++++++++ .../Accessory/AccessoryNonEmptyStringType.php | 5 + .../Accessory/AccessoryNonFalsyStringType.php | 5 + .../Accessory/AccessoryNumericStringType.php | 5 + src/Type/Accessory/HasOffsetType.php | 5 + src/Type/Accessory/HasOffsetValueType.php | 5 + src/Type/Accessory/NonEmptyArrayType.php | 5 + src/Type/Accessory/OversizedArrayType.php | 5 + src/Type/ArrayType.php | 5 + src/Type/CallableType.php | 5 + src/Type/ClassStringType.php | 5 + src/Type/ClosureType.php | 5 + src/Type/Constant/ConstantStringType.php | 5 + src/Type/FloatType.php | 5 + src/Type/IntersectionType.php | 5 + src/Type/IterableType.php | 5 + src/Type/JustNullableTypeTrait.php | 5 + src/Type/MixedType.php | 17 + src/Type/NeverType.php | 5 + src/Type/NullType.php | 5 + src/Type/ObjectType.php | 5 + src/Type/StaticType.php | 5 + src/Type/StrictMixedType.php | 5 + src/Type/StringType.php | 5 + src/Type/Traits/LateResolvableTypeTrait.php | 5 + src/Type/Traits/ObjectTypeTrait.php | 5 + src/Type/Type.php | 2 + src/Type/UnionType.php | 5 + src/Type/VoidType.php | 5 + 32 files changed, 547 insertions(+), 1 deletion(-) create mode 100644 src/Type/Accessory/AccessoryLowercaseStringType.php diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index c0fa6c5585..06098d7938 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -46,6 +46,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; @@ -216,9 +217,11 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco ]); case 'string': - case 'lowercase-string': return new StringType(); + case 'lowercase-string': + return new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); + case 'literal-string': return new IntersectionType([new StringType(), new AccessoryLiteralStringType()]); @@ -287,10 +290,16 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco ]); case 'non-empty-string': + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + case 'non-empty-lowercase-string': return new IntersectionType([ new StringType(), new AccessoryNonEmptyStringType(), + new AccessoryLowercaseStringType(), ]); case 'truthy-string': diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index 511087818d..d32cc8882c 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -387,6 +387,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index dd4af579ac..110fbea58c 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -297,6 +297,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/AccessoryLowercaseStringType.php b/src/Type/Accessory/AccessoryLowercaseStringType.php new file mode 100644 index 0000000000..0e66c5d021 --- /dev/null +++ b/src/Type/Accessory/AccessoryLowercaseStringType.php @@ -0,0 +1,378 @@ +acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + { + if ($type instanceof MixedType) { + return AcceptsResult::createNo(); + } + if ($type instanceof CompoundType) { + return $type->isAcceptedWithReasonBy($this, $strictTypes); + } + + return new AcceptsResult($type->isLiteralString(), []); + } + + public function isSuperTypeOf(Type $type): TrinaryLogic + { + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + if ($this->equals($type)) { + return TrinaryLogic::createYes(); + } + + return $type->isLiteralString(); + } + + public function isSubTypeOf(Type $otherType): TrinaryLogic + { + if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOf($this); + } + + return $otherType->isLiteralString() + ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + { + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return new AcceptsResult($this->isSubTypeOf($acceptingType), []); + } + + public function equals(Type $type): bool + { + return $type instanceof self; + } + + public function describe(VerbosityLevel $level): string + { + return 'lowercase-string'; + } + + public function isOffsetAccessible(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function hasOffsetValueType(Type $offsetType): TrinaryLogic + { + return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); + } + + public function getOffsetValueType(Type $offsetType): Type + { + if ($this->hasOffsetValueType($offsetType)->no()) { + return new ErrorType(); + } + + return new StringType(); + } + + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + $stringOffset = (new StringType())->setOffsetValueType($offsetType, $valueType, $unionValues); + + if ($stringOffset instanceof ErrorType) { + return $stringOffset; + } + + if ($valueType->isLowercaseString()->yes()) { + return $this; + } + + return new StringType(); + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); + } + + public function toNumber(): Type + { + return new ErrorType(); + } + + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + + public function toInteger(): Type + { + return new IntegerType(); + } + + public function toFloat(): Type + { + return new FloatType(); + } + + public function toString(): Type + { + return $this; + } + + public function toBoolean(): BooleanType + { + return new BooleanType(); + } + + public function toArray(): Type + { + return new ConstantArrayType( + [new ConstantIntegerType(0)], + [$this], + [1], + [], + TrinaryLogic::createYes(), + ); + } + + public function toArrayKey(): Type + { + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNumericString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isClassStringType(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ObjectWithoutClassType(); + } + + public function hasMethod(string $methodName): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + + public function traverse(callable $cb): Type + { + return $this; + } + + public function traverseSimultaneously(Type $right, 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(); + } + + public function exponentiate(Type $exponent): Type + { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode('lowercase-string'); + } + +} diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 16566ee530..b911c21fbb 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -294,6 +294,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/AccessoryNonFalsyStringType.php b/src/Type/Accessory/AccessoryNonFalsyStringType.php index dacccd3e31..5703420e9f 100644 --- a/src/Type/Accessory/AccessoryNonFalsyStringType.php +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -294,6 +294,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index 9d37625612..9cb274782d 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -296,6 +296,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index 5ed39d9797..2194a10cef 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -297,6 +297,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index ae1ef84f56..853645c91a 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -353,6 +353,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index 1f2abd69b6..293e1f111f 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -364,6 +364,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Accessory/OversizedArrayType.php b/src/Type/Accessory/OversizedArrayType.php index 2e9da3ae05..fa64240e89 100644 --- a/src/Type/Accessory/OversizedArrayType.php +++ b/src/Type/Accessory/OversizedArrayType.php @@ -360,6 +360,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index d2eaf1bc24..4543e36b77 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -358,6 +358,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index ce30d8983c..82b3645a27 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -591,6 +591,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/ClassStringType.php b/src/Type/ClassStringType.php index 802d17e162..eaca9d4572 100644 --- a/src/Type/ClassStringType.php +++ b/src/Type/ClassStringType.php @@ -69,6 +69,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index 967b850525..e55847aae0 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -713,6 +713,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Constant/ConstantStringType.php b/src/Type/Constant/ConstantStringType.php index fcd467fbbb..4c42ce01d4 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -343,6 +343,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean(strtolower($this->value) === $this->value); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { if ($offsetType->isInteger()->yes()) { diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index 2111d4d912..0ffa96e002 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -229,6 +229,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 8bf38bddda..7a81ed039c 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -637,6 +637,11 @@ public function isLiteralString(): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString()); } + public function isLowercaseString(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLowercaseString()); + } + public function isClassStringType(): TrinaryLogic { return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isClassStringType()); diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index f36cab2e7f..6ef8ff5ee3 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -372,6 +372,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/JustNullableTypeTrait.php b/src/Type/JustNullableTypeTrait.php index 294b7d1d2b..7f48131262 100644 --- a/src/Type/JustNullableTypeTrait.php +++ b/src/Type/JustNullableTypeTrait.php @@ -147,6 +147,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 865611056f..f72e8f3d76 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -21,6 +21,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; @@ -916,6 +917,22 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isLowercaseString(): TrinaryLogic + { + if ($this->subtractedType !== null) { + $lowercaseString = TypeCombinator::intersect( + new StringType(), + new AccessoryLowercaseStringType(), + ); + + if ($this->subtractedType->isSuperTypeOf($lowercaseString)->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { if ($this->subtractedType !== null) { diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index 5d488d319c..1523562cab 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -476,6 +476,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/NullType.php b/src/Type/NullType.php index a59f86e868..ab50c2040e 100644 --- a/src/Type/NullType.php +++ b/src/Type/NullType.php @@ -295,6 +295,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 1732c3c627..b756e3f77d 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -1045,6 +1045,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index bb55a6c9fa..e0d2924951 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -565,6 +565,11 @@ public function isLiteralString(): TrinaryLogic return $this->getStaticObjectType()->isLiteralString(); } + public function isLowercaseString(): TrinaryLogic + { + return $this->getStaticObjectType()->isLowercaseString(); + } + public function isClassStringType(): TrinaryLogic { return $this->getStaticObjectType()->isClassStringType(); diff --git a/src/Type/StrictMixedType.php b/src/Type/StrictMixedType.php index cc114d5c34..00a9141233 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -275,6 +275,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 5fe9ae444a..9fcd1e1895 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -240,6 +240,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index bff88f730e..fb03a4d170 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -452,6 +452,11 @@ public function isLiteralString(): TrinaryLogic return $this->resolve()->isLiteralString(); } + public function isLowercaseString(): TrinaryLogic + { + return $this->resolve()->isLowercaseString(); + } + public function isClassStringType(): TrinaryLogic { return $this->resolve()->isClassStringType(); diff --git a/src/Type/Traits/ObjectTypeTrait.php b/src/Type/Traits/ObjectTypeTrait.php index 0c37e439c7..8ca9c69c29 100644 --- a/src/Type/Traits/ObjectTypeTrait.php +++ b/src/Type/Traits/ObjectTypeTrait.php @@ -198,6 +198,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Type.php b/src/Type/Type.php index 727e2f8a31..398529994e 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -261,6 +261,8 @@ public function isNonFalsyString(): TrinaryLogic; public function isLiteralString(): TrinaryLogic; + public function isLowercaseString(): TrinaryLogic; + public function isClassStringType(): TrinaryLogic; public function isVoid(): TrinaryLogic; diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index af4ede1b40..08b8d2c186 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -612,6 +612,11 @@ public function isLiteralString(): TrinaryLogic return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString()); } + public function isLowercaseString(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isLowercaseString()); + } + public function isClassStringType(): TrinaryLogic { return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isClassStringType()); diff --git a/src/Type/VoidType.php b/src/Type/VoidType.php index 8cbd1076ba..add4ffca45 100644 --- a/src/Type/VoidType.php +++ b/src/Type/VoidType.php @@ -207,6 +207,11 @@ public function isLiteralString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); From 148ee3070affc9191720d57f39833fb2a4a9e6cf Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 13 Sep 2024 09:09:21 +0200 Subject: [PATCH 02/26] Update signature --- resources/functionMap.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index b79549a45d..80c57788d6 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -6361,7 +6361,7 @@ 'mb_strripos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], 'mb_strrpos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], 'mb_strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'part='=>'bool', 'encoding='=>'string'], -'mb_strtolower' => ['string', 'str'=>'string', 'encoding='=>'string'], +'mb_strtolower' => ['lowercase-string', 'str'=>'string', 'encoding='=>'string'], 'mb_strtoupper' => ['string', 'str'=>'string', 'encoding='=>'string'], 'mb_strwidth' => ['0|positive-int', 'str'=>'string', 'encoding='=>'string'], 'mb_substitute_character' => ['mixed', 'substchar='=>'mixed'], @@ -12085,7 +12085,7 @@ 'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'mixed', 'before_needle='=>'bool'], 'strtok' => ['non-empty-string|false', 'str'=>'string', 'token'=>'string'], 'strtok\'1' => ['non-empty-string|false', 'token'=>'string'], -'strtolower' => ['string', 'str'=>'string'], +'strtolower' => ['lowercase-string', 'str'=>'string'], 'strtotime' => ['int|false', 'time'=>'string', 'now='=>'int'], 'strtoupper' => ['string', 'str'=>'string'], 'strtr' => ['string', 'str'=>'string', 'from'=>'string', 'to'=>'string'], From 85180dc1073578a90d6d697ae41669f0ba1a86bf Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 13 Sep 2024 09:10:19 +0200 Subject: [PATCH 03/26] Update StrCaseReturnType --- .../StrCaseFunctionsReturnTypeExtension.php | 38 +++++++++++-------- tests/PHPStan/Analyser/nsrt/str-casing.php | 37 ++++++++++++------ 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php index 39355b579e..01f2fb8006 100644 --- a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php +++ b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php @@ -5,6 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; @@ -65,9 +66,14 @@ public function getTypeFromFunctionCall( } $modes = []; + $keepLowercase = false; + $forceLowercase = false; + if ($fnName === 'mb_convert_case') { $modeType = $scope->getType($args[1]->value); $modes = array_map(static fn ($mode) => $mode->getValue(), TypeUtils::getConstantIntegers($modeType)); + $forceLowercase = count(array_diff($modes, [MB_CASE_LOWER, MB_CASE_LOWER_SIMPLE])) === 0; + $keepLowercase = count(array_diff($modes, [MB_CASE_LOWER, MB_CASE_LOWER_SIMPLE, MB_CASE_FOLD, MB_CASE_FOLD_SIMPLE])) === 0; } elseif (in_array($fnName, ['ucwords', 'mb_convert_kana'], true)) { if (count($args) >= 2) { $modeType = $scope->getType($args[1]->value); @@ -75,6 +81,10 @@ public function getTypeFromFunctionCall( } else { $modes = $fnName === 'mb_convert_kana' ? ['KV'] : [" \t\r\n\f\v"]; } + } elseif (in_array($fnName, ['strtolower', 'mb_strtolower'])) { + $forceLowercase = true; + } elseif (in_array($fnName, ['lcfirst', 'mb_lcfirst'])) { + $keepLowercase = true; } $constantStrings = array_map(static fn ($type) => $type->getValue(), $argType->getConstantStrings()); @@ -101,25 +111,23 @@ public function getTypeFromFunctionCall( } } - if ($argType->isNumericString()->yes()) { - return new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]); + $accessoryTypes = []; + if ($forceLowercase || ($keepLowercase && $argType->isLowercaseString()->yes())) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); } - if ($argType->isNonFalsyString()->yes()) { - return new IntersectionType([ - new StringType(), - new AccessoryNonFalsyStringType(), - ]); + if ($argType->isNumericString()->yes()) { + $accessoryTypes[] = new AccessoryNumericStringType(); + } elseif ($argType->isNonFalsyString()->yes()) { + $accessoryTypes[] = new AccessoryNonFalsyStringType(); + } elseif ($argType->isNonEmptyString()->yes()) { + $accessoryTypes[] = new AccessoryNonEmptyStringType(); } - if ($argType->isNonEmptyString()->yes()) { - return new IntersectionType([ - new StringType(), - new AccessoryNonEmptyStringType(), - ]); + if (count($accessoryTypes) > 0) { + $accessoryTypes[] = new StringType(); + + return new IntersectionType($accessoryTypes); } return new StringType(); diff --git a/tests/PHPStan/Analyser/nsrt/str-casing.php b/tests/PHPStan/Analyser/nsrt/str-casing.php index df4883845a..3f0c76f250 100644 --- a/tests/PHPStan/Analyser/nsrt/str-casing.php +++ b/tests/PHPStan/Analyser/nsrt/str-casing.php @@ -8,13 +8,14 @@ class Foo { /** * @param numeric-string $numericS * @param non-empty-string $nonE + * @param lowercase-string $lowercaseS * @param literal-string $literal * @param 'foo'|'Foo' $edgeUnion * @param MB_CASE_UPPER|MB_CASE_LOWER|MB_CASE_TITLE|MB_CASE_FOLD|MB_CASE_UPPER_SIMPLE|MB_CASE_LOWER_SIMPLE|MB_CASE_TITLE_SIMPLE|MB_CASE_FOLD_SIMPLE $caseMode * @param 'aKV'|'hA'|'AH'|'K'|'KV'|'RNKV' $kanaMode * @param mixed $mixed */ - public function bar($numericS, $nonE, $literal, $edgeUnion, $caseMode, $kanaMode, $mixed) { + public function bar($numericS, $nonE, $lowercaseS, $literal, $edgeUnion, $caseMode, $kanaMode, $mixed) { assertType("'abc'", strtolower('ABC')); assertType("'ABC'", strtoupper('abc')); assertType("'abc'", mb_strtolower('ABC')); @@ -37,51 +38,65 @@ public function bar($numericS, $nonE, $literal, $edgeUnion, $caseMode, $kanaMode assertType("'Abc123アガば漢'|'Abc123あか゛ば漢'|'Abc123アカ゛ば漢'|'Abc123アガば漢'|'Abc123アガバ漢'", mb_convert_kana('Abc123アガば漢', $kanaMode)); assertType("non-falsy-string", mb_convert_kana('Abc123アガば漢', $mixed)); - assertType("numeric-string", strtolower($numericS)); + assertType("lowercase-string&numeric-string", strtolower($numericS)); assertType("numeric-string", strtoupper($numericS)); - assertType("numeric-string", mb_strtolower($numericS)); + assertType("lowercase-string&numeric-string", mb_strtolower($numericS)); assertType("numeric-string", mb_strtoupper($numericS)); assertType("numeric-string", lcfirst($numericS)); assertType("numeric-string", ucfirst($numericS)); assertType("numeric-string", ucwords($numericS)); assertType("numeric-string", mb_convert_case($numericS, MB_CASE_UPPER)); - assertType("numeric-string", mb_convert_case($numericS, MB_CASE_LOWER)); + assertType("lowercase-string&numeric-string", mb_convert_case($numericS, MB_CASE_LOWER)); assertType("numeric-string", mb_convert_case($numericS, $mixed)); assertType("numeric-string", mb_convert_kana($numericS)); assertType("numeric-string", mb_convert_kana($numericS, $mixed)); - assertType("non-empty-string", strtolower($nonE)); + assertType("lowercase-string&non-empty-string", strtolower($nonE)); assertType("non-empty-string", strtoupper($nonE)); - assertType("non-empty-string", mb_strtolower($nonE)); + assertType("lowercase-string&non-empty-string", mb_strtolower($nonE)); assertType("non-empty-string", mb_strtoupper($nonE)); assertType("non-empty-string", lcfirst($nonE)); assertType("non-empty-string", ucfirst($nonE)); assertType("non-empty-string", ucwords($nonE)); assertType("non-empty-string", mb_convert_case($nonE, MB_CASE_UPPER)); - assertType("non-empty-string", mb_convert_case($nonE, MB_CASE_LOWER)); + assertType("lowercase-string&non-empty-string", mb_convert_case($nonE, MB_CASE_LOWER)); assertType("non-empty-string", mb_convert_case($nonE, $mixed)); assertType("non-empty-string", mb_convert_kana($nonE)); assertType("non-empty-string", mb_convert_kana($nonE, $mixed)); - assertType("string", strtolower($literal)); + assertType("lowercase-string", strtolower($literal)); assertType("string", strtoupper($literal)); - assertType("string", mb_strtolower($literal)); + assertType("lowercase-string", mb_strtolower($literal)); assertType("string", mb_strtoupper($literal)); assertType("string", lcfirst($literal)); assertType("string", ucfirst($literal)); assertType("string", ucwords($literal)); assertType("string", mb_convert_case($literal, MB_CASE_UPPER)); - assertType("string", mb_convert_case($literal, MB_CASE_LOWER)); + assertType("lowercase-string", mb_convert_case($literal, MB_CASE_LOWER)); assertType("string", mb_convert_case($literal, $mixed)); assertType("string", mb_convert_kana($literal)); assertType("string", mb_convert_kana($literal, $mixed)); + assertType("lowercase-string", strtolower($lowercaseS)); + assertType("string", strtoupper($lowercaseS)); + assertType("lowercase-string", mb_strtolower($lowercaseS)); + assertType("string", mb_strtoupper($lowercaseS)); + assertType("lowercase-string", lcfirst($lowercaseS)); + assertType("string", ucfirst($lowercaseS)); + assertType("string", ucwords($lowercaseS)); + assertType("string", mb_convert_case($lowercaseS, MB_CASE_UPPER)); + assertType("lowercase-string", mb_convert_case($lowercaseS, MB_CASE_LOWER)); + assertType("string", mb_convert_case($lowercaseS, $mixed)); + assertType("lowercase-string", mb_convert_case($lowercaseS, rand(0, 1) ? MB_CASE_LOWER : MB_CASE_LOWER_SIMPLE)); + assertType("string", mb_convert_kana($lowercaseS)); + assertType("string", mb_convert_kana($lowercaseS, $mixed)); + assertType("'foo'", lcfirst($edgeUnion)); } public function foo() { // invalid char conversions still lead to non-falsy-string - assertType("non-falsy-string", mb_strtolower("\xfe\xff\x65\xe5\x67\x2c\x8a\x9e", 'CP1252')); + assertType("lowercase-string&non-falsy-string", mb_strtolower("\xfe\xff\x65\xe5\x67\x2c\x8a\x9e", 'CP1252')); // valid char sequence, but not support non ASCII / UTF-8 encodings assertType("non-falsy-string", mb_convert_kana("\x95\x5c\x8c\xbb", 'SJIS-win')); // invalid UTF-8 sequence From 716942ef51b211686085d6c39586f3dc751e8aad Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 13 Sep 2024 10:06:11 +0200 Subject: [PATCH 04/26] Fix tests --- src/Rules/Api/ApiInstanceofTypeRule.php | 2 ++ src/Type/Constant/ConstantStringType.php | 5 +++++ src/Type/IntersectionType.php | 3 +++ .../Php/StrCaseFunctionsReturnTypeExtension.php | 14 ++++++++++++-- src/Type/VerbosityLevel.php | 2 ++ .../Analyser/LegacyNodeScopeResolverTest.php | 2 +- tests/PHPStan/Analyser/ScopeTest.php | 6 +++--- tests/PHPStan/Analyser/nsrt/bug-2911.php | 2 +- tests/PHPStan/Analyser/nsrt/bug7856.php | 2 +- tests/PHPStan/Analyser/nsrt/non-empty-string.php | 8 ++++---- tests/PHPStan/Analyser/nsrt/non-falsy-string.php | 4 ++-- tests/PHPStan/PhpDoc/TypeDescriptionTest.php | 2 ++ tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php | 2 +- .../Type/Constant/ConstantStringTypeTest.php | 4 ++-- .../Type/Constant/OversizedArrayBuilderTest.php | 4 ++-- tests/PHPStan/Type/TypeToPhpDocNodeTest.php | 8 +++++++- 16 files changed, 50 insertions(+), 20 deletions(-) diff --git a/src/Rules/Api/ApiInstanceofTypeRule.php b/src/Rules/Api/ApiInstanceofTypeRule.php index 1ae24aa0ca..225e65b000 100644 --- a/src/Rules/Api/ApiInstanceofTypeRule.php +++ b/src/Rules/Api/ApiInstanceofTypeRule.php @@ -11,6 +11,7 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; @@ -84,6 +85,7 @@ final class ApiInstanceofTypeRule implements Rule AccessoryArrayListType::class => 'Type::isList()', AccessoryNumericStringType::class => 'Type::isNumericString()', AccessoryLiteralStringType::class => 'Type::isLiteralString()', + AccessoryLowercaseStringType::class => 'Type::isLowercaseString()', AccessoryNonEmptyStringType::class => 'Type::isNonEmptyString()', AccessoryNonFalsyStringType::class => 'Type::isNonFalsyString()', HasMethodType::class => 'Type::hasMethod()', diff --git a/src/Type/Constant/ConstantStringType.php b/src/Type/Constant/ConstantStringType.php index 4c42ce01d4..6468944221 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -20,6 +20,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; @@ -455,6 +456,10 @@ public function generalize(GeneralizePrecision $precision): Type $accessories[] = new AccessoryNonEmptyStringType(); } + if (strtolower($this->getValue()) === $this->getValue()) { + $accessories[] = new AccessoryLowercaseStringType(); + } + return new IntersectionType($accessories); } diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 7a81ed039c..49647a52cd 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -21,6 +21,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; @@ -328,6 +329,7 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) || $type instanceof AccessoryLiteralStringType || $type instanceof AccessoryNumericStringType || $type instanceof AccessoryNonFalsyStringType + || $type instanceof AccessoryLowercaseStringType ) { if ($type instanceof AccessoryNonFalsyStringType) { $nonFalsyStr = true; @@ -1119,6 +1121,7 @@ public function toPhpDocNode(): TypeNode || $type instanceof AccessoryLiteralStringType || $type instanceof AccessoryNumericStringType || $type instanceof AccessoryNonFalsyStringType + || $type instanceof AccessoryLowercaseStringType ) { if ($type instanceof AccessoryNonFalsyStringType) { $nonFalsyStr = true; diff --git a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php index 01f2fb8006..376080e3ef 100644 --- a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php +++ b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php @@ -72,8 +72,18 @@ public function getTypeFromFunctionCall( if ($fnName === 'mb_convert_case') { $modeType = $scope->getType($args[1]->value); $modes = array_map(static fn ($mode) => $mode->getValue(), TypeUtils::getConstantIntegers($modeType)); - $forceLowercase = count(array_diff($modes, [MB_CASE_LOWER, MB_CASE_LOWER_SIMPLE])) === 0; - $keepLowercase = count(array_diff($modes, [MB_CASE_LOWER, MB_CASE_LOWER_SIMPLE, MB_CASE_FOLD, MB_CASE_FOLD_SIMPLE])) === 0; + if (count($modes) > 0) { + $forceLowercase = count(array_diff($modes, [ + MB_CASE_LOWER, + MB_CASE_LOWER_SIMPLE, + ])) === 0; + $keepLowercase = count(array_diff($modes, [ + MB_CASE_LOWER, + MB_CASE_LOWER_SIMPLE, + MB_CASE_FOLD, + MB_CASE_FOLD_SIMPLE, + ])) === 0; + } } elseif (in_array($fnName, ['ucwords', 'mb_convert_kana'], true)) { if (count($args) >= 2) { $modeType = $scope->getType($args[1]->value); diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index d9724fc64d..086401052f 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -4,6 +4,7 @@ use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; @@ -101,6 +102,7 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc || $type instanceof AccessoryNonFalsyStringType || $type instanceof AccessoryLiteralStringType || $type instanceof AccessoryNumericStringType + || $type instanceof AccessoryLowercaseStringType || $type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType ) { diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 234bbb82d3..f03df64fa6 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -8093,7 +8093,7 @@ public function dataArrayKeysInBranches(): array '$arrayAppendedInForeach', ], [ - 'non-empty-array, literal-string&non-falsy-string>', // could be 'array, \'bar\'|\'baz\'|\'foo\'>' + 'non-empty-array, literal-string&lowercase-string&non-falsy-string>', // could be 'array, \'bar\'|\'baz\'|\'foo\'>' '$anotherArrayAppendedInForeach', ], [ diff --git a/tests/PHPStan/Analyser/ScopeTest.php b/tests/PHPStan/Analyser/ScopeTest.php index 99bba5ea9b..fe0644cd30 100644 --- a/tests/PHPStan/Analyser/ScopeTest.php +++ b/tests/PHPStan/Analyser/ScopeTest.php @@ -29,7 +29,7 @@ public function dataGeneralize(): array [ new ConstantStringType('a'), new ConstantStringType('b'), - 'literal-string&non-falsy-string', + 'literal-string&lowercase-string&non-falsy-string', ], [ new ConstantIntegerType(0), @@ -139,7 +139,7 @@ public function dataGeneralize(): array new ConstantIntegerType(1), new ConstantIntegerType(1), ]), - 'non-empty-array', + 'non-empty-array', ], [ new ConstantArrayType([ @@ -154,7 +154,7 @@ public function dataGeneralize(): array new ConstantIntegerType(1), new ConstantIntegerType(2), ]), - 'non-empty-array>', + 'non-empty-array>', ], [ new UnionType([ diff --git a/tests/PHPStan/Analyser/nsrt/bug-2911.php b/tests/PHPStan/Analyser/nsrt/bug-2911.php index 1544279ea2..1d75efdcbe 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-2911.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2911.php @@ -59,7 +59,7 @@ private function getResultSettings(array $settings): array $settings['remove'] = strtolower($settings['remove']); - assertType("non-empty-array&hasOffsetValue('remove', string)", $settings); + assertType("non-empty-array&hasOffsetValue('remove', lowercase-string)", $settings); if (!in_array($settings['remove'], ['first', 'last', 'all'], true)) { throw $this->configException($settings, 'remove'); diff --git a/tests/PHPStan/Analyser/nsrt/bug7856.php b/tests/PHPStan/Analyser/nsrt/bug7856.php index 0b48d36ebc..fcdca30b44 100644 --- a/tests/PHPStan/Analyser/nsrt/bug7856.php +++ b/tests/PHPStan/Analyser/nsrt/bug7856.php @@ -10,7 +10,7 @@ function doFoo() { $endDate = new DateTimeImmutable('+1year'); do { - assertType("array{'+1week', '+1months', '+6months', '+17months'}|array{0: literal-string&non-falsy-string, 1?: literal-string&non-falsy-string, 2?: '+17months'}", $intervals); + assertType("array{'+1week', '+1months', '+6months', '+17months'}|array{0: literal-string&lowercase-string&non-falsy-string, 1?: literal-string&lowercase-string&non-falsy-string, 2?: '+17months'}", $intervals); $periodEnd = $periodEnd->modify(array_shift($intervals)); } while (count($intervals) > 0 && $periodEnd->format('U') < $endDate); } diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string.php b/tests/PHPStan/Analyser/nsrt/non-empty-string.php index 78e536a735..c5554194f6 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string.php @@ -320,12 +320,12 @@ public function doFoo(string $s, string $nonEmpty, string $nonFalsy, int $i, boo assertType('string', strtoupper($s)); assertType('non-empty-string', strtoupper($nonEmpty)); - assertType('string', strtolower($s)); - assertType('non-empty-string', strtolower($nonEmpty)); + assertType('lowercase-string', strtolower($s)); + assertType('lowercase-string&non-empty-string', strtolower($nonEmpty)); assertType('string', mb_strtoupper($s)); assertType('non-empty-string', mb_strtoupper($nonEmpty)); - assertType('string', mb_strtolower($s)); - assertType('non-empty-string', mb_strtolower($nonEmpty)); + assertType('lowercase-string', mb_strtolower($s)); + assertType('lowercase-string&non-empty-string', mb_strtolower($nonEmpty)); assertType('string', lcfirst($s)); assertType('non-empty-string', lcfirst($nonEmpty)); assertType('string', ucfirst($s)); diff --git a/tests/PHPStan/Analyser/nsrt/non-falsy-string.php b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php index f8c6dc40b7..4127dd5870 100644 --- a/tests/PHPStan/Analyser/nsrt/non-falsy-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php @@ -88,9 +88,9 @@ function stringFunctions(string $s, $nonFalsey, $arrayOfNonFalsey, $nonEmptyArra assertType('non-falsy-string', escapeshellcmd($nonFalsey)); assertType('non-falsy-string', strtoupper($nonFalsey)); - assertType('non-falsy-string', strtolower($nonFalsey)); + assertType('lowercase-string&non-falsy-string', strtolower($nonFalsey)); assertType('non-falsy-string', mb_strtoupper($nonFalsey)); - assertType('non-falsy-string', mb_strtolower($nonFalsey)); + assertType('lowercase-string&non-falsy-string', mb_strtolower($nonFalsey)); assertType('non-falsy-string', lcfirst($nonFalsey)); assertType('non-falsy-string', ucfirst($nonFalsey)); assertType('non-falsy-string', ucwords($nonFalsey)); diff --git a/tests/PHPStan/PhpDoc/TypeDescriptionTest.php b/tests/PHPStan/PhpDoc/TypeDescriptionTest.php index 29c3a35231..7c0b2e9cc1 100644 --- a/tests/PHPStan/PhpDoc/TypeDescriptionTest.php +++ b/tests/PHPStan/PhpDoc/TypeDescriptionTest.php @@ -4,6 +4,7 @@ use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -29,6 +30,7 @@ public function dataTest(): iterable yield ['string', new StringType()]; yield ['array', new ArrayType(new MixedType(), new MixedType())]; yield ['literal-string', new IntersectionType([new StringType(), new AccessoryLiteralStringType()])]; + yield ['lowercase-string', new IntersectionType([new StringType(), new AccessoryLowercaseStringType()])]; yield ['non-empty-string', new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()])]; yield ['numeric-string', new IntersectionType([new StringType(), new AccessoryNumericStringType()])]; yield ['literal-string&non-empty-string', new IntersectionType([new StringType(), new AccessoryLiteralStringType(), new AccessoryNonEmptyStringType()])]; diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index 6d374a6f1c..b8627bfee5 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -834,7 +834,7 @@ public function testBug8146bErrors(): void $this->checkBenevolentUnionTypes = true; $this->analyse([__DIR__ . '/data/bug-8146b-errors.php'], [ [ - "Method Bug8146bError\LocationFixtures::getData() should return array, coordinates: array{lat: float, lng: float}}>> but returns array{Bács-Kiskun: array{Ágasegyháza: array{constituencies: array{'Bács-Kiskun 4.', true, false, Bug8146bError\X, null}, coordinates: array{lat: 46.8386043, lng: 19.4502899}}, Akasztó: array{constituencies: array{'Bács-Kiskun 3.'}, coordinates: array{lat: 46.6898175, lng: 19.205086}}, Apostag: array{constituencies: array{'Bács-Kiskun 3.'}, coordinates: array{lat: 46.8812652, lng: 18.9648478}}, Bácsalmás: array{constituencies: array{'Bács-Kiskun 5.'}, coordinates: array{lat: 46.1250396, lng: 19.3357509}}, Bácsbokod: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 46.1234737, lng: 19.155708}}, Bácsborsód: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 46.0989373, lng: 19.1566725}}, Bácsszentgyörgy: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 45.9746039, lng: 19.0398066}}, Bácsszőlős: array{constituencies: array{'Bács-Kiskun 5.'}, coordinates: array{lat: 46.1352003, lng: 19.4215997}}, ...}, Baranya: non-empty-array|(literal-string&non-falsy-string), float|(literal-string&non-falsy-string)>>>, Békés: array{Almáskamarás: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.4617785, lng: 21.092448}}, Battonya: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.2902462, lng: 21.0199215}}, Békés: array{constituencies: array{'Békés 2.'}, coordinates: array{lat: 46.6704899, lng: 21.0434996}}, Békéscsaba: array{constituencies: array{'Békés 1.'}, coordinates: array{lat: 46.6735939, lng: 21.0877309}}, Békéssámson: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.4208677, lng: 20.6176498}}, Békésszentandrás: array{constituencies: array{'Békés 2.'}, coordinates: array{lat: 46.8715996, lng: 20.48336}}, Bélmegyer: array{constituencies: array{'Békés 3.'}, coordinates: array{lat: 46.8726019, lng: 21.1832832}}, Biharugra: array{constituencies: array{'Békés 3.'}, coordinates: array{lat: 46.9691009, lng: 21.5987651}}, ...}, Borsod-Abaúj-Zemplén: non-empty-array|(literal-string&non-falsy-string), float|(literal-string&non-falsy-string)>>>, Budapest: array{Budapest I. ker.: array{constituencies: array{'Budapest 01.'}, coordinates: array{lat: 47.4968219, lng: 19.037458}}, Budapest II. ker.: array{constituencies: array{'Budapest 03.', 'Budapest 04.'}, coordinates: array{lat: 47.5393329, lng: 18.986934}}, Budapest III. ker.: array{constituencies: array{'Budapest 04.', 'Budapest 10.'}, coordinates: array{lat: 47.5671768, lng: 19.0368517}}, Budapest IV. ker.: array{constituencies: array{'Budapest 11.', 'Budapest 12.'}, coordinates: array{lat: 47.5648915, lng: 19.0913149}}, Budapest V. ker.: array{constituencies: array{'Budapest 01.'}, coordinates: array{lat: 47.5002319, lng: 19.0520181}}, Budapest VI. ker.: array{constituencies: array{'Budapest 05.'}, coordinates: array{lat: 47.509863, lng: 19.0625813}}, Budapest VII. ker.: array{constituencies: array{'Budapest 05.'}, coordinates: array{lat: 47.5027289, lng: 19.073376}}, Budapest VIII. ker.: array{constituencies: array{'Budapest 01.', 'Budapest 06.'}, coordinates: array{lat: 47.4894184, lng: 19.070668}}, ...}, Csongrád-Csanád: array{Algyő: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.3329625, lng: 20.207889}}, Ambrózfalva: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.3501417, lng: 20.7313995}}, Apátfalva: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.173317, lng: 20.5800472}}, Árpádhalom: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.6158286, lng: 20.547733}}, Ásotthalom: array{constituencies: array{'Csongrád-Csanád 2.'}, coordinates: array{lat: 46.1995983, lng: 19.7833756}}, Baks: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.5518708, lng: 20.1064166}}, Balástya: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.4261828, lng: 20.004933}}, Bordány: array{constituencies: array{'Csongrád-Csanád 2.'}, coordinates: array{lat: 46.3194213, lng: 19.9227063}}, ...}, Fejér: array{Aba: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 47.0328193, lng: 18.522359}}, Adony: array{constituencies: array{'Fejér 4.'}, coordinates: array{lat: 47.119831, lng: 18.8612469}}, Alap: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 46.8075763, lng: 18.684028}}, Alcsútdoboz: array{constituencies: array{'Fejér 3.'}, coordinates: array{lat: 47.4277067, lng: 18.6030325}}, Alsószentiván: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 46.7910573, lng: 18.732161}}, Bakonycsernye: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.321719, lng: 18.0907379}}, Bakonykúti: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.2458464, lng: 18.195769}}, Balinka: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.3135736, lng: 18.1907168}}, ...}, Győr-Moson-Sopron: array{Abda: array{constituencies: array{'Győr-Moson-Sopron 5.'}, coordinates: array{lat: 47.6962149, lng: 17.5445786}}, Acsalag: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.676095, lng: 17.1977771}}, Ágfalva: array{constituencies: array{'Győr-Moson-Sopron 4.'}, coordinates: array{lat: 47.688862, lng: 16.5110233}}, Agyagosszergény: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.608545, lng: 16.9409912}}, Árpás: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5134127, lng: 17.3931579}}, Ásványráró: array{constituencies: array{'Győr-Moson-Sopron 5.'}, coordinates: array{lat: 47.8287695, lng: 17.499195}}, Babót: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5752269, lng: 17.0758604}}, Bágyogszovát: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5866036, lng: 17.3617273}}, ...}, ...}.", + "Method Bug8146bError\LocationFixtures::getData() should return array, coordinates: array{lat: float, lng: float}}>> but returns array{Bács-Kiskun: array{Ágasegyháza: array{constituencies: array{'Bács-Kiskun 4.', true, false, Bug8146bError\X, null}, coordinates: array{lat: 46.8386043, lng: 19.4502899}}, Akasztó: array{constituencies: array{'Bács-Kiskun 3.'}, coordinates: array{lat: 46.6898175, lng: 19.205086}}, Apostag: array{constituencies: array{'Bács-Kiskun 3.'}, coordinates: array{lat: 46.8812652, lng: 18.9648478}}, Bácsalmás: array{constituencies: array{'Bács-Kiskun 5.'}, coordinates: array{lat: 46.1250396, lng: 19.3357509}}, Bácsbokod: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 46.1234737, lng: 19.155708}}, Bácsborsód: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 46.0989373, lng: 19.1566725}}, Bácsszentgyörgy: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 45.9746039, lng: 19.0398066}}, Bácsszőlős: array{constituencies: array{'Bács-Kiskun 5.'}, coordinates: array{lat: 46.1352003, lng: 19.4215997}}, ...}, Baranya: non-empty-array|(literal-string&lowercase-string&non-falsy-string), float|(literal-string&non-falsy-string)>>>, Békés: array{Almáskamarás: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.4617785, lng: 21.092448}}, Battonya: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.2902462, lng: 21.0199215}}, Békés: array{constituencies: array{'Békés 2.'}, coordinates: array{lat: 46.6704899, lng: 21.0434996}}, Békéscsaba: array{constituencies: array{'Békés 1.'}, coordinates: array{lat: 46.6735939, lng: 21.0877309}}, Békéssámson: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.4208677, lng: 20.6176498}}, Békésszentandrás: array{constituencies: array{'Békés 2.'}, coordinates: array{lat: 46.8715996, lng: 20.48336}}, Bélmegyer: array{constituencies: array{'Békés 3.'}, coordinates: array{lat: 46.8726019, lng: 21.1832832}}, Biharugra: array{constituencies: array{'Békés 3.'}, coordinates: array{lat: 46.9691009, lng: 21.5987651}}, ...}, Borsod-Abaúj-Zemplén: non-empty-array|(literal-string&lowercase-string&non-falsy-string), float|(literal-string&non-falsy-string)>>>, Budapest: array{Budapest I. ker.: array{constituencies: array{'Budapest 01.'}, coordinates: array{lat: 47.4968219, lng: 19.037458}}, Budapest II. ker.: array{constituencies: array{'Budapest 03.', 'Budapest 04.'}, coordinates: array{lat: 47.5393329, lng: 18.986934}}, Budapest III. ker.: array{constituencies: array{'Budapest 04.', 'Budapest 10.'}, coordinates: array{lat: 47.5671768, lng: 19.0368517}}, Budapest IV. ker.: array{constituencies: array{'Budapest 11.', 'Budapest 12.'}, coordinates: array{lat: 47.5648915, lng: 19.0913149}}, Budapest V. ker.: array{constituencies: array{'Budapest 01.'}, coordinates: array{lat: 47.5002319, lng: 19.0520181}}, Budapest VI. ker.: array{constituencies: array{'Budapest 05.'}, coordinates: array{lat: 47.509863, lng: 19.0625813}}, Budapest VII. ker.: array{constituencies: array{'Budapest 05.'}, coordinates: array{lat: 47.5027289, lng: 19.073376}}, Budapest VIII. ker.: array{constituencies: array{'Budapest 01.', 'Budapest 06.'}, coordinates: array{lat: 47.4894184, lng: 19.070668}}, ...}, Csongrád-Csanád: array{Algyő: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.3329625, lng: 20.207889}}, Ambrózfalva: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.3501417, lng: 20.7313995}}, Apátfalva: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.173317, lng: 20.5800472}}, Árpádhalom: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.6158286, lng: 20.547733}}, Ásotthalom: array{constituencies: array{'Csongrád-Csanád 2.'}, coordinates: array{lat: 46.1995983, lng: 19.7833756}}, Baks: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.5518708, lng: 20.1064166}}, Balástya: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.4261828, lng: 20.004933}}, Bordány: array{constituencies: array{'Csongrád-Csanád 2.'}, coordinates: array{lat: 46.3194213, lng: 19.9227063}}, ...}, Fejér: array{Aba: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 47.0328193, lng: 18.522359}}, Adony: array{constituencies: array{'Fejér 4.'}, coordinates: array{lat: 47.119831, lng: 18.8612469}}, Alap: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 46.8075763, lng: 18.684028}}, Alcsútdoboz: array{constituencies: array{'Fejér 3.'}, coordinates: array{lat: 47.4277067, lng: 18.6030325}}, Alsószentiván: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 46.7910573, lng: 18.732161}}, Bakonycsernye: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.321719, lng: 18.0907379}}, Bakonykúti: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.2458464, lng: 18.195769}}, Balinka: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.3135736, lng: 18.1907168}}, ...}, Győr-Moson-Sopron: array{Abda: array{constituencies: array{'Győr-Moson-Sopron 5.'}, coordinates: array{lat: 47.6962149, lng: 17.5445786}}, Acsalag: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.676095, lng: 17.1977771}}, Ágfalva: array{constituencies: array{'Győr-Moson-Sopron 4.'}, coordinates: array{lat: 47.688862, lng: 16.5110233}}, Agyagosszergény: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.608545, lng: 16.9409912}}, Árpás: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5134127, lng: 17.3931579}}, Ásványráró: array{constituencies: array{'Győr-Moson-Sopron 5.'}, coordinates: array{lat: 47.8287695, lng: 17.499195}}, Babót: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5752269, lng: 17.0758604}}, Bágyogszovát: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5866036, lng: 17.3617273}}, ...}, ...}.", 12, "Offset 'constituencies' (non-empty-list) does not accept type array{'Bács-Kiskun 4.', true, false, Bug8146bError\X, null}.", ], diff --git a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php index 976917737b..ae918cb9a5 100644 --- a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php @@ -153,8 +153,8 @@ public function testGeneralize(): void { $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType('NonexistentClass'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('literal-string', (new ConstantStringType(''))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType('a'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&non-empty-string&numeric-string', (new ConstantStringType('0'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string', (new ConstantStringType('a'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-empty-string&numeric-string', (new ConstantStringType('0'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('literal-string&non-falsy-string&numeric-string', (new ConstantStringType('1.123'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType(' 1 1 '))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('literal-string&non-falsy-string&numeric-string', (new ConstantStringType('+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); diff --git a/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php b/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php index e083852496..2e2bcf976c 100644 --- a/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php +++ b/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php @@ -28,7 +28,7 @@ public function dataBuild(): iterable yield [ '[1, 2, 3, ...[1, \'foo\' => 2, 3]]', - 'non-empty-array&oversized-array', + 'non-empty-array&oversized-array', ]; yield [ @@ -49,7 +49,7 @@ public function dataBuild(): iterable ]; yield [ '[1, \'foo\' => 2, 3]', - 'non-empty-array&oversized-array', + 'non-empty-array&oversized-array', ]; } diff --git a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php index 592271bdda..eebdb08014 100644 --- a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php +++ b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php @@ -6,6 +6,7 @@ use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -217,6 +218,11 @@ public function dataToPhpDocNode(): iterable 'literal-string', ]; + yield [ + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + 'lowercase-string', + ]; + yield [ new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), 'non-empty-string', @@ -324,7 +330,7 @@ public function dataToPhpDocNodeWithoutCheckingEquals(): iterable { yield [ new ConstantStringType("foo\nbar\nbaz"), - '(literal-string & non-falsy-string)', + '(literal-string & lowercase-string & non-falsy-string)', ]; yield [ From ff952eefee1fea9e3c8099779b59d8bd11e88b37 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 13 Sep 2024 11:08:58 +0200 Subject: [PATCH 05/26] More dynamicReturnTypes --- ...lodeFunctionDynamicReturnTypeExtension.php | 12 ++++++- .../ImplodeFunctionReturnTypeExtension.php | 4 +++ .../Php/SubstrDynamicReturnTypeExtension.php | 22 +++++++----- .../Analyser/LegacyNodeScopeResolverTest.php | 6 ++-- tests/PHPStan/Analyser/nsrt/bug-4711.php | 4 +-- .../nsrt/lowercase-string-implode.php | 36 +++++++++++++++++++ .../Analyser/nsrt/lowercase-string-subtr.php | 30 ++++++++++++++++ .../Analyser/nsrt/non-empty-string-substr.php | 2 +- .../Analyser/nsrt/non-empty-string.php | 2 +- ...rictComparisonOfDifferentTypesRuleTest.php | 2 +- .../Type/Constant/ConstantStringTypeTest.php | 12 +++---- 11 files changed, 108 insertions(+), 24 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/lowercase-string-implode.php create mode 100644 tests/PHPStan/Analyser/nsrt/lowercase-string-subtr.php diff --git a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php index 9927cc252e..ccad08cefb 100644 --- a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php @@ -7,6 +7,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -14,6 +15,7 @@ use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\StringType; @@ -54,7 +56,15 @@ public function getTypeFromFunctionCall( return new ConstantBooleanType(false); } - $returnType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new StringType())); + $stringType = $scope->getType($args[1]->value); + if ($stringType->isLowercaseString()->yes()) { + $returnValueType = new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); + } else { + $returnValueType = new StringType(); + } + + $returnType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $returnValueType)); + if ( !isset($args[2]) || IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($args[2]->value))->yes() diff --git a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php index bb29690bd6..d29e20a7cc 100644 --- a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php +++ b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Constant\ConstantArrayType; @@ -90,6 +91,9 @@ private function implode(Type $arrayType, Type $separatorType): Type if ($arrayType->getIterableValueType()->isLiteralString()->yes() && $separatorType->isLiteralString()->yes()) { $accessoryTypes[] = new AccessoryLiteralStringType(); } + if ($arrayType->getIterableValueType()->isLowercaseString()->yes() && $separatorType->isLowercaseString()->yes()) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); + } if (count($accessoryTypes) > 0) { $accessoryTypes[] = new StringType(); diff --git a/src/Type/Php/SubstrDynamicReturnTypeExtension.php b/src/Type/Php/SubstrDynamicReturnTypeExtension.php index f40ff3900d..5edc3f8df4 100644 --- a/src/Type/Php/SubstrDynamicReturnTypeExtension.php +++ b/src/Type/Php/SubstrDynamicReturnTypeExtension.php @@ -5,6 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -88,18 +89,21 @@ public function getTypeFromFunctionCall( return TypeCombinator::union(...$results); } + $accessoryTypes = []; + if ($string->isLowercaseString()->yes()) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); + } if ($string->isNonEmptyString()->yes() && ($negativeOffset || $zeroOffset && $positiveLength)) { if ($string->isNonFalsyString()->yes() && !$maybeOneLength) { - return new IntersectionType([ - new StringType(), - new AccessoryNonFalsyStringType(), - ]); - + $accessoryTypes[] = new AccessoryNonFalsyStringType(); + } else { + $accessoryTypes[] = new AccessoryNonEmptyStringType(); } - return new IntersectionType([ - new StringType(), - new AccessoryNonEmptyStringType(), - ]); + } + if (count($accessoryTypes) > 0) { + $accessoryTypes[] = new StringType(); + + return new IntersectionType($accessoryTypes); } return null; diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index f03df64fa6..0cb675d14b 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -1129,11 +1129,11 @@ public function dataArrayDestructuring(): array '$fourthStringArrayForeachList', ], [ - 'string', + 'lowercase-string', '$dateArray[\'Y\']', ], [ - 'string', + 'lowercase-string', '$dateArray[\'m\']', ], [ @@ -1141,7 +1141,7 @@ public function dataArrayDestructuring(): array '$dateArray[\'d\']', ], [ - 'string', + 'lowercase-string', '$intArrayForRewritingFirstElement[0]', ], [ diff --git a/tests/PHPStan/Analyser/nsrt/bug-4711.php b/tests/PHPStan/Analyser/nsrt/bug-4711.php index 46bc69565f..8fbed765a9 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4711.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4711.php @@ -12,8 +12,8 @@ function x(string $string): void { return; } - assertType('non-empty-list', explode($string, '')); - assertType('non-empty-list', explode($string[0], '')); + assertType('non-empty-list', explode($string, '')); + assertType('non-empty-list', explode($string[0], '')); } } diff --git a/tests/PHPStan/Analyser/nsrt/lowercase-string-implode.php b/tests/PHPStan/Analyser/nsrt/lowercase-string-implode.php new file mode 100644 index 0000000000..19fc961f1f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/lowercase-string-implode.php @@ -0,0 +1,36 @@ +', explode($s, 'foo')); + assertType('non-empty-list', explode($s, 'FOO')); + } +} + +class ImplodingStrings +{ + + /** + * @param lowercase-string $ls + * @param array $commonStrings + * @param array $lowercaseStrings + */ + public function doFoo(string $s, string $ls, array $commonStrings, array $lowercaseStrings): void + { + assertType('string', implode($s, $commonStrings)); + assertType('string', implode($s, $lowercaseStrings)); + assertType('string', implode($ls, $commonStrings)); + assertType('lowercase-string', implode($ls, $lowercaseStrings)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/lowercase-string-subtr.php b/tests/PHPStan/Analyser/nsrt/lowercase-string-subtr.php new file mode 100644 index 0000000000..b2a2ff43fa --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/lowercase-string-subtr.php @@ -0,0 +1,30 @@ += 8.0 + +namespace LowercaseStringSubstr; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @param lowercase-string $lowercase + */ + public function doSubstr(string $lowercase): void + { + assertType('lowercase-string', substr($lowercase, 5)); + assertType('lowercase-string', substr($lowercase, -5)); + assertType('lowercase-string', substr($lowercase, 0, 5)); + } + + /** + * @param lowercase-string $lowercase + */ + public function doMbSubstr(string $lowercase): void + { + assertType('lowercase-string', mb_substr($lowercase, 5)); + assertType('lowercase-string', mb_substr($lowercase, -5)); + assertType('lowercase-string', mb_substr($lowercase, 0, 5)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string-substr.php b/tests/PHPStan/Analyser/nsrt/non-empty-string-substr.php index c1f85c4df7..42dacb886f 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string-substr.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string-substr.php @@ -54,7 +54,7 @@ public function doMbSubstr(string $s, $nonEmpty, $positiveInt, $postiveRange, $n assertType('string', mb_substr($s, 0, $positiveInt)); assertType('non-empty-string', mb_substr($nonEmpty, 0, $positiveInt)); - assertType('non-empty-string', mb_substr("déjà_vu", 0, $positiveInt)); + assertType('lowercase-string&non-empty-string', mb_substr("déjà_vu", 0, $positiveInt)); assertType("'déjà_vu'", mb_substr("déjà_vu", 0)); assertType("'déj'", mb_substr("déjà_vu", 0, 3)); } diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string.php b/tests/PHPStan/Analyser/nsrt/non-empty-string.php index c5554194f6..19a5d6b96a 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string.php @@ -127,7 +127,7 @@ public function doFoo3(string $s): void */ public function doFoo4(string $s): void { - assertType('non-empty-list', explode($s, 'foo')); + assertType('non-empty-list', explode($s, 'foo')); } /** diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index 2bfd60e993..fe9b8fa079 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -566,7 +566,7 @@ public function testBug6939(): void $this->analyse([__DIR__ . '/data/bug-6939.php'], [ [ - 'Strict comparison using === between string and false will always evaluate to false.', + 'Strict comparison using === between lowercase-string and false will always evaluate to false.', 10, ], ]); diff --git a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php index ae918cb9a5..3845352fd6 100644 --- a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php @@ -155,12 +155,12 @@ public function testGeneralize(): void $this->assertSame('literal-string', (new ConstantStringType(''))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('literal-string&lowercase-string&non-falsy-string', (new ConstantStringType('a'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('literal-string&lowercase-string&non-empty-string&numeric-string', (new ConstantStringType('0'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&non-falsy-string&numeric-string', (new ConstantStringType('1.123'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType(' 1 1 '))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&non-falsy-string&numeric-string', (new ConstantStringType('+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType('+1+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&non-falsy-string&numeric-string', (new ConstantStringType('1e9'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType('1e91e9'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&numeric-string', (new ConstantStringType('1.123'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string', (new ConstantStringType(' 1 1 '))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&numeric-string', (new ConstantStringType('+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string', (new ConstantStringType('+1+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&numeric-string', (new ConstantStringType('1e9'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string', (new ConstantStringType('1e91e9'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('string', (new ConstantStringType(''))->generalize(GeneralizePrecision::lessSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('string', (new ConstantStringType('a'))->generalize(GeneralizePrecision::lessSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType(stdClass::class))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); From ba3ad199e0bfd0d287b8249c17994f008764ec14 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 13 Sep 2024 12:39:28 +0200 Subject: [PATCH 06/26] Fix --- src/Type/Accessory/AccessoryLowercaseStringType.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Type/Accessory/AccessoryLowercaseStringType.php b/src/Type/Accessory/AccessoryLowercaseStringType.php index 0e66c5d021..e15f200c1c 100644 --- a/src/Type/Accessory/AccessoryLowercaseStringType.php +++ b/src/Type/Accessory/AccessoryLowercaseStringType.php @@ -81,7 +81,7 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult return $type->isAcceptedWithReasonBy($this, $strictTypes); } - return new AcceptsResult($type->isLiteralString(), []); + return new AcceptsResult($type->isLowercaseString(), []); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -94,7 +94,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return TrinaryLogic::createYes(); } - return $type->isLiteralString(); + return $type->isLowercaseString(); } public function isSubTypeOf(Type $otherType): TrinaryLogic @@ -103,7 +103,7 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic return $otherType->isSuperTypeOf($this); } - return $otherType->isLiteralString() + return $otherType->isLowercaseString() ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); } @@ -148,7 +148,7 @@ public function getOffsetValueType(Type $offsetType): Type return new ErrorType(); } - return new StringType(); + return new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); } public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type From 9f303b02692df99a4c4131da2c176e10b4871f77 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 13 Sep 2024 13:18:28 +0200 Subject: [PATCH 07/26] Fix --- phpstan-baseline.neon | 5 +++++ src/Type/Php/StrCaseFunctionsReturnTypeExtension.php | 12 ++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 79f1b546ea..e5f34c65a2 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -716,6 +716,11 @@ parameters: count: 1 path: src/Type/Accessory/AccessoryNumericStringType.php + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/Accessory/AccessoryLowercaseStringType.php + - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" count: 1 diff --git a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php index 376080e3ef..f44f8789aa 100644 --- a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php +++ b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php @@ -75,13 +75,13 @@ public function getTypeFromFunctionCall( if (count($modes) > 0) { $forceLowercase = count(array_diff($modes, [ MB_CASE_LOWER, - MB_CASE_LOWER_SIMPLE, + 5, // MB_CASE_LOWER_SIMPLE ])) === 0; $keepLowercase = count(array_diff($modes, [ MB_CASE_LOWER, - MB_CASE_LOWER_SIMPLE, - MB_CASE_FOLD, - MB_CASE_FOLD_SIMPLE, + 5, // MB_CASE_LOWER_SIMPLE + 3, // MB_CASE_FOLD, + 7, // MB_CASE_FOLD_SIMPLE ])) === 0; } } elseif (in_array($fnName, ['ucwords', 'mb_convert_kana'], true)) { @@ -91,9 +91,9 @@ public function getTypeFromFunctionCall( } else { $modes = $fnName === 'mb_convert_kana' ? ['KV'] : [" \t\r\n\f\v"]; } - } elseif (in_array($fnName, ['strtolower', 'mb_strtolower'])) { + } elseif (in_array($fnName, ['strtolower', 'mb_strtolower'], true)) { $forceLowercase = true; - } elseif (in_array($fnName, ['lcfirst', 'mb_lcfirst'])) { + } elseif (in_array($fnName, ['lcfirst', 'mb_lcfirst'], true)) { $keepLowercase = true; } From 1d1627b75a14cb7d1c569eaf3d98386b0ecaea06 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 13 Sep 2024 13:27:33 +0200 Subject: [PATCH 08/26] Fix --- src/Php/PhpVersion.php | 5 +++++ src/Type/Php/SubstrDynamicReturnTypeExtension.php | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index 4eae1e59f9..b275c464eb 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -348,4 +348,9 @@ public function deprecatesImplicitlyNullableParameterTypes(): bool return $this->versionId >= 80400; } + public function substrReturnFalseInsteadOfEmptyString(): bool + { + return $this->versionId < 80000; + } + } diff --git a/src/Type/Php/SubstrDynamicReturnTypeExtension.php b/src/Type/Php/SubstrDynamicReturnTypeExtension.php index 5edc3f8df4..0a25484f59 100644 --- a/src/Type/Php/SubstrDynamicReturnTypeExtension.php +++ b/src/Type/Php/SubstrDynamicReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -26,6 +27,10 @@ final class SubstrDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return in_array($functionReflection->getName(), ['substr', 'mb_substr'], true); @@ -90,10 +95,12 @@ public function getTypeFromFunctionCall( } $accessoryTypes = []; + $isNotEmpty = false; if ($string->isLowercaseString()->yes()) { $accessoryTypes[] = new AccessoryLowercaseStringType(); } if ($string->isNonEmptyString()->yes() && ($negativeOffset || $zeroOffset && $positiveLength)) { + $isNotEmpty = true; if ($string->isNonFalsyString()->yes() && !$maybeOneLength) { $accessoryTypes[] = new AccessoryNonFalsyStringType(); } else { @@ -103,6 +110,13 @@ public function getTypeFromFunctionCall( if (count($accessoryTypes) > 0) { $accessoryTypes[] = new StringType(); + if (!$isNotEmpty && $this->phpVersion->substrReturnFalseInsteadOfEmptyString()) { + return TypeCombinator::union( + new ConstantBooleanType(false), + new IntersectionType($accessoryTypes) + ); + } + return new IntersectionType($accessoryTypes); } From 1f540aa1f1e64bb50a78c6ebb59a9a7999a4f477 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 13 Sep 2024 14:54:12 +0200 Subject: [PATCH 09/26] Fix cs --- src/Type/Constant/ConstantStringType.php | 1 + .../StrCaseFunctionsReturnTypeExtension.php | 18 ++++++++++-------- .../Php/SubstrDynamicReturnTypeExtension.php | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Type/Constant/ConstantStringType.php b/src/Type/Constant/ConstantStringType.php index 6468944221..382911e69d 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -49,6 +49,7 @@ use function is_numeric; use function key; use function strlen; +use function strtolower; use function substr; use function substr_count; diff --git a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php index f44f8789aa..149caff081 100644 --- a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php +++ b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php @@ -16,11 +16,13 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function array_diff; use function array_map; use function count; use function in_array; use function is_callable; use function mb_check_encoding; +use const MB_CASE_LOWER; final class StrCaseFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -74,15 +76,15 @@ public function getTypeFromFunctionCall( $modes = array_map(static fn ($mode) => $mode->getValue(), TypeUtils::getConstantIntegers($modeType)); if (count($modes) > 0) { $forceLowercase = count(array_diff($modes, [ - MB_CASE_LOWER, - 5, // MB_CASE_LOWER_SIMPLE - ])) === 0; + MB_CASE_LOWER, + 5, // MB_CASE_LOWER_SIMPLE + ])) === 0; $keepLowercase = count(array_diff($modes, [ - MB_CASE_LOWER, - 5, // MB_CASE_LOWER_SIMPLE - 3, // MB_CASE_FOLD, - 7, // MB_CASE_FOLD_SIMPLE - ])) === 0; + MB_CASE_LOWER, + 5, // MB_CASE_LOWER_SIMPLE + 3, // MB_CASE_FOLD, + 7, // MB_CASE_FOLD_SIMPLE + ])) === 0; } } elseif (in_array($fnName, ['ucwords', 'mb_convert_kana'], true)) { if (count($args) >= 2) { diff --git a/src/Type/Php/SubstrDynamicReturnTypeExtension.php b/src/Type/Php/SubstrDynamicReturnTypeExtension.php index 0a25484f59..4364f2374f 100644 --- a/src/Type/Php/SubstrDynamicReturnTypeExtension.php +++ b/src/Type/Php/SubstrDynamicReturnTypeExtension.php @@ -113,7 +113,7 @@ public function getTypeFromFunctionCall( if (!$isNotEmpty && $this->phpVersion->substrReturnFalseInsteadOfEmptyString()) { return TypeCombinator::union( new ConstantBooleanType(false), - new IntersectionType($accessoryTypes) + new IntersectionType($accessoryTypes), ); } From befce48ff1be558ddbd6c9da609c7fa42702c36f Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 13 Sep 2024 15:51:14 +0200 Subject: [PATCH 10/26] Fix baseline --- phpstan-baseline.neon | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e5f34c65a2..b7c18db8d9 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -691,6 +691,11 @@ parameters: count: 1 path: src/Type/Accessory/AccessoryLiteralStringType.php + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/Accessory/AccessoryLowercaseStringType.php + - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" count: 1 @@ -716,11 +721,6 @@ parameters: count: 1 path: src/Type/Accessory/AccessoryNumericStringType.php - - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" - count: 1 - path: src/Type/Accessory/AccessoryLowercaseStringType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" count: 1 From 31a526034b1142864480056474f72d7112cd7815 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 13 Sep 2024 15:55:07 +0200 Subject: [PATCH 11/26] Fix tests --- tests/PHPStan/Analyser/NodeScopeResolverTest.php | 6 ++++++ tests/PHPStan/Analyser/data/explode-php74.php | 14 ++++++++++++++ tests/PHPStan/Analyser/data/explode-php80.php | 14 ++++++++++++++ .../Analyser/nsrt/lowercase-string-implode.php | 14 -------------- 4 files changed, 34 insertions(+), 14 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/explode-php74.php create mode 100644 tests/PHPStan/Analyser/data/explode-php80.php diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index a4bfe75364..2816b5ce52 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -51,6 +51,12 @@ private static function findTestFiles(): iterable yield __DIR__ . '/../Rules/Methods/data/bug-6856.php'; + if (PHP_VERSION_ID < 80000) { + yield __DIR__ . '/data/explode-php74.php'; + } else { + yield __DIR__ . '/data/explode-php80.php'; + } + if (PHP_VERSION_ID >= 80000) { yield __DIR__ . '/../Reflection/data/unionTypes.php'; yield __DIR__ . '/../Reflection/data/mixedType.php'; diff --git a/tests/PHPStan/Analyser/data/explode-php74.php b/tests/PHPStan/Analyser/data/explode-php74.php new file mode 100644 index 0000000000..efdd404424 --- /dev/null +++ b/tests/PHPStan/Analyser/data/explode-php74.php @@ -0,0 +1,14 @@ +|false', explode($s, 'foo')); + assertType('non-empty-list|false', explode($s, 'FOO')); + } +} diff --git a/tests/PHPStan/Analyser/data/explode-php80.php b/tests/PHPStan/Analyser/data/explode-php80.php new file mode 100644 index 0000000000..4fa4539356 --- /dev/null +++ b/tests/PHPStan/Analyser/data/explode-php80.php @@ -0,0 +1,14 @@ +', explode($s, 'foo')); + assertType('non-empty-list', explode($s, 'FOO')); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/lowercase-string-implode.php b/tests/PHPStan/Analyser/nsrt/lowercase-string-implode.php index 19fc961f1f..3aff9f3389 100644 --- a/tests/PHPStan/Analyser/nsrt/lowercase-string-implode.php +++ b/tests/PHPStan/Analyser/nsrt/lowercase-string-implode.php @@ -2,21 +2,7 @@ namespace LowercaseStringImplode; -use function htmlspecialchars; -use function lcfirst; use function PHPStan\Testing\assertType; -use function strtolower; -use function strtoupper; -use function ucfirst; - -class ExplodingStrings -{ - public function doFoo(string $s): void - { - assertType('non-empty-list', explode($s, 'foo')); - assertType('non-empty-list', explode($s, 'FOO')); - } -} class ImplodingStrings { From 81e8090358ab66bc1e6d2d3c99981b423a449070 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 13 Sep 2024 22:52:13 +0200 Subject: [PATCH 12/26] Add support for Concat operator --- src/Reflection/InitializerExprTypeResolver.php | 4 ++++ tests/PHPStan/Analyser/nsrt/constant-string-unions.php | 6 +++--- .../Analyser/nsrt/foreach-partially-non-iterable.php | 2 +- .../Rules/Functions/CallToFunctionParametersRuleTest.php | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 1eec9736d1..8b015c91ab 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -27,6 +27,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; @@ -478,6 +479,9 @@ public function resolveConcatType(Type $left, Type $right): Type if ($leftStringType->isLiteralString()->and($rightStringType->isLiteralString())->yes()) { $accessoryTypes[] = new AccessoryLiteralStringType(); } + if ($leftStringType->isLowercaseString()->and($rightStringType->isLowercaseString())->yes()) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); + } $leftNumericStringNonEmpty = TypeCombinator::remove($leftStringType, new ConstantStringType('')); if ($leftNumericStringNonEmpty->isNumericString()->yes()) { diff --git a/tests/PHPStan/Analyser/nsrt/constant-string-unions.php b/tests/PHPStan/Analyser/nsrt/constant-string-unions.php index 06881808e8..b97b117179 100644 --- a/tests/PHPStan/Analyser/nsrt/constant-string-unions.php +++ b/tests/PHPStan/Analyser/nsrt/constant-string-unions.php @@ -72,7 +72,7 @@ public function testLimit(string $s15, string $s16, string $s17) { // union should contain 32 elements assertType("'1'|'10'|'10a'|'11'|'11a'|'12'|'12a'|'13'|'13a'|'14'|'14a'|'15'|'15a'|'16'|'16a'|'1a'|'2'|'2a'|'3'|'3a'|'4'|'4a'|'5'|'5a'|'6'|'6a'|'7'|'7a'|'8'|'8a'|'9'|'9a'", $s16); // fallback to the more general form - assertType("literal-string&non-falsy-string", $s17); + assertType("literal-string&lowercase-string&non-falsy-string", $s17); $left = rand() ? 'a' : 'b'; $right = rand() ? 'x' : 'y'; $left .= $right; @@ -80,7 +80,7 @@ public function testLimit(string $s15, string $s16, string $s17) { $left .= $right; assertType("'axxx'|'axxy'|'axyx'|'axyy'|'ayxx'|'ayxy'|'ayyx'|'ayyy'|'bxxx'|'bxxy'|'bxyx'|'bxyy'|'byxx'|'byxy'|'byyx'|'byyy'", $left); $left .= $right; - assertType("literal-string&non-falsy-string", $left); + assertType("literal-string&lowercase-string&non-falsy-string", $left); $left = rand() ? 'a' : 'b'; $right = rand() ? 'x' : 'y'; @@ -89,7 +89,7 @@ public function testLimit(string $s15, string $s16, string $s17) { $left = "{$left}{$right}"; assertType("'axxx'|'axxy'|'axyx'|'axyy'|'ayxx'|'ayxy'|'ayyx'|'ayyy'|'bxxx'|'bxxy'|'bxyx'|'bxyy'|'byxx'|'byxy'|'byyx'|'byyy'", $left); $left = "{$left}{$right}"; - assertType("literal-string&non-falsy-string", $left); + assertType("literal-string&lowercase-string&non-falsy-string", $left); } /** diff --git a/tests/PHPStan/Analyser/nsrt/foreach-partially-non-iterable.php b/tests/PHPStan/Analyser/nsrt/foreach-partially-non-iterable.php index ad8e375e58..a1d7279252 100644 --- a/tests/PHPStan/Analyser/nsrt/foreach-partially-non-iterable.php +++ b/tests/PHPStan/Analyser/nsrt/foreach-partially-non-iterable.php @@ -29,7 +29,7 @@ public function sayHello(\stdClass $s): void foreach ($s as $k => $v) { $a .= 'test'; } - assertType('(literal-string&non-falsy-string)|null', $a); + assertType('(literal-string&lowercase-string&non-falsy-string)|null', $a); } } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index a47624e8e8..4b00fb720e 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -636,7 +636,7 @@ public function testArrayUdiffCallback(): void 6, ], [ - 'Parameter #3 $data_comp_func of function array_udiff expects callable(1|2|3|4|5|6, 1|2|3|4|5|6): int, Closure(int, int): (literal-string&non-falsy-string&numeric-string) given.', + 'Parameter #3 $data_comp_func of function array_udiff expects callable(1|2|3|4|5|6, 1|2|3|4|5|6): int, Closure(int, int): (literal-string&lowercase-string&non-falsy-string&numeric-string) given.', 14, ], [ From 8fbe212df6435752e67ccf074764f5f096e32b1a Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 14 Sep 2024 09:53:15 +0200 Subject: [PATCH 13/26] Add tests --- tests/PHPStan/Analyser/nsrt/more-types.php | 8 +++- ...rictComparisonOfDifferentTypesRuleTest.php | 48 +++++++++++++++++++ .../Comparison/data/lowercase-string.php | 31 ++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Comparison/data/lowercase-string.php diff --git a/tests/PHPStan/Analyser/nsrt/more-types.php b/tests/PHPStan/Analyser/nsrt/more-types.php index 70dc86d3d9..9c3296c3d8 100644 --- a/tests/PHPStan/Analyser/nsrt/more-types.php +++ b/tests/PHPStan/Analyser/nsrt/more-types.php @@ -17,6 +17,8 @@ class Foo * @param non-empty-scalar $nonEmptyScalar * @param empty-scalar $emptyScalar * @param non-empty-mixed $nonEmptyMixed + * @param lowercase-string $lowercaseString + * @param non-empty-lowercase-string $nonEmptyLowercaseString */ public function doFoo( $pureCallable, @@ -27,7 +29,9 @@ public function doFoo( $nonEmptyLiteralString, $nonEmptyScalar, $emptyScalar, - $nonEmptyMixed + $nonEmptyMixed, + $lowercaseString, + $nonEmptyLowercaseString, ): void { assertType('pure-callable(): mixed', $pureCallable); @@ -39,6 +43,8 @@ public function doFoo( assertType('float|int|int<1, max>|non-falsy-string|true', $nonEmptyScalar); assertType("0|0.0|''|'0'|false", $emptyScalar); assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $nonEmptyMixed); + assertType('lowercase-string', $lowercaseString); + assertType('lowercase-string&non-empty-string', $nonEmptyLowercaseString); } } diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index fe9b8fa079..33e7465bf2 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -1057,6 +1057,54 @@ public function testBug10697(): void $this->analyse([__DIR__ . '/data/bug-10697.php'], []); } + public function testLowercaseString(): void + { + $errors = [ + [ + "Strict comparison using === between lowercase-string and 'AB' will always evaluate to false.", + 10, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + "Strict comparison using === between 'AB' and lowercase-string will always evaluate to false.", + 11, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + "Strict comparison using !== between 'AB' and lowercase-string will always evaluate to true.", + 12, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + "Strict comparison using === between lowercase-string and 'aBc' will always evaluate to false.", + 15, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + "Strict comparison using !== between lowercase-string and 'aBc' will always evaluate to true.", + 16, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]; + + if (PHP_VERSION_ID < 80000) { + $errors[] = [ + "Strict comparison using === between lowercase-string|false and 'AB' will always evaluate to false.", + 28, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ]; + } else { + $errors[] = [ + "Strict comparison using === between lowercase-string and 'AB' will always evaluate to false.", + 28, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ]; + } + + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/lowercase-string.php'], $errors); + } + public function testBug10493(): void { $this->checkAlwaysTrueStrictComparison = true; diff --git a/tests/PHPStan/Rules/Comparison/data/lowercase-string.php b/tests/PHPStan/Rules/Comparison/data/lowercase-string.php new file mode 100644 index 0000000000..772939626a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/lowercase-string.php @@ -0,0 +1,31 @@ + Date: Sun, 15 Sep 2024 19:57:23 +0200 Subject: [PATCH 14/26] Wip --- .../AccessoryLowercaseStringType.php | 3 - .../Rules/Methods/CallMethodsRuleTest.php | 23 ++++++++ .../Rules/Methods/data/lowercase-string.php | 33 +++++++++++ tests/PHPStan/Type/TypeCombinatorTest.php | 58 +++++++++++++++++++ 4 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 tests/PHPStan/Rules/Methods/data/lowercase-string.php diff --git a/src/Type/Accessory/AccessoryLowercaseStringType.php b/src/Type/Accessory/AccessoryLowercaseStringType.php index e15f200c1c..2b1519a103 100644 --- a/src/Type/Accessory/AccessoryLowercaseStringType.php +++ b/src/Type/Accessory/AccessoryLowercaseStringType.php @@ -74,9 +74,6 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { - if ($type instanceof MixedType) { - return AcceptsResult::createNo(); - } if ($type instanceof CompoundType) { return $type->isAcceptedWithReasonBy($this, $strictTypes); } diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 8bb53330d4..93b5b9303e 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3354,6 +3354,29 @@ public function testTraitMixin(): void $this->analyse([__DIR__ . '/data/trait-mixin.php'], []); } + public function testLowercaseString(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/lowercase-string.php'], [ + [ + 'Parameter #1 $s of method LowercaseString\Bar::acceptLowercaseString() expects lowercase-string, \'NotLowerCase\' given.', + 26, + ], + [ + 'Parameter #1 $s of method LowercaseString\Bar::acceptLowercaseString() expects lowercase-string, string given.', + 28, + ], + [ + 'Parameter #1 $s of method LowercaseString\Bar::acceptLowercaseString() expects lowercase-string, numeric-string given.', + 30, + ], + ]); + } + public function testBug10159(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/data/lowercase-string.php b/tests/PHPStan/Rules/Methods/data/lowercase-string.php new file mode 100644 index 0000000000..40c475e9e5 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/lowercase-string.php @@ -0,0 +1,33 @@ +acceptLowercaseString('NotLowerCase'); + $this->acceptLowercaseString('lowercase'); + $this->acceptLowercaseString($string); + $this->acceptLowercaseString($lowercaseString); + $this->acceptLowercaseString($numericString); + $this->acceptLowercaseString($nonEmptyLowercaseString); + } +} diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 95e11da973..5e04a84612 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -19,6 +19,7 @@ use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; @@ -2591,6 +2592,46 @@ public function dataUnion(): iterable IntersectionType::class, 'array&hasOffsetValue(\'thing\', mixed)', ]; + yield [ + [ + new StringType(), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + StringType::class, + 'string', + ]; + yield [ + [ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|numeric-string', + ]; + yield [ + [ + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|non-falsy-string', + ]; + yield [ + [ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|non-empty-string', + ]; + yield [ + [ + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|literal-string', + ]; } /** @@ -4325,6 +4366,23 @@ public function dataIntersect(): iterable NeverType::class, '*NEVER*=implicit', ]; + + yield [ + [ + new ConstantStringType('FOO'), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ConstantStringType('foo'), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + ConstantStringType::class, + '\'foo\'', + ]; } /** From f249cff1f486bb2a5d09b499649672d3e0052a11 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 15 Sep 2024 22:19:44 +0200 Subject: [PATCH 15/26] Add TypeCombinator tests --- tests/PHPStan/Type/TypeCombinatorTest.php | 120 ++++++++++++++-------- 1 file changed, 80 insertions(+), 40 deletions(-) diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 5e04a84612..55dc30542b 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -1969,6 +1969,46 @@ public function dataUnion(): iterable UnionType::class, 'literal-string|numeric-string', ], + [ + [ + new StringType(), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + StringType::class, + 'string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|numeric-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|non-falsy-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|non-empty-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + UnionType::class, + 'literal-string|lowercase-string', + ], [ [ TemplateTypeFactory::create( @@ -2592,46 +2632,6 @@ public function dataUnion(): iterable IntersectionType::class, 'array&hasOffsetValue(\'thing\', mixed)', ]; - yield [ - [ - new StringType(), - new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), - ], - StringType::class, - 'string', - ]; - yield [ - [ - new IntersectionType([new StringType(), new AccessoryNumericStringType()]), - new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), - ], - UnionType::class, - 'lowercase-string|numeric-string', - ]; - yield [ - [ - new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), - new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), - ], - UnionType::class, - 'lowercase-string|non-falsy-string', - ]; - yield [ - [ - new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), - new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), - ], - UnionType::class, - 'lowercase-string|non-empty-string', - ]; - yield [ - [ - new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), - new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), - ], - UnionType::class, - 'lowercase-string|literal-string', - ]; } /** @@ -3894,6 +3894,46 @@ public function dataIntersect(): iterable NeverType::class, '*NEVER*=implicit', ], + [ + [ + new StringType(), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + IntersectionType::class, + 'lowercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + IntersectionType::class, + 'lowercase-string&numeric-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + IntersectionType::class, + 'lowercase-string&non-falsy-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + IntersectionType::class, + 'lowercase-string&non-empty-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + IntersectionType::class, + 'literal-string&lowercase-string', + ], ]; if (PHP_VERSION_ID < 80100) { From 343e55b3497b19657821233e5ae4303ef8932274 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 15 Sep 2024 22:59:32 +0200 Subject: [PATCH 16/26] WIP --- .../AccessoryLowercaseStringType.php | 1 - src/Type/IntersectionType.php | 4 +++ src/Type/VerbosityLevel.php | 8 ++++-- tests/PHPStan/Type/IntersectionTypeTest.php | 28 +++++++++++++++++++ 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/Type/Accessory/AccessoryLowercaseStringType.php b/src/Type/Accessory/AccessoryLowercaseStringType.php index 2b1519a103..195a807dbc 100644 --- a/src/Type/Accessory/AccessoryLowercaseStringType.php +++ b/src/Type/Accessory/AccessoryLowercaseStringType.php @@ -17,7 +17,6 @@ use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; -use PHPStan\Type\MixedType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 49647a52cd..22a4d811e4 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -331,6 +331,10 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) || $type instanceof AccessoryNonFalsyStringType || $type instanceof AccessoryLowercaseStringType ) { + if ($type instanceof AccessoryLowercaseStringType && !$level->isPrecise()) { + continue; + } + if ($type instanceof AccessoryNonFalsyStringType) { $nonFalsyStr = true; } diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index 086401052f..69a5c2fcd1 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -87,6 +87,10 @@ public function isPrecise(): bool /** @api */ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acceptedType = null): self { + $verboseLevelCallback = static function (Type $acceptingType): VerbosityLevel { + return $acceptingType->isLowercaseString()->yes() ? self::precise() : self::value(); + }; + $moreVerboseCallback = static function (Type $type, callable $traverse) use (&$moreVerbose): Type { if ($type->isCallable()->yes()) { $moreVerbose = true; @@ -121,7 +125,7 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc TypeTraverser::map($acceptingType, $moreVerboseCallback); if ($moreVerbose) { - return self::value(); + return $verboseLevelCallback($acceptingType); } if ($acceptedType === null) { @@ -160,7 +164,7 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc $moreVerbose = false; TypeTraverser::map($acceptedType, $moreVerboseCallback); - return $moreVerbose ? self::value() : self::typeOnly(); + return $moreVerbose ? $verboseLevelCallback($acceptingType) : self::typeOnly(); } /** diff --git a/tests/PHPStan/Type/IntersectionTypeTest.php b/tests/PHPStan/Type/IntersectionTypeTest.php index aae9faa0bd..7c9bcc0722 100644 --- a/tests/PHPStan/Type/IntersectionTypeTest.php +++ b/tests/PHPStan/Type/IntersectionTypeTest.php @@ -7,6 +7,7 @@ use ObjectTypeEnums\FooEnum; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -447,4 +448,31 @@ public function testGetEnumCases( } } + public function dataDescribe(): iterable + { + yield [ + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + VerbosityLevel::typeOnly(), + 'string', + ]; + yield [ + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + VerbosityLevel::value(), + 'string', + ]; + yield [ + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + VerbosityLevel::precise(), + 'lowercase-string', + ]; + } + + /** + * @dataProvider dataDescribe + */ + public function testDescribe(IntersectionType $type, VerbosityLevel $verbosityLevel, string $expected): void + { + static::assertSame($expected, $type->describe($verbosityLevel)); + } + } From bd26dd5a6e929d881f78997c880093a02afe39ff Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 16 Sep 2024 10:26:01 +0200 Subject: [PATCH 17/26] Rework --- src/Type/VerbosityLevel.php | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index 69a5c2fcd1..7934506aea 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -87,17 +87,13 @@ public function isPrecise(): bool /** @api */ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acceptedType = null): self { - $verboseLevelCallback = static function (Type $acceptingType): VerbosityLevel { - return $acceptingType->isLowercaseString()->yes() ? self::precise() : self::value(); - }; - - $moreVerboseCallback = static function (Type $type, callable $traverse) use (&$moreVerbose): Type { + $moreVerboseCallback = static function (Type $type, callable $traverse) use (&$verboseLevel): Type { if ($type->isCallable()->yes()) { - $moreVerbose = true; + $verboseLevel = self::value(); return $type; } if ($type->isConstantValue()->yes() && $type->isNull()->no()) { - $moreVerbose = true; + $verboseLevel = self::value(); return $type; } if ( @@ -106,26 +102,29 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc || $type instanceof AccessoryNonFalsyStringType || $type instanceof AccessoryLiteralStringType || $type instanceof AccessoryNumericStringType - || $type instanceof AccessoryLowercaseStringType || $type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType ) { - $moreVerbose = true; + $verboseLevel = self::value(); + return $type; + } + if ($type instanceof AccessoryLowercaseStringType) { + $verboseLevel = self::precise(); return $type; } if ($type instanceof IntegerRangeType) { - $moreVerbose = true; + $verboseLevel = self::value(); return $type; } return $traverse($type); }; - /** @var bool $moreVerbose */ - $moreVerbose = false; + /** @var VerbosityLevel|null $verboseLevel */ + $verboseLevel = null; TypeTraverser::map($acceptingType, $moreVerboseCallback); - if ($moreVerbose) { - return $verboseLevelCallback($acceptingType); + if (null !== $verboseLevel) { + return $verboseLevel; } if ($acceptedType === null) { @@ -160,11 +159,11 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc return self::typeOnly(); } - /** @var bool $moreVerbose */ - $moreVerbose = false; + /** @var VerbosityLevel|null $verboseLevel */ + $verboseLevel = null; TypeTraverser::map($acceptedType, $moreVerboseCallback); - return $moreVerbose ? $verboseLevelCallback($acceptingType) : self::typeOnly(); + return null !== $verboseLevel ? self::value() : self::typeOnly(); } /** From 48b80eeadeeef29eb0829f1bd6d07eda7ab636db Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 16 Sep 2024 11:33:43 +0200 Subject: [PATCH 18/26] Update test --- tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index b8627bfee5..6d374a6f1c 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -834,7 +834,7 @@ public function testBug8146bErrors(): void $this->checkBenevolentUnionTypes = true; $this->analyse([__DIR__ . '/data/bug-8146b-errors.php'], [ [ - "Method Bug8146bError\LocationFixtures::getData() should return array, coordinates: array{lat: float, lng: float}}>> but returns array{Bács-Kiskun: array{Ágasegyháza: array{constituencies: array{'Bács-Kiskun 4.', true, false, Bug8146bError\X, null}, coordinates: array{lat: 46.8386043, lng: 19.4502899}}, Akasztó: array{constituencies: array{'Bács-Kiskun 3.'}, coordinates: array{lat: 46.6898175, lng: 19.205086}}, Apostag: array{constituencies: array{'Bács-Kiskun 3.'}, coordinates: array{lat: 46.8812652, lng: 18.9648478}}, Bácsalmás: array{constituencies: array{'Bács-Kiskun 5.'}, coordinates: array{lat: 46.1250396, lng: 19.3357509}}, Bácsbokod: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 46.1234737, lng: 19.155708}}, Bácsborsód: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 46.0989373, lng: 19.1566725}}, Bácsszentgyörgy: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 45.9746039, lng: 19.0398066}}, Bácsszőlős: array{constituencies: array{'Bács-Kiskun 5.'}, coordinates: array{lat: 46.1352003, lng: 19.4215997}}, ...}, Baranya: non-empty-array|(literal-string&lowercase-string&non-falsy-string), float|(literal-string&non-falsy-string)>>>, Békés: array{Almáskamarás: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.4617785, lng: 21.092448}}, Battonya: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.2902462, lng: 21.0199215}}, Békés: array{constituencies: array{'Békés 2.'}, coordinates: array{lat: 46.6704899, lng: 21.0434996}}, Békéscsaba: array{constituencies: array{'Békés 1.'}, coordinates: array{lat: 46.6735939, lng: 21.0877309}}, Békéssámson: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.4208677, lng: 20.6176498}}, Békésszentandrás: array{constituencies: array{'Békés 2.'}, coordinates: array{lat: 46.8715996, lng: 20.48336}}, Bélmegyer: array{constituencies: array{'Békés 3.'}, coordinates: array{lat: 46.8726019, lng: 21.1832832}}, Biharugra: array{constituencies: array{'Békés 3.'}, coordinates: array{lat: 46.9691009, lng: 21.5987651}}, ...}, Borsod-Abaúj-Zemplén: non-empty-array|(literal-string&lowercase-string&non-falsy-string), float|(literal-string&non-falsy-string)>>>, Budapest: array{Budapest I. ker.: array{constituencies: array{'Budapest 01.'}, coordinates: array{lat: 47.4968219, lng: 19.037458}}, Budapest II. ker.: array{constituencies: array{'Budapest 03.', 'Budapest 04.'}, coordinates: array{lat: 47.5393329, lng: 18.986934}}, Budapest III. ker.: array{constituencies: array{'Budapest 04.', 'Budapest 10.'}, coordinates: array{lat: 47.5671768, lng: 19.0368517}}, Budapest IV. ker.: array{constituencies: array{'Budapest 11.', 'Budapest 12.'}, coordinates: array{lat: 47.5648915, lng: 19.0913149}}, Budapest V. ker.: array{constituencies: array{'Budapest 01.'}, coordinates: array{lat: 47.5002319, lng: 19.0520181}}, Budapest VI. ker.: array{constituencies: array{'Budapest 05.'}, coordinates: array{lat: 47.509863, lng: 19.0625813}}, Budapest VII. ker.: array{constituencies: array{'Budapest 05.'}, coordinates: array{lat: 47.5027289, lng: 19.073376}}, Budapest VIII. ker.: array{constituencies: array{'Budapest 01.', 'Budapest 06.'}, coordinates: array{lat: 47.4894184, lng: 19.070668}}, ...}, Csongrád-Csanád: array{Algyő: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.3329625, lng: 20.207889}}, Ambrózfalva: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.3501417, lng: 20.7313995}}, Apátfalva: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.173317, lng: 20.5800472}}, Árpádhalom: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.6158286, lng: 20.547733}}, Ásotthalom: array{constituencies: array{'Csongrád-Csanád 2.'}, coordinates: array{lat: 46.1995983, lng: 19.7833756}}, Baks: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.5518708, lng: 20.1064166}}, Balástya: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.4261828, lng: 20.004933}}, Bordány: array{constituencies: array{'Csongrád-Csanád 2.'}, coordinates: array{lat: 46.3194213, lng: 19.9227063}}, ...}, Fejér: array{Aba: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 47.0328193, lng: 18.522359}}, Adony: array{constituencies: array{'Fejér 4.'}, coordinates: array{lat: 47.119831, lng: 18.8612469}}, Alap: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 46.8075763, lng: 18.684028}}, Alcsútdoboz: array{constituencies: array{'Fejér 3.'}, coordinates: array{lat: 47.4277067, lng: 18.6030325}}, Alsószentiván: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 46.7910573, lng: 18.732161}}, Bakonycsernye: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.321719, lng: 18.0907379}}, Bakonykúti: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.2458464, lng: 18.195769}}, Balinka: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.3135736, lng: 18.1907168}}, ...}, Győr-Moson-Sopron: array{Abda: array{constituencies: array{'Győr-Moson-Sopron 5.'}, coordinates: array{lat: 47.6962149, lng: 17.5445786}}, Acsalag: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.676095, lng: 17.1977771}}, Ágfalva: array{constituencies: array{'Győr-Moson-Sopron 4.'}, coordinates: array{lat: 47.688862, lng: 16.5110233}}, Agyagosszergény: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.608545, lng: 16.9409912}}, Árpás: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5134127, lng: 17.3931579}}, Ásványráró: array{constituencies: array{'Győr-Moson-Sopron 5.'}, coordinates: array{lat: 47.8287695, lng: 17.499195}}, Babót: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5752269, lng: 17.0758604}}, Bágyogszovát: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5866036, lng: 17.3617273}}, ...}, ...}.", + "Method Bug8146bError\LocationFixtures::getData() should return array, coordinates: array{lat: float, lng: float}}>> but returns array{Bács-Kiskun: array{Ágasegyháza: array{constituencies: array{'Bács-Kiskun 4.', true, false, Bug8146bError\X, null}, coordinates: array{lat: 46.8386043, lng: 19.4502899}}, Akasztó: array{constituencies: array{'Bács-Kiskun 3.'}, coordinates: array{lat: 46.6898175, lng: 19.205086}}, Apostag: array{constituencies: array{'Bács-Kiskun 3.'}, coordinates: array{lat: 46.8812652, lng: 18.9648478}}, Bácsalmás: array{constituencies: array{'Bács-Kiskun 5.'}, coordinates: array{lat: 46.1250396, lng: 19.3357509}}, Bácsbokod: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 46.1234737, lng: 19.155708}}, Bácsborsód: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 46.0989373, lng: 19.1566725}}, Bácsszentgyörgy: array{constituencies: array{'Bács-Kiskun 6.'}, coordinates: array{lat: 45.9746039, lng: 19.0398066}}, Bácsszőlős: array{constituencies: array{'Bács-Kiskun 5.'}, coordinates: array{lat: 46.1352003, lng: 19.4215997}}, ...}, Baranya: non-empty-array|(literal-string&non-falsy-string), float|(literal-string&non-falsy-string)>>>, Békés: array{Almáskamarás: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.4617785, lng: 21.092448}}, Battonya: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.2902462, lng: 21.0199215}}, Békés: array{constituencies: array{'Békés 2.'}, coordinates: array{lat: 46.6704899, lng: 21.0434996}}, Békéscsaba: array{constituencies: array{'Békés 1.'}, coordinates: array{lat: 46.6735939, lng: 21.0877309}}, Békéssámson: array{constituencies: array{'Békés 4.'}, coordinates: array{lat: 46.4208677, lng: 20.6176498}}, Békésszentandrás: array{constituencies: array{'Békés 2.'}, coordinates: array{lat: 46.8715996, lng: 20.48336}}, Bélmegyer: array{constituencies: array{'Békés 3.'}, coordinates: array{lat: 46.8726019, lng: 21.1832832}}, Biharugra: array{constituencies: array{'Békés 3.'}, coordinates: array{lat: 46.9691009, lng: 21.5987651}}, ...}, Borsod-Abaúj-Zemplén: non-empty-array|(literal-string&non-falsy-string), float|(literal-string&non-falsy-string)>>>, Budapest: array{Budapest I. ker.: array{constituencies: array{'Budapest 01.'}, coordinates: array{lat: 47.4968219, lng: 19.037458}}, Budapest II. ker.: array{constituencies: array{'Budapest 03.', 'Budapest 04.'}, coordinates: array{lat: 47.5393329, lng: 18.986934}}, Budapest III. ker.: array{constituencies: array{'Budapest 04.', 'Budapest 10.'}, coordinates: array{lat: 47.5671768, lng: 19.0368517}}, Budapest IV. ker.: array{constituencies: array{'Budapest 11.', 'Budapest 12.'}, coordinates: array{lat: 47.5648915, lng: 19.0913149}}, Budapest V. ker.: array{constituencies: array{'Budapest 01.'}, coordinates: array{lat: 47.5002319, lng: 19.0520181}}, Budapest VI. ker.: array{constituencies: array{'Budapest 05.'}, coordinates: array{lat: 47.509863, lng: 19.0625813}}, Budapest VII. ker.: array{constituencies: array{'Budapest 05.'}, coordinates: array{lat: 47.5027289, lng: 19.073376}}, Budapest VIII. ker.: array{constituencies: array{'Budapest 01.', 'Budapest 06.'}, coordinates: array{lat: 47.4894184, lng: 19.070668}}, ...}, Csongrád-Csanád: array{Algyő: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.3329625, lng: 20.207889}}, Ambrózfalva: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.3501417, lng: 20.7313995}}, Apátfalva: array{constituencies: array{'Csongrád-Csanád 4.'}, coordinates: array{lat: 46.173317, lng: 20.5800472}}, Árpádhalom: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.6158286, lng: 20.547733}}, Ásotthalom: array{constituencies: array{'Csongrád-Csanád 2.'}, coordinates: array{lat: 46.1995983, lng: 19.7833756}}, Baks: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.5518708, lng: 20.1064166}}, Balástya: array{constituencies: array{'Csongrád-Csanád 3.'}, coordinates: array{lat: 46.4261828, lng: 20.004933}}, Bordány: array{constituencies: array{'Csongrád-Csanád 2.'}, coordinates: array{lat: 46.3194213, lng: 19.9227063}}, ...}, Fejér: array{Aba: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 47.0328193, lng: 18.522359}}, Adony: array{constituencies: array{'Fejér 4.'}, coordinates: array{lat: 47.119831, lng: 18.8612469}}, Alap: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 46.8075763, lng: 18.684028}}, Alcsútdoboz: array{constituencies: array{'Fejér 3.'}, coordinates: array{lat: 47.4277067, lng: 18.6030325}}, Alsószentiván: array{constituencies: array{'Fejér 5.'}, coordinates: array{lat: 46.7910573, lng: 18.732161}}, Bakonycsernye: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.321719, lng: 18.0907379}}, Bakonykúti: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.2458464, lng: 18.195769}}, Balinka: array{constituencies: array{'Fejér 2.'}, coordinates: array{lat: 47.3135736, lng: 18.1907168}}, ...}, Győr-Moson-Sopron: array{Abda: array{constituencies: array{'Győr-Moson-Sopron 5.'}, coordinates: array{lat: 47.6962149, lng: 17.5445786}}, Acsalag: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.676095, lng: 17.1977771}}, Ágfalva: array{constituencies: array{'Győr-Moson-Sopron 4.'}, coordinates: array{lat: 47.688862, lng: 16.5110233}}, Agyagosszergény: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.608545, lng: 16.9409912}}, Árpás: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5134127, lng: 17.3931579}}, Ásványráró: array{constituencies: array{'Győr-Moson-Sopron 5.'}, coordinates: array{lat: 47.8287695, lng: 17.499195}}, Babót: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5752269, lng: 17.0758604}}, Bágyogszovát: array{constituencies: array{'Győr-Moson-Sopron 3.'}, coordinates: array{lat: 47.5866036, lng: 17.3617273}}, ...}, ...}.", 12, "Offset 'constituencies' (non-empty-list) does not accept type array{'Bács-Kiskun 4.', true, false, Bug8146bError\X, null}.", ], From 038152164056af9152d7340bb73fb7cfd10f39d6 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 16 Sep 2024 11:46:40 +0200 Subject: [PATCH 19/26] fix test --- src/Type/UnionTypeHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/UnionTypeHelper.php b/src/Type/UnionTypeHelper.php index 485c482644..f91ea5cb45 100644 --- a/src/Type/UnionTypeHelper.php +++ b/src/Type/UnionTypeHelper.php @@ -117,7 +117,7 @@ public static function sortTypes(array $types): array } if ($a->isString()->yes() && $b->isString()->yes()) { - return self::compareStrings($a->describe(VerbosityLevel::value()), $b->describe(VerbosityLevel::value())); + return self::compareStrings($a->describe(VerbosityLevel::precise()), $b->describe(VerbosityLevel::precise())); } return self::compareStrings($a->describe(VerbosityLevel::typeOnly()), $b->describe(VerbosityLevel::typeOnly())); From d72fa24da5b76263ef4ab9f17f560c414de3f729 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 16 Sep 2024 11:49:20 +0200 Subject: [PATCH 20/26] Fix cs --- src/Type/VerbosityLevel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index 7934506aea..0e73c2631d 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -123,7 +123,7 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc $verboseLevel = null; TypeTraverser::map($acceptingType, $moreVerboseCallback); - if (null !== $verboseLevel) { + if ($verboseLevel !== null) { return $verboseLevel; } @@ -163,7 +163,7 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc $verboseLevel = null; TypeTraverser::map($acceptedType, $moreVerboseCallback); - return null !== $verboseLevel ? self::value() : self::typeOnly(); + return $verboseLevel !== null ? self::value() : self::typeOnly(); } /** From 1bf8d1c27fb9318201f2c4c258037c261c623e42 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 16 Sep 2024 11:49:52 +0200 Subject: [PATCH 21/26] Fix test --- .../Comparison/StrictComparisonOfDifferentTypesRuleTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index 33e7465bf2..e6ff6be2d7 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -566,7 +566,7 @@ public function testBug6939(): void $this->analyse([__DIR__ . '/data/bug-6939.php'], [ [ - 'Strict comparison using === between lowercase-string and false will always evaluate to false.', + 'Strict comparison using === between string and false will always evaluate to false.', 10, ], ]); From d141ece980553efd664f248fba4edac1e8cdc651 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 16 Sep 2024 12:02:03 +0200 Subject: [PATCH 22/26] update rule --- .../Comparison/StrictComparisonOfDifferentTypesRule.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php index f4ea23258b..079d4f21ec 100644 --- a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php +++ b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php @@ -66,8 +66,8 @@ public function processNode(Node $node, Scope $scope): array $addTip(RuleErrorBuilder::message(sprintf( 'Strict comparison using %s between %s and %s will always evaluate to false.', $node->getOperatorSigil(), - $leftType->describe(VerbosityLevel::value()), - $rightType->describe(VerbosityLevel::value()), + $leftType->describe(VerbosityLevel::getRecommendedLevelByType($leftType, $rightType)), + $rightType->describe(VerbosityLevel::getRecommendedLevelByType($rightType, $leftType)), )))->identifier(sprintf('%s.alwaysFalse', $node instanceof Node\Expr\BinaryOp\Identical ? 'identical' : 'notIdentical'))->build(), ]; } elseif ($this->checkAlwaysTrueStrictComparison) { @@ -79,8 +79,8 @@ public function processNode(Node $node, Scope $scope): array $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( 'Strict comparison using %s between %s and %s will always evaluate to true.', $node->getOperatorSigil(), - $leftType->describe(VerbosityLevel::value()), - $rightType->describe(VerbosityLevel::value()), + $leftType->describe(VerbosityLevel::getRecommendedLevelByType($leftType, $rightType)), + $rightType->describe(VerbosityLevel::getRecommendedLevelByType($rightType, $leftType)), ))); if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { $errorBuilder->addTip('Remove remaining cases below this one and this error will disappear too.'); From 57ec9ee25b24f9db51889a9dd111f063fe2dd1c4 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 16 Sep 2024 15:52:24 +0200 Subject: [PATCH 23/26] Try --- src/Type/VerbosityLevel.php | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index 0e73c2631d..1a5151381b 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -87,13 +87,13 @@ public function isPrecise(): bool /** @api */ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acceptedType = null): self { - $moreVerboseCallback = static function (Type $type, callable $traverse) use (&$verboseLevel): Type { + $moreVerboseCallback = static function (Type $type, callable $traverse) use (&$moreVerbose, &$veryVerbose): Type { if ($type->isCallable()->yes()) { - $verboseLevel = self::value(); + $moreVerbose = true; return $type; } if ($type->isConstantValue()->yes() && $type->isNull()->no()) { - $verboseLevel = self::value(); + $moreVerbose = true; return $type; } if ( @@ -105,26 +105,33 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc || $type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType ) { - $verboseLevel = self::value(); + $moreVerbose = true; return $type; } if ($type instanceof AccessoryLowercaseStringType) { - $verboseLevel = self::precise(); + $moreVerbose = true; + $veryVerbose = true; return $type; } if ($type instanceof IntegerRangeType) { - $verboseLevel = self::value(); + $moreVerbose = true; return $type; } return $traverse($type); }; - /** @var VerbosityLevel|null $verboseLevel */ - $verboseLevel = null; + /** @var bool $moreVerbose */ + $moreVerbose = false; + /** @var bool $veryVerbose */ + $veryVerbose = false; TypeTraverser::map($acceptingType, $moreVerboseCallback); - if ($verboseLevel !== null) { - return $verboseLevel; + if ($veryVerbose) { + return self::precise(); + } + + if ($moreVerbose) { + return self::value(); } if ($acceptedType === null) { @@ -159,11 +166,13 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc return self::typeOnly(); } - /** @var VerbosityLevel|null $verboseLevel */ - $verboseLevel = null; + /** @var bool $moreVerbose */ + $moreVerbose = false; + /** @var bool $veryVerbose */ + $veryVerbose = false; TypeTraverser::map($acceptedType, $moreVerboseCallback); - return $verboseLevel !== null ? self::value() : self::typeOnly(); + return $moreVerbose ? self::value() : self::typeOnly(); } /** From da37d6ec4ef24d0240f7f80f8b14bee76bb8fb2e Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 26 Sep 2024 10:46:22 +0200 Subject: [PATCH 24/26] Review --- .../Comparison/StrictComparisonOfDifferentTypesRule.php | 8 ++++---- src/Type/VerbosityLevel.php | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php index 079d4f21ec..f4ea23258b 100644 --- a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php +++ b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php @@ -66,8 +66,8 @@ public function processNode(Node $node, Scope $scope): array $addTip(RuleErrorBuilder::message(sprintf( 'Strict comparison using %s between %s and %s will always evaluate to false.', $node->getOperatorSigil(), - $leftType->describe(VerbosityLevel::getRecommendedLevelByType($leftType, $rightType)), - $rightType->describe(VerbosityLevel::getRecommendedLevelByType($rightType, $leftType)), + $leftType->describe(VerbosityLevel::value()), + $rightType->describe(VerbosityLevel::value()), )))->identifier(sprintf('%s.alwaysFalse', $node instanceof Node\Expr\BinaryOp\Identical ? 'identical' : 'notIdentical'))->build(), ]; } elseif ($this->checkAlwaysTrueStrictComparison) { @@ -79,8 +79,8 @@ public function processNode(Node $node, Scope $scope): array $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( 'Strict comparison using %s between %s and %s will always evaluate to true.', $node->getOperatorSigil(), - $leftType->describe(VerbosityLevel::getRecommendedLevelByType($leftType, $rightType)), - $rightType->describe(VerbosityLevel::getRecommendedLevelByType($rightType, $leftType)), + $leftType->describe(VerbosityLevel::value()), + $rightType->describe(VerbosityLevel::value()), ))); if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { $errorBuilder->addTip('Remove remaining cases below this one and this error will disappear too.'); diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index 1a5151381b..52b66c154f 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -172,6 +172,10 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc $veryVerbose = false; TypeTraverser::map($acceptedType, $moreVerboseCallback); + if ($veryVerbose) { + return self::precise(); + } + return $moreVerbose ? self::value() : self::typeOnly(); } From 6efe887854b0d8ae18406d1b3537f14dd2100ecb Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 26 Sep 2024 11:01:09 +0200 Subject: [PATCH 25/26] Remove test --- tests/PHPStan/PhpDoc/TypeDescriptionTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/PHPStan/PhpDoc/TypeDescriptionTest.php b/tests/PHPStan/PhpDoc/TypeDescriptionTest.php index 7c0b2e9cc1..29c3a35231 100644 --- a/tests/PHPStan/PhpDoc/TypeDescriptionTest.php +++ b/tests/PHPStan/PhpDoc/TypeDescriptionTest.php @@ -4,7 +4,6 @@ use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\Accessory\AccessoryLiteralStringType; -use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -30,7 +29,6 @@ public function dataTest(): iterable yield ['string', new StringType()]; yield ['array', new ArrayType(new MixedType(), new MixedType())]; yield ['literal-string', new IntersectionType([new StringType(), new AccessoryLiteralStringType()])]; - yield ['lowercase-string', new IntersectionType([new StringType(), new AccessoryLowercaseStringType()])]; yield ['non-empty-string', new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()])]; yield ['numeric-string', new IntersectionType([new StringType(), new AccessoryNumericStringType()])]; yield ['literal-string&non-empty-string', new IntersectionType([new StringType(), new AccessoryLiteralStringType(), new AccessoryNonEmptyStringType()])]; From 7b1ade2632172a423550f357761904bdb5b8d75d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 26 Sep 2024 12:59:48 +0200 Subject: [PATCH 26/26] Improve verbosity in StrictComparisonOfDifferentTypesRule --- .../StrictComparisonOfDifferentTypesRule.php | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php index f4ea23258b..c703e93cb2 100644 --- a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php +++ b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php @@ -7,6 +7,7 @@ use PHPStan\Parser\LastConditionVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -61,13 +62,32 @@ public function processNode(Node $node, Scope $scope): array return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); }; + $verbosity = VerbosityLevel::value(); + if ( + ( + $leftType->isConstantScalarValue()->yes() + && $leftType->isString()->yes() + && $rightType->isConstantScalarValue()->no() + && $rightType->isString()->yes() + && TrinaryLogic::extremeIdentity($leftType->isLowercaseString(), $rightType->isLowercaseString())->maybe() + ) || ( + $rightType->isConstantScalarValue()->yes() + && $rightType->isString()->yes() + && $leftType->isConstantScalarValue()->no() + && $leftType->isString()->yes() + && TrinaryLogic::extremeIdentity($leftType->isLowercaseString(), $rightType->isLowercaseString())->maybe() + ) + ) { + $verbosity = VerbosityLevel::precise(); + } + if (!$nodeType->getValue()) { return [ $addTip(RuleErrorBuilder::message(sprintf( 'Strict comparison using %s between %s and %s will always evaluate to false.', $node->getOperatorSigil(), - $leftType->describe(VerbosityLevel::value()), - $rightType->describe(VerbosityLevel::value()), + $leftType->describe($verbosity), + $rightType->describe($verbosity), )))->identifier(sprintf('%s.alwaysFalse', $node instanceof Node\Expr\BinaryOp\Identical ? 'identical' : 'notIdentical'))->build(), ]; } elseif ($this->checkAlwaysTrueStrictComparison) { @@ -79,8 +99,8 @@ public function processNode(Node $node, Scope $scope): array $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( 'Strict comparison using %s between %s and %s will always evaluate to true.', $node->getOperatorSigil(), - $leftType->describe(VerbosityLevel::value()), - $rightType->describe(VerbosityLevel::value()), + $leftType->describe($verbosity), + $rightType->describe($verbosity), ))); if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { $errorBuilder->addTip('Remove remaining cases below this one and this error will disappear too.');