Test Setup Failed
Push — master ( f173ef...619c38 )
by Matthew
05:05
created

PropertyDocblockManipulator::clearCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 5
rs 10
c 0
b 0
f 0
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');
0 ignored issues
show
Deprecated Code introduced by
The method PhpParser\Comment::getFilePos() has been deprecated with message: Use getStartFilePos() instead

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.

Loading history...
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()
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
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>
0 ignored issues
show
Documentation introduced by
The doc-type array<int, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
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