Skip to content

Commit 9d2ff8b

Browse files
committed
Make dumpType() more consistent with PHPStan types
1 parent 98e2b6e commit 9d2ff8b

File tree

4 files changed

+239
-12
lines changed

4 files changed

+239
-12
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Internal;
4+
5+
use function addcslashes;
6+
use function preg_match;
7+
use function str_contains;
8+
use function strtr;
9+
use function sprintf;
10+
11+
final class StringLiteralHelper
12+
{
13+
14+
public static function escape(string $string): string
15+
{
16+
return addcslashes($string, "\0..\37\177");
17+
}
18+
19+
public static function escapeAndQuoteIfNeeded(string $string): string
20+
{
21+
if ($string === '') {
22+
return "''";
23+
}
24+
25+
$escaped = self::escape($string);
26+
27+
if (str_contains($escaped, '\\') || str_contains($escaped, "'")) {
28+
return sprintf('"%s"', strtr($escaped, ['"' => '\\"']));
29+
}
30+
31+
if (preg_match('/[!-\/:-@\[-^`\{-~]/', $escaped) === 1 || preg_match('/\p{Zs}/u', $escaped) === 1) {
32+
return sprintf("'%s'", strtr($escaped, ['\\' => '\\\\']));
33+
}
34+
35+
return $escaped;
36+
}
37+
38+
public static function quote(string $string): string
39+
{
40+
if ($string === '') {
41+
return "''";
42+
}
43+
44+
$escaped = self::escape($string);
45+
46+
if (str_contains($escaped, '\\') || str_contains($escaped, "'")) {
47+
return sprintf('"%s"', strtr($escaped, ['"' => '\\"']));
48+
}
49+
50+
return sprintf("'%s'", strtr($escaped, ['\\' => '\\\\']));
51+
}
52+
53+
}

src/Type/Constant/ConstantArrayType.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PHPStan\Analyser\OutOfClassScope;
77
use PHPStan\DependencyInjection\BleedingEdgeToggle;
88
use PHPStan\Internal\CombinationsHelper;
9+
use PHPStan\Internal\StringLiteralHelper;
910
use PHPStan\Php\PhpVersion;
1011
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
1112
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
@@ -1474,11 +1475,7 @@ public function describe(VerbosityLevel $level): string
14741475

14751476
$keyDescription = $keyType->getValue();
14761477
if (is_string($keyDescription)) {
1477-
if (str_contains($keyDescription, '"')) {
1478-
$keyDescription = sprintf('\'%s\'', $keyDescription);
1479-
} elseif (str_contains($keyDescription, '\'')) {
1480-
$keyDescription = sprintf('"%s"', $keyDescription);
1481-
}
1478+
$keyDescription = StringLiteralHelper::escapeAndQuoteIfNeeded($keyDescription);
14821479
}
14831480

14841481
$valueTypeDescription = $valueType->describe($level);

src/Type/Constant/ConstantStringType.php

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PhpParser\Node\Name;
88
use PHPStan\Analyser\OutOfClassScope;
99
use PHPStan\DependencyInjection\BleedingEdgeToggle;
10+
use PHPStan\Internal\StringLiteralHelper;
1011
use PHPStan\PhpDocParser\Ast\ConstExpr\QuoteAwareConstExprStringNode;
1112
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
1213
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
@@ -43,7 +44,6 @@
4344
use PHPStan\Type\Type;
4445
use PHPStan\Type\TypeCombinator;
4546
use PHPStan\Type\VerbosityLevel;
46-
use function addcslashes;
4747
use function in_array;
4848
use function is_float;
4949
use function is_int;
@@ -139,12 +139,7 @@ function (): string {
139139

140140
private function export(string $value): string
141141
{
142-
$escapedValue = addcslashes($value, "\0..\37");
143-
if ($escapedValue !== $value) {
144-
return '"' . addcslashes($value, "\0..\37\\\"") . '"';
145-
}
146-
147-
return "'" . addcslashes($value, '\\\'') . "'";
142+
return StringLiteralHelper::quote($value);
148143
}
149144

150145
public function isSuperTypeOf(Type $type): TrinaryLogic

tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,188 @@ public function testRuleInUse(): void
5050
]);
5151
}
5252

