Test Setup Failed
Push — master ( 0034f2...cf67b9 )
by Matthew
04:46
created

PropertyDocblockManipulator::__construct()   B

Complexity

Conditions 5
Paths 10

Size

Total Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 10
nop 3
dl 0
loc 39
rs 8.9848
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 string|null */
70
    private $type_description;
71
72
    public static function getForProperty(
73
        ProjectAnalyzer $project_analyzer,
74
        string $file_path,
75
        string $property_id,
76
        Property $stmt
77
    ) : self {
78
        if (isset(self::$manipulators[$file_path][$property_id])) {
79
            return self::$manipulators[$file_path][$property_id];
80
        }
81
82
        $manipulator
83
            = self::$manipulators[$file_path][$property_id]
84
            = self::$ordered_manipulators[$file_path][$stmt->getLine()]
85
            = new self($project_analyzer, $stmt, $file_path);
86
87
        return $manipulator;
88
    }
89
90
    private function __construct(
91
        ProjectAnalyzer $project_analyzer,
92
        Property $stmt,
93
        string $file_path
94
    ) {
95
        $this->stmt = $stmt;
96
        $docblock = $stmt->getDocComment();
97
        $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...
98
        $this->docblock_end = (int)$stmt->getAttribute('startFilePos');
99
100
        $codebase = $project_analyzer->getCodebase();
101
102
        $file_contents = $codebase->getFileContents($file_path);
103
104
        if (count($stmt->props) > 1) {
105
            throw new \UnexpectedValueException('Cannot replace multiple properties');
106
        }
107
108
        $prop = $stmt->props[0];
109
110
        if ($stmt->type) {
111
            $this->typehint_start = (int)$stmt->type->getAttribute('startFilePos');
112
            $this->typehint_end = (int)$stmt->type->getAttribute('endFilePos');
113
        }
114
115
        $this->typehint_area_start = (int)$prop->getAttribute('startFilePos') - 1;
116
117
        $preceding_newline_pos = strrpos($file_contents, "\n", $this->docblock_end - strlen($file_contents));
118
119
        if ($preceding_newline_pos === false) {
120
            $this->indentation = '';
121
122
            return;
123
        }
124
125
        $first_line = substr($file_contents, $preceding_newline_pos + 1, $this->docblock_end - $preceding_newline_pos);
126
127
        $this->indentation = str_replace(ltrim($first_line), '', $first_line);
128
    }
129
130
    public function setType(
131
        ?string $php_type,
132
        string $new_type,
133
        string $phpdoc_type,
134
        bool $is_php_compatible,
135
        ?string $description = null
136
    ) : void {
137
        $new_type = str_replace(['<mixed, mixed>', '<array-key, mixed>'], '', $new_type);
138
139
        $this->new_php_type = $php_type;
140
        $this->new_phpdoc_type = $phpdoc_type;
141
        $this->new_psalm_type = $new_type;
142
        $this->type_is_php_compatible = $is_php_compatible;
143
        $this->type_description = $description;
144
    }
145
146
    /**
147
     * Gets a new docblock given the existing docblock, if one exists, and the updated return types
148
     * and/or parameters
149
     *
150
     * @return string
151
     */
152
    private function getDocblock()
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
153
    {
154
        $docblock = $this->stmt->getDocComment();
155
156
        if ($docblock) {
157
            $parsed_docblock = DocComment::parsePreservingLength($docblock);
158
        } else {
159
            $parsed_docblock = new \Psalm\Internal\Scanner\ParsedDocblock('', []);
160
        }
161
162
        $modified_docblock = false;
163
164
        $old_phpdoc_type = null;
165
        if (isset($parsed_docblock->tags['var'])) {
166
            $old_phpdoc_type = array_shift($parsed_docblock->tags['var']);
167
        }
168
169
        if ($this->new_phpdoc_type
170
            && $this->new_phpdoc_type !== $old_phpdoc_type
171
        ) {
172
            $modified_docblock = true;
173
            $parsed_docblock->tags['var'] = [
174
                $this->new_phpdoc_type
175
                    . ($this->type_description ? (' ' . $this->type_description) : ''),
176
            ];
177
        }
178
179
        $old_psalm_type = null;
180
        if (isset($parsed_docblock->tags['psalm-var'])) {
181
            $old_psalm_type = array_shift($parsed_docblock->tags['psalm-var']);
182
        }
183
184
        if ($this->new_psalm_type
185
            && $this->new_phpdoc_type !== $this->new_psalm_type
186
            && $this->new_psalm_type !== $old_psalm_type
187
        ) {
188
            $modified_docblock = true;
189
            $parsed_docblock->tags['psalm-var'] = [$this->new_psalm_type];
190
        }
191
192
        if (!$parsed_docblock->tags && !$parsed_docblock->description) {
193
            return '';
194
        }
195
196
        if (!$modified_docblock) {
197
            return (string)$docblock . "\n" . $this->indentation;
198
        }
199
200
        return $parsed_docblock->render($this->indentation);
201
    }
202
203
    /**
204
     * @param  string $file_path
205
     *
206
     * @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...
207
     */
208
    public static function getManipulationsForFile($file_path)
209
    {
210
        if (!isset(self::$manipulators[$file_path])) {
211
            return [];
212
        }
213
214
        $file_manipulations = [];
215
216
        foreach (self::$ordered_manipulators[$file_path] as $manipulator) {
217
            if ($manipulator->new_php_type) {
218
                if ($manipulator->typehint_start && $manipulator->typehint_end) {
219
                    $file_manipulations[$manipulator->typehint_start] = new FileManipulation(
220
                        $manipulator->typehint_start,
221
                        $manipulator->typehint_end,
222
                        $manipulator->new_php_type
223
                    );
224
                } else {
225
                    $file_manipulations[$manipulator->typehint_area_start] = new FileManipulation(
226
                        $manipulator->typehint_area_start,
227
                        $manipulator->typehint_area_start,
228
                        ' ' . $manipulator->new_php_type
229
                    );
230
                }
231
            } elseif ($manipulator->new_php_type === ''
232
                && $manipulator->new_phpdoc_type
233
                && $manipulator->typehint_start
234
                && $manipulator->typehint_end
235
            ) {
236
                $file_manipulations[$manipulator->typehint_start] = new FileManipulation(
237
                    $manipulator->typehint_start,
238
                    $manipulator->typehint_end,
239
                    ''
240
                );
241
            }
242
243
            if (!$manipulator->new_php_type
244
                || !$manipulator->type_is_php_compatible
245
                || $manipulator->docblock_start !== $manipulator->docblock_end
246
            ) {
247
                $file_manipulations[$manipulator->docblock_start] = new FileManipulation(
248
                    $manipulator->docblock_start,
249
                    $manipulator->docblock_end,
250
                    $manipulator->getDocblock()
251
                );
252
            }
253
        }
254
255
        return $file_manipulations;
256
    }
257
}
258