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.