|
1
|
|
|
<?php |
|
2
|
|
|
namespace Psalm\Internal\Analyzer; |
|
3
|
|
|
|
|
4
|
|
|
use PhpParser; |
|
5
|
|
|
use Psalm\Aliases; |
|
6
|
|
|
use Psalm\DocComment; |
|
7
|
|
|
use Psalm\Exception\DocblockParseException; |
|
8
|
|
|
use Psalm\Exception\IncorrectDocblockException; |
|
9
|
|
|
use Psalm\Exception\TypeParseTreeException; |
|
10
|
|
|
use Psalm\FileSource; |
|
11
|
|
|
use Psalm\Internal\Scanner\ClassLikeDocblockComment; |
|
12
|
|
|
use Psalm\Internal\Scanner\DocblockParser; |
|
13
|
|
|
use Psalm\Internal\Scanner\FunctionDocblockComment; |
|
14
|
|
|
use Psalm\Internal\Scanner\VarDocblockComment; |
|
15
|
|
|
use Psalm\Internal\Scanner\ParsedDocblock; |
|
16
|
|
|
use Psalm\Internal\Type\ParseTree; |
|
17
|
|
|
use Psalm\Internal\Type\ParseTreeCreator; |
|
18
|
|
|
use Psalm\Internal\Type\TypeAlias; |
|
19
|
|
|
use Psalm\Internal\Type\TypeParser; |
|
20
|
|
|
use Psalm\Internal\Type\TypeTokenizer; |
|
21
|
|
|
use Psalm\Type; |
|
22
|
|
|
use function array_column; |
|
23
|
|
|
use function array_unique; |
|
24
|
|
|
use function trim; |
|
25
|
|
|
use function substr_count; |
|
26
|
|
|
use function strlen; |
|
27
|
|
|
use function preg_replace; |
|
28
|
|
|
use function str_replace; |
|
29
|
|
|
use function preg_match; |
|
30
|
|
|
use function count; |
|
31
|
|
|
use function reset; |
|
32
|
|
|
use function preg_split; |
|
33
|
|
|
use const PREG_SPLIT_DELIM_CAPTURE; |
|
34
|
|
|
use const PREG_SPLIT_NO_EMPTY; |
|
35
|
|
|
use function array_shift; |
|
36
|
|
|
use function implode; |
|
37
|
|
|
use function substr; |
|
38
|
|
|
use function strpos; |
|
39
|
|
|
use function strtolower; |
|
40
|
|
|
use function in_array; |
|
41
|
|
|
use function explode; |
|
42
|
|
|
use function array_merge; |
|
43
|
|
|
use const PREG_OFFSET_CAPTURE; |
|
44
|
|
|
use function rtrim; |
|
45
|
|
|
|
|
46
|
|
|
/** |
|
47
|
|
|
* @internal |
|
48
|
|
|
*/ |
|
49
|
|
|
class CommentAnalyzer |
|
50
|
|
|
{ |
|
51
|
|
|
const TYPE_REGEX = '(\??\\\?[\(\)A-Za-z0-9_&\<\.=,\>\[\]\-\{\}:|?\\\\]*|\$[a-zA-Z_0-9_]+)'; |
|
52
|
|
|
|
|
53
|
|
|
/** |
|
54
|
|
|
* @param array<string, array<string, array{Type\Union}>>|null $template_type_map |
|
55
|
|
|
* @param array<string, TypeAlias> $type_aliases |
|
56
|
|
|
* |
|
57
|
|
|
* @throws DocblockParseException if there was a problem parsing the docblock |
|
58
|
|
|
* |
|
59
|
|
|
* @return VarDocblockComment[] |
|
60
|
|
|
*/ |
|
61
|
|
|
public static function getTypeFromComment( |
|
62
|
|
|
PhpParser\Comment\Doc $comment, |
|
63
|
|
|
FileSource $source, |
|
64
|
|
|
Aliases $aliases, |
|
65
|
|
|
array $template_type_map = null, |
|
66
|
|
|
?array $type_aliases = null |
|
67
|
|
|
) { |
|
68
|
|
|
$parsed_docblock = DocComment::parsePreservingLength($comment); |
|
69
|
|
|
|
|
70
|
|
|
return self::arrayToDocblocks( |
|
71
|
|
|
$comment, |
|
72
|
|
|
$parsed_docblock, |
|
73
|
|
|
$source, |
|
74
|
|
|
$aliases, |
|
75
|
|
|
$template_type_map, |
|
76
|
|
|
$type_aliases |
|
77
|
|
|
); |
|
78
|
|
|
} |
|
79
|
|
|
|
|
80
|
|
|
/** |
|
81
|
|
|
* @param array<string, array<string, array{Type\Union}>>|null $template_type_map |
|
82
|
|
|
* @param array<string, TypeAlias> $type_aliases |
|
83
|
|
|
* |
|
84
|
|
|
* @return VarDocblockComment[] |
|
85
|
|
|
* |
|
86
|
|
|
* @throws DocblockParseException if there was a problem parsing the docblock |
|
87
|
|
|
*/ |
|
88
|
|
|
public static function arrayToDocblocks( |
|
89
|
|
|
PhpParser\Comment\Doc $comment, |
|
90
|
|
|
ParsedDocblock $parsed_docblock, |
|
91
|
|
|
FileSource $source, |
|
92
|
|
|
Aliases $aliases, |
|
93
|
|
|
array $template_type_map = null, |
|
94
|
|
|
?array $type_aliases = null |
|
95
|
|
|
) : array { |
|
96
|
|
|
$var_id = null; |
|
97
|
|
|
|
|
98
|
|
|
$var_type_tokens = null; |
|
99
|
|
|
$original_type = null; |
|
100
|
|
|
|
|
101
|
|
|
$var_comments = []; |
|
102
|
|
|
|
|
103
|
|
|
$comment_text = $comment->getText(); |
|
104
|
|
|
|
|
105
|
|
|
$var_line_number = $comment->getLine(); |
|
|
|
|
|
|
106
|
|
|
|
|
107
|
|
|
if (isset($parsed_docblock->combined_tags['var'])) { |
|
108
|
|
|
foreach ($parsed_docblock->combined_tags['var'] as $offset => $var_line) { |
|
109
|
|
|
$var_line = trim($var_line); |
|
110
|
|
|
|
|
111
|
|
|
if (!$var_line) { |
|
112
|
|
|
continue; |
|
113
|
|
|
} |
|
114
|
|
|
|
|
115
|
|
|
$type_start = null; |
|
116
|
|
|
$type_end = null; |
|
117
|
|
|
|
|
118
|
|
|
$line_parts = self::splitDocLine($var_line); |
|
119
|
|
|
|
|
120
|
|
|
$line_number = $comment->getLine() + substr_count($comment_text, "\n", 0, $offset); |
|
|
|
|
|
|
121
|
|
|
|
|
122
|
|
|
if ($line_parts && $line_parts[0]) { |
|
123
|
|
|
$type_start = $offset + $comment->getFilePos(); |
|
|
|
|
|
|
124
|
|
|
$type_end = $type_start + strlen($line_parts[0]); |
|
125
|
|
|
|
|
126
|
|
|
$line_parts[0] = self::sanitizeDocblockType($line_parts[0]); |
|
127
|
|
|
|
|
128
|
|
|
if ($line_parts[0] === '' |
|
129
|
|
|
|| ($line_parts[0][0] === '$' |
|
130
|
|
|
&& !preg_match('/^\$this(\||$)/', $line_parts[0])) |
|
131
|
|
|
) { |
|
132
|
|
|
throw new IncorrectDocblockException('Misplaced variable'); |
|
133
|
|
|
} |
|
134
|
|
|
|
|
135
|
|
|
try { |
|
136
|
|
|
$var_type_tokens = TypeTokenizer::getFullyQualifiedTokens( |
|
137
|
|
|
$line_parts[0], |
|
138
|
|
|
$aliases, |
|
139
|
|
|
$template_type_map, |
|
140
|
|
|
$type_aliases |
|
141
|
|
|
); |
|
142
|
|
|
} catch (TypeParseTreeException $e) { |
|
143
|
|
|
throw new DocblockParseException($line_parts[0] . ' is not a valid type'); |
|
144
|
|
|
} |
|
145
|
|
|
|
|
146
|
|
|
$original_type = $line_parts[0]; |
|
147
|
|
|
|
|
148
|
|
|
$var_line_number = $line_number; |
|
149
|
|
|
|
|
150
|
|
|
if (count($line_parts) > 1 && $line_parts[1][0] === '$') { |
|
151
|
|
|
$var_id = $line_parts[1]; |
|
152
|
|
|
} |
|
153
|
|
|
} |
|
154
|
|
|
|
|
155
|
|
|
if (!$var_type_tokens || !$original_type) { |
|
156
|
|
|
continue; |
|
157
|
|
|
} |
|
158
|
|
|
|
|
159
|
|
|
try { |
|
160
|
|
|
$defined_type = TypeParser::parseTokens( |
|
161
|
|
|
$var_type_tokens, |
|
162
|
|
|
null, |
|
163
|
|
|
$template_type_map ?: [], |
|
164
|
|
|
$type_aliases ?: [] |
|
165
|
|
|
); |
|
166
|
|
|
} catch (TypeParseTreeException $e) { |
|
167
|
|
|
throw new DocblockParseException( |
|
168
|
|
|
$line_parts[0] . |
|
169
|
|
|
' is not a valid type' . |
|
170
|
|
|
' (from ' . |
|
171
|
|
|
$source->getFilePath() . |
|
172
|
|
|
':' . |
|
173
|
|
|
$comment->getLine() . |
|
|
|
|
|
|
174
|
|
|
')' |
|
175
|
|
|
); |
|
176
|
|
|
} |
|
177
|
|
|
|
|
178
|
|
|
$defined_type->setFromDocblock(); |
|
179
|
|
|
|
|
180
|
|
|
$var_comment = new VarDocblockComment(); |
|
181
|
|
|
$var_comment->type = $defined_type; |
|
182
|
|
|
$var_comment->original_type = $original_type; |
|
183
|
|
|
$var_comment->var_id = $var_id; |
|
184
|
|
|
$var_comment->line_number = $var_line_number; |
|
185
|
|
|
$var_comment->type_start = $type_start; |
|
|
|
|
|
|
186
|
|
|
$var_comment->type_end = $type_end; |
|
|
|
|
|
|
187
|
|
|
|
|
188
|
|
|
self::decorateVarDocblockComment($var_comment, $parsed_docblock); |
|
189
|
|
|
|
|
190
|
|
|
$var_comments[] = $var_comment; |
|
191
|
|
|
} |
|
192
|
|
|
} |
|
193
|
|
|
|
|
194
|
|
|
if (!$var_comments |
|
195
|
|
|
&& (isset($parsed_docblock->tags['deprecated']) |
|
196
|
|
|
|| isset($parsed_docblock->tags['internal']) |
|
197
|
|
|
|| isset($parsed_docblock->tags['readonly']) |
|
198
|
|
|
|| isset($parsed_docblock->tags['psalm-readonly']) |
|
199
|
|
|
|| isset($parsed_docblock->tags['psalm-readonly-allow-private-mutation']) |
|
200
|
|
|
|| isset($parsed_docblock->tags['psalm-taint-escape']) |
|
201
|
|
|
|| isset($parsed_docblock->tags['psalm-internal'])) |
|
202
|
|
|
) { |
|
203
|
|
|
$var_comment = new VarDocblockComment(); |
|
204
|
|
|
|
|
205
|
|
|
self::decorateVarDocblockComment($var_comment, $parsed_docblock); |
|
206
|
|
|
|
|
207
|
|
|
$var_comments[] = $var_comment; |
|
208
|
|
|
} |
|
209
|
|
|
|
|
210
|
|
|
return $var_comments; |
|
211
|
|
|
} |
|
212
|
|
|
|
|
213
|
|
|
private static function decorateVarDocblockComment( |
|
214
|
|
|
VarDocblockComment $var_comment, |
|
215
|
|
|
ParsedDocblock $parsed_docblock |
|
216
|
|
|
) : void { |
|
217
|
|
|
$var_comment->deprecated = isset($parsed_docblock->tags['deprecated']); |
|
218
|
|
|
$var_comment->internal = isset($parsed_docblock->tags['internal']); |
|
219
|
|
|
$var_comment->readonly = isset($parsed_docblock->tags['readonly']) |
|
220
|
|
|
|| isset($parsed_docblock->tags['psalm-readonly']) |
|
221
|
|
|
|| isset($parsed_docblock->tags['psalm-readonly-allow-private-mutation']); |
|
222
|
|
|
|
|
223
|
|
|
$var_comment->allow_private_mutation |
|
224
|
|
|
= isset($parsed_docblock->tags['psalm-allow-private-mutation']) |
|
225
|
|
|
|| isset($parsed_docblock->tags['psalm-readonly-allow-private-mutation']); |
|
226
|
|
|
|
|
227
|
|
|
if (isset($parsed_docblock->tags['psalm-taint-escape'])) { |
|
228
|
|
|
foreach ($parsed_docblock->tags['psalm-taint-escape'] as $param) { |
|
229
|
|
|
$param = trim($param); |
|
230
|
|
|
$var_comment->removed_taints[] = $param; |
|
231
|
|
|
} |
|
232
|
|
|
} |
|
233
|
|
|
|
|
234
|
|
|
if (isset($parsed_docblock->tags['psalm-internal'])) { |
|
235
|
|
|
$psalm_internal = reset($parsed_docblock->tags['psalm-internal']); |
|
236
|
|
|
|
|
237
|
|
|
if (!$psalm_internal) { |
|
238
|
|
|
throw new DocblockParseException('psalm-internal annotation used without specifying namespace'); |
|
239
|
|
|
} |
|
240
|
|
|
|
|
241
|
|
|
$var_comment->psalm_internal = reset($parsed_docblock->tags['psalm-internal']); |
|
242
|
|
|
|
|
243
|
|
|
if (!$var_comment->internal) { |
|
244
|
|
|
throw new DocblockParseException('@psalm-internal annotation used without @internal'); |
|
245
|
|
|
} |
|
246
|
|
|
} |
|
247
|
|
|
} |
|
248
|
|
|
|
|
249
|
|
|
private static function sanitizeDocblockType(string $docblock_type) : string |
|
250
|
|
|
{ |
|
251
|
|
|
$docblock_type = preg_replace('@^[ \t]*\*@m', '', $docblock_type); |
|
252
|
|
|
$docblock_type = preg_replace('/,\n\s+\}/', '}', $docblock_type); |
|
253
|
|
|
return str_replace("\n", '', $docblock_type); |
|
254
|
|
|
} |
|
255
|
|
|
|
|
256
|
|
|
/** |
|
257
|
|
|
* @param Aliases $aliases |
|
258
|
|
|
* @param array<string, TypeAlias> $type_aliases |
|
259
|
|
|
* |
|
260
|
|
|
* @throws DocblockParseException if there was a problem parsing the docblock |
|
261
|
|
|
* |
|
262
|
|
|
* @return array<string, TypeAlias\InlineTypeAlias> |
|
|
|
|
|
|
263
|
|
|
*/ |
|
264
|
|
|
public static function getTypeAliasesFromComment( |
|
265
|
|
|
PhpParser\Comment\Doc $comment, |
|
266
|
|
|
Aliases $aliases, |
|
267
|
|
|
?array $type_aliases, |
|
268
|
|
|
?string $self_fqcln |
|
269
|
|
|
) { |
|
270
|
|
|
$parsed_docblock = DocComment::parsePreservingLength($comment); |
|
271
|
|
|
|
|
272
|
|
|
if (!isset($parsed_docblock->tags['psalm-type'])) { |
|
273
|
|
|
return []; |
|
274
|
|
|
} |
|
275
|
|
|
|
|
276
|
|
|
return self::getTypeAliasesFromCommentLines( |
|
277
|
|
|
$parsed_docblock->tags['psalm-type'], |
|
278
|
|
|
$aliases, |
|
279
|
|
|
$type_aliases, |
|
280
|
|
|
$self_fqcln |
|
281
|
|
|
); |
|
282
|
|
|
} |
|
283
|
|
|
|
|
284
|
|
|
/** |
|
285
|
|
|
* @param array<string> $type_alias_comment_lines |
|
286
|
|
|
* @param Aliases $aliases |
|
287
|
|
|
* @param array<string, TypeAlias> $type_aliases |
|
288
|
|
|
* |
|
289
|
|
|
* @throws DocblockParseException if there was a problem parsing the docblock |
|
290
|
|
|
* |
|
291
|
|
|
* @return array<string, TypeAlias\InlineTypeAlias> |
|
|
|
|
|
|
292
|
|
|
*/ |
|
293
|
|
|
private static function getTypeAliasesFromCommentLines( |
|
294
|
|
|
array $type_alias_comment_lines, |
|
295
|
|
|
Aliases $aliases, |
|
296
|
|
|
?array $type_aliases, |
|
297
|
|
|
?string $self_fqcln |
|
298
|
|
|
) { |
|
299
|
|
|
$type_alias_tokens = []; |
|
300
|
|
|
|
|
301
|
|
|
foreach ($type_alias_comment_lines as $var_line) { |
|
302
|
|
|
$var_line = trim($var_line); |
|
303
|
|
|
|
|
304
|
|
|
if (!$var_line) { |
|
305
|
|
|
continue; |
|
306
|
|
|
} |
|
307
|
|
|
|
|
308
|
|
|
$var_line = preg_replace('/[ \t]+/', ' ', preg_replace('@^[ \t]*\*@m', '', $var_line)); |
|
309
|
|
|
$var_line = preg_replace('/,\n\s+\}/', '}', $var_line); |
|
310
|
|
|
$var_line = str_replace("\n", '', $var_line); |
|
311
|
|
|
|
|
312
|
|
|
$var_line_parts = preg_split('/( |=)/', $var_line, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); |
|
313
|
|
|
|
|
314
|
|
|
if (!$var_line_parts) { |
|
315
|
|
|
continue; |
|
316
|
|
|
} |
|
317
|
|
|
|
|
318
|
|
|
$type_alias = array_shift($var_line_parts); |
|
319
|
|
|
|
|
320
|
|
|
if (!isset($var_line_parts[0])) { |
|
321
|
|
|
continue; |
|
322
|
|
|
} |
|
323
|
|
|
|
|
324
|
|
|
if ($var_line_parts[0] === ' ') { |
|
325
|
|
|
array_shift($var_line_parts); |
|
326
|
|
|
} |
|
327
|
|
|
|
|
328
|
|
|
if ($var_line_parts[0] === '=') { |
|
329
|
|
|
array_shift($var_line_parts); |
|
330
|
|
|
} |
|
331
|
|
|
|
|
332
|
|
|
if (!isset($var_line_parts[0])) { |
|
333
|
|
|
continue; |
|
334
|
|
|
} |
|
335
|
|
|
|
|
336
|
|
|
if ($var_line_parts[0] === ' ') { |
|
337
|
|
|
array_shift($var_line_parts); |
|
338
|
|
|
} |
|
339
|
|
|
|
|
340
|
|
|
$type_string = str_replace("\n", '', implode('', $var_line_parts)); |
|
341
|
|
|
|
|
342
|
|
|
$type_string = preg_replace('/>[^>^\}]*$/', '>', $type_string); |
|
343
|
|
|
$type_string = preg_replace('/\}[^>^\}]*$/', '}', $type_string); |
|
344
|
|
|
|
|
345
|
|
|
try { |
|
346
|
|
|
$type_tokens = TypeTokenizer::getFullyQualifiedTokens( |
|
347
|
|
|
$type_string, |
|
348
|
|
|
$aliases, |
|
349
|
|
|
null, |
|
350
|
|
|
$type_alias_tokens + $type_aliases, |
|
351
|
|
|
$self_fqcln |
|
352
|
|
|
); |
|
353
|
|
|
} catch (TypeParseTreeException $e) { |
|
354
|
|
|
throw new DocblockParseException($type_string . ' is not a valid type'); |
|
355
|
|
|
} |
|
356
|
|
|
|
|
357
|
|
|
$type_alias_tokens[$type_alias] = new TypeAlias\InlineTypeAlias($type_tokens); |
|
358
|
|
|
} |
|
359
|
|
|
|
|
360
|
|
|
return $type_alias_tokens; |
|
361
|
|
|
} |
|
362
|
|
|
|
|
363
|
|
|
/** |
|
364
|
|
|
* @param int $line_number |
|
|
|
|
|
|
365
|
|
|
* |
|
366
|
|
|
* @throws DocblockParseException if there was a problem parsing the docblock |
|
367
|
|
|
* |
|
368
|
|
|
* @return FunctionDocblockComment |
|
369
|
|
|
*/ |
|
370
|
|
|
public static function extractFunctionDocblockInfo(PhpParser\Comment\Doc $comment) |
|
371
|
|
|
{ |
|
372
|
|
|
$parsed_docblock = DocComment::parsePreservingLength($comment); |
|
373
|
|
|
|
|
374
|
|
|
$comment_text = $comment->getText(); |
|
375
|
|
|
|
|
376
|
|
|
$info = new FunctionDocblockComment(); |
|
377
|
|
|
|
|
378
|
|
|
self::checkDuplicatedTags($parsed_docblock); |
|
379
|
|
|
|
|
380
|
|
|
if (isset($parsed_docblock->combined_tags['return'])) { |
|
381
|
|
|
self::extractReturnType( |
|
382
|
|
|
$comment, |
|
383
|
|
|
$parsed_docblock->combined_tags['return'], |
|
384
|
|
|
$info |
|
385
|
|
|
); |
|
386
|
|
|
} |
|
387
|
|
|
|
|
388
|
|
|
if (isset($parsed_docblock->combined_tags['param'])) { |
|
389
|
|
|
foreach ($parsed_docblock->combined_tags['param'] as $offset => $param) { |
|
390
|
|
|
$line_parts = self::splitDocLine($param); |
|
391
|
|
|
|
|
392
|
|
|
if (count($line_parts) === 1 && isset($line_parts[0][0]) && $line_parts[0][0] === '$') { |
|
393
|
|
|
continue; |
|
394
|
|
|
} |
|
395
|
|
|
|
|
396
|
|
|
if (count($line_parts) > 1) { |
|
397
|
|
|
if (preg_match('/^&?(\.\.\.)?&?\$[A-Za-z0-9_]+,?$/', $line_parts[1]) |
|
398
|
|
|
&& $line_parts[0][0] !== '{' |
|
399
|
|
|
) { |
|
400
|
|
|
$line_parts[1] = str_replace('&', '', $line_parts[1]); |
|
401
|
|
|
|
|
402
|
|
|
$line_parts[1] = preg_replace('/,$/', '', $line_parts[1]); |
|
403
|
|
|
|
|
404
|
|
|
$start = $offset + $comment->getFilePos(); |
|
|
|
|
|
|
405
|
|
|
$end = $start + strlen($line_parts[0]); |
|
406
|
|
|
|
|
407
|
|
|
$line_parts[0] = self::sanitizeDocblockType($line_parts[0]); |
|
408
|
|
|
|
|
409
|
|
|
if ($line_parts[0] === '' |
|
410
|
|
|
|| ($line_parts[0][0] === '$' |
|
411
|
|
|
&& !preg_match('/^\$this(\||$)/', $line_parts[0])) |
|
412
|
|
|
) { |
|
413
|
|
|
throw new IncorrectDocblockException('Misplaced variable'); |
|
414
|
|
|
} |
|
415
|
|
|
|
|
416
|
|
|
$info->params[] = [ |
|
417
|
|
|
'name' => trim($line_parts[1]), |
|
418
|
|
|
'type' => $line_parts[0], |
|
419
|
|
|
'line_number' => $comment->getLine() + substr_count($comment_text, "\n", 0, $offset), |
|
|
|
|
|
|
420
|
|
|
'start' => $start, |
|
421
|
|
|
'end' => $end, |
|
422
|
|
|
]; |
|
423
|
|
|
} |
|
424
|
|
|
} else { |
|
425
|
|
|
throw new DocblockParseException('Badly-formatted @param'); |
|
426
|
|
|
} |
|
427
|
|
|
} |
|
428
|
|
|
} |
|
429
|
|
|
|
|
430
|
|
|
if (isset($parsed_docblock->tags['param-out'])) { |
|
431
|
|
|
foreach ($parsed_docblock->tags['param-out'] as $offset => $param) { |
|
432
|
|
|
$line_parts = self::splitDocLine($param); |
|
433
|
|
|
|
|
434
|
|
|
if (count($line_parts) === 1 && isset($line_parts[0][0]) && $line_parts[0][0] === '$') { |
|
435
|
|
|
continue; |
|
436
|
|
|
} |
|
437
|
|
|
|
|
438
|
|
|
if (count($line_parts) > 1) { |
|
439
|
|
|
if (!preg_match('/\[[^\]]+\]/', $line_parts[0]) |
|
440
|
|
|
&& preg_match('/^(\.\.\.)?&?\$[A-Za-z0-9_]+,?$/', $line_parts[1]) |
|
441
|
|
|
&& $line_parts[0][0] !== '{' |
|
442
|
|
|
) { |
|
443
|
|
|
if ($line_parts[1][0] === '&') { |
|
444
|
|
|
$line_parts[1] = substr($line_parts[1], 1); |
|
445
|
|
|
} |
|
446
|
|
|
|
|
447
|
|
|
$line_parts[0] = str_replace("\n", '', preg_replace('@^[ \t]*\*@m', '', $line_parts[0])); |
|
448
|
|
|
|
|
449
|
|
|
if ($line_parts[0] === '' |
|
450
|
|
|
|| ($line_parts[0][0] === '$' |
|
451
|
|
|
&& !preg_match('/^\$this(\||$)/', $line_parts[0])) |
|
452
|
|
|
) { |
|
453
|
|
|
throw new IncorrectDocblockException('Misplaced variable'); |
|
454
|
|
|
} |
|
455
|
|
|
|
|
456
|
|
|
$line_parts[1] = preg_replace('/,$/', '', $line_parts[1]); |
|
457
|
|
|
|
|
458
|
|
|
$info->params_out[] = [ |
|
459
|
|
|
'name' => trim($line_parts[1]), |
|
460
|
|
|
'type' => str_replace("\n", '', $line_parts[0]), |
|
461
|
|
|
'line_number' => $comment->getLine() + substr_count($comment_text, "\n", 0, $offset), |
|
|
|
|
|
|
462
|
|
|
]; |
|
463
|
|
|
} |
|
464
|
|
|
} else { |
|
465
|
|
|
throw new DocblockParseException('Badly-formatted @param'); |
|
466
|
|
|
} |
|
467
|
|
|
} |
|
468
|
|
|
} |
|
469
|
|
|
|
|
470
|
|
|
if (isset($parsed_docblock->tags['psalm-self-out'])) { |
|
471
|
|
|
foreach ($parsed_docblock->tags['psalm-self-out'] as $offset => $param) { |
|
472
|
|
|
$line_parts = self::splitDocLine($param); |
|
473
|
|
|
|
|
474
|
|
|
if (count($line_parts) > 0) { |
|
475
|
|
|
$line_parts[0] = str_replace("\n", '', preg_replace('@^[ \t]*\*@m', '', $line_parts[0])); |
|
476
|
|
|
|
|
477
|
|
|
$info->self_out = [ |
|
478
|
|
|
'type' => str_replace("\n", '', $line_parts[0]), |
|
479
|
|
|
'line_number' => $comment->getLine() + substr_count($comment_text, "\n", 0, $offset), |
|
|
|
|
|
|
480
|
|
|
]; |
|
481
|
|
|
} |
|
482
|
|
|
} |
|
483
|
|
|
} |
|
484
|
|
|
|
|
485
|
|
|
if (isset($parsed_docblock->tags['psalm-flow'])) { |
|
486
|
|
|
foreach ($parsed_docblock->tags['psalm-flow'] as $param) { |
|
487
|
|
|
$info->flows[] = trim($param); |
|
488
|
|
|
} |
|
489
|
|
|
} |
|
490
|
|
|
|
|
491
|
|
|
if (isset($parsed_docblock->tags['psalm-taint-sink'])) { |
|
492
|
|
|
foreach ($parsed_docblock->tags['psalm-taint-sink'] as $param) { |
|
493
|
|
|
$param_parts = preg_split('/\s+/', trim($param)); |
|
494
|
|
|
|
|
495
|
|
|
if (count($param_parts) === 2) { |
|
496
|
|
|
$info->taint_sink_params[] = ['name' => $param_parts[1], 'taint' => $param_parts[0]]; |
|
497
|
|
|
} |
|
498
|
|
|
} |
|
499
|
|
|
} |
|
500
|
|
|
|
|
501
|
|
|
// support for MediaWiki taint plugin |
|
502
|
|
|
if (isset($parsed_docblock->tags['param-taint'])) { |
|
503
|
|
|
foreach ($parsed_docblock->tags['param-taint'] as $param) { |
|
504
|
|
|
$param_parts = preg_split('/\s+/', trim($param)); |
|
505
|
|
|
|
|
506
|
|
|
if (count($param_parts) === 2) { |
|
507
|
|
|
$taint_type = $param_parts[1]; |
|
508
|
|
|
|
|
509
|
|
|
if (substr($taint_type, 0, 5) === 'exec_') { |
|
510
|
|
|
$taint_type = substr($taint_type, 5); |
|
511
|
|
|
|
|
512
|
|
|
if ($taint_type === 'tainted') { |
|
513
|
|
|
$taint_type = 'input'; |
|
514
|
|
|
} |
|
515
|
|
|
|
|
516
|
|
|
if ($taint_type === 'misc') { |
|
517
|
|
|
$taint_type = 'text'; |
|
518
|
|
|
} |
|
519
|
|
|
|
|
520
|
|
|
$info->taint_sink_params[] = ['name' => $param_parts[0], 'taint' => $taint_type]; |
|
521
|
|
|
} |
|
522
|
|
|
} |
|
523
|
|
|
} |
|
524
|
|
|
} |
|
525
|
|
|
|
|
526
|
|
|
if (isset($parsed_docblock->tags['psalm-taint-source'])) { |
|
527
|
|
|
foreach ($parsed_docblock->tags['psalm-taint-source'] as $param) { |
|
528
|
|
|
$param_parts = preg_split('/\s+/', trim($param)); |
|
529
|
|
|
|
|
530
|
|
|
if ($param_parts[0]) { |
|
531
|
|
|
$info->taint_source_types[] = $param_parts[0]; |
|
532
|
|
|
} |
|
533
|
|
|
} |
|
534
|
|
|
} elseif (isset($parsed_docblock->tags['return-taint'])) { |
|
535
|
|
|
// support for MediaWiki taint plugin |
|
536
|
|
|
foreach ($parsed_docblock->tags['return-taint'] as $param) { |
|
537
|
|
|
$param_parts = preg_split('/\s+/', trim($param)); |
|
538
|
|
|
|
|
539
|
|
|
if ($param_parts[0]) { |
|
540
|
|
|
if ($param_parts[0] === 'tainted') { |
|
541
|
|
|
$param_parts[0] = 'input'; |
|
542
|
|
|
} |
|
543
|
|
|
|
|
544
|
|
|
if ($param_parts[0] === 'misc') { |
|
545
|
|
|
$param_parts[0] = 'text'; |
|
546
|
|
|
} |
|
547
|
|
|
|
|
548
|
|
|
if ($param_parts[0] !== 'none') { |
|
549
|
|
|
$info->taint_source_types[] = $param_parts[0]; |
|
550
|
|
|
} |
|
551
|
|
|
} |
|
552
|
|
|
} |
|
553
|
|
|
} |
|
554
|
|
|
|
|
555
|
|
|
if (isset($parsed_docblock->tags['psalm-taint-unescape'])) { |
|
556
|
|
|
foreach ($parsed_docblock->tags['psalm-taint-unescape'] as $param) { |
|
557
|
|
|
$param = trim($param); |
|
558
|
|
|
$info->added_taints[] = $param; |
|
559
|
|
|
} |
|
560
|
|
|
} |
|
561
|
|
|
|
|
562
|
|
|
if (isset($parsed_docblock->tags['psalm-taint-escape'])) { |
|
563
|
|
|
foreach ($parsed_docblock->tags['psalm-taint-escape'] as $param) { |
|
564
|
|
|
$param = trim($param); |
|
565
|
|
|
$info->removed_taints[] = $param; |
|
566
|
|
|
} |
|
567
|
|
|
} |
|
568
|
|
|
|
|
569
|
|
|
if (isset($parsed_docblock->tags['psalm-assert-untainted'])) { |
|
570
|
|
|
foreach ($parsed_docblock->tags['psalm-assert-untainted'] as $param) { |
|
571
|
|
|
$param = trim($param); |
|
572
|
|
|
|
|
573
|
|
|
$info->assert_untainted_params[] = ['name' => $param]; |
|
574
|
|
|
} |
|
575
|
|
|
} |
|
576
|
|
|
|
|
577
|
|
|
if (isset($parsed_docblock->tags['psalm-taint-specialize'])) { |
|
578
|
|
|
$info->specialize_call = true; |
|
579
|
|
|
} |
|
580
|
|
|
|
|
581
|
|
|
if (isset($parsed_docblock->tags['global'])) { |
|
582
|
|
|
foreach ($parsed_docblock->tags['global'] as $offset => $global) { |
|
583
|
|
|
$line_parts = self::splitDocLine($global); |
|
584
|
|
|
|
|
585
|
|
|
if (count($line_parts) === 1 && isset($line_parts[0][0]) && $line_parts[0][0] === '$') { |
|
586
|
|
|
continue; |
|
587
|
|
|
} |
|
588
|
|
|
|
|
589
|
|
|
if (count($line_parts) > 1) { |
|
590
|
|
|
if (!preg_match('/\[[^\]]+\]/', $line_parts[0]) |
|
591
|
|
|
&& preg_match('/^(\.\.\.)?&?\$[A-Za-z0-9_]+,?$/', $line_parts[1]) |
|
592
|
|
|
&& $line_parts[0][0] !== '{' |
|
593
|
|
|
) { |
|
594
|
|
|
if ($line_parts[1][0] === '&') { |
|
595
|
|
|
$line_parts[1] = substr($line_parts[1], 1); |
|
596
|
|
|
} |
|
597
|
|
|
|
|
598
|
|
|
if ($line_parts[0][0] === '$' && !preg_match('/^\$this(\||$)/', $line_parts[0])) { |
|
599
|
|
|
throw new IncorrectDocblockException('Misplaced variable'); |
|
600
|
|
|
} |
|
601
|
|
|
|
|
602
|
|
|
$line_parts[1] = preg_replace('/,$/', '', $line_parts[1]); |
|
603
|
|
|
|
|
604
|
|
|
$info->globals[] = [ |
|
605
|
|
|
'name' => $line_parts[1], |
|
606
|
|
|
'type' => $line_parts[0], |
|
607
|
|
|
'line_number' => $comment->getLine() + substr_count($comment_text, "\n", 0, $offset), |
|
|
|
|
|
|
608
|
|
|
]; |
|
609
|
|
|
} |
|
610
|
|
|
} else { |
|
611
|
|
|
throw new DocblockParseException('Badly-formatted @param'); |
|
612
|
|
|
} |
|
613
|
|
|
} |
|
614
|
|
|
} |
|
615
|
|
|
|
|
616
|
|
|
if (isset($parsed_docblock->tags['deprecated'])) { |
|
617
|
|
|
$info->deprecated = true; |
|
618
|
|
|
} |
|
619
|
|
|
|
|
620
|
|
|
if (isset($parsed_docblock->tags['internal'])) { |
|
621
|
|
|
$info->internal = true; |
|
622
|
|
|
} |
|
623
|
|
|
|
|
624
|
|
|
if (isset($parsed_docblock->tags['psalm-internal'])) { |
|
625
|
|
|
$psalm_internal = reset($parsed_docblock->tags['psalm-internal']); |
|
626
|
|
|
if ($psalm_internal) { |
|
627
|
|
|
$info->psalm_internal = $psalm_internal; |
|
628
|
|
|
} else { |
|
629
|
|
|
throw new DocblockParseException('@psalm-internal annotation used without specifying namespace'); |
|
630
|
|
|
} |
|
631
|
|
|
$info->psalm_internal = reset($parsed_docblock->tags['psalm-internal']); |
|
632
|
|
|
|
|
633
|
|
|
if (! $info->internal) { |
|
634
|
|
|
throw new DocblockParseException('@psalm-internal annotation used without @internal'); |
|
635
|
|
|
} |
|
636
|
|
|
} |
|
637
|
|
|
|
|
638
|
|
|
if (isset($parsed_docblock->tags['psalm-suppress'])) { |
|
639
|
|
|
foreach ($parsed_docblock->tags['psalm-suppress'] as $offset => $suppress_entry) { |
|
640
|
|
|
$info->suppressed_issues[$offset + $comment->getFilePos()] = preg_split('/[\s]+/', $suppress_entry)[0]; |
|
|
|
|
|
|
641
|
|
|
} |
|
642
|
|
|
} |
|
643
|
|
|
|
|
644
|
|
|
if (isset($parsed_docblock->tags['throws'])) { |
|
645
|
|
|
foreach ($parsed_docblock->tags['throws'] as $offset => $throws_entry) { |
|
646
|
|
|
$throws_class = preg_split('/[\s]+/', $throws_entry)[0]; |
|
647
|
|
|
|
|
648
|
|
|
if (!$throws_class) { |
|
649
|
|
|
throw new IncorrectDocblockException('Unexpectedly empty @throws'); |
|
650
|
|
|
} |
|
651
|
|
|
|
|
652
|
|
|
$info->throws[] = [ |
|
653
|
|
|
$throws_class, |
|
654
|
|
|
$offset + $comment->getFilePos(), |
|
|
|
|
|
|
655
|
|
|
$comment->getLine() + substr_count($comment->getText(), "\n", 0, $offset) |
|
|
|
|
|
|
656
|
|
|
]; |
|
657
|
|
|
} |
|
658
|
|
|
} |
|
659
|
|
|
|
|
660
|
|
|
if (strpos(strtolower($parsed_docblock->description), '@inheritdoc') !== false |
|
661
|
|
|
|| isset($parsed_docblock->tags['inheritdoc']) |
|
662
|
|
|
|| isset($parsed_docblock->tags['inheritDoc']) |
|
663
|
|
|
) { |
|
664
|
|
|
$info->inheritdoc = true; |
|
665
|
|
|
} |
|
666
|
|
|
|
|
667
|
|
|
if (isset($parsed_docblock->combined_tags['template'])) { |
|
668
|
|
|
foreach ($parsed_docblock->combined_tags['template'] as $template_line) { |
|
669
|
|
|
$template_type = preg_split('/[\s]+/', preg_replace('@^[ \t]*\*@m', '', $template_line)); |
|
670
|
|
|
|
|
671
|
|
|
$template_name = array_shift($template_type); |
|
672
|
|
|
|
|
673
|
|
|
if (!$template_name) { |
|
674
|
|
|
throw new IncorrectDocblockException('Empty @template tag'); |
|
675
|
|
|
} |
|
676
|
|
|
|
|
677
|
|
|
if (count($template_type) > 1 |
|
678
|
|
|
&& in_array(strtolower($template_type[0]), ['as', 'super', 'of'], true) |
|
679
|
|
|
) { |
|
680
|
|
|
$template_modifier = strtolower(array_shift($template_type)); |
|
681
|
|
|
$info->templates[] = [ |
|
682
|
|
|
$template_name, |
|
683
|
|
|
$template_modifier, |
|
684
|
|
|
implode(' ', $template_type), |
|
685
|
|
|
false |
|
686
|
|
|
]; |
|
687
|
|
|
} else { |
|
688
|
|
|
$info->templates[] = [$template_name, null, null, false]; |
|
689
|
|
|
} |
|
690
|
|
|
} |
|
691
|
|
|
} |
|
692
|
|
|
|
|
693
|
|
|
if (isset($parsed_docblock->tags['template-typeof'])) { |
|
694
|
|
|
foreach ($parsed_docblock->tags['template-typeof'] as $template_typeof) { |
|
695
|
|
|
$typeof_parts = preg_split('/[\s]+/', preg_replace('@^[ \t]*\*@m', '', $template_typeof)); |
|
696
|
|
|
|
|
697
|
|
|
if ($typeof_parts === false || count($typeof_parts) < 2 || $typeof_parts[1][0] !== '$') { |
|
698
|
|
|
throw new IncorrectDocblockException('Misplaced variable'); |
|
699
|
|
|
} |
|
700
|
|
|
|
|
701
|
|
|
$info->template_typeofs[] = [ |
|
702
|
|
|
'template_type' => $typeof_parts[0], |
|
703
|
|
|
'param_name' => substr($typeof_parts[1], 1), |
|
704
|
|
|
]; |
|
705
|
|
|
} |
|
706
|
|
|
} |
|
707
|
|
|
|
|
708
|
|
|
if (isset($parsed_docblock->tags['psalm-assert'])) { |
|
709
|
|
|
foreach ($parsed_docblock->tags['psalm-assert'] as $assertion) { |
|
710
|
|
|
$line_parts = self::splitDocLine($assertion); |
|
711
|
|
|
|
|
712
|
|
|
if (count($line_parts) < 2 || $line_parts[1][0] !== '$') { |
|
713
|
|
|
throw new IncorrectDocblockException('Misplaced variable'); |
|
714
|
|
|
} |
|
715
|
|
|
|
|
716
|
|
|
$line_parts[0] = self::sanitizeDocblockType($line_parts[0]); |
|
717
|
|
|
|
|
718
|
|
|
$info->assertions[] = [ |
|
719
|
|
|
'type' => $line_parts[0], |
|
720
|
|
|
'param_name' => substr($line_parts[1], 1), |
|
721
|
|
|
]; |
|
722
|
|
|
} |
|
723
|
|
|
} |
|
724
|
|
|
|
|
725
|
|
|
if (isset($parsed_docblock->tags['psalm-assert-if-true'])) { |
|
726
|
|
|
foreach ($parsed_docblock->tags['psalm-assert-if-true'] as $assertion) { |
|
727
|
|
|
$line_parts = self::splitDocLine($assertion); |
|
728
|
|
|
|
|
729
|
|
|
if (count($line_parts) < 2 || $line_parts[1][0] !== '$') { |
|
730
|
|
|
throw new IncorrectDocblockException('Misplaced variable'); |
|
731
|
|
|
} |
|
732
|
|
|
|
|
733
|
|
|
$info->if_true_assertions[] = [ |
|
734
|
|
|
'type' => $line_parts[0], |
|
735
|
|
|
'param_name' => substr($line_parts[1], 1), |
|
736
|
|
|
]; |
|
737
|
|
|
} |
|
738
|
|
|
} |
|
739
|
|
|
|
|
740
|
|
|
if (isset($parsed_docblock->tags['psalm-assert-if-false'])) { |
|
741
|
|
|
foreach ($parsed_docblock->tags['psalm-assert-if-false'] as $assertion) { |
|
742
|
|
|
$line_parts = self::splitDocLine($assertion); |
|
743
|
|
|
|
|
744
|
|
|
if (count($line_parts) < 2 || $line_parts[1][0] !== '$') { |
|
745
|
|
|
throw new IncorrectDocblockException('Misplaced variable'); |
|
746
|
|
|
} |
|
747
|
|
|
|
|
748
|
|
|
$info->if_false_assertions[] = [ |
|
749
|
|
|
'type' => $line_parts[0], |
|
750
|
|
|
'param_name' => substr($line_parts[1], 1), |
|
751
|
|
|
]; |
|
752
|
|
|
} |
|
753
|
|
|
} |
|
754
|
|
|
|
|
755
|
|
|
$info->variadic = isset($parsed_docblock->tags['psalm-variadic']); |
|
756
|
|
|
$info->pure = isset($parsed_docblock->tags['psalm-pure']) |
|
757
|
|
|
|| isset($parsed_docblock->tags['pure']); |
|
758
|
|
|
|
|
759
|
|
|
if (isset($parsed_docblock->tags['psalm-mutation-free'])) { |
|
760
|
|
|
$info->mutation_free = true; |
|
761
|
|
|
} |
|
762
|
|
|
|
|
763
|
|
|
if (isset($parsed_docblock->tags['psalm-external-mutation-free'])) { |
|
764
|
|
|
$info->external_mutation_free = true; |
|
765
|
|
|
} |
|
766
|
|
|
|
|
767
|
|
|
$info->ignore_nullable_return = isset($parsed_docblock->tags['psalm-ignore-nullable-return']); |
|
768
|
|
|
$info->ignore_falsable_return = isset($parsed_docblock->tags['psalm-ignore-falsable-return']); |
|
769
|
|
|
|
|
770
|
|
|
return $info; |
|
771
|
|
|
} |
|
772
|
|
|
|
|
773
|
|
|
/** |
|
774
|
|
|
* @param array<int, string> $return_specials |
|
775
|
|
|
* @return void |
|
776
|
|
|
*/ |
|
777
|
|
|
private static function extractReturnType( |
|
778
|
|
|
PhpParser\Comment\Doc $comment, |
|
779
|
|
|
array $return_specials, |
|
780
|
|
|
FunctionDocblockComment $info |
|
781
|
|
|
) { |
|
782
|
|
|
foreach ($return_specials as $offset => $return_block) { |
|
783
|
|
|
$return_lines = explode("\n", $return_block); |
|
784
|
|
|
|
|
785
|
|
|
if (!trim($return_lines[0])) { |
|
786
|
|
|
return; |
|
787
|
|
|
} |
|
788
|
|
|
|
|
789
|
|
|
$return_block = trim($return_block); |
|
790
|
|
|
|
|
791
|
|
|
if (!$return_block) { |
|
792
|
|
|
return; |
|
793
|
|
|
} |
|
794
|
|
|
|
|
795
|
|
|
$line_parts = self::splitDocLine($return_block); |
|
796
|
|
|
|
|
797
|
|
|
if ($line_parts[0][0] !== '{') { |
|
798
|
|
|
if ($line_parts[0][0] === '$' && !preg_match('/^\$this(\||$)/', $line_parts[0])) { |
|
799
|
|
|
throw new IncorrectDocblockException('Misplaced variable'); |
|
800
|
|
|
} |
|
801
|
|
|
|
|
802
|
|
|
$start = $offset + $comment->getFilePos(); |
|
|
|
|
|
|
803
|
|
|
$end = $start + strlen($line_parts[0]); |
|
804
|
|
|
|
|
805
|
|
|
$line_parts[0] = self::sanitizeDocblockType($line_parts[0]); |
|
806
|
|
|
|
|
807
|
|
|
$info->return_type = array_shift($line_parts); |
|
808
|
|
|
$info->return_type_description = $line_parts ? implode(' ', $line_parts) : null; |
|
809
|
|
|
|
|
810
|
|
|
$info->return_type_line_number |
|
811
|
|
|
= $comment->getLine() + substr_count($comment->getText(), "\n", 0, $offset); |
|
|
|
|
|
|
812
|
|
|
$info->return_type_start = $start; |
|
|
|
|
|
|
813
|
|
|
$info->return_type_end = $end; |
|
|
|
|
|
|
814
|
|
|
} else { |
|
815
|
|
|
throw new DocblockParseException('Badly-formatted @return type'); |
|
816
|
|
|
} |
|
817
|
|
|
|
|
818
|
|
|
break; |
|
819
|
|
|
} |
|
820
|
|
|
} |
|
821
|
|
|
|
|
822
|
|
|
/** |
|
823
|
|
|
* @throws DocblockParseException if there was a problem parsing the docblock |
|
824
|
|
|
* |
|
825
|
|
|
* @return ClassLikeDocblockComment |
|
826
|
|
|
* @psalm-suppress MixedArrayAccess |
|
827
|
|
|
*/ |
|
828
|
|
|
public static function extractClassLikeDocblockInfo( |
|
829
|
|
|
\PhpParser\Node $node, |
|
830
|
|
|
PhpParser\Comment\Doc $comment, |
|
831
|
|
|
Aliases $aliases |
|
832
|
|
|
) { |
|
833
|
|
|
$parsed_docblock = DocComment::parsePreservingLength($comment); |
|
834
|
|
|
$codebase = ProjectAnalyzer::getInstance()->getCodebase(); |
|
835
|
|
|
|
|
836
|
|
|
$info = new ClassLikeDocblockComment(); |
|
837
|
|
|
|
|
838
|
|
|
if (isset($parsed_docblock->combined_tags['template'])) { |
|
839
|
|
|
foreach ($parsed_docblock->combined_tags['template'] as $offset => $template_line) { |
|
840
|
|
|
$template_type = preg_split('/[\s]+/', preg_replace('@^[ \t]*\*@m', '', $template_line)); |
|
841
|
|
|
|
|
842
|
|
|
$template_name = array_shift($template_type); |
|
843
|
|
|
|
|
844
|
|
|
if (!$template_name) { |
|
845
|
|
|
throw new IncorrectDocblockException('Empty @template tag'); |
|
846
|
|
|
} |
|
847
|
|
|
|
|
848
|
|
|
if (count($template_type) > 1 |
|
849
|
|
|
&& in_array(strtolower($template_type[0]), ['as', 'super', 'of'], true) |
|
850
|
|
|
) { |
|
851
|
|
|
$template_modifier = strtolower(array_shift($template_type)); |
|
852
|
|
|
$info->templates[] = [ |
|
853
|
|
|
$template_name, |
|
854
|
|
|
$template_modifier, |
|
855
|
|
|
implode(' ', $template_type), |
|
856
|
|
|
false, |
|
857
|
|
|
$offset |
|
858
|
|
|
]; |
|
859
|
|
|
} else { |
|
860
|
|
|
$info->templates[] = [$template_name, null, null, false, $offset]; |
|
861
|
|
|
} |
|
862
|
|
|
} |
|
863
|
|
|
} |
|
864
|
|
|
|
|
865
|
|
|
if (isset($parsed_docblock->combined_tags['template-covariant'])) { |
|
866
|
|
|
foreach ($parsed_docblock->combined_tags['template-covariant'] as $offset => $template_line) { |
|
867
|
|
|
$template_type = preg_split('/[\s]+/', preg_replace('@^[ \t]*\*@m', '', $template_line)); |
|
868
|
|
|
|
|
869
|
|
|
$template_name = array_shift($template_type); |
|
870
|
|
|
|
|
871
|
|
|
if (!$template_name) { |
|
872
|
|
|
throw new IncorrectDocblockException('Empty @template-covariant tag'); |
|
873
|
|
|
} |
|
874
|
|
|
|
|
875
|
|
|
if (count($template_type) > 1 |
|
876
|
|
|
&& in_array(strtolower($template_type[0]), ['as', 'super', 'of'], true) |
|
877
|
|
|
) { |
|
878
|
|
|
$template_modifier = strtolower(array_shift($template_type)); |
|
879
|
|
|
$info->templates[] = [ |
|
880
|
|
|
$template_name, |
|
881
|
|
|
$template_modifier, |
|
882
|
|
|
implode(' ', $template_type), |
|
883
|
|
|
true, |
|
884
|
|
|
$offset |
|
885
|
|
|
]; |
|
886
|
|
|
} else { |
|
887
|
|
|
$info->templates[] = [$template_name, null, null, true, $offset]; |
|
888
|
|
|
} |
|
889
|
|
|
} |
|
890
|
|
|
} |
|
891
|
|
|
|
|
892
|
|
|
if (isset($parsed_docblock->combined_tags['extends'])) { |
|
893
|
|
|
foreach ($parsed_docblock->combined_tags['extends'] as $template_line) { |
|
894
|
|
|
$info->template_extends[] = trim(preg_replace('@^[ \t]*\*@m', '', $template_line)); |
|
895
|
|
|
} |
|
896
|
|
|
} |
|
897
|
|
|
|
|
898
|
|
|
if (isset($parsed_docblock->combined_tags['implements'])) { |
|
899
|
|
|
foreach ($parsed_docblock->combined_tags['implements'] as $template_line) { |
|
900
|
|
|
$info->template_implements[] = trim(preg_replace('@^[ \t]*\*@m', '', $template_line)); |
|
901
|
|
|
} |
|
902
|
|
|
} |
|
903
|
|
|
|
|
904
|
|
|
if (isset($parsed_docblock->tags['psalm-yield']) |
|
905
|
|
|
) { |
|
906
|
|
|
$yield = reset($parsed_docblock->tags['psalm-yield']); |
|
907
|
|
|
|
|
908
|
|
|
$info->yield = trim(preg_replace('@^[ \t]*\*@m', '', $yield)); |
|
909
|
|
|
} |
|
910
|
|
|
|
|
911
|
|
|
if (isset($parsed_docblock->tags['deprecated'])) { |
|
912
|
|
|
$info->deprecated = true; |
|
913
|
|
|
} |
|
914
|
|
|
|
|
915
|
|
|
if (isset($parsed_docblock->tags['internal'])) { |
|
916
|
|
|
$info->internal = true; |
|
917
|
|
|
} |
|
918
|
|
|
|
|
919
|
|
|
if (isset($parsed_docblock->tags['final'])) { |
|
920
|
|
|
$info->final = true; |
|
921
|
|
|
} |
|
922
|
|
|
|
|
923
|
|
|
if (isset($parsed_docblock->tags['psalm-internal'])) { |
|
924
|
|
|
$psalm_internal = reset($parsed_docblock->tags['psalm-internal']); |
|
925
|
|
|
if ($psalm_internal) { |
|
926
|
|
|
$info->psalm_internal = $psalm_internal; |
|
927
|
|
|
} else { |
|
928
|
|
|
throw new DocblockParseException('psalm-internal annotation used without specifying namespace'); |
|
929
|
|
|
} |
|
930
|
|
|
|
|
931
|
|
|
if (! $info->internal) { |
|
932
|
|
|
throw new DocblockParseException('@psalm-internal annotation used without @internal'); |
|
933
|
|
|
} |
|
934
|
|
|
} |
|
935
|
|
|
|
|
936
|
|
|
if (isset($parsed_docblock->tags['mixin'])) { |
|
937
|
|
|
$mixin = trim(reset($parsed_docblock->tags['mixin'])); |
|
938
|
|
|
$doc_line_parts = self::splitDocLine($mixin); |
|
939
|
|
|
$mixin = $doc_line_parts[0]; |
|
940
|
|
|
|
|
941
|
|
|
if ($mixin) { |
|
942
|
|
|
$info->mixin = $mixin; |
|
943
|
|
|
} else { |
|
944
|
|
|
throw new DocblockParseException('@mixin annotation used without specifying class'); |
|
945
|
|
|
} |
|
946
|
|
|
} |
|
947
|
|
|
|
|
948
|
|
|
if (isset($parsed_docblock->tags['psalm-seal-properties'])) { |
|
949
|
|
|
$info->sealed_properties = true; |
|
950
|
|
|
} |
|
951
|
|
|
|
|
952
|
|
|
if (isset($parsed_docblock->tags['psalm-seal-methods'])) { |
|
953
|
|
|
$info->sealed_methods = true; |
|
954
|
|
|
} |
|
955
|
|
|
|
|
956
|
|
|
if (isset($parsed_docblock->tags['psalm-immutable']) |
|
957
|
|
|
|| isset($parsed_docblock->tags['psalm-mutation-free']) |
|
958
|
|
|
) { |
|
959
|
|
|
$info->mutation_free = true; |
|
960
|
|
|
$info->external_mutation_free = true; |
|
961
|
|
|
$info->taint_specialize = true; |
|
962
|
|
|
} |
|
963
|
|
|
|
|
964
|
|
|
if (isset($parsed_docblock->tags['psalm-external-mutation-free'])) { |
|
965
|
|
|
$info->external_mutation_free = true; |
|
966
|
|
|
} |
|
967
|
|
|
|
|
968
|
|
|
if (isset($parsed_docblock->tags['psalm-taint-specialize'])) { |
|
969
|
|
|
$info->taint_specialize = true; |
|
970
|
|
|
} |
|
971
|
|
|
|
|
972
|
|
|
if (isset($parsed_docblock->tags['psalm-override-property-visibility'])) { |
|
973
|
|
|
$info->override_property_visibility = true; |
|
974
|
|
|
} |
|
975
|
|
|
|
|
976
|
|
|
if (isset($parsed_docblock->tags['psalm-override-method-visibility'])) { |
|
977
|
|
|
$info->override_method_visibility = true; |
|
978
|
|
|
} |
|
979
|
|
|
|
|
980
|
|
|
if (isset($parsed_docblock->tags['psalm-suppress'])) { |
|
981
|
|
|
foreach ($parsed_docblock->tags['psalm-suppress'] as $offset => $suppress_entry) { |
|
982
|
|
|
$info->suppressed_issues[$offset + $comment->getFilePos()] = preg_split('/[\s]+/', $suppress_entry)[0]; |
|
|
|
|
|
|
983
|
|
|
} |
|
984
|
|
|
} |
|
985
|
|
|
|
|
986
|
|
|
if (isset($parsed_docblock->tags['psalm-import-type'])) { |
|
987
|
|
|
foreach ($parsed_docblock->tags['psalm-import-type'] as $imported_type_entry) { |
|
988
|
|
|
/** @psalm-suppress InvalidPropertyAssignmentValue */ |
|
989
|
|
|
$info->imported_types[] = preg_split('/[\s]+/', $imported_type_entry); |
|
990
|
|
|
} |
|
991
|
|
|
} |
|
992
|
|
|
|
|
993
|
|
|
if (isset($parsed_docblock->combined_tags['method'])) { |
|
994
|
|
|
foreach ($parsed_docblock->combined_tags['method'] as $offset => $method_entry) { |
|
995
|
|
|
$method_entry = preg_replace('/[ \t]+/', ' ', trim($method_entry)); |
|
996
|
|
|
|
|
997
|
|
|
$docblock_lines = []; |
|
998
|
|
|
|
|
999
|
|
|
$is_static = false; |
|
1000
|
|
|
|
|
1001
|
|
|
$has_return = false; |
|
1002
|
|
|
|
|
1003
|
|
|
if (!preg_match('/^([a-z_A-Z][a-z_0-9A-Z]+) *\(/', $method_entry, $matches)) { |
|
1004
|
|
|
$doc_line_parts = self::splitDocLine($method_entry); |
|
1005
|
|
|
|
|
1006
|
|
|
if ($doc_line_parts[0] === 'static' && !strpos($doc_line_parts[1], '(')) { |
|
1007
|
|
|
$is_static = true; |
|
1008
|
|
|
array_shift($doc_line_parts); |
|
1009
|
|
|
} |
|
1010
|
|
|
|
|
1011
|
|
|
if (count($doc_line_parts) > 1) { |
|
1012
|
|
|
$docblock_lines[] = '@return ' . array_shift($doc_line_parts); |
|
1013
|
|
|
$has_return = true; |
|
1014
|
|
|
|
|
1015
|
|
|
$method_entry = implode(' ', $doc_line_parts); |
|
1016
|
|
|
} |
|
1017
|
|
|
} |
|
1018
|
|
|
|
|
1019
|
|
|
$method_entry = trim(preg_replace('/\/\/.*/', '', $method_entry)); |
|
1020
|
|
|
|
|
1021
|
|
|
$method_entry = preg_replace( |
|
1022
|
|
|
'/array\(([0-9a-zA-Z_\'\" ]+,)*([0-9a-zA-Z_\'\" ]+)\)/', |
|
1023
|
|
|
'[]', |
|
1024
|
|
|
$method_entry |
|
1025
|
|
|
); |
|
1026
|
|
|
|
|
1027
|
|
|
$end_of_method_regex = '/(?<!array\()\) ?(\: ?(\??[\\\\a-zA-Z0-9_]+))?/'; |
|
1028
|
|
|
|
|
1029
|
|
|
if (preg_match($end_of_method_regex, $method_entry, $matches, PREG_OFFSET_CAPTURE)) { |
|
1030
|
|
|
$method_entry = substr($method_entry, 0, (int) $matches[0][1] + strlen((string) $matches[0][0])); |
|
1031
|
|
|
} |
|
1032
|
|
|
|
|
1033
|
|
|
$method_entry = str_replace([', ', '( '], [',', '('], $method_entry); |
|
1034
|
|
|
$method_entry = preg_replace('/ (?!(\$|\.\.\.|&))/', '', trim($method_entry)); |
|
1035
|
|
|
|
|
1036
|
|
|
// replace array bracket contents |
|
1037
|
|
|
$method_entry = preg_replace('/\[([0-9a-zA-Z_\'\" ]+,)*([0-9a-zA-Z_\'\" ]+)\]/', '[]', $method_entry); |
|
1038
|
|
|
|
|
1039
|
|
|
if (!$method_entry) { |
|
1040
|
|
|
throw new DocblockParseException('No @method entry specified'); |
|
1041
|
|
|
} |
|
1042
|
|
|
|
|
1043
|
|
|
try { |
|
1044
|
|
|
$parse_tree_creator = new ParseTreeCreator( |
|
1045
|
|
|
TypeTokenizer::getFullyQualifiedTokens( |
|
1046
|
|
|
$method_entry, |
|
1047
|
|
|
$aliases, |
|
1048
|
|
|
null |
|
1049
|
|
|
) |
|
1050
|
|
|
); |
|
1051
|
|
|
|
|
1052
|
|
|
$method_tree = $parse_tree_creator->create(); |
|
1053
|
|
|
} catch (TypeParseTreeException $e) { |
|
1054
|
|
|
throw new DocblockParseException($method_entry . ' is not a valid method'); |
|
1055
|
|
|
} |
|
1056
|
|
|
|
|
1057
|
|
|
if (!$method_tree instanceof ParseTree\MethodWithReturnTypeTree |
|
1058
|
|
|
&& !$method_tree instanceof ParseTree\MethodTree) { |
|
1059
|
|
|
throw new DocblockParseException($method_entry . ' is not a valid method'); |
|
1060
|
|
|
} |
|
1061
|
|
|
|
|
1062
|
|
|
if ($method_tree instanceof ParseTree\MethodWithReturnTypeTree) { |
|
1063
|
|
|
if (!$has_return) { |
|
1064
|
|
|
$docblock_lines[] = '@return ' . TypeParser::getTypeFromTree( |
|
1065
|
|
|
$method_tree->children[1], |
|
1066
|
|
|
$codebase |
|
1067
|
|
|
)->toNamespacedString($aliases->namespace, $aliases->uses, null, false); |
|
1068
|
|
|
} |
|
1069
|
|
|
|
|
1070
|
|
|
$method_tree = $method_tree->children[0]; |
|
1071
|
|
|
} |
|
1072
|
|
|
|
|
1073
|
|
|
if (!$method_tree instanceof ParseTree\MethodTree) { |
|
1074
|
|
|
throw new DocblockParseException($method_entry . ' is not a valid method'); |
|
1075
|
|
|
} |
|
1076
|
|
|
|
|
1077
|
|
|
$args = []; |
|
1078
|
|
|
|
|
1079
|
|
|
foreach ($method_tree->children as $method_tree_child) { |
|
1080
|
|
|
if (!$method_tree_child instanceof ParseTree\MethodParamTree) { |
|
1081
|
|
|
throw new DocblockParseException($method_entry . ' is not a valid method'); |
|
1082
|
|
|
} |
|
1083
|
|
|
|
|
1084
|
|
|
$args[] = ($method_tree_child->byref ? '&' : '') |
|
1085
|
|
|
. ($method_tree_child->variadic ? '...' : '') |
|
1086
|
|
|
. $method_tree_child->name |
|
1087
|
|
|
. ($method_tree_child->default != '' ? ' = ' . $method_tree_child->default : ''); |
|
1088
|
|
|
|
|
1089
|
|
|
|
|
1090
|
|
|
if ($method_tree_child->children) { |
|
1091
|
|
|
try { |
|
1092
|
|
|
$param_type = TypeParser::getTypeFromTree($method_tree_child->children[0], $codebase); |
|
1093
|
|
|
} catch (\Exception $e) { |
|
1094
|
|
|
throw new DocblockParseException( |
|
1095
|
|
|
'Badly-formatted @method string ' . $method_entry . ' - ' . $e |
|
1096
|
|
|
); |
|
1097
|
|
|
} |
|
1098
|
|
|
$docblock_lines[] = '@param \\' . $param_type . ' ' |
|
1099
|
|
|
. ($method_tree_child->variadic ? '...' : '') |
|
1100
|
|
|
. $method_tree_child->name; |
|
1101
|
|
|
} |
|
1102
|
|
|
} |
|
1103
|
|
|
|
|
1104
|
|
|
$function_string = 'function ' . $method_tree->value . '(' . implode(', ', $args) . ')'; |
|
1105
|
|
|
|
|
1106
|
|
|
if ($is_static) { |
|
1107
|
|
|
$function_string = 'static ' . $function_string; |
|
1108
|
|
|
} |
|
1109
|
|
|
|
|
1110
|
|
|
$function_docblock = $docblock_lines ? "/**\n * " . implode("\n * ", $docblock_lines) . "\n*/\n" : ""; |
|
1111
|
|
|
|
|
1112
|
|
|
$php_string = '<?php class A { ' . $function_docblock . ' public ' . $function_string . '{} }'; |
|
1113
|
|
|
|
|
1114
|
|
|
try { |
|
1115
|
|
|
$statements = \Psalm\Internal\Provider\StatementsProvider::parseStatements($php_string); |
|
1116
|
|
|
} catch (\Exception $e) { |
|
1117
|
|
|
throw new DocblockParseException('Badly-formatted @method string ' . $method_entry); |
|
1118
|
|
|
} |
|
1119
|
|
|
|
|
1120
|
|
|
if (!$statements |
|
1121
|
|
|
|| !$statements[0] instanceof \PhpParser\Node\Stmt\Class_ |
|
1122
|
|
|
|| !isset($statements[0]->stmts[0]) |
|
1123
|
|
|
|| !$statements[0]->stmts[0] instanceof \PhpParser\Node\Stmt\ClassMethod |
|
1124
|
|
|
) { |
|
1125
|
|
|
throw new DocblockParseException('Badly-formatted @method string ' . $method_entry); |
|
1126
|
|
|
} |
|
1127
|
|
|
|
|
1128
|
|
|
/** @var \PhpParser\Comment\Doc */ |
|
1129
|
|
|
$node_doc_comment = $node->getDocComment(); |
|
1130
|
|
|
|
|
1131
|
|
|
$statements[0]->stmts[0]->setAttribute('startLine', $node_doc_comment->getLine()); |
|
1132
|
|
|
$statements[0]->stmts[0]->setAttribute('startFilePos', $node_doc_comment->getFilePos()); |
|
1133
|
|
|
$statements[0]->stmts[0]->setAttribute('endFilePos', $node->getAttribute('startFilePos')); |
|
1134
|
|
|
|
|
1135
|
|
|
if ($doc_comment = $statements[0]->stmts[0]->getDocComment()) { |
|
1136
|
|
|
$statements[0]->stmts[0]->setDocComment( |
|
1137
|
|
|
new \PhpParser\Comment\Doc( |
|
1138
|
|
|
$doc_comment->getText(), |
|
1139
|
|
|
$comment->getLine() + substr_count($comment->getText(), "\n", 0, $offset), |
|
|
|
|
|
|
1140
|
|
|
$node_doc_comment->getFilePos() |
|
1141
|
|
|
) |
|
1142
|
|
|
); |
|
1143
|
|
|
} |
|
1144
|
|
|
|
|
1145
|
|
|
$info->methods[] = $statements[0]->stmts[0]; |
|
1146
|
|
|
} |
|
1147
|
|
|
} |
|
1148
|
|
|
|
|
1149
|
|
|
self::addMagicPropertyToInfo($comment, $info, $parsed_docblock->tags, 'property'); |
|
1150
|
|
|
self::addMagicPropertyToInfo($comment, $info, $parsed_docblock->tags, 'psalm-property'); |
|
1151
|
|
|
self::addMagicPropertyToInfo($comment, $info, $parsed_docblock->tags, 'property-read'); |
|
1152
|
|
|
self::addMagicPropertyToInfo($comment, $info, $parsed_docblock->tags, 'psalm-property-read'); |
|
1153
|
|
|
self::addMagicPropertyToInfo($comment, $info, $parsed_docblock->tags, 'property-write'); |
|
1154
|
|
|
self::addMagicPropertyToInfo($comment, $info, $parsed_docblock->tags, 'psalm-property-write'); |
|
1155
|
|
|
|
|
1156
|
|
|
return $info; |
|
1157
|
|
|
} |
|
1158
|
|
|
|
|
1159
|
|
|
/** |
|
1160
|
|
|
* @param ClassLikeDocblockComment $info |
|
1161
|
|
|
* @param array<string, array<int, string>> $specials |
|
1162
|
|
|
* @param 'property'|'psalm-property'|'property-read'| |
|
1163
|
|
|
* 'psalm-property-read'|'property-write'|'psalm-property-write' $property_tag |
|
1164
|
|
|
* |
|
1165
|
|
|
* @throws DocblockParseException |
|
1166
|
|
|
* |
|
1167
|
|
|
* @return void |
|
1168
|
|
|
*/ |
|
1169
|
|
|
protected static function addMagicPropertyToInfo( |
|
1170
|
|
|
PhpParser\Comment\Doc $comment, |
|
1171
|
|
|
ClassLikeDocblockComment $info, |
|
1172
|
|
|
array $specials, |
|
1173
|
|
|
string $property_tag |
|
1174
|
|
|
) : void { |
|
1175
|
|
|
$magic_property_comments = isset($specials[$property_tag]) ? $specials[$property_tag] : []; |
|
1176
|
|
|
|
|
1177
|
|
|
foreach ($magic_property_comments as $offset => $property) { |
|
1178
|
|
|
$line_parts = self::splitDocLine($property); |
|
1179
|
|
|
|
|
1180
|
|
|
if (count($line_parts) === 1 && isset($line_parts[0][0]) && $line_parts[0][0] === '$') { |
|
1181
|
|
|
continue; |
|
1182
|
|
|
} |
|
1183
|
|
|
|
|
1184
|
|
|
if (count($line_parts) > 1) { |
|
1185
|
|
|
if (preg_match('/^&?\$[A-Za-z0-9_]+,?$/', $line_parts[1]) |
|
1186
|
|
|
&& $line_parts[0][0] !== '{' |
|
1187
|
|
|
) { |
|
1188
|
|
|
$line_parts[1] = str_replace('&', '', $line_parts[1]); |
|
1189
|
|
|
|
|
1190
|
|
|
$line_parts[1] = preg_replace('/,$/', '', $line_parts[1]); |
|
1191
|
|
|
|
|
1192
|
|
|
$start = $offset + $comment->getFilePos(); |
|
|
|
|
|
|
1193
|
|
|
$end = $start + strlen($line_parts[0]); |
|
1194
|
|
|
|
|
1195
|
|
|
$line_parts[0] = str_replace("\n", '', preg_replace('@^[ \t]*\*@m', '', $line_parts[0])); |
|
1196
|
|
|
|
|
1197
|
|
|
if ($line_parts[0] === '' |
|
1198
|
|
|
|| ($line_parts[0][0] === '$' |
|
1199
|
|
|
&& !preg_match('/^\$this(\||$)/', $line_parts[0])) |
|
1200
|
|
|
) { |
|
1201
|
|
|
throw new IncorrectDocblockException('Misplaced variable'); |
|
1202
|
|
|
} |
|
1203
|
|
|
|
|
1204
|
|
|
$name = trim($line_parts[1]); |
|
1205
|
|
|
|
|
1206
|
|
|
if (!preg_match('/^\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$/', $name)) { |
|
1207
|
|
|
throw new DocblockParseException('Badly-formatted @property name'); |
|
1208
|
|
|
} |
|
1209
|
|
|
|
|
1210
|
|
|
$info->properties[] = [ |
|
1211
|
|
|
'name' => $name, |
|
1212
|
|
|
'type' => $line_parts[0], |
|
1213
|
|
|
'line_number' => $comment->getLine() + substr_count($comment->getText(), "\n", 0, $offset), |
|
|
|
|
|
|
1214
|
|
|
'tag' => $property_tag, |
|
1215
|
|
|
'start' => $start, |
|
1216
|
|
|
'end' => $end, |
|
1217
|
|
|
]; |
|
1218
|
|
|
} |
|
1219
|
|
|
} else { |
|
1220
|
|
|
throw new DocblockParseException('Badly-formatted @property'); |
|
1221
|
|
|
} |
|
1222
|
|
|
} |
|
1223
|
|
|
} |
|
1224
|
|
|
|
|
1225
|
|
|
/** |
|
1226
|
|
|
* @param string $return_block |
|
1227
|
|
|
* |
|
1228
|
|
|
* @throws DocblockParseException if an invalid string is found |
|
1229
|
|
|
* |
|
1230
|
|
|
* @return array<string> |
|
1231
|
|
|
*/ |
|
1232
|
|
|
public static function splitDocLine($return_block) |
|
1233
|
|
|
{ |
|
1234
|
|
|
$brackets = ''; |
|
1235
|
|
|
|
|
1236
|
|
|
$type = ''; |
|
1237
|
|
|
|
|
1238
|
|
|
$expects_callable_return = false; |
|
1239
|
|
|
|
|
1240
|
|
|
$return_block = str_replace("\t", ' ', $return_block); |
|
1241
|
|
|
|
|
1242
|
|
|
$quote_char = null; |
|
1243
|
|
|
$escaped = false; |
|
1244
|
|
|
|
|
1245
|
|
|
for ($i = 0, $l = strlen($return_block); $i < $l; ++$i) { |
|
1246
|
|
|
$char = $return_block[$i]; |
|
1247
|
|
|
$next_char = $i < $l - 1 ? $return_block[$i + 1] : null; |
|
1248
|
|
|
$last_char = $i > 0 ? $return_block[$i - 1] : null; |
|
1249
|
|
|
|
|
1250
|
|
|
if ($quote_char) { |
|
1251
|
|
|
if ($char === $quote_char && $i > 1 && !$escaped) { |
|
1252
|
|
|
$quote_char = null; |
|
1253
|
|
|
|
|
1254
|
|
|
$type .= $char; |
|
1255
|
|
|
|
|
1256
|
|
|
continue; |
|
1257
|
|
|
} |
|
1258
|
|
|
|
|
1259
|
|
|
if ($char === '\\' && !$escaped && ($next_char === $quote_char || $next_char === '\\')) { |
|
1260
|
|
|
$escaped = true; |
|
1261
|
|
|
|
|
1262
|
|
|
$type .= $char; |
|
1263
|
|
|
|
|
1264
|
|
|
continue; |
|
1265
|
|
|
} |
|
1266
|
|
|
|
|
1267
|
|
|
$escaped = false; |
|
1268
|
|
|
|
|
1269
|
|
|
$type .= $char; |
|
1270
|
|
|
|
|
1271
|
|
|
continue; |
|
1272
|
|
|
} |
|
1273
|
|
|
|
|
1274
|
|
|
if ($char === '"' || $char === '\'') { |
|
1275
|
|
|
$quote_char = $char; |
|
1276
|
|
|
|
|
1277
|
|
|
$type .= $char; |
|
1278
|
|
|
|
|
1279
|
|
|
continue; |
|
1280
|
|
|
} |
|
1281
|
|
|
|
|
1282
|
|
|
if ($char === ':' && $last_char === ')') { |
|
1283
|
|
|
$expects_callable_return = true; |
|
1284
|
|
|
|
|
1285
|
|
|
$type .= $char; |
|
1286
|
|
|
|
|
1287
|
|
|
continue; |
|
1288
|
|
|
} |
|
1289
|
|
|
|
|
1290
|
|
|
if ($char === '[' || $char === '{' || $char === '(' || $char === '<') { |
|
1291
|
|
|
$brackets .= $char; |
|
1292
|
|
|
} elseif ($char === ']' || $char === '}' || $char === ')' || $char === '>') { |
|
1293
|
|
|
$last_bracket = substr($brackets, -1); |
|
1294
|
|
|
$brackets = substr($brackets, 0, -1); |
|
1295
|
|
|
|
|
1296
|
|
|
if (($char === ']' && $last_bracket !== '[') |
|
1297
|
|
|
|| ($char === '}' && $last_bracket !== '{') |
|
1298
|
|
|
|| ($char === ')' && $last_bracket !== '(') |
|
1299
|
|
|
|| ($char === '>' && $last_bracket !== '<') |
|
1300
|
|
|
) { |
|
1301
|
|
|
throw new DocblockParseException('Invalid string ' . $return_block); |
|
1302
|
|
|
} |
|
1303
|
|
|
} elseif ($char === ' ') { |
|
1304
|
|
|
if ($brackets) { |
|
1305
|
|
|
$expects_callable_return = false; |
|
1306
|
|
|
$type .= ' '; |
|
1307
|
|
|
continue; |
|
1308
|
|
|
} |
|
1309
|
|
|
|
|
1310
|
|
|
if ($next_char === '|' || $next_char === '&') { |
|
1311
|
|
|
$nexter_char = $i < $l - 2 ? $return_block[$i + 2] : null; |
|
1312
|
|
|
|
|
1313
|
|
|
if ($nexter_char === ' ') { |
|
1314
|
|
|
++$i; |
|
1315
|
|
|
$type .= $next_char . ' '; |
|
1316
|
|
|
continue; |
|
1317
|
|
|
} |
|
1318
|
|
|
} |
|
1319
|
|
|
|
|
1320
|
|
|
if ($last_char === '|' || $last_char === '&') { |
|
1321
|
|
|
$type .= ' '; |
|
1322
|
|
|
continue; |
|
1323
|
|
|
} |
|
1324
|
|
|
|
|
1325
|
|
|
if ($next_char === ':') { |
|
1326
|
|
|
++$i; |
|
1327
|
|
|
$type .= ' :'; |
|
1328
|
|
|
$expects_callable_return = true; |
|
1329
|
|
|
continue; |
|
1330
|
|
|
} |
|
1331
|
|
|
|
|
1332
|
|
|
if ($expects_callable_return) { |
|
1333
|
|
|
$type .= ' '; |
|
1334
|
|
|
$expects_callable_return = false; |
|
1335
|
|
|
continue; |
|
1336
|
|
|
} |
|
1337
|
|
|
|
|
1338
|
|
|
$remaining = trim(preg_replace('@^[ \t]*\* *@m', ' ', substr($return_block, $i + 1))); |
|
1339
|
|
|
|
|
1340
|
|
|
if ($remaining) { |
|
1341
|
|
|
/** @var array<string> */ |
|
1342
|
|
|
return array_merge([rtrim($type)], preg_split('/[ \s]+/', $remaining)); |
|
1343
|
|
|
} |
|
1344
|
|
|
|
|
1345
|
|
|
return [$type]; |
|
1346
|
|
|
} |
|
1347
|
|
|
|
|
1348
|
|
|
$expects_callable_return = false; |
|
1349
|
|
|
|
|
1350
|
|
|
$type .= $char; |
|
1351
|
|
|
} |
|
1352
|
|
|
|
|
1353
|
|
|
return [$type]; |
|
1354
|
|
|
} |
|
1355
|
|
|
|
|
1356
|
|
|
/** |
|
1357
|
|
|
* @param ParsedDocblock $parsed_docblock |
|
1358
|
|
|
* |
|
1359
|
|
|
* @return void |
|
1360
|
|
|
* |
|
1361
|
|
|
* @throws DocblockParseException if a duplicate is found |
|
1362
|
|
|
*/ |
|
1363
|
|
|
private static function checkDuplicatedTags(ParsedDocblock $parsed_docblock) |
|
1364
|
|
|
{ |
|
1365
|
|
|
if (count($parsed_docblock->tags['return'] ?? []) > 1 |
|
1366
|
|
|
|| count($parsed_docblock->tags['psalm-return'] ?? []) > 1 |
|
1367
|
|
|
|| count($parsed_docblock->tags['phpstan-return'] ?? []) > 1 |
|
1368
|
|
|
) { |
|
1369
|
|
|
throw new DocblockParseException('Found duplicated @return or prefixed @return tag'); |
|
1370
|
|
|
} |
|
1371
|
|
|
|
|
1372
|
|
|
self::checkDuplicatedParams($parsed_docblock->tags['param'] ?? []); |
|
1373
|
|
|
self::checkDuplicatedParams($parsed_docblock->tags['psalm-param'] ?? []); |
|
1374
|
|
|
self::checkDuplicatedParams($parsed_docblock->tags['phpstan-param'] ?? []); |
|
1375
|
|
|
} |
|
1376
|
|
|
|
|
1377
|
|
|
/** |
|
1378
|
|
|
* @param array<int, string> $param |
|
1379
|
|
|
* |
|
1380
|
|
|
* @return void |
|
1381
|
|
|
* |
|
1382
|
|
|
* @throws DocblockParseException if a duplicate is found |
|
1383
|
|
|
*/ |
|
1384
|
|
|
private static function checkDuplicatedParams(array $param) |
|
1385
|
|
|
{ |
|
1386
|
|
|
$list_names = self::extractAllParamNames($param); |
|
1387
|
|
|
|
|
1388
|
|
|
if (count($list_names) !== count(array_unique($list_names))) { |
|
1389
|
|
|
throw new DocblockParseException('Found duplicated @param or prefixed @param tag'); |
|
1390
|
|
|
} |
|
1391
|
|
|
} |
|
1392
|
|
|
|
|
1393
|
|
|
/** |
|
1394
|
|
|
* @param array<int, string> $lines |
|
1395
|
|
|
* |
|
1396
|
|
|
* @return list<string> |
|
|
|
|
|
|
1397
|
|
|
*/ |
|
1398
|
|
|
private static function extractAllParamNames(array $lines) |
|
1399
|
|
|
{ |
|
1400
|
|
|
$names = []; |
|
1401
|
|
|
|
|
1402
|
|
|
foreach ($lines as $line) { |
|
1403
|
|
|
$split_by_dollar = explode('$', $line, 2); |
|
1404
|
|
|
if (count($split_by_dollar) > 1) { |
|
1405
|
|
|
$split_by_space = explode(' ', $split_by_dollar[1], 2); |
|
1406
|
|
|
$names[] = $split_by_space[0]; |
|
1407
|
|
|
} |
|
1408
|
|
|
} |
|
1409
|
|
|
|
|
1410
|
|
|
return $names; |
|
1411
|
|
|
} |
|
1412
|
|
|
} |
|
1413
|
|
|
|
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.