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", 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/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/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 @@ +