From 4f9ff6ca55d93017262befbdeba2589612a0f395 Mon Sep 17 00:00:00 2001 From: Marian <42134098+IanDelMar@users.noreply.github.com> Date: Wed, 10 Sep 2025 23:15:04 +0200 Subject: [PATCH 1/3] Fix stripslashes_from_strings_only extension --- ...ngsOnlyDynamicFunctionReturnTypeExtension.php | 16 ++++++++++++++-- tests/data/stripslashes-from-strings-only.php | 6 +++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/StripslashesFromStringsOnlyDynamicFunctionReturnTypeExtension.php b/src/StripslashesFromStringsOnlyDynamicFunctionReturnTypeExtension.php index c556a15..8be0360 100644 --- a/src/StripslashesFromStringsOnlyDynamicFunctionReturnTypeExtension.php +++ b/src/StripslashesFromStringsOnlyDynamicFunctionReturnTypeExtension.php @@ -4,8 +4,11 @@ namespace SzepeViktor\PHPStan\WordPress; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Name\FullyQualified; use PHPStan\Analyser\Scope; +use PHPStan\Node\Expr\TypeExpr; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Type; @@ -32,16 +35,25 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return TypeTraverser::map( $argType, - static function (Type $type, callable $traverse): Type { + static function (Type $type, callable $traverse) use ($scope): Type { if ($type instanceof UnionType) { return $traverse($type); } + if (! $type->isString()->yes()) { + return $type; + } + if ($type instanceof ConstantStringType) { return new ConstantStringType(stripslashes($type->getValue())); } - return $type; + return $scope->getType( + new FuncCall( + new FullyQualified('stripslashes'), + [new Arg(new TypeExpr($type))] + ) + ); } ); } diff --git a/tests/data/stripslashes-from-strings-only.php b/tests/data/stripslashes-from-strings-only.php index e6ea1ad..25daeac 100644 --- a/tests/data/stripslashes-from-strings-only.php +++ b/tests/data/stripslashes-from-strings-only.php @@ -10,8 +10,12 @@ assertType("''", stripslashes_from_strings_only('')); assertType("'foo'", stripslashes_from_strings_only('foo')); assertType("'foo\'s bar'", stripslashes_from_strings_only('foo\'s bar')); +/** @var non-empty-string $value */ +assertType('string', stripslashes_from_strings_only($value)); +/** @var non-falsy-string $value */ +assertType('string', stripslashes_from_strings_only($value)); /** @var lowercase-string $value */ -assertType('lowercase-string', stripslashes_from_strings_only($value)); +assertType('string', stripslashes_from_strings_only($value)); assertType('array{}', stripslashes_from_strings_only([])); assertType("array{'foo'}", stripslashes_from_strings_only(['foo'])); From 3935e450afc8fb11de72e5ed0e90115fa99077a0 Mon Sep 17 00:00:00 2001 From: Marian <42134098+IanDelMar@users.noreply.github.com> Date: Thu, 11 Sep 2025 21:30:11 +0200 Subject: [PATCH 2/3] Add return type extension for slashit functions --- extension.neon | 4 + phpcs.xml.dist | 1 + ...ionsDynamicFunctionReturnTypeExtension.php | 97 +++++++++++++++++++ tests/DynamicReturnTypeExtensionTest.php | 9 +- .../{apply_filters.php => apply-filters.php} | 0 tests/data/{esc_sql.php => esc-sql.php} | 0 ...{shortcode_atts.php => shortcode-atts.php} | 0 tests/data/slashit-functions.php | 73 ++++++++++++++ .../{wp_parse_url.php => wp-parse-url.php} | 0 9 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 src/SlashitFunctionsDynamicFunctionReturnTypeExtension.php rename tests/data/{apply_filters.php => apply-filters.php} (100%) rename tests/data/{esc_sql.php => esc-sql.php} (100%) rename tests/data/{shortcode_atts.php => shortcode-atts.php} (100%) create mode 100644 tests/data/slashit-functions.php rename tests/data/{wp_parse_url.php => wp-parse-url.php} (100%) diff --git a/extension.neon b/extension.neon index 00a8965..79ec298 100644 --- a/extension.neon +++ b/extension.neon @@ -21,6 +21,10 @@ services: class: SzepeViktor\PHPStan\WordPress\StripslashesFromStringsOnlyDynamicFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: SzepeViktor\PHPStan\WordPress\SlashitFunctionsDynamicFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension - class: SzepeViktor\PHPStan\WordPress\WpParseUrlFunctionDynamicReturnTypeExtension tags: diff --git a/phpcs.xml.dist b/phpcs.xml.dist index c71d768..f4d17ce 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -25,6 +25,7 @@ + src/SlashitFunctionsDynamicFunctionReturnTypeExtension.php src/StripslashesFromStringsOnlyDynamicFunctionReturnTypeExtension.php src/WpSlashDynamicFunctionReturnTypeExtension.php diff --git a/src/SlashitFunctionsDynamicFunctionReturnTypeExtension.php b/src/SlashitFunctionsDynamicFunctionReturnTypeExtension.php new file mode 100644 index 0000000..3c51687 --- /dev/null +++ b/src/SlashitFunctionsDynamicFunctionReturnTypeExtension.php @@ -0,0 +1,97 @@ +getName(), + [ + 'backslashit', + 'trailingslashit', + 'untrailingslashit', + ], + true + ); + } + + /** + * @see https://developer.wordpress.org/reference/functions/backslashit/ + * @see https://developer.wordpress.org/reference/functions/trailingslashit/ + * @see https://developer.wordpress.org/reference/functions/untrailingslashit/ + */ + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) === 0) { + return null; + } + + $argType = $scope->isDeclareStrictTypes() + ? $scope->getType($functionCall->getArgs()[0]->value) + : $scope->getType($functionCall->getArgs()[0]->value)->toString(); + + if (! $argType->isString()->yes()) { + return null; + } + + $functionName = $functionReflection->getName(); + + if (strpos($functionName, 'trailingslashit') !== false) { + $type = $scope->getType(new FuncCall(new FullyQualified('rtrim'), [$functionCall->getArgs()[0], new Arg(new String_('/\\'))])); + + if ($functionName === 'untrailingslashit') { + return $type; + } + + return $scope->getType(new Concat(new TypeExpr($type), new String_('/'))); + } + + if (! ($functionName === 'backslashit')) { + return null; + } + + return TypeTraverser::map( + $argType, + static function (Type $type, callable $traverse): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + + if ($type instanceof ConstantStringType) { + if ($type->getValue() === '') { + return $type; + } + return $traverse($type->generalize(GeneralizePrecision::moreSpecific())); + } + + if ($type->isNumericString()->or($type->isNonEmptyString())->yes()) { + return new AccessoryNonFalsyStringType(); + } + + return $type; + } + ); + } +} diff --git a/tests/DynamicReturnTypeExtensionTest.php b/tests/DynamicReturnTypeExtensionTest.php index 2b09ef1..4f3df6c 100644 --- a/tests/DynamicReturnTypeExtensionTest.php +++ b/tests/DynamicReturnTypeExtensionTest.php @@ -12,13 +12,14 @@ class DynamicReturnTypeExtensionTest extends \PHPStan\Testing\TypeInferenceTestC public function dataFileAsserts(): iterable { // Path to a file with actual asserts of expected types: - yield from self::gatherAssertTypes(__DIR__ . '/data/apply_filters.php'); + 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/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/shortcode-atts.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/slashit-functions.php'); yield from self::gatherAssertTypes(__DIR__ . '/data/stripslashes-from-strings-only.php'); - yield from self::gatherAssertTypes(__DIR__ . '/data/wp_parse_url.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/wp-parse-url.php'); yield from self::gatherAssertTypes(__DIR__ . '/data/wp-slash.php'); } diff --git a/tests/data/apply_filters.php b/tests/data/apply-filters.php similarity index 100% rename from tests/data/apply_filters.php rename to tests/data/apply-filters.php diff --git a/tests/data/esc_sql.php b/tests/data/esc-sql.php similarity index 100% rename from tests/data/esc_sql.php rename to tests/data/esc-sql.php diff --git a/tests/data/shortcode_atts.php b/tests/data/shortcode-atts.php similarity index 100% rename from tests/data/shortcode_atts.php rename to tests/data/shortcode-atts.php diff --git a/tests/data/slashit-functions.php b/tests/data/slashit-functions.php new file mode 100644 index 0000000..aeefc1b --- /dev/null +++ b/tests/data/slashit-functions.php @@ -0,0 +1,73 @@ + Date: Thu, 11 Sep 2025 23:57:54 +0200 Subject: [PATCH 3/3] Bump phpstan version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3ea181d..602a473 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "require": { "php": "^7.4 || ^8.0", "php-stubs/wordpress-stubs": "^6.6.2", - "phpstan/phpstan": "^2.0" + "phpstan/phpstan": "^2.1.18" }, "require-dev": { "composer/composer": "^2.1.14",