1
|
|
|
<?php |
2
|
|
|
namespace Psalm\Internal\FileManipulation; |
3
|
|
|
|
4
|
|
|
use PhpParser; |
5
|
|
|
use function array_shift; |
6
|
|
|
use function count; |
7
|
|
|
use function ltrim; |
8
|
|
|
use PhpParser\Node\Stmt\Property; |
9
|
|
|
use function preg_match; |
10
|
|
|
use Psalm\DocComment; |
11
|
|
|
use Psalm\FileManipulation; |
12
|
|
|
use Psalm\Internal\Analyzer\CommentAnalyzer; |
13
|
|
|
use Psalm\Internal\Analyzer\ProjectAnalyzer; |
14
|
|
|
use function str_replace; |
15
|
|
|
use function str_split; |
16
|
|
|
use function strlen; |
17
|
|
|
use function strpos; |
18
|
|
|
use function strrpos; |
19
|
|
|
use function substr; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* @internal |
23
|
|
|
*/ |
24
|
|
|
class PropertyDocblockManipulator |
25
|
|
|
{ |
26
|
|
|
/** @var array<string, array<string, self>> */ |
27
|
|
|
private static $manipulators = []; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Manipulators ordered by line number |
31
|
|
|
* |
32
|
|
|
* @var array<string, array<int, self>> |
33
|
|
|
*/ |
34
|
|
|
private static $ordered_manipulators = []; |
35
|
|
|
|
36
|
|
|
/** @var Property */ |
37
|
|
|
private $stmt; |
38
|
|
|
|
39
|
|
|
/** @var int */ |
40
|
|
|
private $docblock_start; |
41
|
|
|
|
42
|
|
|
/** @var int */ |
43
|
|
|
private $docblock_end; |
44
|
|
|
|
45
|
|
|
/** @var null|int */ |
46
|
|
|
private $typehint_start; |
47
|
|
|
|
48
|
|
|
/** @var int */ |
49
|
|
|
private $typehint_area_start; |
50
|
|
|
|
51
|
|
|
/** @var null|int */ |
52
|
|
|
private $typehint_end; |
53
|
|
|
|
54
|
|
|
/** @var null|string */ |
55
|
|
|
private $new_php_type; |
56
|
|
|
|
57
|
|
|
/** @var bool */ |
58
|
|
|
private $type_is_php_compatible = false; |
59
|
|
|
|
60
|
|
|
/** @var null|string */ |
61
|
|
|
private $new_phpdoc_type; |
62
|
|
|
|
63
|
|
|
/** @var null|string */ |
64
|
|
|
private $new_psalm_type; |
65
|
|
|
|
66
|
|
|
/** @var string */ |
67
|
|
|
private $indentation; |
68
|
|
|
|
69
|
|
|
/** @var bool */ |
70
|
|
|
private $add_newline = false; |
71
|
|
|
|
72
|
|
|
/** @var string|null */ |
73
|
|
|
private $type_description; |
74
|
|
|
|
75
|
|
|
public static function getForProperty( |
76
|
|
|
ProjectAnalyzer $project_analyzer, |
77
|
|
|
string $file_path, |
78
|
|
|
string $property_id, |
79
|
|
|
Property $stmt |
80
|
|
|
) : self { |
81
|
|
|
if (isset(self::$manipulators[$file_path][$property_id])) { |
82
|
|
|
return self::$manipulators[$file_path][$property_id]; |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
$manipulator |
86
|
|
|
= self::$manipulators[$file_path][$property_id] |
87
|
|
|
= self::$ordered_manipulators[$file_path][$stmt->getLine()] |
88
|
|
|
= new self($project_analyzer, $stmt, $file_path); |
89
|
|
|
|
90
|
|
|
return $manipulator; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
private function __construct( |
94
|
|
|
ProjectAnalyzer $project_analyzer, |
95
|
|
|
Property $stmt, |
96
|
|
|
string $file_path |
97
|
|
|
) { |
98
|
|
|
$this->stmt = $stmt; |
99
|
|
|
$docblock = $stmt->getDocComment(); |
100
|
|
|
$this->docblock_start = $docblock ? $docblock->getFilePos() : (int)$stmt->getAttribute('startFilePos'); |
|
|
|
|
101
|
|
|
$this->docblock_end = (int)$stmt->getAttribute('startFilePos'); |
102
|
|
|
|
103
|
|
|
$codebase = $project_analyzer->getCodebase(); |
104
|
|
|
|
105
|
|
|
$file_contents = $codebase->getFileContents($file_path); |
106
|
|
|
|
107
|
|
|
if (count($stmt->props) > 1) { |
108
|
|
|
throw new \UnexpectedValueException('Cannot replace multiple properties'); |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
$prop = $stmt->props[0]; |
112
|
|
|
|
113
|
|
|
if ($stmt->type) { |
114
|
|
|
$this->typehint_start = (int)$stmt->type->getAttribute('startFilePos'); |
115
|
|
|
$this->typehint_end = (int)$stmt->type->getAttribute('endFilePos'); |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
$this->typehint_area_start = (int)$prop->getAttribute('startFilePos') - 1; |
119
|
|
|
|
120
|
|
|
$preceding_newline_pos = strrpos($file_contents, "\n", $this->docblock_end - strlen($file_contents)); |
121
|
|
|
|
122
|
|
|
if ($preceding_newline_pos === false) { |
123
|
|
|
$this->indentation = ''; |
124
|
|
|
|
125
|
|
|
return; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
if (!$docblock) { |
129
|
|
|
$preceding_semicolon_pos = strrpos($file_contents, ";", $preceding_newline_pos - strlen($file_contents)); |
130
|
|
|
|
131
|
|
|
if ($preceding_semicolon_pos) { |
132
|
|
|
$preceding_space = substr( |
133
|
|
|
$file_contents, |
134
|
|
|
$preceding_semicolon_pos + 1, |
135
|
|
|
$preceding_newline_pos - $preceding_semicolon_pos - 1 |
136
|
|
|
); |
137
|
|
|
|
138
|
|
|
if (!\substr_count($preceding_space, "\n")) { |
139
|
|
|
$this->add_newline = true; |
140
|
|
|
} |
141
|
|
|
} |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
$first_line = substr($file_contents, $preceding_newline_pos + 1, $this->docblock_end - $preceding_newline_pos); |
145
|
|
|
|
146
|
|
|
$this->indentation = str_replace(ltrim($first_line), '', $first_line); |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
public function setType( |
150
|
|
|
?string $php_type, |
151
|
|
|
string $new_type, |
152
|
|
|
string $phpdoc_type, |
153
|
|
|
bool $is_php_compatible, |
154
|
|
|
?string $description = null |
155
|
|
|
) : void { |
156
|
|
|
$new_type = str_replace(['<mixed, mixed>', '<array-key, mixed>', '<empty, empty>'], '', $new_type); |
157
|
|
|
|
158
|
|
|
$this->new_php_type = $php_type; |
159
|
|
|
$this->new_phpdoc_type = $phpdoc_type; |
160
|
|
|
$this->new_psalm_type = $new_type; |
161
|
|
|
$this->type_is_php_compatible = $is_php_compatible; |
162
|
|
|
$this->type_description = $description; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* Gets a new docblock given the existing docblock, if one exists, and the updated return types |
167
|
|
|
* and/or parameters |
168
|
|
|
* |
169
|
|
|
* @return string |
170
|
|
|
*/ |
171
|
|
|
private function getDocblock() |
|
|
|
|
172
|
|
|
{ |
173
|
|
|
$docblock = $this->stmt->getDocComment(); |
174
|
|
|
|
175
|
|
|
if ($docblock) { |
176
|
|
|
$parsed_docblock = DocComment::parsePreservingLength($docblock); |
177
|
|
|
} else { |
178
|
|
|
$parsed_docblock = new \Psalm\Internal\Scanner\ParsedDocblock('', []); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
$modified_docblock = false; |
182
|
|
|
|
183
|
|
|
$old_phpdoc_type = null; |
184
|
|
|
if (isset($parsed_docblock->tags['var'])) { |
185
|
|
|
$old_phpdoc_type = array_shift($parsed_docblock->tags['var']); |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
if ($this->new_phpdoc_type |
189
|
|
|
&& $this->new_phpdoc_type !== $old_phpdoc_type |
190
|
|
|
) { |
191
|
|
|
$modified_docblock = true; |
192
|
|
|
$parsed_docblock->tags['var'] = [ |
193
|
|
|
$this->new_phpdoc_type |
194
|
|
|
. ($this->type_description ? (' ' . $this->type_description) : ''), |
195
|
|
|
]; |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
$old_psalm_type = null; |
199
|
|
|
if (isset($parsed_docblock->tags['psalm-var'])) { |
200
|
|
|
$old_psalm_type = array_shift($parsed_docblock->tags['psalm-var']); |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
if ($this->new_psalm_type |
204
|
|
|
&& $this->new_phpdoc_type !== $this->new_psalm_type |
205
|
|
|
&& $this->new_psalm_type !== $old_psalm_type |
206
|
|
|
) { |
207
|
|
|
$modified_docblock = true; |
208
|
|
|
$parsed_docblock->tags['psalm-var'] = [$this->new_psalm_type]; |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
if (!$parsed_docblock->tags && !$parsed_docblock->description) { |
212
|
|
|
return ''; |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
if (!$modified_docblock) { |
216
|
|
|
return (string)$docblock . "\n" . $this->indentation; |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
return $parsed_docblock->render($this->indentation); |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* @param string $file_path |
224
|
|
|
* |
225
|
|
|
* @return array<int, FileManipulation> |
|
|
|
|
226
|
|
|
*/ |
227
|
|
|
public static function getManipulationsForFile($file_path) |
228
|
|
|
{ |
229
|
|
|
if (!isset(self::$manipulators[$file_path])) { |
230
|
|
|
return []; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
$file_manipulations = []; |
234
|
|
|
|
235
|
|
|
foreach (self::$ordered_manipulators[$file_path] as $manipulator) { |
236
|
|
|
if ($manipulator->new_php_type) { |
237
|
|
|
if ($manipulator->typehint_start && $manipulator->typehint_end) { |
238
|
|
|
$file_manipulations[$manipulator->typehint_start] = new FileManipulation( |
239
|
|
|
$manipulator->typehint_start, |
240
|
|
|
$manipulator->typehint_end, |
241
|
|
|
$manipulator->new_php_type |
242
|
|
|
); |
243
|
|
|
} else { |
244
|
|
|
$file_manipulations[$manipulator->typehint_area_start] = new FileManipulation( |
245
|
|
|
$manipulator->typehint_area_start, |
246
|
|
|
$manipulator->typehint_area_start, |
247
|
|
|
' ' . $manipulator->new_php_type |
248
|
|
|
); |
249
|
|
|
} |
250
|
|
|
} elseif ($manipulator->new_php_type === '' |
251
|
|
|
&& $manipulator->new_phpdoc_type |
252
|
|
|
&& $manipulator->typehint_start |
253
|
|
|
&& $manipulator->typehint_end |
254
|
|
|
) { |
255
|
|
|
$file_manipulations[$manipulator->typehint_start] = new FileManipulation( |
256
|
|
|
$manipulator->typehint_start, |
257
|
|
|
$manipulator->typehint_end, |
258
|
|
|
'' |
259
|
|
|
); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
if (!$manipulator->new_php_type |
263
|
|
|
|| !$manipulator->type_is_php_compatible |
264
|
|
|
|| $manipulator->docblock_start !== $manipulator->docblock_end |
265
|
|
|
) { |
266
|
|
|
$file_manipulations[$manipulator->docblock_start] = new FileManipulation( |
267
|
|
|
$manipulator->docblock_start |
268
|
|
|
- ($manipulator->add_newline ? strlen($manipulator->indentation) : 0), |
269
|
|
|
$manipulator->docblock_end, |
270
|
|
|
($manipulator->add_newline ? "\n" . $manipulator->indentation : '') |
271
|
|
|
. $manipulator->getDocblock() |
272
|
|
|
); |
273
|
|
|
} |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
return $file_manipulations; |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
/** |
280
|
|
|
* @return void |
281
|
|
|
*/ |
282
|
|
|
public static function clearCache() |
283
|
|
|
{ |
284
|
|
|
self::$manipulators = []; |
285
|
|
|
self::$ordered_manipulators = []; |
286
|
|
|
} |
287
|
|
|
} |
288
|
|
|
|
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.