Skip to content

Commit 4143d06

Browse files
Add slashit functions extension & fix stripslashes extension (#299)
* Fix stripslashes_from_strings_only extension * Add return type extension for slashit functions * Bump phpstan version --------- Co-authored-by: Viktor Szépe <viktor@szepe.net>
1 parent 49b833d commit 4143d06

12 files changed

+200
-8
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"require": {
1414
"php": "^7.4 || ^8.0",
1515
"php-stubs/wordpress-stubs": "^6.6.2",
16-
"phpstan/phpstan": "^2.0"
16+
"phpstan/phpstan": "^2.1.18"
1717
},
1818
"require-dev": {
1919
"composer/composer": "^2.1.14",

extension.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ services:
2121
class: SzepeViktor\PHPStan\WordPress\StripslashesFromStringsOnlyDynamicFunctionReturnTypeExtension
2222
tags:
2323
- phpstan.broker.dynamicFunctionReturnTypeExtension
24+
-
25+
class: SzepeViktor\PHPStan\WordPress\SlashitFunctionsDynamicFunctionReturnTypeExtension
26+
tags:
27+
- phpstan.broker.dynamicFunctionReturnTypeExtension
2428
-
2529
class: SzepeViktor\PHPStan\WordPress\WpParseUrlFunctionDynamicReturnTypeExtension
2630
tags:

phpcs.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
<!-- TypeTraverser uses callable $traverse -->
2727
<rule ref="NeutronStandard.Functions.VariableFunctions.VariableFunction">
28+
<exclude-pattern>src/SlashitFunctionsDynamicFunctionReturnTypeExtension.php</exclude-pattern>
2829
<exclude-pattern>src/StripslashesFromStringsOnlyDynamicFunctionReturnTypeExtension.php</exclude-pattern>
2930
<exclude-pattern>src/WpSlashDynamicFunctionReturnTypeExtension.php</exclude-pattern>
3031
</rule>
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SzepeViktor\PHPStan\WordPress;
6+
7+
use PhpParser\Node\Arg;
8+
use PhpParser\Node\Expr\BinaryOp\Concat;
9+
use PhpParser\Node\Expr\FuncCall;
10+
use PhpParser\Node\Name\FullyQualified;
11+
use PhpParser\Node\Scalar\String_;
12+
use PHPStan\Analyser\Scope;
13+
use PHPStan\Node\Expr\TypeExpr;
14+
use PHPStan\Reflection\FunctionReflection;
15+
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
16+
use PHPStan\Type\Constant\ConstantStringType;
17+
use PHPStan\Type\GeneralizePrecision;
18+
use PHPStan\Type\IntersectionType;
19+
use PHPStan\Type\Type;
20+
use PHPStan\Type\TypeTraverser;
21+
use PHPStan\Type\UnionType;
22+
23+
final class SlashitFunctionsDynamicFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension
24+
{
25+
use NormalizedArguments;
26+
27+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
28+
{
29+
return in_array(
30+
$functionReflection->getName(),
31+
[
32+
'backslashit',
33+
'trailingslashit',
34+
'untrailingslashit',
35+
],
36+
true
37+
);
38+
}
39+
40+
/**
41+
* @see https://developer.wordpress.org/reference/functions/backslashit/
42+
* @see https://developer.wordpress.org/reference/functions/trailingslashit/
43+
* @see https://developer.wordpress.org/reference/functions/untrailingslashit/
44+
*/
45+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type
46+
{
47+
if (count($functionCall->getArgs()) === 0) {
48+
return null;
49+
}
50+
51+
$argType = $scope->isDeclareStrictTypes()
52+
? $scope->getType($functionCall->getArgs()[0]->value)
53+
: $scope->getType($functionCall->getArgs()[0]->value)->toString();
54+
55+
if (! $argType->isString()->yes()) {
56+
return null;
57+
}
58+
59+
$functionName = $functionReflection->getName();
60+
61+
if (strpos($functionName, 'trailingslashit') !== false) {
62+
$type = $scope->getType(new FuncCall(new FullyQualified('rtrim'), [$functionCall->getArgs()[0], new Arg(new String_('/\\'))]));
63+
64+
if ($functionName === 'untrailingslashit') {
65+
return $type;
66+
}
67+
68+
return $scope->getType(new Concat(new TypeExpr($type), new String_('/')));
69+
}
70+
71+
if (! ($functionName === 'backslashit')) {
72+
return null;
73+
}
74+
75+
return TypeTraverser::map(
76+
$argType,
77+
static function (Type $type, callable $traverse): Type {
78+
if ($type instanceof UnionType || $type instanceof IntersectionType) {
79+
return $traverse($type);
80+
}
81+
82+
if ($type instanceof ConstantStringType) {
83+
if ($type->getValue() === '') {
84+
return $type;
85+
}
86+
return $traverse($type->generalize(GeneralizePrecision::moreSpecific()));
87+
}
88+
89+
if ($type->isNumericString()->or($type->isNonEmptyString())->yes()) {
90+
return new AccessoryNonFalsyStringType();
91+
}
92+
93+
return $type;
94+
}
95+
);
96+
}
97+
}

src/StripslashesFromStringsOnlyDynamicFunctionReturnTypeExtension.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44

55
namespace SzepeViktor\PHPStan\WordPress;
66

7+
use PhpParser\Node\Arg;
78
use PhpParser\Node\Expr\FuncCall;
9+
use PhpParser\Node\Name\FullyQualified;
810
use PHPStan\Analyser\Scope;
11+
use PHPStan\Node\Expr\TypeExpr;
912
use PHPStan\Reflection\FunctionReflection;
1013
use PHPStan\Type\Constant\ConstantStringType;
1114
use PHPStan\Type\Type;
@@ -32,16 +35,25 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
3235

3336
return TypeTraverser::map(
3437
$argType,
35-
static function (Type $type, callable $traverse): Type {
38+
static function (Type $type, callable $traverse) use ($scope): Type {
3639
if ($type instanceof UnionType) {
3740
return $traverse($type);
3841
}
3942

43+
if (! $type->isString()->yes()) {
44+
return $type;
45+
}
46+
4047
if ($type instanceof ConstantStringType) {
4148
return new ConstantStringType(stripslashes($type->getValue()));
4249
}
4350

44-
return $type;
51+
return $scope->getType(
52+
new FuncCall(
53+
new FullyQualified('stripslashes'),
54+
[new Arg(new TypeExpr($type))]
55+
)
56+
);
4557
}
4658
);
4759
}

tests/DynamicReturnTypeExtensionTest.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ class DynamicReturnTypeExtensionTest extends \PHPStan\Testing\TypeInferenceTestC
1212
public function dataFileAsserts(): iterable
1313
{
1414
// Path to a file with actual asserts of expected types:
15-
yield from self::gatherAssertTypes(__DIR__ . '/data/apply_filters.php');
15+
yield from self::gatherAssertTypes(__DIR__ . '/data/apply-filters.php');
1616
yield from self::gatherAssertTypes(__DIR__ . '/data/ApplyFiltersTestClass.php');
17-
yield from self::gatherAssertTypes(__DIR__ . '/data/esc_sql.php');
17+
yield from self::gatherAssertTypes(__DIR__ . '/data/esc-sql.php');
1818
yield from self::gatherAssertTypes(__DIR__ . '/data/normalize-whitespace.php');
19-
yield from self::gatherAssertTypes(__DIR__ . '/data/shortcode_atts.php');
19+
yield from self::gatherAssertTypes(__DIR__ . '/data/shortcode-atts.php');
20+
yield from self::gatherAssertTypes(__DIR__ . '/data/slashit-functions.php');
2021
yield from self::gatherAssertTypes(__DIR__ . '/data/stripslashes-from-strings-only.php');
21-
yield from self::gatherAssertTypes(__DIR__ . '/data/wp_parse_url.php');
22+
yield from self::gatherAssertTypes(__DIR__ . '/data/wp-parse-url.php');
2223
yield from self::gatherAssertTypes(__DIR__ . '/data/wp-slash.php');
2324
}
2425

File renamed without changes.
File renamed without changes.
File renamed without changes.

tests/data/slashit-functions.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SzepeViktor\PHPStan\WordPress\Tests;
6+
7+
use function backslashit;
8+
use function trailingslashit;
9+
use function untrailingslashit;
10+
use function PHPStan\Testing\assertType;
11+
12+
/*
13+
* trailingslashit()
14+
*/
15+
16+
assertType("'/'", trailingslashit(''));
17+
assertType("'0/'", trailingslashit('0'));
18+
assertType("'foo/'", trailingslashit('foo'));
19+
assertType("'foo/'", trailingslashit('foo/'));
20+
assertType("'foo/'", trailingslashit('foo//'));
21+
assertType("'foo/'", trailingslashit('foo\\'));
22+
23+
/** @var non-empty-string $nonEmptyString */
24+
assertType('non-falsy-string', trailingslashit($nonEmptyString));
25+
26+
/** @var non-falsy-string $nonFalsyString */
27+
assertType('non-falsy-string', trailingslashit($nonFalsyString));
28+
29+
/** @var lowercase-string&non-empty-string $lowercaseNonEmptyString */
30+
assertType('lowercase-string&non-falsy-string', trailingslashit($lowercaseNonEmptyString));
31+
32+
/*
33+
* untrailingslashit()
34+
*/
35+
36+
assertType("''", untrailingslashit(''));
37+
assertType("'0'", untrailingslashit('0'));
38+
assertType("'foo'", untrailingslashit('foo'));
39+
assertType("'foo'", untrailingslashit('foo/'));
40+
assertType("'foo'", untrailingslashit('foo//'));
41+
assertType("'foo'", untrailingslashit('foo\\'));
42+
43+
/** @var non-empty-string $nonEmptyString */
44+
assertType('string', untrailingslashit($nonEmptyString));
45+
46+
/** @var non-falsy-string $nonFalsyString */
47+
assertType('string', untrailingslashit($nonFalsyString));
48+
49+
/** @var lowercase-string&non-empty-string $lowercaseNonEmptyString */
50+
assertType('lowercase-string', untrailingslashit($lowercaseNonEmptyString));
51+
52+
/*
53+
* backslashit()
54+
*/
55+
56+
assertType("''", backslashit(''));
57+
assertType('literal-string&lowercase-string&non-falsy-string&uppercase-string', backslashit('0'));
58+
assertType('literal-string&lowercase-string&non-falsy-string', backslashit('foo'));
59+
60+
/** @var non-empty-string $nonEmptyString */
61+
assertType('non-falsy-string', backslashit($nonEmptyString));
62+
63+
/** @var non-falsy-string $nonFalsyString */
64+
assertType('non-falsy-string', backslashit($nonFalsyString));
65+
66+
/** @var lowercase-string&non-empty-string $lowercaseNonEmptyString */
67+
assertType('lowercase-string&non-falsy-string', backslashit($lowercaseNonEmptyString));
68+
69+
/** @var numeric-string $numericString */
70+
assertType('non-falsy-string', backslashit($numericString));
71+
72+
/** @var string $aString */
73+
assertType('string', backslashit($aString));

0 commit comments

Comments
 (0)