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/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..d155be352a 100644 --- a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php +++ b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php @@ -75,19 +75,26 @@ public function processNode(Node $node, Scope $scope): array }; $verbosity = VerbosityLevel::value(); + if ( ( $leftType->isConstantScalarValue()->yes() && !$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() + && ( + TrinaryLogic::extremeIdentity($leftType->isLowercaseString(), $rightType->isLowercaseString())->maybe() + || TrinaryLogic::extremeIdentity($leftType->isUppercaseString(), $rightType->isUppercaseString())->maybe() + ) ) ) { $verbosity = VerbosityLevel::precise(); 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..672998fdb8 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::createMaybe(); + } + 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..3ce287d614 --- /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::createMaybe(); + } + + 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..72fecc7de6 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,11 +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()) { + if ( + ($type instanceof AccessoryLowercaseStringType || $type instanceof AccessoryUppercaseStringType) + && !$level->isPrecise() + ) { continue; } - if ($type instanceof AccessoryNonFalsyStringType) { $nonFalsyStr = true; } @@ -659,6 +663,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 +1175,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..d675eee45c 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; @@ -57,8 +58,16 @@ public function getTypeFromFunctionCall( } $stringType = $scope->getType($args[1]->value); + $accessory = []; if ($stringType->isLowercaseString()->yes()) { - $returnValueType = new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); + $accessory[] = new AccessoryLowercaseStringType(); + } + if ($stringType->isUppercaseString()->yes()) { + $accessory[] = new AccessoryUppercaseStringType(); + } + if (count($accessory) > 0) { + $accessory[] = new StringType(); + $returnValueType = new IntersectionType($accessory); } 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/ParseStrParameterOutTypeExtension.php b/src/Type/Php/ParseStrParameterOutTypeExtension.php new file mode 100644 index 0000000000..e0c7aa47e5 --- /dev/null +++ b/src/Type/Php/ParseStrParameterOutTypeExtension.php @@ -0,0 +1,60 @@ +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) { + $accessory[] = new StringType(); + $valueType = new IntersectionType($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/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/Php/TrimFunctionDynamicReturnTypeExtension.php b/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..df06d98b43 --- /dev/null +++ b/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php @@ -0,0 +1,52 @@ +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) { + $accessory[] = new StringType(); + return new IntersectionType($accessory); + } + + return new StringType(); + } + +} 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..d6c1463c81 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -69,13 +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 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 {} @@ -314,21 +314,6 @@ function is_callable(mixed $value, bool $syntax_only = false, ?string &$callable */ function abs($num) {} -/** - * @return ($string is lowercase-string ? lowercase-string : string) - */ -function trim(string $string, string $characters = " \n\r\t\v\x00"): string {} - -/** - * @return ($string is lowercase-string ? lowercase-string : string) - */ -function ltrim(string $string, string $characters = " \n\r\t\v\x00"): string {} - -/** - * @return ($string is lowercase-string ? lowercase-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/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/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')); } } 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/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() 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], '')); } } 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 @@ += 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..fbd7a3eaf7 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/uppercase-string.php @@ -0,0 +1,31 @@ +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..0e125a4a81 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,54 @@ 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', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + UnionType::class, + 'lowercase-string|uppercase-string', + ], [ [ TemplateTypeFactory::create( @@ -3934,6 +3983,54 @@ 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', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + new IntersectionType([new StringType(), new AccessoryUppercaseStringType()]), + ], + IntersectionType::class, + 'lowercase-string&uppercase-string', + ], ]; if (PHP_VERSION_ID < 80100) { @@ -4423,6 +4520,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,