From 551f6de0c3364a386d2d0046a24eb205d15af67e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 21 May 2024 14:10:52 +0200 Subject: [PATCH 1/6] Narrow to non-empty-string via `Strings::length` --- extension.neon | 1 + stubs/Utils/Strings.stub | 19 +++++++++++++++++ ...ltiplierTest.php => TypeInferenceTest.php} | 5 ++++- tests/Type/Nette/data/strings.php | 21 +++++++++++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 stubs/Utils/Strings.stub rename tests/Type/Nette/{MultiplierTest.php => TypeInferenceTest.php} (84%) create mode 100644 tests/Type/Nette/data/strings.php diff --git a/extension.neon b/extension.neon index 2895326..c348b20 100644 --- a/extension.neon +++ b/extension.neon @@ -29,6 +29,7 @@ parameters: - stubs/Utils/Html.stub - stubs/Utils/Paginator.stub - stubs/Utils/Random.stub + - stubs/Utils/Strings.stub universalObjectCratesClasses: - Nette\Application\UI\ITemplate - Nette\Application\UI\Template diff --git a/stubs/Utils/Strings.stub b/stubs/Utils/Strings.stub new file mode 100644 index 0000000..1673d93 --- /dev/null +++ b/stubs/Utils/Strings.stub @@ -0,0 +1,19 @@ +gatherAssertTypes(__DIR__ . '/data/multiplier.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/strings.php'); } /** diff --git a/tests/Type/Nette/data/strings.php b/tests/Type/Nette/data/strings.php new file mode 100644 index 0000000..7ff9f8e --- /dev/null +++ b/tests/Type/Nette/data/strings.php @@ -0,0 +1,21 @@ + Date: Wed, 24 Jun 2026 09:14:43 +0200 Subject: [PATCH 2/6] Update TypeInferenceTest.php --- tests/Type/Nette/TypeInferenceTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Type/Nette/TypeInferenceTest.php b/tests/Type/Nette/TypeInferenceTest.php index a413456..2004f31 100644 --- a/tests/Type/Nette/TypeInferenceTest.php +++ b/tests/Type/Nette/TypeInferenceTest.php @@ -27,7 +27,6 @@ public function dataFileAsserts(): iterable yield from self::gatherAssertTypes(__DIR__ . '/data/multiplier.php'); } - yield from $this->gatherAssertTypes(__DIR__ . '/data/multiplier.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/strings.php'); } From 010eb1d0aa6d154cbbe0cc057e6a2c20d9405aef Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 24 Jun 2026 09:22:58 +0200 Subject: [PATCH 3/6] Update Strings.stub --- stubs/Utils/Strings.stub | 2 -- 1 file changed, 2 deletions(-) diff --git a/stubs/Utils/Strings.stub b/stubs/Utils/Strings.stub index 1673d93..35756c0 100644 --- a/stubs/Utils/Strings.stub +++ b/stubs/Utils/Strings.stub @@ -10,8 +10,6 @@ use Nette; */ class Strings { - use Nette\StaticClass; - /** @phpstan-assert-if-true non-empty-string $s */ public static function length(string $s): int {} From cba69aefeb18dcf915414bd95c28e38d2bd66fc8 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 24 Jun 2026 10:31:22 +0200 Subject: [PATCH 4/6] use extension --- extension.neon | 11 +++- ...tringsLengthDynamicReturnTypeExtension.php | 54 +++++++++++++++++ .../StringsLengthTypeSpecifiyingExtension.php | 60 +++++++++++++++++++ .../Nette/data/strings.php => strings.php | 0 stubs/Utils/Strings.stub | 17 ------ tests/Type/Nette/TypeInferenceTest.php | 2 +- tests/Type/Nette/data/strings-length.php | 24 ++++++++ 7 files changed, 149 insertions(+), 19 deletions(-) create mode 100644 src/Type/Nette/StringsLengthDynamicReturnTypeExtension.php create mode 100644 src/Type/Nette/StringsLengthTypeSpecifiyingExtension.php rename tests/Type/Nette/data/strings.php => strings.php (100%) delete mode 100644 stubs/Utils/Strings.stub create mode 100644 tests/Type/Nette/data/strings-length.php diff --git a/extension.neon b/extension.neon index c348b20..3f5c7a2 100644 --- a/extension.neon +++ b/extension.neon @@ -29,7 +29,6 @@ parameters: - stubs/Utils/Html.stub - stubs/Utils/Paginator.stub - stubs/Utils/Random.stub - - stubs/Utils/Strings.stub universalObjectCratesClasses: - Nette\Application\UI\ITemplate - Nette\Application\UI\Template @@ -138,3 +137,13 @@ services: class: PHPStan\Type\Nette\StringsReplaceCallbackClosureTypeExtension tags: - phpstan.staticMethodParameterClosureTypeExtension + + - + class: PHPStan\Type\Nette\StringsLengthTypeSpecifiyingExtension + tags: + - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension + + - + class: PHPStan\Type\Nette\StringsLengthDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension diff --git a/src/Type/Nette/StringsLengthDynamicReturnTypeExtension.php b/src/Type/Nette/StringsLengthDynamicReturnTypeExtension.php new file mode 100644 index 0000000..1ebdce5 --- /dev/null +++ b/src/Type/Nette/StringsLengthDynamicReturnTypeExtension.php @@ -0,0 +1,54 @@ +getName() === 'length'; + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + $args = $methodCall->getArgs(); + $stringArg = $args[0] ?? null; + + if ($stringArg === null) { + return new SpecifiedTypes(); + } + + $type = $scope->getType($stringArg->value); + if ($type->isNonEmptyString()->yes()) { + return IntegerRangeType::fromInterval(1, null); + } + + return null; + } + +} diff --git a/src/Type/Nette/StringsLengthTypeSpecifiyingExtension.php b/src/Type/Nette/StringsLengthTypeSpecifiyingExtension.php new file mode 100644 index 0000000..d48bcc6 --- /dev/null +++ b/src/Type/Nette/StringsLengthTypeSpecifiyingExtension.php @@ -0,0 +1,60 @@ +typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return Strings::class; + } + + public function isStaticMethodSupported(MethodReflection $staticMethodReflection, StaticCall $node, TypeSpecifierContext $context): bool + { + return $context->true() && $staticMethodReflection->getName() === 'length'; + } + + public function specifyTypes(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + $args = $node->getArgs(); + $stringArg = $args[0] ?? null; + + if ($stringArg === null) { + return new SpecifiedTypes(); + } + + $type = $scope->getType($stringArg->value); + if (!$type->isString()->yes()) { + return new SpecifiedTypes(); + } + + return $this->typeSpecifier->create( + $stringArg->value, + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + $context, + $scope, + ); + } + +} diff --git a/tests/Type/Nette/data/strings.php b/strings.php similarity index 100% rename from tests/Type/Nette/data/strings.php rename to strings.php diff --git a/stubs/Utils/Strings.stub b/stubs/Utils/Strings.stub deleted file mode 100644 index 35756c0..0000000 --- a/stubs/Utils/Strings.stub +++ /dev/null @@ -1,17 +0,0 @@ -gatherAssertTypes(__DIR__ . '/data/strings.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/strings-length.php'); } /** diff --git a/tests/Type/Nette/data/strings-length.php b/tests/Type/Nette/data/strings-length.php new file mode 100644 index 0000000..5b5cf43 --- /dev/null +++ b/tests/Type/Nette/data/strings-length.php @@ -0,0 +1,24 @@ +', Strings::length($string)); + } else { + assertType('string', $string); + assertType('0', Strings::length($string)); + } + assertType('string', $string); + assertType('int', Strings::length($string)); + + if (Strings::length($string) === 0) { + assertType('string', $string); + } + assertType('string', $string); +} From 18b39c1fe75ae6752d657efc060c3d5ae401f2fe Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 24 Jun 2026 10:31:33 +0200 Subject: [PATCH 5/6] cs --- .../Nette/StringsLengthDynamicReturnTypeExtension.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Type/Nette/StringsLengthDynamicReturnTypeExtension.php b/src/Type/Nette/StringsLengthDynamicReturnTypeExtension.php index 1ebdce5..127068a 100644 --- a/src/Type/Nette/StringsLengthDynamicReturnTypeExtension.php +++ b/src/Type/Nette/StringsLengthDynamicReturnTypeExtension.php @@ -3,23 +3,13 @@ namespace PHPStan\Type\Nette; use Nette\Utils\Strings; -use PhpParser\Node\Arg; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Reflection\MethodReflection; -use PHPStan\TrinaryLogic; -use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\IntegerRangeType; -use PHPStan\Type\NullType; -use PHPStan\Type\Php\RegexArrayShapeMatcher; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use function array_key_exists; -use const PREG_OFFSET_CAPTURE; -use const PREG_UNMATCHED_AS_NULL; class StringsLengthDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { From e08f62ccd3368ba8343b93524bb7842c7d887a05 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 24 Jun 2026 10:35:38 +0200 Subject: [PATCH 6/6] cs --- src/Type/Nette/StringsLengthDynamicReturnTypeExtension.php | 3 +-- tests/Type/Nette/data/strings-length.php | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Type/Nette/StringsLengthDynamicReturnTypeExtension.php b/src/Type/Nette/StringsLengthDynamicReturnTypeExtension.php index 127068a..f2abed7 100644 --- a/src/Type/Nette/StringsLengthDynamicReturnTypeExtension.php +++ b/src/Type/Nette/StringsLengthDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use Nette\Utils\Strings; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\IntegerRangeType; @@ -30,7 +29,7 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, $stringArg = $args[0] ?? null; if ($stringArg === null) { - return new SpecifiedTypes(); + return null; } $type = $scope->getType($stringArg->value); diff --git a/tests/Type/Nette/data/strings-length.php b/tests/Type/Nette/data/strings-length.php index 5b5cf43..7d6161e 100644 --- a/tests/Type/Nette/data/strings-length.php +++ b/tests/Type/Nette/data/strings-length.php @@ -22,3 +22,10 @@ function doFoo(string $string) { } assertType('string', $string); } + +/** + * @param non-empty-string $nonES + */ +function doBar(string $nonES) { + assertType('int<1, max>', Strings::length($nonES)); +}