Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

<!-- TypeTraverser uses callable $traverse -->
<rule ref="NeutronStandard.Functions.VariableFunctions.VariableFunction">
<exclude-pattern>src/SlashitFunctionsDynamicFunctionReturnTypeExtension.php</exclude-pattern>
<exclude-pattern>src/StripslashesFromStringsOnlyDynamicFunctionReturnTypeExtension.php</exclude-pattern>
<exclude-pattern>src/WpSlashDynamicFunctionReturnTypeExtension.php</exclude-pattern>
</rule>
Expand Down
97 changes: 97 additions & 0 deletions src/SlashitFunctionsDynamicFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

declare(strict_types=1);

namespace SzepeViktor\PHPStan\WordPress;

use PhpParser\Node\Arg;
use PhpParser\Node\Expr\BinaryOp\Concat;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Scalar\String_;
use PHPStan\Analyser\Scope;
use PHPStan\Node\Expr\TypeExpr;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\GeneralizePrecision;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\UnionType;

final class SlashitFunctionsDynamicFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension
{
use NormalizedArguments;

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return in_array(
$functionReflection->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;
}
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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))]
)
);
}
);
}
Expand Down
9 changes: 5 additions & 4 deletions tests/DynamicReturnTypeExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
73 changes: 73 additions & 0 deletions tests/data/slashit-functions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

declare(strict_types=1);

namespace SzepeViktor\PHPStan\WordPress\Tests;

use function backslashit;
use function trailingslashit;
use function untrailingslashit;
use function PHPStan\Testing\assertType;

/*
* trailingslashit()
*/

assertType("'/'", trailingslashit(''));
assertType("'0/'", trailingslashit('0'));
assertType("'foo/'", trailingslashit('foo'));
assertType("'foo/'", trailingslashit('foo/'));
assertType("'foo/'", trailingslashit('foo//'));
assertType("'foo/'", trailingslashit('foo\\'));

/** @var non-empty-string $nonEmptyString */
assertType('non-falsy-string', trailingslashit($nonEmptyString));

/** @var non-falsy-string $nonFalsyString */
assertType('non-falsy-string', trailingslashit($nonFalsyString));

/** @var lowercase-string&non-empty-string $lowercaseNonEmptyString */
assertType('lowercase-string&non-falsy-string', trailingslashit($lowercaseNonEmptyString));

/*
* untrailingslashit()
*/

assertType("''", untrailingslashit(''));
assertType("'0'", untrailingslashit('0'));
assertType("'foo'", untrailingslashit('foo'));
assertType("'foo'", untrailingslashit('foo/'));
assertType("'foo'", untrailingslashit('foo//'));
assertType("'foo'", untrailingslashit('foo\\'));

/** @var non-empty-string $nonEmptyString */
assertType('string', untrailingslashit($nonEmptyString));

/** @var non-falsy-string $nonFalsyString */
assertType('string', untrailingslashit($nonFalsyString));

/** @var lowercase-string&non-empty-string $lowercaseNonEmptyString */
assertType('lowercase-string', untrailingslashit($lowercaseNonEmptyString));

/*
* backslashit()
*/

assertType("''", backslashit(''));
assertType('literal-string&lowercase-string&non-falsy-string&uppercase-string', backslashit('0'));
assertType('literal-string&lowercase-string&non-falsy-string', backslashit('foo'));

/** @var non-empty-string $nonEmptyString */
assertType('non-falsy-string', backslashit($nonEmptyString));

/** @var non-falsy-string $nonFalsyString */
assertType('non-falsy-string', backslashit($nonFalsyString));

/** @var lowercase-string&non-empty-string $lowercaseNonEmptyString */
assertType('lowercase-string&non-falsy-string', backslashit($lowercaseNonEmptyString));

/** @var numeric-string $numericString */
assertType('non-falsy-string', backslashit($numericString));

/** @var string $aString */
assertType('string', backslashit($aString));
6 changes: 5 additions & 1 deletion tests/data/stripslashes-from-strings-only.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']));
Expand Down
File renamed without changes.