53+
public function testRuleSymbols(): void
54+
{
55+
$this->analyse([__DIR__ . '/data/dump-type-symbols.php'], [
56+
[
57+
'Dumped type: array{"\000": \'NUL\', NUL: "\000"}',
58+
6,
59+
],
60+
[
61+
'Dumped type: array{"\001": \'SOH\', SOH: "\001"}',
62+
7,
63+
],
64+
[
65+
'Dumped type: array{"\002": \'STX\', STX: "\002"}',
66+
8
67+
],
68+
[
69+
'Dumped type: array{"\003": \'ETX\', ETX: "\003"}',
70+
9,
71+
],
72+
[
73+
'Dumped type: array{"\004": \'EOT\', EOT: "\004"}',
74+
10,
75+
],
76+
[
77+
'Dumped type: array{"\005": \'ENQ\', ENQ: "\005"}',
78+
11,
79+
],
80+
[
81+
'Dumped type: array{"\006": \'ACK\', ACK: "\006"}',
82+
12,
83+
],
84+
[
85+
'Dumped type: array{"\a": \'BEL\', BEL: "\a"}',
86+
13,
87+
],
88+
[
89+
'Dumped type: array{"\b": \'BS\', BS: "\b"}',
90+
14,
91+
],
92+
[
93+
'Dumped type: array{"\t": \'HT\', HT: "\t"}',
94+
15,
95+
],
96+
[
97+
'Dumped type: array{"\n": \'LF\', LF: "\n"}',
98+
16,
99+
],
100+
[
101+
'Dumped type: array{"\v": \'VT\', VT: "\v"}',
102+
17,
103+
],
104+
[
105+
'Dumped type: array{"\f": \'FF\', FF: "\f"}',
106+
18,
107+
],
108+
[
109+
'Dumped type: array{"\r": \'CR\', CR: "\r"}',
110+
19,
111+
],
112+
[
113+
'Dumped type: array{"\016": \'SO\', SO: "\016"}',
114+
20,
115+
],
116+
[
117+
'Dumped type: array{"\017": \'SI\', SI: "\017"}',
118+
21,
119+
],
120+
[
121+
'Dumped type: array{"\020": \'DLE\', DLE: "\020"}',
122+
22,
123+
],
124+
[
125+
'Dumped type: array{"\021": \'DC1\', DC1: "\021"}',
126+
23,
127+
],
128+
[
129+
'Dumped type: array{"\022": \'DC2\', DC2: "\022"}',
130+
24,
131+
],
132+
[
133+
'Dumped type: array{"\023": \'DC3\', DC3: "\023"}',
134+
25,
135+
],
136+
[
137+
'Dumped type: array{"\024": \'DC4\', DC4: "\024"}',
138+
26,
139+
],
140+
[
141+
'Dumped type: array{"\025": \'NAK\', NAK: "\025"}',
142+
27,
143+
],
144+
[
145+
'Dumped type: array{"\026": \'SYN\', SYN: "\026"}',
146+
28,
147+
],
148+
[
149+
'Dumped type: array{"\027": \'ETB\', ETB: "\027"}',
150+
29,
151+
],
152+
[
153+
'Dumped type: array{"\030": \'CAN\', CAN: "\030"}',
154+
30,
155+
],
156+
[
157+
'Dumped type: array{"\031": \'EM\', EM: "\031"}',
158+
31,
159+
],
160+
[
161+
'Dumped type: array{"\032": \'SUB\', SUB: "\032"}',
162+
32,
163+
],
164+
[
165+
'Dumped type: array{"\033": \'ESC\', ESC: "\033"}',
166+
33,
167+
],
168+
[
169+
'Dumped type: array{"\034": \'FS\', FS: "\034"}',
170+
34,
171+
],
172+
[
173+
'Dumped type: array{"\035": \'GS\', GS: "\035"}',
174+
35,
175+
],
176+
[
177+
'Dumped type: array{"\036": \'RS\', RS: "\036"}',
178+
36,
179+
],
180+
[
181+
'Dumped type: array{"\037": \'US\', US: "\037"}',
182+
37,
183+
],
184+
[
185+
'Dumped type: array{"\177": \'DEL\', DEL: "\177"}',
186+
38,
187+
],
188+
[
189+
'Dumped type: array{\' \': \'SP\', SP: \' \'}',
190+
41,
191+
],
192+
[
193+
"Dumped type: array{'foo ': 'ends with SP', ' foo': 'starts with SP', ' foo ': 'surrounded by SP', foo: 'no SP'}",
194+
42,
195+
],
196+
[
197+
"Dumped type: array{'foo?': 'foo?'}",
198+
45,
199+
],
200+
[
201+
"Dumped type: array{shallwedance: 'yes'}",
202+
46,
203+
],
204+
[
205+
"Dumped type: array{'shallwedance?': 'yes'}",
206+
47,
207+
],
208+
[
209+
"Dumped type: array{'Shall we dance': 'yes'}",
210+
48,
211+
],
212+
[
213+
"Dumped type: array{'Shall we dance?': 'yes'}",
214+
49,
215+
],
216+
[
217+
"Dumped type: array{shall_we_dance: 'yes'}",
218+
50,
219+
],
220+
[
221+
"Dumped type: array{'shall_we_dance?': 'yes'}",
222+
51,
223+
],
224+
[
225+
"Dumped type: array{'shall-we-dance': 'yes'}",
226+
52,
227+
],
228+
[
229+
"Dumped type: array{'shall-we-dance?': 'yes'}",
230+
53,
231+
],
232+
]);
233+
}
234+
53235
public function testBug7803(): void
54236
{
55237
$this->analyse([__DIR__ . '/data/bug-7803.php'], [

0 commit comments

Comments
 (0)