From b92294682217a03aeb6332170477ee75c40e4d7d Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 26 Jul 2025 16:07:29 +0200 Subject: [PATCH 1/4] Add DateIntervalFormatDynamicReturnTypeExtension --- ...tervalFormatDynamicReturnTypeExtension.php | 89 +++++++++++++++++++ .../Analyser/nsrt/date-interval-format.php | 40 +++++++++ 2 files changed, 129 insertions(+) create mode 100644 src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/nsrt/date-interval-format.php diff --git a/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php b/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..aec738e40e --- /dev/null +++ b/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php @@ -0,0 +1,89 @@ +getName() === 'format'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + $arguments = $methodCall->getArgs(); + + if (!isset($arguments[0])) { + return null; + } + + $arg = $scope->getType($arguments[0]->value); + + $constantStrings = $arg->getConstantStrings(); + if (count($constantStrings) === 0) { + if ($arg->isNonEmptyString()->yes()) { + return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); + } + + + return null; + } + + // The worst case scenario for the non-falsy-string check is that every number are 0. + $dateInterval = new DateInterval('P0D'); + + $possibleReturnTypes = []; + foreach ($constantStrings as $string) { + $value = $dateInterval->format($string->getValue()); + + $accessories = []; + if (is_numeric($value)) { + $accessories[] = new AccessoryNumericStringType(); + } + if ($value !== '0' && $value !== '') { + $accessories[] = new AccessoryNonFalsyStringType(); + } elseif ($value !== '') { + $accessories[] = new AccessoryNonEmptyStringType(); + } + if (strtolower($value) === $value) { + $accessories[] = new AccessoryLowercaseStringType(); + } + if (strtoupper($value) === $value) { + $accessories[] = new AccessoryUppercaseStringType(); + } + + if (count($accessories) === 0) { + return null; + } + + $possibleReturnTypes[] = new IntersectionType([new StringType(), ...$accessories]); + } + + return TypeCombinator::union(...$possibleReturnTypes); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/date-interval-format.php b/tests/PHPStan/Analyser/nsrt/date-interval-format.php new file mode 100644 index 0000000000..691d9e3328 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/date-interval-format.php @@ -0,0 +1,40 @@ +format($string)); + assertType('non-empty-string', $dateInterval->format($nonEmptyString)); + + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $dateInterval->format('%Y')); // '00' + assertType('lowercase-string&non-empty-string&numeric-string&uppercase-string', $dateInterval->format('%y')); // '0' + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $dateInterval->format($unionString1)); + assertType('lowercase-string&non-empty-string&numeric-string&uppercase-string', $dateInterval->format($unionString2)); + + assertType('non-falsy-string&uppercase-string', $dateInterval->format('%Y DAYS')); + assertType('non-falsy-string&uppercase-string', $dateInterval->format($unionString1. ' DAYS')); + + assertType('lowercase-string&non-falsy-string', $dateInterval->format('%Y days')); + assertType('lowercase-string&non-falsy-string', $dateInterval->format($unionString1. ' days')); + } +} From b911d1842c5180aab13540b0857e8cb289b21b17 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 26 Jul 2025 16:18:13 +0200 Subject: [PATCH 2/4] Simplify --- ...tervalFormatDynamicReturnTypeExtension.php | 36 +++++-------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php b/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php index aec738e40e..8592ed939c 100644 --- a/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php +++ b/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php @@ -7,12 +7,11 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; -use PHPStan\Type\Accessory\AccessoryLowercaseStringType; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; 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\DynamicMethodReturnTypeExtension; +use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; @@ -53,37 +52,18 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return null; } - // The worst case scenario for the non-falsy-string check is that every number are 0. + // The worst case scenario for the non-falsy-string check is that every number is 0. $dateInterval = new DateInterval('P0D'); $possibleReturnTypes = []; foreach ($constantStrings as $string) { $value = $dateInterval->format($string->getValue()); - - $accessories = []; - if (is_numeric($value)) { - $accessories[] = new AccessoryNumericStringType(); - } - if ($value !== '0' && $value !== '') { - $accessories[] = new AccessoryNonFalsyStringType(); - } elseif ($value !== '') { - $accessories[] = new AccessoryNonEmptyStringType(); - } - if (strtolower($value) === $value) { - $accessories[] = new AccessoryLowercaseStringType(); - } - if (strtoupper($value) === $value) { - $accessories[] = new AccessoryUppercaseStringType(); - } - - if (count($accessories) === 0) { - return null; - } - - $possibleReturnTypes[] = new IntersectionType([new StringType(), ...$accessories]); + $possibleReturnTypes[] = new ConstantStringType($value); } - return TypeCombinator::union(...$possibleReturnTypes); + $result = TypeCombinator::union(...$possibleReturnTypes)->generalize(GeneralizePrecision::moreSpecific()); + + return TypeCombinator::remove($result, new AccessoryLiteralStringType()); } } From 7fcfbec8f519366d865e0589332a971c86ee3235 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 26 Jul 2025 16:20:26 +0200 Subject: [PATCH 3/4] Fix --- .../Php/DateIntervalFormatDynamicReturnTypeExtension.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php b/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php index 8592ed939c..354bfc46d0 100644 --- a/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php +++ b/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php @@ -48,7 +48,6 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); } - return null; } @@ -61,9 +60,10 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method $possibleReturnTypes[] = new ConstantStringType($value); } - $result = TypeCombinator::union(...$possibleReturnTypes)->generalize(GeneralizePrecision::moreSpecific()); - - return TypeCombinator::remove($result, new AccessoryLiteralStringType()); + return TypeCombinator::remove( + TypeCombinator::union(...$possibleReturnTypes)->generalize(GeneralizePrecision::moreSpecific()), + new AccessoryLiteralStringType(), + ); } } From ffb94a4eec5385d4706500f37389df86b0c2f467 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 26 Jul 2025 16:25:29 +0200 Subject: [PATCH 4/4] Fix --- ...tervalFormatDynamicReturnTypeExtension.php | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php b/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php index 354bfc46d0..f6d32b02ed 100644 --- a/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php +++ b/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php @@ -7,16 +7,20 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; -use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; -use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\DynamicMethodReturnTypeExtension; -use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function count; +use function is_numeric; +use function strtolower; +use function strtoupper; #[AutowiredService] final class DateIntervalFormatDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension @@ -57,13 +61,31 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method $possibleReturnTypes = []; foreach ($constantStrings as $string) { $value = $dateInterval->format($string->getValue()); - $possibleReturnTypes[] = new ConstantStringType($value); + + $accessories = []; + if (is_numeric($value)) { + $accessories[] = new AccessoryNumericStringType(); + } + if ($value !== '0' && $value !== '') { + $accessories[] = new AccessoryNonFalsyStringType(); + } elseif ($value !== '') { + $accessories[] = new AccessoryNonEmptyStringType(); + } + if (strtolower($value) === $value) { + $accessories[] = new AccessoryLowercaseStringType(); + } + if (strtoupper($value) === $value) { + $accessories[] = new AccessoryUppercaseStringType(); + } + + if (count($accessories) === 0) { + return null; + } + + $possibleReturnTypes[] = new IntersectionType([new StringType(), ...$accessories]); } - return TypeCombinator::remove( - TypeCombinator::union(...$possibleReturnTypes)->generalize(GeneralizePrecision::moreSpecific()), - new AccessoryLiteralStringType(), - ); + return TypeCombinator::union(...$possibleReturnTypes); } }