From 15551b136c8e88557a30b7fba924e47d0acdb593 Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Fri, 1 Nov 2024 15:51:41 -0500 Subject: [PATCH 01/24] Introduce uppercase-string --- phpstan-baseline.neon | 5 + resources/functionMap.php | 4 +- src/PhpDoc/TypeNodeResolver.php | 11 + .../InitializerExprTypeResolver.php | 6 + src/Rules/Api/ApiInstanceofTypeRule.php | 2 + .../StrictComparisonOfDifferentTypesRule.php | 19 + src/Type/Accessory/AccessoryArrayListType.php | 5 + .../Accessory/AccessoryLiteralStringType.php | 5 + .../AccessoryLowercaseStringType.php | 5 + .../Accessory/AccessoryNonEmptyStringType.php | 5 + .../Accessory/AccessoryNonFalsyStringType.php | 5 + .../Accessory/AccessoryNumericStringType.php | 5 + .../AccessoryUppercaseStringType.php | 390 ++++++++++++++++++ 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 | 11 + src/Type/FloatType.php | 5 + src/Type/IntersectionType.php | 11 + 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 + ...lodeFunctionDynamicReturnTypeExtension.php | 3 + .../ImplodeFunctionReturnTypeExtension.php | 4 + ...eUrlFunctionDynamicReturnTypeExtension.php | 115 +++++- ...aceFunctionsDynamicReturnTypeExtension.php | 5 + .../StrCaseFunctionsReturnTypeExtension.php | 21 + .../Php/StrPadFunctionReturnTypeExtension.php | 4 + .../StrRepeatFunctionReturnTypeExtension.php | 5 + .../Php/SubstrDynamicReturnTypeExtension.php | 4 + 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/VerbosityLevel.php | 6 +- src/Type/VoidType.php | 5 + stubs/core.stub | 14 +- tests/PHPStan/Analyser/data/explode-php80.php | 3 +- .../nsrt/isset-coalesce-empty-type.php | 2 +- .../PHPStan/Analyser/nsrt/literal-string.php | 14 +- ...-subtr.php => lowercase-string-substr.php} | 0 tests/PHPStan/Analyser/nsrt/more-types.php | 6 + .../Analyser/nsrt/non-empty-string.php | 8 +- .../Analyser/nsrt/non-falsy-string.php | 4 +- tests/PHPStan/Analyser/nsrt/str-casing.php | 24 +- .../nsrt/uppercase-string-implode.php | 22 + .../Analyser/nsrt/uppercase-string-pad.php | 23 ++ .../nsrt/uppercase-string-parse-url.php | 26 ++ .../Analyser/nsrt/uppercase-string-parse.php | 36 ++ .../Analyser/nsrt/uppercase-string-repeat.php | 19 + .../nsrt/uppercase-string-replace.php | 42 ++ .../Analyser/nsrt/uppercase-string-substr.php | 30 ++ .../Analyser/nsrt/uppercase-string-trim.php | 29 ++ ...rictComparisonOfDifferentTypesRuleTest.php | 48 +++ .../Comparison/data/uppercase-string.php | 31 ++ .../CallToFunctionParametersRuleTest.php | 4 - .../Rules/Generators/YieldTypeRuleTest.php | 4 +- .../Rules/Methods/CallMethodsRuleTest.php | 23 ++ .../Rules/Methods/data/uppercase-string.php | 33 ++ .../Type/Constant/ConstantStringTypeTest.php | 12 +- .../Constant/OversizedArrayBuilderTest.php | 9 + tests/PHPStan/Type/IntersectionTypeTest.php | 16 + tests/PHPStan/Type/TypeCombinatorTest.php | 98 +++++ tests/PHPStan/Type/TypeToPhpDocNodeTest.php | 11 + 75 files changed, 1305 insertions(+), 66 deletions(-) create mode 100644 src/Type/Accessory/AccessoryUppercaseStringType.php rename tests/PHPStan/Analyser/nsrt/{lowercase-string-subtr.php => lowercase-string-substr.php} (100%) create mode 100644 tests/PHPStan/Analyser/nsrt/uppercase-string-implode.php create mode 100644 tests/PHPStan/Analyser/nsrt/uppercase-string-pad.php create mode 100644 tests/PHPStan/Analyser/nsrt/uppercase-string-parse-url.php create mode 100644 tests/PHPStan/Analyser/nsrt/uppercase-string-parse.php create mode 100644 tests/PHPStan/Analyser/nsrt/uppercase-string-repeat.php create mode 100644 tests/PHPStan/Analyser/nsrt/uppercase-string-replace.php create mode 100644 tests/PHPStan/Analyser/nsrt/uppercase-string-substr.php create mode 100644 tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php create mode 100644 tests/PHPStan/Rules/Comparison/data/uppercase-string.php create mode 100644 tests/PHPStan/Rules/Methods/data/uppercase-string.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 498207dbe7..1662d1fa9a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -715,6 +715,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/AccessoryUppercaseStringType.php + - message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" count: 1 diff --git a/resources/functionMap.php b/resources/functionMap.php index 620138e0bc..9891593eb3 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -6362,7 +6362,7 @@ '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' => ['lowercase-string', 'str'=>'string', 'encoding='=>'string'], -'mb_strtoupper' => ['string', 'str'=>'string', 'encoding='=>'string'], +'mb_strtoupper' => ['uppercase-string', 'str'=>'string', 'encoding='=>'string'], 'mb_strwidth' => ['0|positive-int', 'str'=>'string', 'encoding='=>'string'], 'mb_substitute_character' => ['mixed', 'substchar='=>'mixed'], 'mb_substr' => ['string', 'str'=>'string', 'start'=>'int', 'length='=>'?int', 'encoding='=>'string'], @@ -12087,7 +12087,7 @@ 'strtok\'1' => ['non-empty-string|false', 'token'=>'string'], 'strtolower' => ['lowercase-string', 'str'=>'string'], 'strtotime' => ['int|false', 'time'=>'string', 'now='=>'int'], -'strtoupper' => ['string', 'str'=>'string'], +'strtoupper' => ['uppercase-string', 'str'=>'string'], 'strtr' => ['string', 'str'=>'string', 'from'=>'string', 'to'=>'string'], 'strtr\'1' => ['string', 'str'=>'string', 'replace_pairs'=>'array'], 'strval' => ['string', 'var'=>'__stringAndStringable|int|float|bool|resource|null'], diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 43f93cb8f5..744265adf6 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -50,6 +50,7 @@ use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; @@ -223,6 +224,9 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco case 'lowercase-string': return new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); + case 'uppercase-string': + return new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]); + case 'literal-string': return new IntersectionType([new StringType(), new AccessoryLiteralStringType()]); @@ -303,6 +307,13 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco new AccessoryLowercaseStringType(), ]); + case 'non-empty-uppercase-string': + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + new AccessoryUppercaseStringType(), + ]); + case 'truthy-string': case 'non-falsy-string': return new IntersectionType([ diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 8cedcd1272..a55fd7831d 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -31,6 +31,7 @@ use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; @@ -480,10 +481,15 @@ 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(); } + if ($leftStringType->isUppercaseString()->and($rightStringType->isUppercaseString())->yes()) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + } + $leftNumericStringNonEmpty = TypeCombinator::remove($leftStringType, new ConstantStringType('')); if ($leftNumericStringNonEmpty->isNumericString()->yes()) { $allRightConstantsZeroOrMore = false; diff --git a/src/Rules/Api/ApiInstanceofTypeRule.php b/src/Rules/Api/ApiInstanceofTypeRule.php index 225e65b000..ccda700696 100644 --- a/src/Rules/Api/ApiInstanceofTypeRule.php +++ b/src/Rules/Api/ApiInstanceofTypeRule.php @@ -16,6 +16,7 @@ use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\AccessoryType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\HasMethodType; use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\HasPropertyType; @@ -86,6 +87,7 @@ final class ApiInstanceofTypeRule implements Rule AccessoryNumericStringType::class => 'Type::isNumericString()', AccessoryLiteralStringType::class => 'Type::isLiteralString()', AccessoryLowercaseStringType::class => 'Type::isLowercaseString()', + AccessoryUppercaseStringType::class => 'Type::isUppercaseString()', AccessoryNonEmptyStringType::class => 'Type::isNonEmptyString()', AccessoryNonFalsyStringType::class => 'Type::isNonFalsyString()', HasMethodType::class => 'Type::hasMethod()', diff --git a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php index 7d9c764132..08d202f229 100644 --- a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php +++ b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php @@ -75,6 +75,7 @@ public function processNode(Node $node, Scope $scope): array }; $verbosity = VerbosityLevel::value(); + if ( ( $leftType->isConstantScalarValue()->yes() @@ -93,6 +94,24 @@ public function processNode(Node $node, Scope $scope): array $verbosity = VerbosityLevel::precise(); } + if ( + ( + $leftType->isConstantScalarValue()->yes() + && !$leftType->isString()->no() + && !$rightType->isConstantScalarValue()->yes() + && !$rightType->isString()->no() + && TrinaryLogic::extremeIdentity($leftType->isUppercaseString(), $rightType->isUppercaseString())->maybe() + ) || ( + $rightType->isConstantScalarValue()->yes() + && !$rightType->isString()->no() + && !$leftType->isConstantScalarValue()->yes() + && !$leftType->isString()->no() + && TrinaryLogic::extremeIdentity($leftType->isUppercaseString(), $rightType->isUppercaseString())->maybe() + ) + ) { + $verbosity = VerbosityLevel::precise(); + } + if (!$nodeType->getValue()) { return [ $addTip(RuleErrorBuilder::message(sprintf( diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index 491af5b813..c2eea34349 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -414,6 +414,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): 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 700063570e..d0970ba346 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -313,6 +313,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): 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 index fe57034af1..a564ea1029 100644 --- a/src/Type/Accessory/AccessoryLowercaseStringType.php +++ b/src/Type/Accessory/AccessoryLowercaseStringType.php @@ -309,6 +309,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 6587e85d35..117779e376 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -310,6 +310,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): 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 2a56264475..4bfb46f436 100644 --- a/src/Type/Accessory/AccessoryNonFalsyStringType.php +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -310,6 +310,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): 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 1ab8d31eff..79c83c7b0f 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -312,6 +312,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Accessory/AccessoryUppercaseStringType.php b/src/Type/Accessory/AccessoryUppercaseStringType.php new file mode 100644 index 0000000000..df2816324b --- /dev/null +++ b/src/Type/Accessory/AccessoryUppercaseStringType.php @@ -0,0 +1,390 @@ +acceptsWithReason($type, $strictTypes)->result; + } + + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult + { + if ($type instanceof CompoundType) { + return $type->isAcceptedWithReasonBy($this, $strictTypes); + } + + return new AcceptsResult($type->isUppercaseString(), []); + } + + public function isSuperTypeOf(Type $type): TrinaryLogic + { + return $this->isSuperTypeOfWithReason($type)->result; + } + + public function isSuperTypeOfWithReason(Type $type): IsSuperTypeOfResult + { + if ($type instanceof CompoundType) { + return $type->isSubTypeOfWithReason($this); + } + + if ($this->equals($type)) { + return IsSuperTypeOfResult::createYes(); + } + + return new IsSuperTypeOfResult($type->isUppercaseString(), []); + } + + public function isSubTypeOf(Type $otherType): TrinaryLogic + { + return $this->isSubTypeOfWithReason($otherType)->result; + } + + public function isSubTypeOfWithReason(Type $otherType): IsSuperTypeOfResult + { + if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOfWithReason($this); + } + + return (new IsSuperTypeOfResult($otherType->isUppercaseString(), [])) + ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe()); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + { + return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result; + } + + public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return $this->isSubTypeOfWithReason($acceptingType)->toAcceptsResult(); + } + + public function equals(Type $type): bool + { + return $type instanceof self; + } + + public function describe(VerbosityLevel $level): string + { + return 'uppercase-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 IntersectionType([new StringType(), new AccessoryUppercaseStringType()]); + } + + 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->isUppercaseString()->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::createNo(); + } + + public function isUppercaseString(): 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('uppercase-string'); + } + +} diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index f1ccf3c2a2..23650ed8d3 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -329,6 +329,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): 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 600fa6ca63..b673a3e7eb 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -385,6 +385,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): 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 c822edcc7a..7b9312e6c5 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -390,6 +390,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): 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 4d39431f7b..3de317ba0e 100644 --- a/src/Type/Accessory/OversizedArrayType.php +++ b/src/Type/Accessory/OversizedArrayType.php @@ -379,6 +379,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index c3fc5050b1..74b47dea0b 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -373,6 +373,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index d9bbea57e6..f182372fc0 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -606,6 +606,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/ClassStringType.php b/src/Type/ClassStringType.php index 2f58e27d15..30e761b660 100644 --- a/src/Type/ClassStringType.php +++ b/src/Type/ClassStringType.php @@ -79,6 +79,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index bbe6d5a73e..d12dda936e 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -723,6 +723,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): 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 6350d17cef..f0e78cf903 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -24,6 +24,7 @@ use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\ClassStringType; use PHPStan\Type\CompoundType; use PHPStan\Type\ConstantScalarType; @@ -51,6 +52,7 @@ use function key; use function strlen; use function strtolower; +use function strtoupper; use function substr; use function substr_count; @@ -356,6 +358,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createFromBoolean(strtolower($this->value) === $this->value); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean(strtoupper($this->value) === $this->value); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { if ($offsetType->isInteger()->yes()) { @@ -467,6 +474,10 @@ public function generalize(GeneralizePrecision $precision): Type $accessories[] = new AccessoryLowercaseStringType(); } + if (strtoupper($this->getValue()) === $this->getValue()) { + $accessories[] = new AccessoryUppercaseStringType(); + } + return new IntersectionType($accessories); } diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index bd5b2a9082..40ea01b5db 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -239,6 +239,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 6a8d2ab329..ca8efb12a6 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -26,6 +26,7 @@ use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\AccessoryType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; @@ -341,10 +342,14 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) || $type instanceof AccessoryNumericStringType || $type instanceof AccessoryNonFalsyStringType || $type instanceof AccessoryLowercaseStringType + || $type instanceof AccessoryUppercaseStringType ) { if ($type instanceof AccessoryLowercaseStringType && !$level->isPrecise()) { continue; } + if ($type instanceof AccessoryUppercaseStringType && !$level->isPrecise()) { + continue; + } if ($type instanceof AccessoryNonFalsyStringType) { $nonFalsyStr = true; @@ -659,6 +664,11 @@ public function isLowercaseString(): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLowercaseString()); } + public function isUppercaseString(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isUppercaseString()); + } + public function isClassStringType(): TrinaryLogic { return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isClassStringType()); @@ -1166,6 +1176,7 @@ public function toPhpDocNode(): TypeNode || $type instanceof AccessoryNumericStringType || $type instanceof AccessoryNonFalsyStringType || $type instanceof AccessoryLowercaseStringType + || $type instanceof AccessoryUppercaseStringType ) { if ($type instanceof AccessoryNonFalsyStringType) { $nonFalsyStr = true; diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index 662d2e1b0c..2d0c632298 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -387,6 +387,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/JustNullableTypeTrait.php b/src/Type/JustNullableTypeTrait.php index df6bee28b8..9545fff1da 100644 --- a/src/Type/JustNullableTypeTrait.php +++ b/src/Type/JustNullableTypeTrait.php @@ -157,6 +157,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 0b9d87394a..3ae9434a95 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -25,6 +25,7 @@ use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\OversizedArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -963,6 +964,22 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): TrinaryLogic + { + if ($this->subtractedType !== null) { + $uppercaseString = TypeCombinator::intersect( + new StringType(), + new AccessoryUppercaseStringType(), + ); + + if ($this->subtractedType->isSuperTypeOf($uppercaseString)->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 45f9cb05f3..a3d8daf1fe 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -496,6 +496,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/NullType.php b/src/Type/NullType.php index 7b60d5d3cb..2bd76023f8 100644 --- a/src/Type/NullType.php +++ b/src/Type/NullType.php @@ -305,6 +305,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 7427aa167c..9c633d62da 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -1046,6 +1046,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php index ccad08cefb..4591933a94 100644 --- a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php @@ -8,6 +8,7 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -59,6 +60,8 @@ public function getTypeFromFunctionCall( $stringType = $scope->getType($args[1]->value); if ($stringType->isLowercaseString()->yes()) { $returnValueType = new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); + } elseif ($stringType->isUppercaseString()->yes()) { + $returnValueType = new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]); } else { $returnValueType = new StringType(); } diff --git a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php index d29e20a7cc..5c680b5b62 100644 --- a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php +++ b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php @@ -9,6 +9,7 @@ use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantScalarType; @@ -94,6 +95,9 @@ private function implode(Type $arrayType, Type $separatorType): Type if ($arrayType->getIterableValueType()->isLowercaseString()->yes() && $separatorType->isLowercaseString()->yes()) { $accessoryTypes[] = new AccessoryLowercaseStringType(); } + if ($arrayType->getIterableValueType()->isUppercaseString()->yes() && $separatorType->isUppercaseString()->yes()) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + } if (count($accessoryTypes) > 0) { $accessoryTypes[] = new StringType(); diff --git a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php index 19d6c20b26..1a559b37d6 100644 --- a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php @@ -7,6 +7,7 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -39,16 +40,24 @@ final class ParseUrlFunctionDynamicReturnTypeExtension implements DynamicFunctio /** @var array|null */ private ?array $componentTypesPairedStrings = null; + private ?Type $allComponentsTogetherType = null; + /** @var array|null */ private ?array $componentTypesPairedConstantsForLowercaseString = null; /** @var array|null */ private ?array $componentTypesPairedStringsForLowercaseString = null; - private ?Type $allComponentsTogetherType = null; - private ?Type $allComponentsTogetherTypeForLowercaseString = null; + /** @var array|null */ + private ?array $componentTypesPairedConstantsForUppercaseString = null; + + /** @var array|null */ + private ?array $componentTypesPairedStringsForUppercaseString = null; + + private ?Type $allComponentsTogetherTypeForUppercaseString = null; + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'parse_url'; @@ -67,12 +76,18 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $componentType = $scope->getType($functionCall->getArgs()[1]->value); if (!$componentType->isConstantValue()->yes()) { - return $this->createAllComponentsReturnType($urlType->isLowercaseString()->yes()); + return $this->createAllComponentsReturnType( + $urlType->isLowercaseString()->yes(), + $urlType->isUppercaseString()->yes(), + ); } $componentType = $componentType->toInteger(); if (!$componentType instanceof ConstantIntegerType) { - return $this->createAllComponentsReturnType($urlType->isLowercaseString()->yes()); + return $this->createAllComponentsReturnType( + $urlType->isLowercaseString()->yes(), + $urlType->isLowercaseString()->yes(), + ); } } else { $componentType = new ConstantIntegerType(-1); @@ -96,7 +111,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($componentType->getValue() === -1) { return TypeCombinator::union( - $this->createComponentsArray($urlType->isLowercaseString()->yes()), + $this->createComponentsArray( + $urlType->isLowercaseString()->yes(), + $urlType->isUppercaseString()->yes(), + ), new ConstantBooleanType(false), ); } @@ -105,10 +123,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $this->componentTypesPairedConstantsForLowercaseString[$componentType->getValue()] ?? new ConstantBooleanType(false); } + if ($urlType->isUppercaseString()->yes()) { + return $this->componentTypesPairedConstantsForUppercaseString[$componentType->getValue()] ?? new ConstantBooleanType(false); + } + return $this->componentTypesPairedConstants[$componentType->getValue()] ?? new ConstantBooleanType(false); } - private function createAllComponentsReturnType(bool $urlIsLowercase): Type + private function createAllComponentsReturnType(bool $urlIsLowercase, bool $urlIsUppercase): Type { if ($urlIsLowercase) { if ($this->allComponentsTogetherTypeForLowercaseString === null) { @@ -117,7 +139,7 @@ private function createAllComponentsReturnType(bool $urlIsLowercase): Type new NullType(), IntegerRangeType::fromInterval(0, 65535), new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), - $this->createComponentsArray(true), + $this->createComponentsArray(true, false), ]; $this->allComponentsTogetherTypeForLowercaseString = TypeCombinator::union(...$returnTypes); @@ -126,13 +148,29 @@ private function createAllComponentsReturnType(bool $urlIsLowercase): Type return $this->allComponentsTogetherTypeForLowercaseString; } + if ($urlIsUppercase) { + if ($this->allComponentsTogetherTypeForUppercaseString === null) { + $returnTypes = [ + new ConstantBooleanType(false), + new NullType(), + IntegerRangeType::fromInterval(0, 65535), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + $this->createComponentsArray(false, true), + ]; + + $this->allComponentsTogetherTypeForUppercaseString = TypeCombinator::union(...$returnTypes); + } + + return $this->allComponentsTogetherTypeForUppercaseString; + } + if ($this->allComponentsTogetherType === null) { $returnTypes = [ new ConstantBooleanType(false), new NullType(), IntegerRangeType::fromInterval(0, 65535), new StringType(), - $this->createComponentsArray(false), + $this->createComponentsArray(false, false), ]; $this->allComponentsTogetherType = TypeCombinator::union(...$returnTypes); @@ -141,7 +179,7 @@ private function createAllComponentsReturnType(bool $urlIsLowercase): Type return $this->allComponentsTogetherType; } - private function createComponentsArray(bool $urlIsLowercase): Type + private function createComponentsArray(bool $urlIsLowercase, bool $urlIsUppercase): Type { $builder = ConstantArrayTypeBuilder::createEmpty(); @@ -153,6 +191,14 @@ private function createComponentsArray(bool $urlIsLowercase): Type foreach ($this->componentTypesPairedStringsForLowercaseString as $componentName => $componentValueType) { $builder->setOffsetValueType(new ConstantStringType($componentName), $componentValueType, true); } + } elseif ($urlIsUppercase) { + if ($this->componentTypesPairedStringsForUppercaseString === null) { + throw new ShouldNotHappenException(); + } + + foreach ($this->componentTypesPairedStringsForUppercaseString as $componentName => $componentValueType) { + $builder->setOffsetValueType(new ConstantStringType($componentName), $componentValueType, true); + } } else { if ($this->componentTypesPairedStrings === null) { throw new ShouldNotHappenException(); @@ -173,13 +219,10 @@ private function cacheReturnTypes(): void } $string = new StringType(); - $lowercaseString = new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); $port = IntegerRangeType::fromInterval(0, 65535); $false = new ConstantBooleanType(false); $null = new NullType(); - $stringOrFalseOrNull = TypeCombinator::union($string, $false, $null); - $lowercaseStringOrFalseOrNull = TypeCombinator::union($lowercaseString, $false, $null); $portOrFalseOrNull = TypeCombinator::union($port, $false, $null); $this->componentTypesPairedConstants = [ @@ -192,16 +235,6 @@ private function cacheReturnTypes(): void PHP_URL_QUERY => $stringOrFalseOrNull, PHP_URL_FRAGMENT => $stringOrFalseOrNull, ]; - $this->componentTypesPairedConstantsForLowercaseString = [ - PHP_URL_SCHEME => $lowercaseStringOrFalseOrNull, - PHP_URL_HOST => $lowercaseStringOrFalseOrNull, - PHP_URL_PORT => $portOrFalseOrNull, - PHP_URL_USER => $lowercaseStringOrFalseOrNull, - PHP_URL_PASS => $lowercaseStringOrFalseOrNull, - PHP_URL_PATH => $lowercaseStringOrFalseOrNull, - PHP_URL_QUERY => $lowercaseStringOrFalseOrNull, - PHP_URL_FRAGMENT => $lowercaseStringOrFalseOrNull, - ]; $this->componentTypesPairedStrings = [ 'scheme' => $string, @@ -213,6 +246,20 @@ private function cacheReturnTypes(): void 'query' => $string, 'fragment' => $string, ]; + + $lowercaseString = new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); + $lowercaseStringOrFalseOrNull = TypeCombinator::union($lowercaseString, $false, $null); + + $this->componentTypesPairedConstantsForLowercaseString = [ + PHP_URL_SCHEME => $lowercaseStringOrFalseOrNull, + PHP_URL_HOST => $lowercaseStringOrFalseOrNull, + PHP_URL_PORT => $portOrFalseOrNull, + PHP_URL_USER => $lowercaseStringOrFalseOrNull, + PHP_URL_PASS => $lowercaseStringOrFalseOrNull, + PHP_URL_PATH => $lowercaseStringOrFalseOrNull, + PHP_URL_QUERY => $lowercaseStringOrFalseOrNull, + PHP_URL_FRAGMENT => $lowercaseStringOrFalseOrNull, + ]; $this->componentTypesPairedStringsForLowercaseString = [ 'scheme' => $lowercaseString, 'host' => $lowercaseString, @@ -223,6 +270,30 @@ private function cacheReturnTypes(): void 'query' => $lowercaseString, 'fragment' => $lowercaseString, ]; + + $uppercaseString = new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]); + $uppercaseStringOrFalseOrNull = TypeCombinator::union($uppercaseString, $false, $null); + + $this->componentTypesPairedConstantsForUppercaseString = [ + PHP_URL_SCHEME => $uppercaseStringOrFalseOrNull, + PHP_URL_HOST => $uppercaseStringOrFalseOrNull, + PHP_URL_PORT => $portOrFalseOrNull, + PHP_URL_USER => $uppercaseStringOrFalseOrNull, + PHP_URL_PASS => $uppercaseStringOrFalseOrNull, + PHP_URL_PATH => $uppercaseStringOrFalseOrNull, + PHP_URL_QUERY => $uppercaseStringOrFalseOrNull, + PHP_URL_FRAGMENT => $uppercaseStringOrFalseOrNull, + ]; + $this->componentTypesPairedStringsForUppercaseString = [ + 'scheme' => $uppercaseString, + 'host' => $uppercaseString, + 'port' => $port, + 'user' => $uppercaseString, + 'pass' => $uppercaseString, + 'path' => $uppercaseString, + 'query' => $uppercaseString, + 'fragment' => $uppercaseString, + ]; } } diff --git a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php index c80c10d9c0..db046bbe10 100644 --- a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php @@ -9,6 +9,7 @@ use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; @@ -100,6 +101,10 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( $accessories[] = new AccessoryLowercaseStringType(); } + if ($subjectArgumentType->isUppercaseString()->yes() && $replaceArgumentType->isUppercaseString()->yes()) { + $accessories[] = new AccessoryUppercaseStringType(); + } + if (count($accessories) > 0) { $accessories[] = new StringType(); return new IntersectionType($accessories); diff --git a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php index 149caff081..db36561ab6 100644 --- a/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php +++ b/src/Type/Php/StrCaseFunctionsReturnTypeExtension.php @@ -9,6 +9,7 @@ use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; @@ -23,6 +24,7 @@ use function is_callable; use function mb_check_encoding; use const MB_CASE_LOWER; +use const MB_CASE_UPPER; final class StrCaseFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -70,6 +72,8 @@ public function getTypeFromFunctionCall( $modes = []; $keepLowercase = false; $forceLowercase = false; + $keepUppercase = false; + $forceUppercase = false; if ($fnName === 'mb_convert_case') { $modeType = $scope->getType($args[1]->value); @@ -85,6 +89,16 @@ public function getTypeFromFunctionCall( 3, // MB_CASE_FOLD, 7, // MB_CASE_FOLD_SIMPLE ])) === 0; + $forceUppercase = count(array_diff($modes, [ + MB_CASE_UPPER, + 4, // MB_CASE_UPPER_SIMPLE + ])) === 0; + $keepUppercase = count(array_diff($modes, [ + MB_CASE_UPPER, + 4, // MB_CASE_UPPER_SIMPLE + 3, // MB_CASE_FOLD, + 7, // MB_CASE_FOLD_SIMPLE + ])) === 0; } } elseif (in_array($fnName, ['ucwords', 'mb_convert_kana'], true)) { if (count($args) >= 2) { @@ -97,6 +111,10 @@ public function getTypeFromFunctionCall( $forceLowercase = true; } elseif (in_array($fnName, ['lcfirst', 'mb_lcfirst'], true)) { $keepLowercase = true; + } elseif (in_array($fnName, ['strtoupper', 'mb_strtoupper'], true)) { + $forceUppercase = true; + } elseif (in_array($fnName, ['ucfirst', 'mb_ucfirst'], true)) { + $keepUppercase = true; } $constantStrings = array_map(static fn ($type) => $type->getValue(), $argType->getConstantStrings()); @@ -127,6 +145,9 @@ public function getTypeFromFunctionCall( if ($forceLowercase || ($keepLowercase && $argType->isLowercaseString()->yes())) { $accessoryTypes[] = new AccessoryLowercaseStringType(); } + if ($forceUppercase || ($keepUppercase && $argType->isUppercaseString()->yes())) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + } if ($argType->isNumericString()->yes()) { $accessoryTypes[] = new AccessoryNumericStringType(); diff --git a/src/Type/Php/StrPadFunctionReturnTypeExtension.php b/src/Type/Php/StrPadFunctionReturnTypeExtension.php index 92b0ec286d..d556fff1be 100644 --- a/src/Type/Php/StrPadFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrPadFunctionReturnTypeExtension.php @@ -9,6 +9,7 @@ use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntersectionType; @@ -57,6 +58,9 @@ public function getTypeFromFunctionCall( if ($inputType->isLowercaseString()->yes() && ($padStringType === null || $padStringType->isLowercaseString()->yes())) { $accessoryTypes[] = new AccessoryLowercaseStringType(); } + if ($inputType->isUppercaseString()->yes() && ($padStringType === null || $padStringType->isUppercaseString()->yes())) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + } if (count($accessoryTypes) > 0) { $accessoryTypes[] = new StringType(); diff --git a/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php b/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php index 632fea85f0..98afacb7d7 100644 --- a/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php @@ -11,6 +11,7 @@ use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -98,6 +99,10 @@ public function getTypeFromFunctionCall( $accessoryTypes[] = new AccessoryLowercaseStringType(); } + if ($inputType->isUppercaseString()->yes()) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + } + if (count($accessoryTypes) > 0) { $accessoryTypes[] = new StringType(); return new IntersectionType($accessoryTypes); diff --git a/src/Type/Php/SubstrDynamicReturnTypeExtension.php b/src/Type/Php/SubstrDynamicReturnTypeExtension.php index 4364f2374f..df1d0cf060 100644 --- a/src/Type/Php/SubstrDynamicReturnTypeExtension.php +++ b/src/Type/Php/SubstrDynamicReturnTypeExtension.php @@ -9,6 +9,7 @@ use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; @@ -99,6 +100,9 @@ public function getTypeFromFunctionCall( if ($string->isLowercaseString()->yes()) { $accessoryTypes[] = new AccessoryLowercaseStringType(); } + if ($string->isUppercaseString()->yes()) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + } if ($string->isNonEmptyString()->yes() && ($negativeOffset || $zeroOffset && $positiveLength)) { $isNotEmpty = true; if ($string->isNonFalsyString()->yes() && !$maybeOneLength) { diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index 5fd6f7e358..7056fc5901 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -580,6 +580,11 @@ public function isLowercaseString(): TrinaryLogic return $this->getStaticObjectType()->isLowercaseString(); } + public function isUppercaseString(): TrinaryLogic + { + return $this->getStaticObjectType()->isUppercaseString(); + } + public function isClassStringType(): TrinaryLogic { return $this->getStaticObjectType()->isClassStringType(); diff --git a/src/Type/StrictMixedType.php b/src/Type/StrictMixedType.php index 13b0fee7de..c839779a55 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -290,6 +290,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 9fcd1e1895..c49cf7823c 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -245,6 +245,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isUppercaseString(): 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 76855da1f4..e6355875ca 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -468,6 +468,11 @@ public function isLowercaseString(): TrinaryLogic return $this->resolve()->isLowercaseString(); } + public function isUppercaseString(): TrinaryLogic + { + return $this->resolve()->isUppercaseString(); + } + public function isClassStringType(): TrinaryLogic { return $this->resolve()->isClassStringType(); diff --git a/src/Type/Traits/ObjectTypeTrait.php b/src/Type/Traits/ObjectTypeTrait.php index 8ca9c69c29..7cf3659a50 100644 --- a/src/Type/Traits/ObjectTypeTrait.php +++ b/src/Type/Traits/ObjectTypeTrait.php @@ -203,6 +203,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Type.php b/src/Type/Type.php index 6c5064f4ea..6562ba2005 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -274,6 +274,8 @@ public function isLiteralString(): TrinaryLogic; public function isLowercaseString(): TrinaryLogic; + public function isUppercaseString(): TrinaryLogic; + public function isClassStringType(): TrinaryLogic; public function isVoid(): TrinaryLogic; diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index f0bef45903..6f4cbf462b 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -650,6 +650,11 @@ public function isLowercaseString(): TrinaryLogic return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isLowercaseString()); } + public function isUppercaseString(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isUppercaseString()); + } + public function isClassStringType(): TrinaryLogic { return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isClassStringType()); diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index 47b0d2477c..988b2a2405 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -8,6 +8,7 @@ use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateType; @@ -108,7 +109,10 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc $moreVerbose = true; return $type; } - if ($type instanceof AccessoryLowercaseStringType) { + if ( + $type instanceof AccessoryLowercaseStringType + || $type instanceof AccessoryUppercaseStringType + ) { $moreVerbose = true; $veryVerbose = true; return $type; diff --git a/src/Type/VoidType.php b/src/Type/VoidType.php index 4353c6efdd..18b3d719c1 100644 --- a/src/Type/VoidType.php +++ b/src/Type/VoidType.php @@ -217,6 +217,11 @@ public function isLowercaseString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isClassStringType(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/stubs/core.stub b/stubs/core.stub index abd719b4f5..265d27e7b3 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -69,7 +69,13 @@ function str_shuffle(string $string): string {} /** * @param array $result - * @param-out ($string is lowercase-string ? array|lowercase-string> : array|string>) $result + * @param-out ($string is lowercase-string + * ? array|lowercase-string> + * : ($string is uppercase-string + * ? array|uppercase-string> + * : array|string> + * ) + * ) $result */ function parse_str(string $string, array &$result): void {} @@ -315,17 +321,17 @@ function is_callable(mixed $value, bool $syntax_only = false, ?string &$callable function abs($num) {} /** - * @return ($string is lowercase-string ? lowercase-string : string) + * @return ($string is lowercase-string ? lowercase-string : ($string is uppercase-string ? uppercase-string : string)) */ function trim(string $string, string $characters = " \n\r\t\v\x00"): string {} /** - * @return ($string is lowercase-string ? lowercase-string : string) + * @return ($string is lowercase-string ? lowercase-string : ($string is uppercase-string ? uppercase-string : string)) */ function ltrim(string $string, string $characters = " \n\r\t\v\x00"): string {} /** - * @return ($string is lowercase-string ? lowercase-string : string) + * @return ($string is lowercase-string ? lowercase-string : ($string is uppercase-string ? uppercase-string : string)) */ function rtrim(string $string, string $characters = " \n\r\t\v\x00"): string {} diff --git a/tests/PHPStan/Analyser/data/explode-php80.php b/tests/PHPStan/Analyser/data/explode-php80.php index 4fa4539356..1c01239587 100644 --- a/tests/PHPStan/Analyser/data/explode-php80.php +++ b/tests/PHPStan/Analyser/data/explode-php80.php @@ -9,6 +9,7 @@ class ExplodingStrings public function doFoo(string $s): void { assertType('non-empty-list', explode($s, 'foo')); - assertType('non-empty-list', explode($s, 'FOO')); + assertType('non-empty-list', explode($s, 'FOO')); + assertType('non-empty-list', explode($s, 'Foo')); } } diff --git a/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type.php b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type.php index a38116b682..8ffb1ddb89 100644 --- a/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type.php +++ b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type.php @@ -472,7 +472,7 @@ function coalesce() assertType('int<0, max>', rand() ?? false); - assertType('0|lowercase-string', preg_replace('', '', '') ?? 0); + assertType('0|(lowercase-string&uppercase-string)', preg_replace('', '', '') ?? 0); $foo = new FooCoalesce(); diff --git a/tests/PHPStan/Analyser/nsrt/literal-string.php b/tests/PHPStan/Analyser/nsrt/literal-string.php index 93bf8949d9..c30fbdac80 100644 --- a/tests/PHPStan/Analyser/nsrt/literal-string.php +++ b/tests/PHPStan/Analyser/nsrt/literal-string.php @@ -37,8 +37,9 @@ public function doFoo($literalString, string $string, $numericString) str_repeat('a', 99) ); assertType('literal-string&lowercase-string&non-falsy-string', str_repeat('a', 100)); - assertType('literal-string&lowercase-string&non-empty-string&numeric-string', str_repeat('0', 100)); // could be non-falsy-string - assertType('literal-string&lowercase-string&non-falsy-string&numeric-string', str_repeat('1', 100)); + assertType('literal-string&non-falsy-string&uppercase-string', str_repeat('A', 100)); + assertType('literal-string&lowercase-string&non-empty-string&numeric-string&uppercase-string', str_repeat('0', 100)); // could be non-falsy-string + assertType('literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string', str_repeat('1', 100)); // Repeating a numeric type multiple times can lead to a non-numeric type: 3v4l.org/aRBdZ assertType('non-empty-string', str_repeat($numericString, 100)); @@ -51,13 +52,14 @@ public function doFoo($literalString, string $string, $numericString) assertType("non-empty-string", str_repeat($numericString, 2)); assertType("literal-string", str_repeat($literalString, 1)); $x = rand(1,2); - assertType("literal-string&lowercase-string&non-falsy-string", str_repeat(' 1 ', $x)); - assertType("literal-string&lowercase-string&non-falsy-string", str_repeat('+1', $x)); + assertType("literal-string&lowercase-string&non-falsy-string&uppercase-string", str_repeat(' 1 ', $x)); + assertType("literal-string&lowercase-string&non-falsy-string&uppercase-string", str_repeat('+1', $x)); assertType("literal-string&lowercase-string&non-falsy-string", str_repeat('1e9', $x)); - assertType("literal-string&lowercase-string&non-falsy-string&numeric-string", str_repeat('19', $x)); + assertType("literal-string&non-falsy-string&uppercase-string", str_repeat('1E9', $x)); + assertType("literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string", str_repeat('19', $x)); $x = rand(0,2); - assertType("literal-string&lowercase-string", str_repeat('19', $x)); + assertType("literal-string&lowercase-string&uppercase-string", str_repeat('19', $x)); $x = rand(-10,-1); assertType("*NEVER*", str_repeat('19', $x)); diff --git a/tests/PHPStan/Analyser/nsrt/lowercase-string-subtr.php b/tests/PHPStan/Analyser/nsrt/lowercase-string-substr.php similarity index 100% rename from tests/PHPStan/Analyser/nsrt/lowercase-string-subtr.php rename to tests/PHPStan/Analyser/nsrt/lowercase-string-substr.php diff --git a/tests/PHPStan/Analyser/nsrt/more-types.php b/tests/PHPStan/Analyser/nsrt/more-types.php index 9c3296c3d8..e98bc06a76 100644 --- a/tests/PHPStan/Analyser/nsrt/more-types.php +++ b/tests/PHPStan/Analyser/nsrt/more-types.php @@ -19,6 +19,8 @@ class Foo * @param non-empty-mixed $nonEmptyMixed * @param lowercase-string $lowercaseString * @param non-empty-lowercase-string $nonEmptyLowercaseString + * @param uppercase-string $uppercaseString + * @param non-empty-uppercase-string $nonEmptyUppercaseString */ public function doFoo( $pureCallable, @@ -32,6 +34,8 @@ public function doFoo( $nonEmptyMixed, $lowercaseString, $nonEmptyLowercaseString, + $uppercaseString, + $nonEmptyUppercaseString, ): void { assertType('pure-callable(): mixed', $pureCallable); @@ -45,6 +49,8 @@ public function doFoo( assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $nonEmptyMixed); assertType('lowercase-string', $lowercaseString); assertType('lowercase-string&non-empty-string', $nonEmptyLowercaseString); + assertType('uppercase-string', $uppercaseString); + assertType('non-empty-string&uppercase-string', $nonEmptyUppercaseString); } } diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string.php b/tests/PHPStan/Analyser/nsrt/non-empty-string.php index 5400b1d31a..cd831db4d8 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string.php @@ -318,12 +318,12 @@ public function doFoo(string $s, string $nonEmpty, string $nonFalsy, int $i, boo assertType('string', escapeshellcmd($s)); assertType('non-empty-string', escapeshellcmd($nonEmpty)); - assertType('string', strtoupper($s)); - assertType('non-empty-string', strtoupper($nonEmpty)); + assertType('uppercase-string', strtoupper($s)); + assertType('non-empty-string&uppercase-string', strtoupper($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('uppercase-string', mb_strtoupper($s)); + assertType('non-empty-string&uppercase-string', mb_strtoupper($nonEmpty)); assertType('lowercase-string', mb_strtolower($s)); assertType('lowercase-string&non-empty-string', mb_strtolower($nonEmpty)); assertType('string', lcfirst($s)); diff --git a/tests/PHPStan/Analyser/nsrt/non-falsy-string.php b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php index 4127dd5870..c5fd9fc1d8 100644 --- a/tests/PHPStan/Analyser/nsrt/non-falsy-string.php +++ b/tests/PHPStan/Analyser/nsrt/non-falsy-string.php @@ -87,9 +87,9 @@ function stringFunctions(string $s, $nonFalsey, $arrayOfNonFalsey, $nonEmptyArra assertType('non-falsy-string', escapeshellarg($nonFalsey)); assertType('non-falsy-string', escapeshellcmd($nonFalsey)); - assertType('non-falsy-string', strtoupper($nonFalsey)); + assertType('non-falsy-string&uppercase-string', strtoupper($nonFalsey)); assertType('lowercase-string&non-falsy-string', strtolower($nonFalsey)); - assertType('non-falsy-string', mb_strtoupper($nonFalsey)); + assertType('non-falsy-string&uppercase-string', mb_strtoupper($nonFalsey)); assertType('lowercase-string&non-falsy-string', mb_strtolower($nonFalsey)); assertType('non-falsy-string', lcfirst($nonFalsey)); assertType('non-falsy-string', ucfirst($nonFalsey)); diff --git a/tests/PHPStan/Analyser/nsrt/str-casing.php b/tests/PHPStan/Analyser/nsrt/str-casing.php index 3f0c76f250..ebdbd8054d 100644 --- a/tests/PHPStan/Analyser/nsrt/str-casing.php +++ b/tests/PHPStan/Analyser/nsrt/str-casing.php @@ -39,52 +39,52 @@ public function bar($numericS, $nonE, $lowercaseS, $literal, $edgeUnion, $caseMo assertType("non-falsy-string", mb_convert_kana('Abc123アガば漢', $mixed)); assertType("lowercase-string&numeric-string", strtolower($numericS)); - assertType("numeric-string", strtoupper($numericS)); + assertType("numeric-string&uppercase-string", strtoupper($numericS)); assertType("lowercase-string&numeric-string", mb_strtolower($numericS)); - assertType("numeric-string", mb_strtoupper($numericS)); + assertType("numeric-string&uppercase-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&uppercase-string", mb_convert_case($numericS, MB_CASE_UPPER)); 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("lowercase-string&non-empty-string", strtolower($nonE)); - assertType("non-empty-string", strtoupper($nonE)); + assertType("non-empty-string&uppercase-string", strtoupper($nonE)); assertType("lowercase-string&non-empty-string", mb_strtolower($nonE)); - assertType("non-empty-string", mb_strtoupper($nonE)); + assertType("non-empty-string&uppercase-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&uppercase-string", mb_convert_case($nonE, MB_CASE_UPPER)); 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("lowercase-string", strtolower($literal)); - assertType("string", strtoupper($literal)); + assertType("uppercase-string", strtoupper($literal)); assertType("lowercase-string", mb_strtolower($literal)); - assertType("string", mb_strtoupper($literal)); + assertType("uppercase-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("uppercase-string", mb_convert_case($literal, MB_CASE_UPPER)); 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("uppercase-string", strtoupper($lowercaseS)); assertType("lowercase-string", mb_strtolower($lowercaseS)); - assertType("string", mb_strtoupper($lowercaseS)); + assertType("uppercase-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("uppercase-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)); diff --git a/tests/PHPStan/Analyser/nsrt/uppercase-string-implode.php b/tests/PHPStan/Analyser/nsrt/uppercase-string-implode.php new file mode 100644 index 0000000000..2ddf808da2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/uppercase-string-implode.php @@ -0,0 +1,22 @@ + $commonStrings + * @param array $uppercaseStrings + */ + public function doFoo(string $s, string $ls, array $commonStrings, array $uppercaseStrings): void + { + assertType('string', implode($s, $commonStrings)); + assertType('string', implode($s, $uppercaseStrings)); + assertType('string', implode($ls, $commonStrings)); + assertType('uppercase-string', implode($ls, $uppercaseStrings)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/uppercase-string-pad.php b/tests/PHPStan/Analyser/nsrt/uppercase-string-pad.php new file mode 100644 index 0000000000..7045582dc4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/uppercase-string-pad.php @@ -0,0 +1,23 @@ +, user?: uppercase-string, pass?: uppercase-string, path?: uppercase-string, query?: uppercase-string, fragment?: uppercase-string}|false', parse_url($uppercase)); + assertType('uppercase-string|false|null', parse_url($uppercase, PHP_URL_SCHEME)); + assertType('uppercase-string|false|null', parse_url($uppercase, PHP_URL_HOST)); + assertType('int<0, 65535>|false|null', parse_url($uppercase, PHP_URL_PORT)); + assertType('uppercase-string|false|null', parse_url($uppercase, PHP_URL_USER)); + assertType('uppercase-string|false|null', parse_url($uppercase, PHP_URL_PASS)); + assertType('uppercase-string|false|null', parse_url($uppercase, PHP_URL_PATH)); + assertType('uppercase-string|false|null', parse_url($uppercase, PHP_URL_QUERY)); + assertType('uppercase-string|false|null', parse_url($uppercase, PHP_URL_FRAGMENT)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/uppercase-string-parse.php b/tests/PHPStan/Analyser/nsrt/uppercase-string-parse.php new file mode 100644 index 0000000000..6504143dc9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/uppercase-string-parse.php @@ -0,0 +1,36 @@ += 8.0 + +namespace UppercaseStringSubstr; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @param uppercase-string $uppercase + */ + public function doSubstr(string $uppercase): void + { + assertType('uppercase-string', substr($uppercase, 5)); + assertType('uppercase-string', substr($uppercase, -5)); + assertType('uppercase-string', substr($uppercase, 0, 5)); + } + + /** + * @param uppercase-string $uppercase + */ + public function doMbSubstr(string $uppercase): void + { + assertType('uppercase-string', mb_substr($uppercase, 5)); + assertType('uppercase-string', mb_substr($uppercase, -5)); + assertType('uppercase-string', mb_substr($uppercase, 0, 5)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php b/tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php new file mode 100644 index 0000000000..0c24268faf --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php @@ -0,0 +1,29 @@ +analyse([__DIR__ . '/data/lowercase-string.php'], $errors); } + public function testUppercaseString(): void + { + $errors = [ + [ + "Strict comparison using === between uppercase-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 uppercase-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 uppercase-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 uppercase-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 uppercase-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 uppercase-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 uppercase-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/uppercase-string.php'], $errors); + } + public function testBug10493(): void { $this->checkAlwaysTrueStrictComparison = true; diff --git a/tests/PHPStan/Rules/Comparison/data/uppercase-string.php b/tests/PHPStan/Rules/Comparison/data/uppercase-string.php new file mode 100644 index 0000000000..5641f0a1d5 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/uppercase-string.php @@ -0,0 +1,31 @@ +, null given.', 20, diff --git a/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php index deec20a074..73cd2b4a6f 100644 --- a/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_EOL; /** * @extends RuleTestCase @@ -66,7 +67,8 @@ public function testBug7484(): void [ 'Generator expects key type K of int|string, (K of int)|string given.', 21, - 'Type string is not always the same as K. It breaks the contract for some argument types, typically subtypes.', + '• Type string is not always the same as K. It breaks the contract for some argument types, typically subtypes.' . PHP_EOL . + '• Type string is not always the same as K. It breaks the contract for some argument types, typically subtypes.', ], ]); } diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index b9c196420d..5c417800f1 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3383,6 +3383,29 @@ public function testLowercaseString(): void ]); } + public function testUppercaseString(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/uppercase-string.php'], [ + [ + 'Parameter #1 $s of method UppercaseString\Bar::acceptUppercaseString() expects uppercase-string, \'NotUpperCase\' given.', + 26, + ], + [ + 'Parameter #1 $s of method UppercaseString\Bar::acceptUppercaseString() expects uppercase-string, string given.', + 28, + ], + [ + 'Parameter #1 $s of method UppercaseString\Bar::acceptUppercaseString() expects uppercase-string, numeric-string given.', + 30, + ], + ]); + } + public function testBug10159(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/data/uppercase-string.php b/tests/PHPStan/Rules/Methods/data/uppercase-string.php new file mode 100644 index 0000000000..de7500ee8c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/uppercase-string.php @@ -0,0 +1,33 @@ +acceptUppercaseString('NotUpperCase'); + $this->acceptUppercaseString('UPPERCASE'); + $this->acceptUppercaseString($string); + $this->acceptUppercaseString($uppercaseString); + $this->acceptUppercaseString($numericString); + $this->acceptUppercaseString($nonEmptyLowercaseString); + } +} diff --git a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php index 036d2b0f1f..2098a1f02b 100644 --- a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php @@ -154,12 +154,12 @@ 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&lowercase-string&non-falsy-string', (new ConstantStringType('a'))->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&lowercase-string&non-empty-string&numeric-string', (new ConstantStringType('0'))->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&non-falsy-string&uppercase-string', (new ConstantStringType('A'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-empty-string&numeric-string&uppercase-string', (new ConstantStringType('0'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string', (new ConstantStringType('1.123'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&uppercase-string', (new ConstantStringType(' 1 1 '))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string', (new ConstantStringType('+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&lowercase-string&non-falsy-string&uppercase-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())); diff --git a/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php b/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php index 2e2bcf976c..cd278837d6 100644 --- a/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php +++ b/tests/PHPStan/Type/Constant/OversizedArrayBuilderTest.php @@ -31,6 +31,11 @@ public function dataBuild(): iterable 'non-empty-array&oversized-array', ]; + yield [ + '[1, 2, 3, ...[1, \'FOO\' => 2, 3]]', + 'non-empty-array&oversized-array', + ]; + yield [ '[1, 2, 2 => 3]', 'non-empty-list&oversized-array', @@ -51,6 +56,10 @@ public function dataBuild(): iterable '[1, \'foo\' => 2, 3]', 'non-empty-array&oversized-array', ]; + yield [ + '[1, \'FOO\' => 2, 3]', + 'non-empty-array&oversized-array', + ]; } /** diff --git a/tests/PHPStan/Type/IntersectionTypeTest.php b/tests/PHPStan/Type/IntersectionTypeTest.php index 7c9bcc0722..0b5c5c0818 100644 --- a/tests/PHPStan/Type/IntersectionTypeTest.php +++ b/tests/PHPStan/Type/IntersectionTypeTest.php @@ -8,6 +8,7 @@ use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -465,6 +466,21 @@ public function dataDescribe(): iterable VerbosityLevel::precise(), 'lowercase-string', ]; + yield [ + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + VerbosityLevel::typeOnly(), + 'string', + ]; + yield [ + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + VerbosityLevel::value(), + 'string', + ]; + yield [ + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + VerbosityLevel::precise(), + 'uppercase-string', + ]; } /** diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 55dc30542b..8667d7c419 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -23,6 +23,7 @@ use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\HasMethodType; use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\HasOffsetValueType; @@ -2009,6 +2010,46 @@ public function dataUnion(): iterable UnionType::class, 'literal-string|lowercase-string', ], + [ + [ + new StringType(), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + StringType::class, + 'string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'numeric-string|uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'non-falsy-string|uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'non-empty-string|uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'literal-string|uppercase-string', + ], [ [ TemplateTypeFactory::create( @@ -3934,6 +3975,46 @@ public function dataIntersect(): iterable IntersectionType::class, 'literal-string&lowercase-string', ], + [ + [ + new StringType(), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + IntersectionType::class, + 'uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + IntersectionType::class, + 'numeric-string&uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + IntersectionType::class, + 'non-falsy-string&uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + IntersectionType::class, + 'non-empty-string&uppercase-string', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLiteralStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + IntersectionType::class, + 'literal-string&uppercase-string', + ], ]; if (PHP_VERSION_ID < 80100) { @@ -4423,6 +4504,23 @@ public function dataIntersect(): iterable ConstantStringType::class, '\'foo\'', ]; + + yield [ + [ + new ConstantStringType('foo'), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + NeverType::class, + '*NEVER*=implicit', + ]; + yield [ + [ + new ConstantStringType('FOO'), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + ConstantStringType::class, + '\'FOO\'', + ]; } /** diff --git a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php index eebdb08014..ee4e6b55c7 100644 --- a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php +++ b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php @@ -9,6 +9,7 @@ use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -223,6 +224,11 @@ public function dataToPhpDocNode(): iterable 'lowercase-string', ]; + yield [ + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + 'uppercase-string', + ]; + yield [ new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), 'non-empty-string', @@ -333,6 +339,11 @@ public function dataToPhpDocNodeWithoutCheckingEquals(): iterable '(literal-string & lowercase-string & non-falsy-string)', ]; + yield [ + new ConstantStringType("FOO\nBAR\nBAZ"), + '(literal-string & non-falsy-string & uppercase-string)', + ]; + yield [ new ConstantIntegerType(PHP_INT_MIN), (string) PHP_INT_MIN, From 09dff31db008c11c0f7b1364f1207aeaaecfe729 Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" <25754+pmjones@users.noreply.github.com> Date: Fri, 8 Nov 2024 15:12:10 -0600 Subject: [PATCH 02/24] Update src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php Co-authored-by: Markus Staab --- src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php index 1a559b37d6..e2c0128964 100644 --- a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php @@ -86,7 +86,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if (!$componentType instanceof ConstantIntegerType) { return $this->createAllComponentsReturnType( $urlType->isLowercaseString()->yes(), - $urlType->isLowercaseString()->yes(), + $urlType->isUppercaseString()->yes(), ); } } else { From c0236dbe12f862ae3f5a4b11c2f9ea8283f07f0d Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Fri, 8 Nov 2024 15:18:51 -0600 Subject: [PATCH 03/24] fix name collision --- tests/PHPStan/Rules/Comparison/data/uppercase-string.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Comparison/data/uppercase-string.php b/tests/PHPStan/Rules/Comparison/data/uppercase-string.php index 5641f0a1d5..fbd7a3eaf7 100644 --- a/tests/PHPStan/Rules/Comparison/data/uppercase-string.php +++ b/tests/PHPStan/Rules/Comparison/data/uppercase-string.php @@ -1,6 +1,6 @@ Date: Fri, 8 Nov 2024 15:25:08 -0600 Subject: [PATCH 04/24] resolve failing test on 7.4 --- tests/PHPStan/Analyser/data/explode-php74.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/data/explode-php74.php b/tests/PHPStan/Analyser/data/explode-php74.php index efdd404424..b205b1d0be 100644 --- a/tests/PHPStan/Analyser/data/explode-php74.php +++ b/tests/PHPStan/Analyser/data/explode-php74.php @@ -9,6 +9,7 @@ class ExplodingStrings public function doFoo(string $s): void { assertType('non-empty-list|false', explode($s, 'foo')); - assertType('non-empty-list|false', explode($s, 'FOO')); + assertType('non-empty-list|false', explode($s, 'FOO')); + assertType('non-empty-list|false', explode($s, 'Foo')); } } From f0e552cceffdd9ccad990024146e374204fa0225 Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Sun, 10 Nov 2024 11:10:23 -0600 Subject: [PATCH 05/24] reasons are now unique --- tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php index 73cd2b4a6f..7adcd5908f 100644 --- a/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php @@ -67,8 +67,7 @@ public function testBug7484(): void [ 'Generator expects key type K of int|string, (K of int)|string given.', 21, - '• Type string is not always the same as K. It breaks the contract for some argument types, typically subtypes.' . PHP_EOL . - '• Type string is not always the same as K. It breaks the contract for some argument types, typically subtypes.', + 'Type string is not always the same as K. It breaks the contract for some argument types, typically subtypes.', ], ]); } From 44a544f405b60d8d7d2fb3c27c2645fad2bc64c4 Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Sun, 10 Nov 2024 11:23:09 -0600 Subject: [PATCH 06/24] remove unused constant --- tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php index 7adcd5908f..deec20a074 100644 --- a/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php @@ -5,7 +5,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; -use const PHP_EOL; /** * @extends RuleTestCase From ccbf6ad6cfadba914edb17147e777aa691457bd2 Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Mon, 18 Nov 2024 11:09:57 -0600 Subject: [PATCH 07/24] resolve feedback This (mostly) resolves https://github.com/phpstan/phpstan-src/pull/3613/files#r1835477700 in that the error reappears, but loses the lowercase-string portion of the error string. --- .../InitializerExprTypeResolver.php | 28 ++++++++++++++----- .../CallToFunctionParametersRuleTest.php | 4 +++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index a55fd7831d..d320b7ec4a 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -482,13 +482,7 @@ public function resolveConcatType(Type $left, Type $right): Type $accessoryTypes[] = new AccessoryLiteralStringType(); } - if ($leftStringType->isLowercaseString()->and($rightStringType->isLowercaseString())->yes()) { - $accessoryTypes[] = new AccessoryLowercaseStringType(); - } - - if ($leftStringType->isUppercaseString()->and($rightStringType->isUppercaseString())->yes()) { - $accessoryTypes[] = new AccessoryUppercaseStringType(); - } + $this->appendAccessoryCasedStringTypes($leftStringType, $rightStringType, $accessoryTypes); $leftNumericStringNonEmpty = TypeCombinator::remove($leftStringType, new ConstantStringType('')); if ($leftNumericStringNonEmpty->isNumericString()->yes()) { @@ -524,6 +518,26 @@ public function resolveConcatType(Type $left, Type $right): Type return new StringType(); } + protected function appendAccessoryCasedStringTypes($leftStringType, $rightStringType, array &$accessoryTypes): void + { + $lower = $leftStringType->isLowercaseString()->and($rightStringType->isLowercaseString())->yes(); + $upper = $leftStringType->isUppercaseString()->and($rightStringType->isUppercaseString())->yes(); + + if ($lower && $upper) { + return; + } + + if ($lower) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); + return; + } + + if ($upper) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + return; + } + } + /** * @param callable(Expr): Type $getTypeCallback */ diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index c06324bfde..40baae6aef 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -637,6 +637,10 @@ public function testArrayUdiffCallback(): void '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(string, string): string given.', 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.', + 14, + ], [ 'Parameter #1 $arr1 of function array_udiff expects array, null given.', 20, From 6378810014998a87d924f9cd332025cfdc8f087a Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Mon, 18 Nov 2024 13:01:54 -0600 Subject: [PATCH 08/24] resolve feedback Addresses https://github.com/phpstan/phpstan-src/pull/3613#discussion_r1835750876 and https://github.com/phpstan/phpstan-src/pull/3613#discussion_r1835751417 The relevate lowercase-string tests do not seem to be affected ... ? --- src/Type/Accessory/AccessoryLowercaseStringType.php | 2 +- src/Type/Accessory/AccessoryUppercaseStringType.php | 2 +- .../PHPStan/Analyser/nsrt/uppercase-string-parse.php | 2 +- .../PHPStan/Analyser/nsrt/uppercase-string-trim.php | 12 ++++++------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Type/Accessory/AccessoryLowercaseStringType.php b/src/Type/Accessory/AccessoryLowercaseStringType.php index a564ea1029..672998fdb8 100644 --- a/src/Type/Accessory/AccessoryLowercaseStringType.php +++ b/src/Type/Accessory/AccessoryLowercaseStringType.php @@ -311,7 +311,7 @@ public function isLowercaseString(): TrinaryLogic public function isUppercaseString(): TrinaryLogic { - return TrinaryLogic::createNo(); + return TrinaryLogic::createMaybe(); } public function isClassStringType(): TrinaryLogic diff --git a/src/Type/Accessory/AccessoryUppercaseStringType.php b/src/Type/Accessory/AccessoryUppercaseStringType.php index df2816324b..3ce287d614 100644 --- a/src/Type/Accessory/AccessoryUppercaseStringType.php +++ b/src/Type/Accessory/AccessoryUppercaseStringType.php @@ -306,7 +306,7 @@ public function isLiteralString(): TrinaryLogic public function isLowercaseString(): TrinaryLogic { - return TrinaryLogic::createNo(); + return TrinaryLogic::createMaybe(); } public function isUppercaseString(): TrinaryLogic diff --git a/tests/PHPStan/Analyser/nsrt/uppercase-string-parse.php b/tests/PHPStan/Analyser/nsrt/uppercase-string-parse.php index 6504143dc9..505af5b81d 100644 --- a/tests/PHPStan/Analyser/nsrt/uppercase-string-parse.php +++ b/tests/PHPStan/Analyser/nsrt/uppercase-string-parse.php @@ -18,7 +18,7 @@ public function parse(string $uppercase, string $string): void if (array_key_exists('foo', $a)) { $value = $a['foo']; if (\is_string($value)) { - assertType('uppercase-string', $value); + assertType('lowercase-string|uppercase-string', $value); } } diff --git a/tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php b/tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php index 0c24268faf..3b93be1246 100644 --- a/tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php +++ b/tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php @@ -12,12 +12,12 @@ class Foo */ public function doTrim(string $uppercase, string $string): void { - assertType('uppercase-string', trim($uppercase)); - assertType('uppercase-string', ltrim($uppercase)); - assertType('uppercase-string', rtrim($uppercase)); - assertType('uppercase-string', trim($uppercase, $string)); - assertType('uppercase-string', ltrim($uppercase, $string)); - assertType('uppercase-string', rtrim($uppercase, $string)); + assertType('lowercase-string|uppercase-string', trim($uppercase)); + assertType('lowercase-string|uppercase-string', ltrim($uppercase)); + assertType('lowercase-string|uppercase-string', rtrim($uppercase)); + assertType('lowercase-string|uppercase-string', trim($uppercase, $string)); + assertType('lowercase-string|uppercase-string', ltrim($uppercase, $string)); + assertType('lowercase-string|uppercase-string', rtrim($uppercase, $string)); assertType('string', trim($string)); assertType('string', ltrim($string)); assertType('string', rtrim($string)); From cfc2a8679fbcb5770e093c8e1e45239b4e32b09d Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Mon, 18 Nov 2024 13:13:45 -0600 Subject: [PATCH 09/24] resolve feedback Addresses https://github.com/phpstan/phpstan-src/pull/3613#discussion_r1835750814 --- .../StrictComparisonOfDifferentTypesRule.php | 28 ++++++------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php index 08d202f229..d155be352a 100644 --- a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php +++ b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php @@ -82,31 +82,19 @@ public function processNode(Node $node, Scope $scope): array && !$leftType->isString()->no() && !$rightType->isConstantScalarValue()->yes() && !$rightType->isString()->no() - && TrinaryLogic::extremeIdentity($leftType->isLowercaseString(), $rightType->isLowercaseString())->maybe() + && ( + TrinaryLogic::extremeIdentity($leftType->isLowercaseString(), $rightType->isLowercaseString())->maybe() + || TrinaryLogic::extremeIdentity($leftType->isUppercaseString(), $rightType->isUppercaseString())->maybe() + ) ) || ( $rightType->isConstantScalarValue()->yes() && !$rightType->isString()->no() && !$leftType->isConstantScalarValue()->yes() && !$leftType->isString()->no() - && TrinaryLogic::extremeIdentity($leftType->isLowercaseString(), $rightType->isLowercaseString())->maybe() - ) - ) { - $verbosity = VerbosityLevel::precise(); - } - - if ( - ( - $leftType->isConstantScalarValue()->yes() - && !$leftType->isString()->no() - && !$rightType->isConstantScalarValue()->yes() - && !$rightType->isString()->no() - && TrinaryLogic::extremeIdentity($leftType->isUppercaseString(), $rightType->isUppercaseString())->maybe() - ) || ( - $rightType->isConstantScalarValue()->yes() - && !$rightType->isString()->no() - && !$leftType->isConstantScalarValue()->yes() - && !$leftType->isString()->no() - && TrinaryLogic::extremeIdentity($leftType->isUppercaseString(), $rightType->isUppercaseString())->maybe() + && ( + TrinaryLogic::extremeIdentity($leftType->isLowercaseString(), $rightType->isLowercaseString())->maybe() + || TrinaryLogic::extremeIdentity($leftType->isUppercaseString(), $rightType->isUppercaseString())->maybe() + ) ) ) { $verbosity = VerbosityLevel::precise(); From 774ff2172f2a0787137414e94f1681f790666df9 Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Mon, 18 Nov 2024 13:19:14 -0600 Subject: [PATCH 10/24] resolve feedback Addresses https://github.com/phpstan/phpstan-src/pull/3613#discussion_r1835751592 --- src/Type/IntersectionType.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index ca8efb12a6..72fecc7de6 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -344,13 +344,12 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) || $type instanceof AccessoryLowercaseStringType || $type instanceof AccessoryUppercaseStringType ) { - if ($type instanceof AccessoryLowercaseStringType && !$level->isPrecise()) { + if ( + ($type instanceof AccessoryLowercaseStringType || $type instanceof AccessoryUppercaseStringType) + && !$level->isPrecise() + ) { continue; } - if ($type instanceof AccessoryUppercaseStringType && !$level->isPrecise()) { - continue; - } - if ($type instanceof AccessoryNonFalsyStringType) { $nonFalsyStr = true; } From 6ecbc0bf49b73978e5fee9c522e70ac43e6a6c59 Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Mon, 18 Nov 2024 13:36:02 -0600 Subject: [PATCH 11/24] resolve feedback Addresses https://github.com/phpstan/phpstan-src/pull/3613#discussion_r1835753441, https://github.com/phpstan/phpstan-src/pull/3613#discussion_r1835753838, https://github.com/phpstan/phpstan-src/pull/3613#discussion_r1835753877 --- stubs/core.stub | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stubs/core.stub b/stubs/core.stub index 265d27e7b3..e430307c52 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -321,17 +321,17 @@ function is_callable(mixed $value, bool $syntax_only = false, ?string &$callable function abs($num) {} /** - * @return ($string is lowercase-string ? lowercase-string : ($string is uppercase-string ? uppercase-string : string)) + * @return ($string is lowercase-string&uppercase-string ? lowercase-string&uppercase-string : ($string is lowercase-string ? lowercase-string : ($string is uppercase-string ? uppercase-string : string))) */ function trim(string $string, string $characters = " \n\r\t\v\x00"): string {} /** - * @return ($string is lowercase-string ? lowercase-string : ($string is uppercase-string ? uppercase-string : string)) + * @return ($string is lowercase-string&uppercase-string ? lowercase-string&uppercase-string : ($string is lowercase-string ? lowercase-string : ($string is uppercase-string ? uppercase-string : string))) */ function ltrim(string $string, string $characters = " \n\r\t\v\x00"): string {} /** - * @return ($string is lowercase-string ? lowercase-string : ($string is uppercase-string ? uppercase-string : string)) + * @return ($string is lowercase-string&uppercase-string ? lowercase-string&uppercase-string : ($string is lowercase-string ? lowercase-string : ($string is uppercase-string ? uppercase-string : string))) */ function rtrim(string $string, string $characters = " \n\r\t\v\x00"): string {} From 46f45e7290fcb5757a611f355adff7614656bf6b Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Mon, 18 Nov 2024 13:52:05 -0600 Subject: [PATCH 12/24] resolve feedback Addresses https://github.com/phpstan/phpstan-src/pull/3613#discussion_r1835753441 --- stubs/core.stub | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/stubs/core.stub b/stubs/core.stub index e430307c52..21ff69d4a4 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -69,12 +69,15 @@ function str_shuffle(string $string): string {} /** * @param array $result - * @param-out ($string is lowercase-string - * ? array|lowercase-string> - * : ($string is uppercase-string - * ? array|uppercase-string> - * : array|string> - * ) + * @param-out ($string is lowercase-string&uppercase-string + * ? array|(lowercase-string&uppercase-string)> + * : ($string is lowercase-string + * ? array|lowercase-string> + * : ($string is uppercase-string + * ? array|uppercase-string> + * : array|string> + * ) + * ) * ) $result */ function parse_str(string $string, array &$result): void {} From 7bb56a87dfdb0f28917e1d70cabf6d91c078c56e Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Mon, 18 Nov 2024 15:24:40 -0600 Subject: [PATCH 13/24] resolve feedback Addresses https://github.com/phpstan/phpstan-src/pull/3613#discussion_r1835752042 --- .../ExplodeFunctionDynamicReturnTypeExtension.php | 13 +++++++++---- .../Analyser/LegacyNodeScopeResolverTest.php | 6 +++--- tests/PHPStan/Analyser/nsrt/bug-4711.php | 4 ++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php index 4591933a94..23034d8206 100644 --- a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php @@ -58,12 +58,17 @@ public function getTypeFromFunctionCall( } $stringType = $scope->getType($args[1]->value); + $accessory = []; if ($stringType->isLowercaseString()->yes()) { - $returnValueType = new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); - } elseif ($stringType->isUppercaseString()->yes()) { - $returnValueType = new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]); + $accessory[] = new AccessoryLowercaseStringType(); + } + if ($stringType->isUppercaseString()->yes()) { + $accessory[] = new AccessoryUppercaseStringType(); + } + if (count($accessory) > 0) { + $returnValueType = new IntersectionType([new StringType(), ...$accessory]); } else { - $returnValueType = new StringType(); + $returnValueType = new StringType(); } $returnType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $returnValueType)); diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index aea1961e32..5434782186 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -1129,11 +1129,11 @@ public function dataArrayDestructuring(): array '$fourthStringArrayForeachList', ], [ - 'lowercase-string', + 'lowercase-string&uppercase-string', '$dateArray[\'Y\']', ], [ - 'lowercase-string', + 'lowercase-string&uppercase-string', '$dateArray[\'m\']', ], [ @@ -1141,7 +1141,7 @@ public function dataArrayDestructuring(): array '$dateArray[\'d\']', ], [ - 'lowercase-string', + 'lowercase-string&uppercase-string', '$intArrayForRewritingFirstElement[0]', ], [ diff --git a/tests/PHPStan/Analyser/nsrt/bug-4711.php b/tests/PHPStan/Analyser/nsrt/bug-4711.php index 8fbed765a9..9050c74c15 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], '')); } } From b861d019bf5efb44868df9c005d602a3986ef5d7 Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Mon, 18 Nov 2024 15:46:34 -0600 Subject: [PATCH 14/24] resolves feedback Addresses https://github.com/phpstan/phpstan-src/pull/3613#discussion_r1847230181 --- .../InitializerExprTypeResolver.php | 28 +++++-------------- .../CallToFunctionParametersRuleTest.php | 2 +- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index d320b7ec4a..a55fd7831d 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -482,7 +482,13 @@ public function resolveConcatType(Type $left, Type $right): Type $accessoryTypes[] = new AccessoryLiteralStringType(); } - $this->appendAccessoryCasedStringTypes($leftStringType, $rightStringType, $accessoryTypes); + if ($leftStringType->isLowercaseString()->and($rightStringType->isLowercaseString())->yes()) { + $accessoryTypes[] = new AccessoryLowercaseStringType(); + } + + if ($leftStringType->isUppercaseString()->and($rightStringType->isUppercaseString())->yes()) { + $accessoryTypes[] = new AccessoryUppercaseStringType(); + } $leftNumericStringNonEmpty = TypeCombinator::remove($leftStringType, new ConstantStringType('')); if ($leftNumericStringNonEmpty->isNumericString()->yes()) { @@ -518,26 +524,6 @@ public function resolveConcatType(Type $left, Type $right): Type return new StringType(); } - protected function appendAccessoryCasedStringTypes($leftStringType, $rightStringType, array &$accessoryTypes): void - { - $lower = $leftStringType->isLowercaseString()->and($rightStringType->isLowercaseString())->yes(); - $upper = $leftStringType->isUppercaseString()->and($rightStringType->isUppercaseString())->yes(); - - if ($lower && $upper) { - return; - } - - if ($lower) { - $accessoryTypes[] = new AccessoryLowercaseStringType(); - return; - } - - if ($upper) { - $accessoryTypes[] = new AccessoryUppercaseStringType(); - return; - } - } - /** * @param callable(Expr): Type $getTypeCallback */ diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 40baae6aef..a89705028b 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -638,7 +638,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&uppercase-string) given.', 14, ], [ From c92d7b213b1d5a2750468bb0b8080a9e4ac58fd0 Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Mon, 18 Nov 2024 15:56:08 -0600 Subject: [PATCH 15/24] resolve feedback Addresses https://github.com/phpstan/phpstan-src/pull/3613#discussion_r1847123496, https://github.com/phpstan/phpstan-src/pull/3613#discussion_r1847222434, https://github.com/phpstan/phpstan-src/pull/3613#discussion_r1847222752 This intentionally causes tests to fail so @VincentLanglet et al. can debug. --- .../PHPStan/Analyser/nsrt/uppercase-string-parse.php | 2 +- .../PHPStan/Analyser/nsrt/uppercase-string-trim.php | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/uppercase-string-parse.php b/tests/PHPStan/Analyser/nsrt/uppercase-string-parse.php index 505af5b81d..6504143dc9 100644 --- a/tests/PHPStan/Analyser/nsrt/uppercase-string-parse.php +++ b/tests/PHPStan/Analyser/nsrt/uppercase-string-parse.php @@ -18,7 +18,7 @@ public function parse(string $uppercase, string $string): void if (array_key_exists('foo', $a)) { $value = $a['foo']; if (\is_string($value)) { - assertType('lowercase-string|uppercase-string', $value); + assertType('uppercase-string', $value); } } diff --git a/tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php b/tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php index 3b93be1246..0c24268faf 100644 --- a/tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php +++ b/tests/PHPStan/Analyser/nsrt/uppercase-string-trim.php @@ -12,12 +12,12 @@ class Foo */ public function doTrim(string $uppercase, string $string): void { - assertType('lowercase-string|uppercase-string', trim($uppercase)); - assertType('lowercase-string|uppercase-string', ltrim($uppercase)); - assertType('lowercase-string|uppercase-string', rtrim($uppercase)); - assertType('lowercase-string|uppercase-string', trim($uppercase, $string)); - assertType('lowercase-string|uppercase-string', ltrim($uppercase, $string)); - assertType('lowercase-string|uppercase-string', rtrim($uppercase, $string)); + assertType('uppercase-string', trim($uppercase)); + assertType('uppercase-string', ltrim($uppercase)); + assertType('uppercase-string', rtrim($uppercase)); + assertType('uppercase-string', trim($uppercase, $string)); + assertType('uppercase-string', ltrim($uppercase, $string)); + assertType('uppercase-string', rtrim($uppercase, $string)); assertType('string', trim($string)); assertType('string', ltrim($string)); assertType('string', rtrim($string)); From 78e136e90f44fe4692bd319781e22d093e3417f4 Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Mon, 18 Nov 2024 16:22:00 -0600 Subject: [PATCH 16/24] resolve feedback Addresses https://github.com/phpstan/phpstan-src/pull/3613#discussion_r1835752521 --- ...eUrlFunctionDynamicReturnTypeExtension.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php index e2c0128964..80c69118c8 100644 --- a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php @@ -132,6 +132,26 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, private function createAllComponentsReturnType(bool $urlIsLowercase, bool $urlIsUppercase): Type { + if ($urlIsLowercase && $urlIsUppercase) { + if ( + $this->allComponentsTogetherTypeForLowercaseString === null + && $this->allComponentsTogetherTypeForLowercaseString === null + ) { + $returnTypes = [ + new ConstantBooleanType(false), + new NullType(), + IntegerRangeType::fromInterval(0, 65535), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType(), new AccessoryUppercaseStringType()]), + $this->createComponentsArray(true, true), + ]; + + $union = TypeCombinator::union(...$returnTypes); + $this->allComponentsTogetherTypeForLowercaseString = $union; + $this->allComponentsTogetherTypeForUppercaseString = $union; + return $union; + } + } + if ($urlIsLowercase) { if ($this->allComponentsTogetherTypeForLowercaseString === null) { $returnTypes = [ From d4056d13a37443f0358c0e27b27f02431d3dbbf0 Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Mon, 18 Nov 2024 16:31:19 -0600 Subject: [PATCH 17/24] resolve feedback Addresses https://github.com/phpstan/phpstan-src/pull/3613#discussion_r1835752747 Instead of a 4-part if/elseif/else, this builds both lowercase and/or uppercase; then if neither was true, builds what would have been the final else case. --- .../Php/ParseUrlFunctionDynamicReturnTypeExtension.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php index 80c69118c8..69495f2182 100644 --- a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php @@ -211,7 +211,9 @@ private function createComponentsArray(bool $urlIsLowercase, bool $urlIsUppercas foreach ($this->componentTypesPairedStringsForLowercaseString as $componentName => $componentValueType) { $builder->setOffsetValueType(new ConstantStringType($componentName), $componentValueType, true); } - } elseif ($urlIsUppercase) { + } + + if ($urlIsUppercase) { if ($this->componentTypesPairedStringsForUppercaseString === null) { throw new ShouldNotHappenException(); } @@ -219,7 +221,9 @@ private function createComponentsArray(bool $urlIsLowercase, bool $urlIsUppercas foreach ($this->componentTypesPairedStringsForUppercaseString as $componentName => $componentValueType) { $builder->setOffsetValueType(new ConstantStringType($componentName), $componentValueType, true); } - } else { + } + + if (! $urlIsLowercase && ! $urlIsUppercase) { if ($this->componentTypesPairedStrings === null) { throw new ShouldNotHappenException(); } From ebd50a32600834cf95370ecafc82f19da1db6a71 Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Tue, 19 Nov 2024 13:54:42 -0600 Subject: [PATCH 18/24] add extension files from @VincentLanglet Copied from https://github.com/phpstan/phpstan-src/commit/22f0cab4b286a0e819056ff07dd3680ccba6e9f6 and https://github.com/phpstan/phpstan-src/commit/38151b7667d240b1da9984036c54e6cd180751da --- conf/config.neon | 10 ++++ .../Php/ParseStrParameterOutTypeExtension.php | 59 +++++++++++++++++++ ...TrimFunctionDynamicReturnTypeExtension.php | 50 ++++++++++++++++ stubs/core.stub | 28 +-------- tests/PHPStan/Analyser/data/param-out.php | 4 +- 5 files changed, 123 insertions(+), 28 deletions(-) create mode 100644 src/Type/Php/ParseStrParameterOutTypeExtension.php create mode 100644 src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php diff --git a/conf/config.neon b/conf/config.neon index 11e845136d..f8ad1af8b3 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1564,6 +1564,11 @@ services: tags: - phpstan.dynamicFunctionThrowTypeExtension + - + class: PHPStan\Type\Php\ParseStrParameterOutTypeExtension + tags: + - phpstan.functionParameterOutTypeExtension + - class: PHPStan\Type\Php\PregMatchTypeSpecifyingExtension tags: @@ -1774,6 +1779,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\TrimFunctionDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\VersionCompareFunctionDynamicReturnTypeExtension tags: diff --git a/src/Type/Php/ParseStrParameterOutTypeExtension.php b/src/Type/Php/ParseStrParameterOutTypeExtension.php new file mode 100644 index 0000000000..73085e471f --- /dev/null +++ b/src/Type/Php/ParseStrParameterOutTypeExtension.php @@ -0,0 +1,59 @@ +getName()), ['parse_str', 'mb_parse_str'], true) + && $parameter->getName() === 'result'; + } + + public function getParameterOutTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, ParameterReflection $parameter, Scope $scope): ?Type + { + $args = $funcCall->getArgs(); + if (count($args) < 1) { + return null; + } + + $stringType = $scope->getType($args[0]->value); + $accessory = []; + if ($stringType->isLowercaseString()->yes()) { + $accessory[] = new AccessoryLowercaseStringType(); + } + if ($stringType->isUppercaseString()->yes()) { + $accessory[] = new AccessoryUppercaseStringType(); + } + if (count($accessory) > 0) { + $valueType = new IntersectionType([new StringType(), ...$accessory]); + } else { + $valueType = new StringType(); + } + + return new ArrayType( + new UnionType([new StringType(), new IntegerType()]), + new UnionType([new ArrayType(new MixedType(), new MixedType()), $valueType]), + ); + } + +} diff --git a/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php b/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..ed99a8e633 --- /dev/null +++ b/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php @@ -0,0 +1,50 @@ +getName(), ['trim', 'rtrim', 'ltrim'], true); + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + if (count($args) < 1) { + return null; + } + + $stringType = $scope->getType($args[0]->value); + $accessory = []; + if ($stringType->isLowercaseString()->yes()) { + $accessory[] = new AccessoryLowercaseStringType(); + } + if ($stringType->isUppercaseString()->yes()) { + $accessory[] = new AccessoryUppercaseStringType(); + } + if (count($accessory) > 0) { + return new IntersectionType([new StringType(), ...$accessory]); + } + + return new StringType(); + } + +} diff --git a/stubs/core.stub b/stubs/core.stub index 21ff69d4a4..d6c1463c81 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -69,22 +69,13 @@ function str_shuffle(string $string): string {} /** * @param array $result - * @param-out ($string is lowercase-string&uppercase-string - * ? array|(lowercase-string&uppercase-string)> - * : ($string is lowercase-string - * ? array|lowercase-string> - * : ($string is uppercase-string - * ? array|uppercase-string> - * : array|string> - * ) - * ) - * ) $result + * @param-out array|string> $result */ function parse_str(string $string, array &$result): void {} /** * @param array $result - * @param-out array|string> $result + * @param-out array|string> $result */ function mb_parse_str(string $string, array &$result): bool {} @@ -323,21 +314,6 @@ function is_callable(mixed $value, bool $syntax_only = false, ?string &$callable */ function abs($num) {} -/** - * @return ($string is lowercase-string&uppercase-string ? lowercase-string&uppercase-string : ($string is lowercase-string ? lowercase-string : ($string is uppercase-string ? uppercase-string : string))) - */ -function trim(string $string, string $characters = " \n\r\t\v\x00"): string {} - -/** - * @return ($string is lowercase-string&uppercase-string ? lowercase-string&uppercase-string : ($string is lowercase-string ? lowercase-string : ($string is uppercase-string ? uppercase-string : string))) - */ -function ltrim(string $string, string $characters = " \n\r\t\v\x00"): string {} - -/** - * @return ($string is lowercase-string&uppercase-string ? lowercase-string&uppercase-string : ($string is lowercase-string ? lowercase-string : ($string is uppercase-string ? uppercase-string : string))) - */ -function rtrim(string $string, string $characters = " \n\r\t\v\x00"): string {} - /** * @return ($categorize is true ? array> : array) */ diff --git a/tests/PHPStan/Analyser/data/param-out.php b/tests/PHPStan/Analyser/data/param-out.php index 05684938b8..341a4069d4 100644 --- a/tests/PHPStan/Analyser/data/param-out.php +++ b/tests/PHPStan/Analyser/data/param-out.php @@ -392,10 +392,10 @@ function fooHeadersSent() { function fooMbParseStr() { mb_parse_str("foo=bar", $output); - assertType('array', $output); + assertType('array', $output); mb_parse_str('email=mail@example.org&city=town&x=1&y[g]=3&f=1.23', $output); - assertType('array', $output); + assertType('array', $output); } function fooPreg() From 96830fdb550b9397af7de60990320945d148a0d4 Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Tue, 19 Nov 2024 14:05:34 -0600 Subject: [PATCH 19/24] cs fixes --- .../Php/ExplodeFunctionDynamicReturnTypeExtension.php | 8 ++++---- src/Type/Php/ParseStrParameterOutTypeExtension.php | 2 +- src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php | 9 +++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php index 23034d8206..3159c8f695 100644 --- a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php @@ -60,15 +60,15 @@ public function getTypeFromFunctionCall( $stringType = $scope->getType($args[1]->value); $accessory = []; if ($stringType->isLowercaseString()->yes()) { - $accessory[] = new AccessoryLowercaseStringType(); + $accessory[] = new AccessoryLowercaseStringType(); } if ($stringType->isUppercaseString()->yes()) { - $accessory[] = new AccessoryUppercaseStringType(); + $accessory[] = new AccessoryUppercaseStringType(); } if (count($accessory) > 0) { - $returnValueType = new IntersectionType([new StringType(), ...$accessory]); + $returnValueType = new IntersectionType([new StringType(), ...$accessory]); } else { - $returnValueType = new StringType(); + $returnValueType = new StringType(); } $returnType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $returnValueType)); diff --git a/src/Type/Php/ParseStrParameterOutTypeExtension.php b/src/Type/Php/ParseStrParameterOutTypeExtension.php index 73085e471f..ce83e25dc6 100644 --- a/src/Type/Php/ParseStrParameterOutTypeExtension.php +++ b/src/Type/Php/ParseStrParameterOutTypeExtension.php @@ -16,7 +16,7 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; - +use function count; use function in_array; use function strtolower; diff --git a/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php b/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php index ed99a8e633..1d78d57cbe 100644 --- a/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php @@ -12,13 +12,14 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use function count; +use function in_array; final class TrimFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return \in_array($functionReflection->getName(), ['trim', 'rtrim', 'ltrim'], true); + return in_array($functionReflection->getName(), ['trim', 'rtrim', 'ltrim'], true); } public function getTypeFromFunctionCall( @@ -35,13 +36,13 @@ public function getTypeFromFunctionCall( $stringType = $scope->getType($args[0]->value); $accessory = []; if ($stringType->isLowercaseString()->yes()) { - $accessory[] = new AccessoryLowercaseStringType(); + $accessory[] = new AccessoryLowercaseStringType(); } if ($stringType->isUppercaseString()->yes()) { - $accessory[] = new AccessoryUppercaseStringType(); + $accessory[] = new AccessoryUppercaseStringType(); } if (count($accessory) > 0) { - return new IntersectionType([new StringType(), ...$accessory]); + return new IntersectionType([new StringType(), ...$accessory]); } return new StringType(); From b4395b2c2fba40a078cd308fe520972f190e002c Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Tue, 19 Nov 2024 14:12:13 -0600 Subject: [PATCH 20/24] check both lower and upper, not just the one (thanks phpstan!) --- src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php index 69495f2182..556be418c6 100644 --- a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php @@ -135,7 +135,7 @@ private function createAllComponentsReturnType(bool $urlIsLowercase, bool $urlIs if ($urlIsLowercase && $urlIsUppercase) { if ( $this->allComponentsTogetherTypeForLowercaseString === null - && $this->allComponentsTogetherTypeForLowercaseString === null + && $this->allComponentsTogetherTypeForUppercaseString === null ) { $returnTypes = [ new ConstantBooleanType(false), From 356e0f8101cd4db94c3788b204b713f39e0db360 Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Tue, 19 Nov 2024 15:56:33 -0600 Subject: [PATCH 21/24] remove uppercase-string from parse_url() per convo w/ @VincentLanglet --- ...eUrlFunctionDynamicReturnTypeExtension.php | 141 +++--------------- .../nsrt/uppercase-string-parse-url.php | 26 ---- 2 files changed, 23 insertions(+), 144 deletions(-) delete mode 100644 tests/PHPStan/Analyser/nsrt/uppercase-string-parse-url.php diff --git a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php index 556be418c6..19d6c20b26 100644 --- a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php @@ -7,7 +7,6 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; -use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -40,23 +39,15 @@ final class ParseUrlFunctionDynamicReturnTypeExtension implements DynamicFunctio /** @var array|null */ private ?array $componentTypesPairedStrings = null; - private ?Type $allComponentsTogetherType = null; - /** @var array|null */ private ?array $componentTypesPairedConstantsForLowercaseString = null; /** @var array|null */ private ?array $componentTypesPairedStringsForLowercaseString = null; - private ?Type $allComponentsTogetherTypeForLowercaseString = null; - - /** @var array|null */ - private ?array $componentTypesPairedConstantsForUppercaseString = null; - - /** @var array|null */ - private ?array $componentTypesPairedStringsForUppercaseString = null; + private ?Type $allComponentsTogetherType = null; - private ?Type $allComponentsTogetherTypeForUppercaseString = null; + private ?Type $allComponentsTogetherTypeForLowercaseString = null; public function isFunctionSupported(FunctionReflection $functionReflection): bool { @@ -76,18 +67,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $componentType = $scope->getType($functionCall->getArgs()[1]->value); if (!$componentType->isConstantValue()->yes()) { - return $this->createAllComponentsReturnType( - $urlType->isLowercaseString()->yes(), - $urlType->isUppercaseString()->yes(), - ); + return $this->createAllComponentsReturnType($urlType->isLowercaseString()->yes()); } $componentType = $componentType->toInteger(); if (!$componentType instanceof ConstantIntegerType) { - return $this->createAllComponentsReturnType( - $urlType->isLowercaseString()->yes(), - $urlType->isUppercaseString()->yes(), - ); + return $this->createAllComponentsReturnType($urlType->isLowercaseString()->yes()); } } else { $componentType = new ConstantIntegerType(-1); @@ -111,10 +96,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($componentType->getValue() === -1) { return TypeCombinator::union( - $this->createComponentsArray( - $urlType->isLowercaseString()->yes(), - $urlType->isUppercaseString()->yes(), - ), + $this->createComponentsArray($urlType->isLowercaseString()->yes()), new ConstantBooleanType(false), ); } @@ -123,35 +105,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $this->componentTypesPairedConstantsForLowercaseString[$componentType->getValue()] ?? new ConstantBooleanType(false); } - if ($urlType->isUppercaseString()->yes()) { - return $this->componentTypesPairedConstantsForUppercaseString[$componentType->getValue()] ?? new ConstantBooleanType(false); - } - return $this->componentTypesPairedConstants[$componentType->getValue()] ?? new ConstantBooleanType(false); } - private function createAllComponentsReturnType(bool $urlIsLowercase, bool $urlIsUppercase): Type + private function createAllComponentsReturnType(bool $urlIsLowercase): Type { - if ($urlIsLowercase && $urlIsUppercase) { - if ( - $this->allComponentsTogetherTypeForLowercaseString === null - && $this->allComponentsTogetherTypeForUppercaseString === null - ) { - $returnTypes = [ - new ConstantBooleanType(false), - new NullType(), - IntegerRangeType::fromInterval(0, 65535), - new IntersectionType([new StringType(), new AccessoryLowercaseStringType(), new AccessoryUppercaseStringType()]), - $this->createComponentsArray(true, true), - ]; - - $union = TypeCombinator::union(...$returnTypes); - $this->allComponentsTogetherTypeForLowercaseString = $union; - $this->allComponentsTogetherTypeForUppercaseString = $union; - return $union; - } - } - if ($urlIsLowercase) { if ($this->allComponentsTogetherTypeForLowercaseString === null) { $returnTypes = [ @@ -159,7 +117,7 @@ private function createAllComponentsReturnType(bool $urlIsLowercase, bool $urlIs new NullType(), IntegerRangeType::fromInterval(0, 65535), new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), - $this->createComponentsArray(true, false), + $this->createComponentsArray(true), ]; $this->allComponentsTogetherTypeForLowercaseString = TypeCombinator::union(...$returnTypes); @@ -168,29 +126,13 @@ private function createAllComponentsReturnType(bool $urlIsLowercase, bool $urlIs return $this->allComponentsTogetherTypeForLowercaseString; } - if ($urlIsUppercase) { - if ($this->allComponentsTogetherTypeForUppercaseString === null) { - $returnTypes = [ - new ConstantBooleanType(false), - new NullType(), - IntegerRangeType::fromInterval(0, 65535), - new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), - $this->createComponentsArray(false, true), - ]; - - $this->allComponentsTogetherTypeForUppercaseString = TypeCombinator::union(...$returnTypes); - } - - return $this->allComponentsTogetherTypeForUppercaseString; - } - if ($this->allComponentsTogetherType === null) { $returnTypes = [ new ConstantBooleanType(false), new NullType(), IntegerRangeType::fromInterval(0, 65535), new StringType(), - $this->createComponentsArray(false, false), + $this->createComponentsArray(false), ]; $this->allComponentsTogetherType = TypeCombinator::union(...$returnTypes); @@ -199,7 +141,7 @@ private function createAllComponentsReturnType(bool $urlIsLowercase, bool $urlIs return $this->allComponentsTogetherType; } - private function createComponentsArray(bool $urlIsLowercase, bool $urlIsUppercase): Type + private function createComponentsArray(bool $urlIsLowercase): Type { $builder = ConstantArrayTypeBuilder::createEmpty(); @@ -211,19 +153,7 @@ private function createComponentsArray(bool $urlIsLowercase, bool $urlIsUppercas foreach ($this->componentTypesPairedStringsForLowercaseString as $componentName => $componentValueType) { $builder->setOffsetValueType(new ConstantStringType($componentName), $componentValueType, true); } - } - - if ($urlIsUppercase) { - if ($this->componentTypesPairedStringsForUppercaseString === null) { - throw new ShouldNotHappenException(); - } - - foreach ($this->componentTypesPairedStringsForUppercaseString as $componentName => $componentValueType) { - $builder->setOffsetValueType(new ConstantStringType($componentName), $componentValueType, true); - } - } - - if (! $urlIsLowercase && ! $urlIsUppercase) { + } else { if ($this->componentTypesPairedStrings === null) { throw new ShouldNotHappenException(); } @@ -243,10 +173,13 @@ private function cacheReturnTypes(): void } $string = new StringType(); + $lowercaseString = new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); $port = IntegerRangeType::fromInterval(0, 65535); $false = new ConstantBooleanType(false); $null = new NullType(); + $stringOrFalseOrNull = TypeCombinator::union($string, $false, $null); + $lowercaseStringOrFalseOrNull = TypeCombinator::union($lowercaseString, $false, $null); $portOrFalseOrNull = TypeCombinator::union($port, $false, $null); $this->componentTypesPairedConstants = [ @@ -259,6 +192,16 @@ private function cacheReturnTypes(): void PHP_URL_QUERY => $stringOrFalseOrNull, PHP_URL_FRAGMENT => $stringOrFalseOrNull, ]; + $this->componentTypesPairedConstantsForLowercaseString = [ + PHP_URL_SCHEME => $lowercaseStringOrFalseOrNull, + PHP_URL_HOST => $lowercaseStringOrFalseOrNull, + PHP_URL_PORT => $portOrFalseOrNull, + PHP_URL_USER => $lowercaseStringOrFalseOrNull, + PHP_URL_PASS => $lowercaseStringOrFalseOrNull, + PHP_URL_PATH => $lowercaseStringOrFalseOrNull, + PHP_URL_QUERY => $lowercaseStringOrFalseOrNull, + PHP_URL_FRAGMENT => $lowercaseStringOrFalseOrNull, + ]; $this->componentTypesPairedStrings = [ 'scheme' => $string, @@ -270,20 +213,6 @@ private function cacheReturnTypes(): void 'query' => $string, 'fragment' => $string, ]; - - $lowercaseString = new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); - $lowercaseStringOrFalseOrNull = TypeCombinator::union($lowercaseString, $false, $null); - - $this->componentTypesPairedConstantsForLowercaseString = [ - PHP_URL_SCHEME => $lowercaseStringOrFalseOrNull, - PHP_URL_HOST => $lowercaseStringOrFalseOrNull, - PHP_URL_PORT => $portOrFalseOrNull, - PHP_URL_USER => $lowercaseStringOrFalseOrNull, - PHP_URL_PASS => $lowercaseStringOrFalseOrNull, - PHP_URL_PATH => $lowercaseStringOrFalseOrNull, - PHP_URL_QUERY => $lowercaseStringOrFalseOrNull, - PHP_URL_FRAGMENT => $lowercaseStringOrFalseOrNull, - ]; $this->componentTypesPairedStringsForLowercaseString = [ 'scheme' => $lowercaseString, 'host' => $lowercaseString, @@ -294,30 +223,6 @@ private function cacheReturnTypes(): void 'query' => $lowercaseString, 'fragment' => $lowercaseString, ]; - - $uppercaseString = new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]); - $uppercaseStringOrFalseOrNull = TypeCombinator::union($uppercaseString, $false, $null); - - $this->componentTypesPairedConstantsForUppercaseString = [ - PHP_URL_SCHEME => $uppercaseStringOrFalseOrNull, - PHP_URL_HOST => $uppercaseStringOrFalseOrNull, - PHP_URL_PORT => $portOrFalseOrNull, - PHP_URL_USER => $uppercaseStringOrFalseOrNull, - PHP_URL_PASS => $uppercaseStringOrFalseOrNull, - PHP_URL_PATH => $uppercaseStringOrFalseOrNull, - PHP_URL_QUERY => $uppercaseStringOrFalseOrNull, - PHP_URL_FRAGMENT => $uppercaseStringOrFalseOrNull, - ]; - $this->componentTypesPairedStringsForUppercaseString = [ - 'scheme' => $uppercaseString, - 'host' => $uppercaseString, - 'port' => $port, - 'user' => $uppercaseString, - 'pass' => $uppercaseString, - 'path' => $uppercaseString, - 'query' => $uppercaseString, - 'fragment' => $uppercaseString, - ]; } } diff --git a/tests/PHPStan/Analyser/nsrt/uppercase-string-parse-url.php b/tests/PHPStan/Analyser/nsrt/uppercase-string-parse-url.php deleted file mode 100644 index e769455c99..0000000000 --- a/tests/PHPStan/Analyser/nsrt/uppercase-string-parse-url.php +++ /dev/null @@ -1,26 +0,0 @@ -, user?: uppercase-string, pass?: uppercase-string, path?: uppercase-string, query?: uppercase-string, fragment?: uppercase-string}|false', parse_url($uppercase)); - assertType('uppercase-string|false|null', parse_url($uppercase, PHP_URL_SCHEME)); - assertType('uppercase-string|false|null', parse_url($uppercase, PHP_URL_HOST)); - assertType('int<0, 65535>|false|null', parse_url($uppercase, PHP_URL_PORT)); - assertType('uppercase-string|false|null', parse_url($uppercase, PHP_URL_USER)); - assertType('uppercase-string|false|null', parse_url($uppercase, PHP_URL_PASS)); - assertType('uppercase-string|false|null', parse_url($uppercase, PHP_URL_PATH)); - assertType('uppercase-string|false|null', parse_url($uppercase, PHP_URL_QUERY)); - assertType('uppercase-string|false|null', parse_url($uppercase, PHP_URL_FRAGMENT)); - } - -} From 4c54a1a44d9d1cced48a6f5253ab569b5778257e Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Tue, 19 Nov 2024 16:06:49 -0600 Subject: [PATCH 22/24] resolve linting on lower PHP versions Incorporates https://github.com/phpstan/phpstan-src/pull/3613#discussion_r1848999121, https://github.com/phpstan/phpstan-src/pull/3613#discussion_r1848999915 --- src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php | 3 ++- src/Type/Php/ParseStrParameterOutTypeExtension.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php index 3159c8f695..d675eee45c 100644 --- a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php @@ -66,7 +66,8 @@ public function getTypeFromFunctionCall( $accessory[] = new AccessoryUppercaseStringType(); } if (count($accessory) > 0) { - $returnValueType = new IntersectionType([new StringType(), ...$accessory]); + $accessory[] = new StringType(); + $returnValueType = new IntersectionType($accessory); } else { $returnValueType = new StringType(); } diff --git a/src/Type/Php/ParseStrParameterOutTypeExtension.php b/src/Type/Php/ParseStrParameterOutTypeExtension.php index ce83e25dc6..e0c7aa47e5 100644 --- a/src/Type/Php/ParseStrParameterOutTypeExtension.php +++ b/src/Type/Php/ParseStrParameterOutTypeExtension.php @@ -45,7 +45,8 @@ public function getParameterOutTypeFromFunctionCall(FunctionReflection $function $accessory[] = new AccessoryUppercaseStringType(); } if (count($accessory) > 0) { - $valueType = new IntersectionType([new StringType(), ...$accessory]); + $accessory[] = new StringType(); + $valueType = new IntersectionType($accessory); } else { $valueType = new StringType(); } From 668d9700cef6be3701c95a575fc08000b2736c7d Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Tue, 19 Nov 2024 16:15:54 -0600 Subject: [PATCH 23/24] resolve linting on lower PHP versions --- src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php b/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php index 1d78d57cbe..df06d98b43 100644 --- a/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php @@ -42,7 +42,8 @@ public function getTypeFromFunctionCall( $accessory[] = new AccessoryUppercaseStringType(); } if (count($accessory) > 0) { - return new IntersectionType([new StringType(), ...$accessory]); + $accessory[] = new StringType(); + return new IntersectionType($accessory); } return new StringType(); From 96773a4d2274ebf9bc68051cc3a10f266aa0a2f1 Mon Sep 17 00:00:00 2001 From: "Paul M. Jones" Date: Tue, 19 Nov 2024 16:44:17 -0600 Subject: [PATCH 24/24] add type combinator tests Addresses https://github.com/phpstan/phpstan-src/pull/3613#discussion_r1847224572, https://github.com/phpstan/phpstan-src/pull/3613#discussion_r1847225043 --- tests/PHPStan/Type/TypeCombinatorTest.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 8667d7c419..0e125a4a81 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -2050,6 +2050,14 @@ public function dataUnion(): iterable UnionType::class, 'literal-string|uppercase-string', ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|uppercase-string', + ], [ [ TemplateTypeFactory::create( @@ -4015,6 +4023,14 @@ public function dataIntersect(): iterable IntersectionType::class, 'literal-string&uppercase-string', ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + IntersectionType::class, + 'lowercase-string&uppercase-string', + ], ]; if (PHP_VERSION_ID < 80100) {