diff --git a/extension.neon b/extension.neon index 2895326..3f5c7a2 100644 --- a/extension.neon +++ b/extension.neon @@ -137,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..f2abed7 --- /dev/null +++ b/src/Type/Nette/StringsLengthDynamicReturnTypeExtension.php @@ -0,0 +1,43 @@ +getName() === 'length'; + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + $args = $methodCall->getArgs(); + $stringArg = $args[0] ?? null; + + if ($stringArg === null) { + return null; + } + + $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/strings.php b/strings.php new file mode 100644 index 0000000..7ff9f8e --- /dev/null +++ b/strings.php @@ -0,0 +1,21 @@ +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..7d6161e --- /dev/null +++ b/tests/Type/Nette/data/strings-length.php @@ -0,0 +1,31 @@ +', 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); +} + +/** + * @param non-empty-string $nonES + */ +function doBar(string $nonES) { + assertType('int<1, max>', Strings::length($nonES)); +}