Skip to content

Commit 8c7f1b3

Browse files
authored
Fix and improve shortcode_atts extension (#265)
* Fix and improve shortcode_atts extension * Update shortcode_atts.php
1 parent 9d5869b commit 8c7f1b3

File tree

2 files changed

+115
-10
lines changed

2 files changed

+115
-10
lines changed

src/ShortcodeAttsDynamicFunctionReturnTypeExtension.php

Lines changed: 87 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
use PhpParser\Node\Expr\FuncCall;
1212
use PHPStan\Analyser\Scope;
1313
use PHPStan\Reflection\FunctionReflection;
14+
use PHPStan\Type\Accessory\NonEmptyArrayType;
15+
use PHPStan\Type\ArrayType;
1416
use PHPStan\Type\Constant\ConstantArrayType;
17+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
1518
use PHPStan\Type\StringType;
1619
use PHPStan\Type\Type;
1720
use PHPStan\Type\TypeCombinator;
@@ -31,20 +34,49 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo
3134
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type
3235
{
3336
$args = $functionCall->getArgs();
34-
if ($args === []) {
37+
38+
if (count($args) < 2) {
3539
return null;
3640
}
3741

38-
$type = $scope->getType($args[0]->value);
42+
$pairsType = $scope->getType($args[0]->value);
43+
$attsType = $scope->getType($args[1]->value);
44+
45+
if ($attsType->isIterableAtLeastOnce()->no()) {
46+
return $pairsType;
47+
}
48+
49+
if (! $this->hasConstantArrays($pairsType)) {
50+
return $this->resolveTypeForNonConstantPairs($pairsType);
51+
}
3952

40-
if (count($type->getConstantArrays()) === 0) {
41-
return $type;
53+
if (! $this->hasConstantArrays($attsType)) {
54+
return $this->resolveTypeForNonConstantAtts($pairsType);
4255
}
4356

44-
$returnType = [];
45-
foreach ($type->getConstantArrays() as $constantArray) {
46-
// shortcode_atts values are coming from the defined defaults or from the actual string shortcode attributes
47-
$returnType[] = new ConstantArrayType(
57+
return $this->resolveTypeForConstantAtts($pairsType, $attsType);
58+
}
59+
60+
protected function resolveTypeForNonConstantPairs(Type $pairsType): Type
61+
{
62+
$keyType = $pairsType->getIterableKeyType();
63+
$valueType = TypeCombinator::union(
64+
$pairsType->getIterableValueType(),
65+
new StringType()
66+
);
67+
$arrayType = new ArrayType($keyType, $valueType);
68+
69+
return $pairsType->isIterableAtLeastOnce()->yes()
70+
? TypeCombinator::intersect($arrayType, new NonEmptyArrayType())
71+
: $arrayType;
72+
}
73+
74+
protected function resolveTypeForNonConstantAtts(Type $pairsType): Type
75+
{
76+
$types = [];
77+
78+
foreach ($pairsType->getConstantArrays() as $constantArray) {
79+
$types[] = new ConstantArrayType(
4880
$constantArray->getKeyTypes(),
4981
array_map(
5082
static function (Type $valueType): Type {
@@ -55,6 +87,52 @@ static function (Type $valueType): Type {
5587
);
5688
}
5789

58-
return TypeCombinator::union(...$returnType);
90+
return TypeCombinator::union(...$types);
91+
}
92+
93+
protected function resolveTypeForConstantAtts(Type $pairsType, Type $attsType): Type
94+
{
95+
$types = [];
96+
97+
foreach ($pairsType->getConstantArrays() as $constantPairsArray) {
98+
foreach ($attsType->getConstantArrays() as $constantAttsArray) {
99+
$types[] = $this->mergeArrays($constantPairsArray, $constantAttsArray);
100+
}
101+
}
102+
103+
return TypeCombinator::union(...$types);
104+
}
105+
106+
protected function mergeArrays(ConstantArrayType $pairsArray, ConstantArrayType $attsArray): Type
107+
{
108+
if (count($attsArray->getKeyTypes()) === 0) {
109+
return $pairsArray;
110+
}
111+
112+
$builder = ConstantArrayTypeBuilder::createFromConstantArray($pairsArray);
113+
114+
foreach ($pairsArray->getKeyTypes() as $keyType) {
115+
$hasOffsetValueType = $attsArray->hasOffsetValueType($keyType);
116+
117+
if ($hasOffsetValueType->no()) {
118+
continue;
119+
}
120+
121+
$valueType = $hasOffsetValueType->yes()
122+
? $attsArray->getOffsetValueType($keyType)
123+
: TypeCombinator::union(
124+
$pairsArray->getOffsetValueType($keyType),
125+
$attsArray->getOffsetValueType($keyType)
126+
);
127+
128+
$builder->setOffsetValueType($keyType, $valueType);
129+
}
130+
131+
return $builder->getArray();
132+
}
133+
134+
protected function hasConstantArrays(Type $type): bool
135+
{
136+
return count($type->getConstantArrays()) > 0;
59137
}
60138
}

tests/data/shortcode_atts.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,41 @@
44

55
namespace SzepeViktor\PHPStan\WordPress\Tests;
66

7+
use function shortcode_atts;
78
use function PHPStan\Testing\assertType;
89

9-
$atts = $_GET['atts'] ?? [];
10+
/** @var array $pairs */
11+
$pairs = $_GET['pairs'];
12+
13+
/** @var array $atts */
14+
$atts = $_GET['atts'];
1015

1116
// shortcode_atts filters atts by returning all atts that are occurring in the pairs
1217
// As atts are supposed to be strings the function is expected to return the pair type or a string
1318

19+
// Constant $pairs and non-constant $atts
1420
assertType('array{}', shortcode_atts([], $atts));
1521
assertType('array{foo: string, bar: string}', shortcode_atts(['foo' => '', 'bar' => ''], $atts));
1622
assertType('array{foo: string, bar: 19|string}', shortcode_atts(['foo' => 'foo-value', 'bar' => 19], $atts));
1723
assertType('array{foo: string|null, bar: 17|string, baz: string}', shortcode_atts(['foo' => null, 'bar' => 17, 'baz' => ''], $atts));
24+
25+
// Constant $pairs and constant $atts
26+
assertType("array{foo: '', bar: '19', baz: 'aString'}", shortcode_atts(['foo' => null, 'bar' => 17, 'baz' => ''], ['foo' => '', 'bar' => '19', 'baz' => 'aString']));
27+
28+
// Constant $pairs and empty $atts
29+
assertType('array{}', shortcode_atts([], []));
30+
assertType("array{foo: '', bar: ''}", shortcode_atts(['foo' => '', 'bar' => ''], []));
31+
assertType("array{foo: 'foo-value', bar: 19}", shortcode_atts(['foo' => 'foo-value', 'bar' => 19], []));
32+
assertType("array{foo: null, bar: 17, baz: ''}", shortcode_atts(['foo' => null, 'bar' => 17, 'baz' => ''], []));
33+
34+
// Non-constant $pairs (mixed) and varying $atts
35+
assertType('array', shortcode_atts($pairs, $atts));
36+
assertType('array', shortcode_atts($pairs, []));
37+
assertType('array', shortcode_atts($pairs, ['foo' => '', 'bar' => '19', 'baz' => '']));
38+
39+
// Non-constant $pairs (int) and varying $atts
40+
/** @var array<string, int> $pairs */
41+
$pairs = $_GET['pairs'];
42+
assertType('array<string, int|string>', shortcode_atts($pairs, $atts));
43+
assertType('array<string, int>', shortcode_atts($pairs, []));
44+
assertType('array<string, int|string>', shortcode_atts($pairs, ['foo' => '', 'bar' => '19', 'baz' => '']));

0 commit comments

Comments
 (0)