From c69b88f4516bc7bfb50dc198310740762be83017 Mon Sep 17 00:00:00 2001 From: Marian <42134098+IanDelMar@users.noreply.github.com> Date: Tue, 9 Sep 2025 03:25:05 +0200 Subject: [PATCH 1/2] Add return type extension for normalize_whitespace --- extension.neon | 4 + ...paceDynamicFunctionReturnTypeExtension.php | 113 ++++++++++++++++++ tests/DynamicReturnTypeExtensionTest.php | 1 + tests/data/normalize-whitespace.php | 28 +++++ 4 files changed, 146 insertions(+) create mode 100644 src/NormalizeWhitespaceDynamicFunctionReturnTypeExtension.php create mode 100644 tests/data/normalize-whitespace.php diff --git a/extension.neon b/extension.neon index a635a1d..00a8965 100644 --- a/extension.neon +++ b/extension.neon @@ -9,6 +9,10 @@ services: class: SzepeViktor\PHPStan\WordPress\EscSqlDynamicFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: SzepeViktor\PHPStan\WordPress\NormalizeWhitespaceDynamicFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension - class: SzepeViktor\PHPStan\WordPress\ShortcodeAttsDynamicFunctionReturnTypeExtension tags: diff --git a/src/NormalizeWhitespaceDynamicFunctionReturnTypeExtension.php b/src/NormalizeWhitespaceDynamicFunctionReturnTypeExtension.php new file mode 100644 index 0000000..edd9c9a --- /dev/null +++ b/src/NormalizeWhitespaceDynamicFunctionReturnTypeExtension.php @@ -0,0 +1,113 @@ +getName() === 'normalize_whitespace'; + } + + /** + * @see https://developer.wordpress.org/reference/functions/normalize_whitespace/ + * + * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter + */ + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 1) { + return null; + } + + $argType = $scope->getType($functionCall->getArgs()[0]->value); + if (! $scope->isDeclareStrictTypes()) { + $argType = $argType->toString(); + } + + if (! $argType->isString()->yes()) { + return null; + } + + $argTypes = $argType instanceof UnionType ? $argType->getTypes() : [$argType]; + + $types = []; + foreach ($argTypes as $type) { + if ($type->isConstantValue()->yes()) { + $types[] = $this->getTypeFromConstantString($type->getConstantStrings()[0]); + continue; + } + + if ($type->isNonFalsyString()->yes()) { + $types[] = $this->getTypeFromNonFalsyString($type); + continue; + } + + if ($type->isNonEmptyString()->yes()) { + $types[] = $this->getTypeFromNonEmptyString($type); + continue; + } + + $types[] = $type; + } + + return TypeCombinator::union(...$types); + } + + private function getTypeFromConstantString(ConstantStringType $type): Type + { + $typeValue = $type->getValue(); + $type = new ConstantStringType(trim($typeValue)); + + if ($type->isNonEmptyString()->no() || $type->isNonFalsyString()->no()) { + return $type; + } + + return $type->generalize(GeneralizePrecision::moreSpecific()); + } + + private function getTypeFromNonFalsyString(Type $type): Type + { + $types = array_merge( + [new StringType(), new AccessoryNonEmptyStringType()], + array_filter( + TypeUtils::getAccessoryTypes($type), + static function (AccessoryType $accessoryType): bool { + return ! $accessoryType->isNonFalsyString()->yes(); + } + ) + ); + + return TypeCombinator::intersect(...$types); + } + + private function getTypeFromNonEmptyString(Type $type): Type + { + $types = array_merge( + [new StringType()], + array_filter( + TypeUtils::getAccessoryTypes($type), + static function (AccessoryType $accessoryType): bool { + return ! $accessoryType->isNonEmptyString()->yes(); + } + ) + ); + + return TypeCombinator::intersect(...$types); + } +} diff --git a/tests/DynamicReturnTypeExtensionTest.php b/tests/DynamicReturnTypeExtensionTest.php index 6a2efe9..2b09ef1 100644 --- a/tests/DynamicReturnTypeExtensionTest.php +++ b/tests/DynamicReturnTypeExtensionTest.php @@ -15,6 +15,7 @@ public function dataFileAsserts(): iterable yield from self::gatherAssertTypes(__DIR__ . '/data/apply_filters.php'); yield from self::gatherAssertTypes(__DIR__ . '/data/ApplyFiltersTestClass.php'); yield from self::gatherAssertTypes(__DIR__ . '/data/esc_sql.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/normalize-whitespace.php'); yield from self::gatherAssertTypes(__DIR__ . '/data/shortcode_atts.php'); yield from self::gatherAssertTypes(__DIR__ . '/data/stripslashes-from-strings-only.php'); yield from self::gatherAssertTypes(__DIR__ . '/data/wp_parse_url.php'); diff --git a/tests/data/normalize-whitespace.php b/tests/data/normalize-whitespace.php new file mode 100644 index 0000000..a6b1077 --- /dev/null +++ b/tests/data/normalize-whitespace.php @@ -0,0 +1,28 @@ + Date: Tue, 9 Sep 2025 05:45:54 +0200 Subject: [PATCH 2/2] Remove assertion with uppercase-string --- tests/data/normalize-whitespace.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/data/normalize-whitespace.php b/tests/data/normalize-whitespace.php index a6b1077..4f31734 100644 --- a/tests/data/normalize-whitespace.php +++ b/tests/data/normalize-whitespace.php @@ -12,7 +12,6 @@ assertType("'0'", normalize_whitespace(' 0 ')); assertType('literal-string&lowercase-string&non-falsy-string', normalize_whitespace(' foo ')); -assertType('literal-string&non-falsy-string&uppercase-string', normalize_whitespace(' FOO ')); assertType('literal-string&non-falsy-string', normalize_whitespace(' Foo ')); /** @var non-empty-string $nonEmptyString */