1
|
|
|
<?php |
2
|
|
|
namespace Psalm\Internal\Analyzer\Statements\Expression\Fetch; |
3
|
|
|
|
4
|
|
|
use PhpParser; |
5
|
|
|
use Psalm\Internal\Analyzer\ClassLikeAnalyzer; |
6
|
|
|
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer; |
7
|
|
|
use Psalm\Internal\Analyzer\NamespaceAnalyzer; |
8
|
|
|
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; |
9
|
|
|
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer; |
10
|
|
|
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier; |
11
|
|
|
use Psalm\Internal\Analyzer\StatementsAnalyzer; |
12
|
|
|
use Psalm\Internal\Type\TemplateResult; |
13
|
|
|
use Psalm\CodeLocation; |
14
|
|
|
use Psalm\Context; |
15
|
|
|
use Psalm\Issue\DeprecatedProperty; |
16
|
|
|
use Psalm\Issue\InvalidPropertyFetch; |
17
|
|
|
use Psalm\Issue\InternalProperty; |
18
|
|
|
use Psalm\Issue\MissingPropertyType; |
19
|
|
|
use Psalm\Issue\MixedPropertyFetch; |
20
|
|
|
use Psalm\Issue\NoInterfaceProperties; |
21
|
|
|
use Psalm\Issue\NullPropertyFetch; |
22
|
|
|
use Psalm\Issue\PossiblyInvalidPropertyFetch; |
23
|
|
|
use Psalm\Issue\PossiblyNullPropertyFetch; |
24
|
|
|
use Psalm\Issue\UndefinedClass; |
25
|
|
|
use Psalm\Issue\UndefinedDocblockClass; |
26
|
|
|
use Psalm\Issue\UndefinedMagicPropertyFetch; |
27
|
|
|
use Psalm\Issue\UndefinedPropertyFetch; |
28
|
|
|
use Psalm\Issue\UndefinedThisPropertyFetch; |
29
|
|
|
use Psalm\Issue\UninitializedProperty; |
30
|
|
|
use Psalm\IssueBuffer; |
31
|
|
|
use Psalm\Type; |
32
|
|
|
use Psalm\Storage\ClassLikeStorage; |
33
|
|
|
use Psalm\Type\Atomic\TGenericObject; |
34
|
|
|
use Psalm\Type\Atomic\TNamedObject; |
35
|
|
|
use Psalm\Type\Atomic\TNull; |
36
|
|
|
use Psalm\Type\Atomic\TObject; |
37
|
|
|
use Psalm\Type\Atomic\TObjectWithProperties; |
38
|
|
|
use function strtolower; |
39
|
|
|
use function array_values; |
40
|
|
|
use function in_array; |
41
|
|
|
use function array_keys; |
42
|
|
|
use Psalm\Internal\Taint\TaintNode; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* @internal |
46
|
|
|
*/ |
47
|
|
|
class InstancePropertyFetchAnalyzer |
48
|
|
|
{ |
49
|
|
|
public static function analyze( |
50
|
|
|
StatementsAnalyzer $statements_analyzer, |
51
|
|
|
PhpParser\Node\Expr\PropertyFetch $stmt, |
52
|
|
|
Context $context, |
53
|
|
|
bool $in_assignment = false |
54
|
|
|
) : bool { |
55
|
|
|
if (!$stmt->name instanceof PhpParser\Node\Identifier) { |
56
|
|
|
if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->name, $context) === false) { |
57
|
|
|
return false; |
58
|
|
|
} |
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->var, $context) === false) { |
62
|
|
|
return false; |
63
|
|
|
} |
64
|
|
|
|
65
|
|
View Code Duplication |
if ($stmt->name instanceof PhpParser\Node\Identifier) { |
|
|
|
|
66
|
|
|
$prop_name = $stmt->name->name; |
67
|
|
|
} elseif (($stmt_name_type = $statements_analyzer->node_data->getType($stmt->name)) |
68
|
|
|
&& $stmt_name_type->isSingleStringLiteral() |
69
|
|
|
) { |
70
|
|
|
$prop_name = $stmt_name_type->getSingleStringLiteral()->value; |
71
|
|
|
} else { |
72
|
|
|
$prop_name = null; |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
$codebase = $statements_analyzer->getCodebase(); |
76
|
|
|
|
77
|
|
|
$stmt_var_id = ExpressionIdentifier::getArrayVarId( |
78
|
|
|
$stmt->var, |
79
|
|
|
$statements_analyzer->getFQCLN(), |
80
|
|
|
$statements_analyzer |
81
|
|
|
); |
82
|
|
|
|
83
|
|
|
$var_id = ExpressionIdentifier::getArrayVarId( |
84
|
|
|
$stmt, |
85
|
|
|
$statements_analyzer->getFQCLN(), |
86
|
|
|
$statements_analyzer |
87
|
|
|
); |
88
|
|
|
|
89
|
|
|
$stmt_type = null; |
|
|
|
|
90
|
|
|
|
91
|
|
|
if ($var_id && $context->hasVariable($var_id, $statements_analyzer)) { |
92
|
|
|
$stmt_type = $context->vars_in_scope[$var_id]; |
93
|
|
|
|
94
|
|
|
// we don't need to check anything |
95
|
|
|
$statements_analyzer->node_data->setType($stmt, $stmt_type); |
96
|
|
|
|
97
|
|
|
if (!$context->collect_initializations |
98
|
|
|
&& !$context->collect_mutations |
99
|
|
|
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath() |
100
|
|
|
&& (!(($parent_source = $statements_analyzer->getSource()) |
101
|
|
|
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) |
102
|
|
|
|| !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) |
103
|
|
|
) { |
104
|
|
|
$codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath()); |
105
|
|
|
} |
106
|
|
|
|
107
|
|
View Code Duplication |
if ($codebase->store_node_types |
|
|
|
|
108
|
|
|
&& !$context->collect_initializations |
109
|
|
|
&& !$context->collect_mutations |
110
|
|
|
) { |
111
|
|
|
$codebase->analyzer->addNodeType( |
112
|
|
|
$statements_analyzer->getFilePath(), |
113
|
|
|
$stmt->name, |
114
|
|
|
$stmt_type->getId() |
115
|
|
|
); |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
if ($stmt_var_id === '$this' |
119
|
|
|
&& !$stmt_type->initialized |
120
|
|
|
&& $context->collect_initializations |
121
|
|
|
&& ($stmt_var_type = $statements_analyzer->node_data->getType($stmt->var)) |
122
|
|
|
&& $stmt_var_type->hasObjectType() |
123
|
|
|
&& $stmt->name instanceof PhpParser\Node\Identifier |
124
|
|
|
) { |
125
|
|
|
$source = $statements_analyzer->getSource(); |
126
|
|
|
|
127
|
|
|
$property_id = null; |
128
|
|
|
|
129
|
|
|
foreach ($stmt_var_type->getAtomicTypes() as $lhs_type_part) { |
130
|
|
|
if ($lhs_type_part instanceof TNamedObject) { |
131
|
|
|
if (!$codebase->classExists($lhs_type_part->value)) { |
132
|
|
|
continue; |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
$property_id = $lhs_type_part->value . '::$' . $stmt->name->name; |
136
|
|
|
} |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
if ($property_id |
140
|
|
|
&& $source instanceof FunctionLikeAnalyzer |
141
|
|
|
&& $source->getMethodName() === '__construct' |
142
|
|
|
&& !$context->inside_unset |
143
|
|
|
) { |
144
|
|
|
if ($context->inside_isset |
145
|
|
|
|| ($context->inside_assignment |
146
|
|
|
&& isset($context->vars_in_scope[$var_id]) |
147
|
|
|
&& $context->vars_in_scope[$var_id]->isNullable() |
148
|
|
|
) |
149
|
|
|
) { |
150
|
|
|
$stmt_type->initialized = true; |
151
|
|
|
} else { |
152
|
|
|
if (IssueBuffer::accepts( |
153
|
|
|
new UninitializedProperty( |
154
|
|
|
'Cannot use uninitialized property ' . $var_id, |
155
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt), |
156
|
|
|
$var_id |
157
|
|
|
), |
158
|
|
|
$statements_analyzer->getSuppressedIssues() |
159
|
|
|
)) { |
160
|
|
|
// fall through |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
$stmt_type->addType(new Type\Atomic\TNull); |
164
|
|
|
} |
165
|
|
|
} |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
if (($stmt_var_type = $statements_analyzer->node_data->getType($stmt->var)) |
169
|
|
|
&& $stmt_var_type->hasObjectType() |
170
|
|
|
&& $stmt->name instanceof PhpParser\Node\Identifier |
171
|
|
|
) { |
172
|
|
|
// log the appearance |
173
|
|
|
foreach ($stmt_var_type->getAtomicTypes() as $lhs_type_part) { |
174
|
|
|
if ($lhs_type_part instanceof TNamedObject) { |
175
|
|
|
if (!$codebase->classExists($lhs_type_part->value)) { |
176
|
|
|
continue; |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
$property_id = $lhs_type_part->value . '::$' . $stmt->name->name; |
180
|
|
|
|
181
|
|
|
self::processTaints( |
182
|
|
|
$statements_analyzer, |
183
|
|
|
$stmt, |
184
|
|
|
$stmt_type, |
185
|
|
|
$property_id, |
186
|
|
|
$codebase->classlike_storage_provider->get($lhs_type_part->value), |
187
|
|
|
$in_assignment |
188
|
|
|
); |
189
|
|
|
|
190
|
|
|
$codebase->properties->propertyExists( |
191
|
|
|
$property_id, |
192
|
|
|
true, |
193
|
|
|
$statements_analyzer, |
194
|
|
|
$context, |
195
|
|
|
$codebase->collect_locations |
196
|
|
|
? new CodeLocation($statements_analyzer->getSource(), $stmt) |
197
|
|
|
: null |
198
|
|
|
); |
199
|
|
|
|
200
|
|
View Code Duplication |
if ($codebase->store_node_types |
|
|
|
|
201
|
|
|
&& !$context->collect_initializations |
202
|
|
|
&& !$context->collect_mutations |
203
|
|
|
) { |
204
|
|
|
$codebase->analyzer->addNodeReference( |
205
|
|
|
$statements_analyzer->getFilePath(), |
206
|
|
|
$stmt->name, |
207
|
|
|
$property_id |
208
|
|
|
); |
209
|
|
|
} |
210
|
|
|
} |
211
|
|
|
} |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
return true; |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
if ($stmt_var_id && $context->hasVariable($stmt_var_id, $statements_analyzer)) { |
218
|
|
|
$stmt_var_type = $context->vars_in_scope[$stmt_var_id]; |
219
|
|
|
} else { |
220
|
|
|
$stmt_var_type = $statements_analyzer->node_data->getType($stmt->var); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
if (!$stmt_var_type) { |
224
|
|
|
return true; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
if ($stmt_var_type->isNull()) { |
228
|
|
|
if (IssueBuffer::accepts( |
229
|
|
|
new NullPropertyFetch( |
230
|
|
|
'Cannot get property on null variable ' . $stmt_var_id, |
231
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt) |
232
|
|
|
), |
233
|
|
|
$statements_analyzer->getSuppressedIssues() |
234
|
|
|
)) { |
235
|
|
|
return false; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
return true; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
if ($stmt_var_type->isEmpty()) { |
242
|
|
|
if (IssueBuffer::accepts( |
243
|
|
|
new MixedPropertyFetch( |
244
|
|
|
'Cannot fetch property on empty var ' . $stmt_var_id, |
245
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt) |
246
|
|
|
), |
247
|
|
|
$statements_analyzer->getSuppressedIssues() |
248
|
|
|
)) { |
249
|
|
|
return false; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
return true; |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
if ($stmt_var_type->hasMixed()) { |
256
|
|
|
if (!$context->collect_initializations |
257
|
|
|
&& !$context->collect_mutations |
258
|
|
|
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath() |
259
|
|
|
&& (!(($parent_source = $statements_analyzer->getSource()) |
260
|
|
|
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) |
261
|
|
|
|| !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) |
262
|
|
|
) { |
263
|
|
|
$codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath()); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
View Code Duplication |
if ($stmt->name instanceof PhpParser\Node\Identifier) { |
|
|
|
|
267
|
|
|
$codebase->analyzer->addMixedMemberName( |
268
|
|
|
'$' . $stmt->name->name, |
269
|
|
|
$context->calling_method_id ?: $statements_analyzer->getFileName() |
270
|
|
|
); |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
if (IssueBuffer::accepts( |
274
|
|
|
new MixedPropertyFetch( |
275
|
|
|
'Cannot fetch property on mixed var ' . $stmt_var_id, |
276
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt) |
277
|
|
|
), |
278
|
|
|
$statements_analyzer->getSuppressedIssues() |
279
|
|
|
)) { |
280
|
|
|
// fall through |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
$statements_analyzer->node_data->setType($stmt, Type::getMixed()); |
284
|
|
|
|
285
|
|
View Code Duplication |
if ($codebase->store_node_types |
|
|
|
|
286
|
|
|
&& !$context->collect_initializations |
287
|
|
|
&& !$context->collect_mutations |
288
|
|
|
) { |
289
|
|
|
$codebase->analyzer->addNodeType( |
290
|
|
|
$statements_analyzer->getFilePath(), |
291
|
|
|
$stmt->name, |
292
|
|
|
$stmt_var_type->getId() |
293
|
|
|
); |
294
|
|
|
} |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
if (!$context->collect_initializations |
298
|
|
|
&& !$context->collect_mutations |
299
|
|
|
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath() |
300
|
|
|
&& (!(($parent_source = $statements_analyzer->getSource()) |
301
|
|
|
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) |
302
|
|
|
|| !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) |
303
|
|
|
) { |
304
|
|
|
$codebase->analyzer->incrementNonMixedCount($statements_analyzer->getRootFilePath()); |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
if ($stmt_var_type->isNullable() && !$stmt_var_type->ignore_nullable_issues) { |
308
|
|
View Code Duplication |
if (!$context->inside_isset) { |
|
|
|
|
309
|
|
|
if (IssueBuffer::accepts( |
310
|
|
|
new PossiblyNullPropertyFetch( |
311
|
|
|
'Cannot get property on possibly null variable ' . $stmt_var_id . ' of type ' . $stmt_var_type, |
312
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt) |
313
|
|
|
), |
314
|
|
|
$statements_analyzer->getSuppressedIssues() |
315
|
|
|
)) { |
316
|
|
|
// fall through |
317
|
|
|
} |
318
|
|
|
} else { |
319
|
|
|
$statements_analyzer->node_data->setType($stmt, Type::getNull()); |
320
|
|
|
} |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
if (!$prop_name) { |
324
|
|
|
if ($stmt_var_type->hasObjectType() && !$context->ignore_variable_property) { |
325
|
|
|
foreach ($stmt_var_type->getAtomicTypes() as $type) { |
326
|
|
View Code Duplication |
if ($type instanceof Type\Atomic\TNamedObject) { |
|
|
|
|
327
|
|
|
$codebase->analyzer->addMixedMemberName( |
328
|
|
|
strtolower($type->value) . '::$', |
329
|
|
|
$context->calling_method_id ?: $statements_analyzer->getFileName() |
330
|
|
|
); |
331
|
|
|
} |
332
|
|
|
} |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
return true; |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
$invalid_fetch_types = []; |
339
|
|
|
$has_valid_fetch_type = false; |
340
|
|
|
|
341
|
|
|
foreach ($stmt_var_type->getAtomicTypes() as $lhs_type_part) { |
342
|
|
|
if ($lhs_type_part instanceof TNull) { |
343
|
|
|
continue; |
344
|
|
|
} |
345
|
|
|
|
346
|
|
|
if ($lhs_type_part instanceof Type\Atomic\TTemplateParam) { |
347
|
|
|
$extra_types = $lhs_type_part->extra_types; |
348
|
|
|
|
349
|
|
|
$lhs_type_part = array_values( |
350
|
|
|
$lhs_type_part->as->getAtomicTypes() |
351
|
|
|
)[0]; |
352
|
|
|
|
353
|
|
|
$lhs_type_part->from_docblock = true; |
354
|
|
|
|
355
|
|
|
if ($lhs_type_part instanceof TNamedObject) { |
356
|
|
|
$lhs_type_part->extra_types = $extra_types; |
357
|
|
|
} |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
if ($lhs_type_part instanceof Type\Atomic\TMixed) { |
361
|
|
|
$statements_analyzer->node_data->setType($stmt, Type::getMixed()); |
362
|
|
|
continue; |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
if ($lhs_type_part instanceof Type\Atomic\TFalse && $stmt_var_type->ignore_falsable_issues) { |
366
|
|
|
continue; |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
if (!$lhs_type_part instanceof TNamedObject && !$lhs_type_part instanceof TObject) { |
370
|
|
|
$invalid_fetch_types[] = (string)$lhs_type_part; |
371
|
|
|
|
372
|
|
|
continue; |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
$has_valid_fetch_type = true; |
376
|
|
|
|
377
|
|
|
if ($lhs_type_part instanceof TObjectWithProperties |
378
|
|
|
&& isset($lhs_type_part->properties[$prop_name]) |
379
|
|
|
) { |
380
|
|
|
if ($stmt_type = $statements_analyzer->node_data->getType($stmt)) { |
381
|
|
|
$statements_analyzer->node_data->setType( |
382
|
|
|
$stmt, |
383
|
|
|
Type::combineUnionTypes( |
384
|
|
|
$lhs_type_part->properties[$prop_name], |
385
|
|
|
$stmt_type |
386
|
|
|
) |
387
|
|
|
); |
388
|
|
|
} else { |
389
|
|
|
$statements_analyzer->node_data->setType($stmt, $lhs_type_part->properties[$prop_name]); |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
continue; |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
// stdClass and SimpleXMLElement are special cases where we cannot infer the return types |
396
|
|
|
// but we don't want to throw an error |
397
|
|
|
// Hack has a similar issue: https://github.com/facebook/hhvm/issues/5164 |
398
|
|
|
if ($lhs_type_part instanceof TObject |
399
|
|
|
|| in_array(strtolower($lhs_type_part->value), ['stdclass', 'simplexmlelement'], true) |
400
|
|
|
) { |
401
|
|
|
$statements_analyzer->node_data->setType($stmt, Type::getMixed()); |
402
|
|
|
|
403
|
|
|
continue; |
404
|
|
|
} |
405
|
|
|
|
406
|
|
|
if (ExpressionAnalyzer::isMock($lhs_type_part->value)) { |
407
|
|
|
$statements_analyzer->node_data->setType($stmt, Type::getMixed()); |
408
|
|
|
continue; |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
$intersection_types = $lhs_type_part->getIntersectionTypes() ?: []; |
412
|
|
|
|
413
|
|
|
$fq_class_name = $lhs_type_part->value; |
414
|
|
|
|
415
|
|
|
$override_property_visibility = false; |
416
|
|
|
|
417
|
|
|
$has_magic_getter = false; |
418
|
|
|
|
419
|
|
|
$class_exists = false; |
420
|
|
|
$interface_exists = false; |
421
|
|
|
|
422
|
|
|
if (!$codebase->classExists($lhs_type_part->value)) { |
423
|
|
View Code Duplication |
if ($codebase->interfaceExists($lhs_type_part->value)) { |
|
|
|
|
424
|
|
|
$interface_exists = true; |
425
|
|
|
$interface_storage = $codebase->classlike_storage_provider->get($lhs_type_part->value); |
426
|
|
|
|
427
|
|
|
$override_property_visibility = $interface_storage->override_property_visibility; |
428
|
|
|
|
429
|
|
|
foreach ($intersection_types as $intersection_type) { |
430
|
|
|
if ($intersection_type instanceof TNamedObject |
431
|
|
|
&& $codebase->classExists($intersection_type->value) |
432
|
|
|
) { |
433
|
|
|
$fq_class_name = $intersection_type->value; |
434
|
|
|
$class_exists = true; |
435
|
|
|
break; |
436
|
|
|
} |
437
|
|
|
} |
438
|
|
|
|
439
|
|
|
if (!$class_exists) { |
440
|
|
|
if (IssueBuffer::accepts( |
441
|
|
|
new NoInterfaceProperties( |
442
|
|
|
'Interfaces cannot have properties', |
443
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt), |
444
|
|
|
$lhs_type_part->value |
445
|
|
|
), |
446
|
|
|
$statements_analyzer->getSuppressedIssues() |
447
|
|
|
)) { |
448
|
|
|
return true; |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
if (!$codebase->methodExists($fq_class_name . '::__set')) { |
452
|
|
|
return true; |
453
|
|
|
} |
454
|
|
|
} |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
if (!$class_exists && !$interface_exists) { |
458
|
|
View Code Duplication |
if ($lhs_type_part->from_docblock) { |
|
|
|
|
459
|
|
|
if (IssueBuffer::accepts( |
460
|
|
|
new UndefinedDocblockClass( |
461
|
|
|
'Cannot set properties of undefined docblock class ' . $lhs_type_part->value, |
462
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt), |
463
|
|
|
$lhs_type_part->value |
464
|
|
|
), |
465
|
|
|
$statements_analyzer->getSuppressedIssues() |
466
|
|
|
)) { |
467
|
|
|
// fall through |
468
|
|
|
} |
469
|
|
|
} else { |
470
|
|
|
if (IssueBuffer::accepts( |
471
|
|
|
new UndefinedClass( |
472
|
|
|
'Cannot set properties of undefined class ' . $lhs_type_part->value, |
473
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt), |
474
|
|
|
$lhs_type_part->value |
475
|
|
|
), |
476
|
|
|
$statements_analyzer->getSuppressedIssues() |
477
|
|
|
)) { |
478
|
|
|
// fall through |
479
|
|
|
} |
480
|
|
|
} |
481
|
|
|
|
482
|
|
|
return true; |
483
|
|
|
} |
484
|
|
|
} else { |
485
|
|
|
$class_exists = true; |
486
|
|
|
} |
487
|
|
|
|
488
|
|
|
$class_storage = $codebase->classlike_storage_provider->get($fq_class_name); |
489
|
|
|
$property_id = $fq_class_name . '::$' . $prop_name; |
490
|
|
|
|
491
|
|
|
$naive_property_exists = $codebase->properties->propertyExists( |
492
|
|
|
$property_id, |
493
|
|
|
true, |
494
|
|
|
$statements_analyzer, |
495
|
|
|
$context, |
496
|
|
|
$codebase->collect_locations ? new CodeLocation($statements_analyzer->getSource(), $stmt) : null |
497
|
|
|
); |
498
|
|
|
|
499
|
|
|
// add method before changing fq_class_name |
500
|
|
|
$get_method_id = new \Psalm\Internal\MethodIdentifier($fq_class_name, '__get'); |
501
|
|
|
|
502
|
|
|
if (!$naive_property_exists |
503
|
|
|
&& $class_storage->mixin instanceof Type\Atomic\TNamedObject |
504
|
|
|
) { |
505
|
|
|
$new_property_id = $class_storage->mixin->value . '::$' . $prop_name; |
506
|
|
|
|
507
|
|
|
try { |
508
|
|
|
$new_class_storage = $codebase->classlike_storage_provider->get($class_storage->mixin->value); |
509
|
|
|
} catch (\InvalidArgumentException $e) { |
510
|
|
|
$new_class_storage = null; |
511
|
|
|
} |
512
|
|
|
|
513
|
|
|
if ($new_class_storage |
514
|
|
|
&& ($codebase->properties->propertyExists( |
515
|
|
|
$new_property_id, |
516
|
|
|
true, |
517
|
|
|
$statements_analyzer, |
518
|
|
|
$context, |
519
|
|
|
$codebase->collect_locations |
520
|
|
|
? new CodeLocation($statements_analyzer->getSource(), $stmt) |
521
|
|
|
: null |
522
|
|
|
) |
523
|
|
|
|| isset($new_class_storage->pseudo_property_get_types['$' . $prop_name])) |
524
|
|
|
) { |
525
|
|
|
$fq_class_name = $class_storage->mixin->value; |
526
|
|
|
$lhs_type_part = clone $class_storage->mixin; |
527
|
|
|
$class_storage = $new_class_storage; |
528
|
|
|
|
529
|
|
|
if (!isset($new_class_storage->pseudo_property_get_types['$' . $prop_name])) { |
530
|
|
|
$naive_property_exists = true; |
531
|
|
|
} |
532
|
|
|
|
533
|
|
|
$property_id = $new_property_id; |
534
|
|
|
} |
535
|
|
|
} |
536
|
|
|
|
537
|
|
|
if ((!$naive_property_exists |
538
|
|
|
|| ($stmt_var_id !== '$this' |
539
|
|
|
&& $fq_class_name !== $context->self |
540
|
|
|
&& ClassLikeAnalyzer::checkPropertyVisibility( |
541
|
|
|
$property_id, |
542
|
|
|
$context, |
543
|
|
|
$statements_analyzer, |
544
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt), |
545
|
|
|
$statements_analyzer->getSuppressedIssues(), |
546
|
|
|
false |
547
|
|
|
) !== true) |
548
|
|
|
) |
549
|
|
|
&& $codebase->methods->methodExists( |
550
|
|
|
$get_method_id, |
551
|
|
|
$context->calling_method_id, |
552
|
|
|
$codebase->collect_locations |
553
|
|
|
? new CodeLocation($statements_analyzer->getSource(), $stmt) |
554
|
|
|
: null, |
555
|
|
|
!$context->collect_initializations |
556
|
|
|
&& !$context->collect_mutations |
557
|
|
|
? $statements_analyzer |
558
|
|
|
: null, |
559
|
|
|
$statements_analyzer->getFilePath() |
560
|
|
|
) |
561
|
|
|
) { |
562
|
|
|
$has_magic_getter = true; |
563
|
|
|
|
564
|
|
View Code Duplication |
if (isset($class_storage->pseudo_property_get_types['$' . $prop_name])) { |
|
|
|
|
565
|
|
|
$stmt_type = clone $class_storage->pseudo_property_get_types['$' . $prop_name]; |
566
|
|
|
|
567
|
|
|
$statements_analyzer->node_data->setType($stmt, $stmt_type); |
568
|
|
|
|
569
|
|
|
self::processTaints( |
570
|
|
|
$statements_analyzer, |
571
|
|
|
$stmt, |
572
|
|
|
$stmt_type, |
573
|
|
|
$property_id, |
574
|
|
|
$class_storage, |
575
|
|
|
$in_assignment |
576
|
|
|
); |
577
|
|
|
continue; |
578
|
|
|
} |
579
|
|
|
|
580
|
|
|
$old_data_provider = $statements_analyzer->node_data; |
581
|
|
|
|
582
|
|
|
$statements_analyzer->node_data = clone $statements_analyzer->node_data; |
583
|
|
|
|
584
|
|
|
$fake_method_call = new PhpParser\Node\Expr\MethodCall( |
585
|
|
|
$stmt->var, |
586
|
|
|
new PhpParser\Node\Identifier('__get', $stmt->name->getAttributes()), |
587
|
|
|
[ |
588
|
|
|
new PhpParser\Node\Arg( |
589
|
|
|
new PhpParser\Node\Scalar\String_( |
590
|
|
|
$prop_name, |
591
|
|
|
$stmt->name->getAttributes() |
592
|
|
|
) |
593
|
|
|
) |
594
|
|
|
] |
595
|
|
|
); |
596
|
|
|
|
597
|
|
|
$suppressed_issues = $statements_analyzer->getSuppressedIssues(); |
598
|
|
|
|
599
|
|
|
if (!in_array('PossiblyNullReference', $suppressed_issues, true)) { |
600
|
|
|
$statements_analyzer->addSuppressedIssues(['PossiblyNullReference']); |
601
|
|
|
} |
602
|
|
|
|
603
|
|
|
if (!in_array('InternalMethod', $suppressed_issues, true)) { |
604
|
|
|
$statements_analyzer->addSuppressedIssues(['InternalMethod']); |
605
|
|
|
} |
606
|
|
|
|
607
|
|
|
\Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze( |
608
|
|
|
$statements_analyzer, |
609
|
|
|
$fake_method_call, |
610
|
|
|
$context, |
611
|
|
|
false |
612
|
|
|
); |
613
|
|
|
|
614
|
|
|
if (!in_array('PossiblyNullReference', $suppressed_issues, true)) { |
615
|
|
|
$statements_analyzer->removeSuppressedIssues(['PossiblyNullReference']); |
616
|
|
|
} |
617
|
|
|
|
618
|
|
|
if (!in_array('InternalMethod', $suppressed_issues, true)) { |
619
|
|
|
$statements_analyzer->removeSuppressedIssues(['InternalMethod']); |
620
|
|
|
} |
621
|
|
|
|
622
|
|
|
$fake_method_call_type = $statements_analyzer->node_data->getType($fake_method_call); |
623
|
|
|
|
624
|
|
|
$statements_analyzer->node_data = $old_data_provider; |
625
|
|
|
|
626
|
|
|
if ($fake_method_call_type) { |
627
|
|
|
$statements_analyzer->node_data->setType($stmt, $fake_method_call_type); |
628
|
|
|
} else { |
629
|
|
|
$statements_analyzer->node_data->setType($stmt, Type::getMixed()); |
630
|
|
|
} |
631
|
|
|
|
632
|
|
|
$property_id = $lhs_type_part->value . '::$' . $prop_name; |
633
|
|
|
|
634
|
|
|
/* |
635
|
|
|
* If we have an explicit list of all allowed magic properties on the class, and we're |
636
|
|
|
* not in that list, fall through |
637
|
|
|
*/ |
638
|
|
|
if (!$class_storage->sealed_properties && !$override_property_visibility) { |
639
|
|
|
continue; |
640
|
|
|
} |
641
|
|
|
|
642
|
|
|
if (!$class_exists) { |
643
|
|
|
$property_id = $lhs_type_part->value . '::$' . $prop_name; |
644
|
|
|
|
645
|
|
|
if (IssueBuffer::accepts( |
646
|
|
|
new UndefinedMagicPropertyFetch( |
647
|
|
|
'Magic instance property ' . $property_id . ' is not defined', |
648
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt), |
649
|
|
|
$property_id |
650
|
|
|
), |
651
|
|
|
$statements_analyzer->getSuppressedIssues() |
652
|
|
|
)) { |
653
|
|
|
// fall through |
654
|
|
|
} |
655
|
|
|
|
656
|
|
|
continue; |
657
|
|
|
} |
658
|
|
|
} |
659
|
|
|
|
660
|
|
View Code Duplication |
if ($codebase->store_node_types |
|
|
|
|
661
|
|
|
&& !$context->collect_initializations |
662
|
|
|
&& !$context->collect_mutations |
663
|
|
|
) { |
664
|
|
|
$codebase->analyzer->addNodeReference( |
665
|
|
|
$statements_analyzer->getFilePath(), |
666
|
|
|
$stmt->name, |
667
|
|
|
$property_id |
668
|
|
|
); |
669
|
|
|
} |
670
|
|
|
|
671
|
|
|
$config = $statements_analyzer->getProjectAnalyzer()->getConfig(); |
672
|
|
|
|
673
|
|
|
if (!$naive_property_exists) { |
674
|
|
View Code Duplication |
if ($config->use_phpdoc_property_without_magic_or_parent |
|
|
|
|
675
|
|
|
&& isset($class_storage->pseudo_property_get_types['$' . $prop_name]) |
676
|
|
|
) { |
677
|
|
|
$stmt_type = clone $class_storage->pseudo_property_get_types['$' . $prop_name]; |
678
|
|
|
|
679
|
|
|
$statements_analyzer->node_data->setType($stmt, $stmt_type); |
680
|
|
|
|
681
|
|
|
self::processTaints( |
682
|
|
|
$statements_analyzer, |
683
|
|
|
$stmt, |
684
|
|
|
$stmt_type, |
685
|
|
|
$property_id, |
686
|
|
|
$class_storage, |
687
|
|
|
$in_assignment |
688
|
|
|
); |
689
|
|
|
continue; |
690
|
|
|
} |
691
|
|
|
|
692
|
|
|
if ($fq_class_name !== $context->self |
693
|
|
|
&& $context->self |
694
|
|
|
&& $codebase->classlikes->classExtends($fq_class_name, $context->self) |
695
|
|
|
&& $codebase->properties->propertyExists( |
696
|
|
|
$context->self . '::$' . $prop_name, |
697
|
|
|
true, |
698
|
|
|
$statements_analyzer, |
699
|
|
|
$context, |
700
|
|
|
$codebase->collect_locations |
701
|
|
|
? new CodeLocation($statements_analyzer->getSource(), $stmt) |
702
|
|
|
: null |
703
|
|
|
) |
704
|
|
|
) { |
705
|
|
|
$property_id = $context->self . '::$' . $prop_name; |
706
|
|
|
} else { |
707
|
|
|
if ($context->inside_isset || $context->collect_initializations) { |
708
|
|
|
return true; |
709
|
|
|
} |
710
|
|
|
|
711
|
|
|
if ($stmt_var_id === '$this') { |
712
|
|
|
if (IssueBuffer::accepts( |
713
|
|
|
new UndefinedThisPropertyFetch( |
714
|
|
|
'Instance property ' . $property_id . ' is not defined', |
715
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt), |
716
|
|
|
$property_id |
717
|
|
|
), |
718
|
|
|
$statements_analyzer->getSuppressedIssues() |
719
|
|
|
)) { |
720
|
|
|
// fall through |
721
|
|
|
} |
722
|
|
View Code Duplication |
} else { |
|
|
|
|
723
|
|
|
if ($has_magic_getter) { |
724
|
|
|
if (IssueBuffer::accepts( |
725
|
|
|
new UndefinedMagicPropertyFetch( |
726
|
|
|
'Magic instance property ' . $property_id . ' is not defined', |
727
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt), |
728
|
|
|
$property_id |
729
|
|
|
), |
730
|
|
|
$statements_analyzer->getSuppressedIssues() |
731
|
|
|
)) { |
732
|
|
|
// fall through |
733
|
|
|
} |
734
|
|
|
} else { |
735
|
|
|
if (IssueBuffer::accepts( |
736
|
|
|
new UndefinedPropertyFetch( |
737
|
|
|
'Instance property ' . $property_id . ' is not defined', |
738
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt), |
739
|
|
|
$property_id |
740
|
|
|
), |
741
|
|
|
$statements_analyzer->getSuppressedIssues() |
742
|
|
|
)) { |
743
|
|
|
// fall through |
744
|
|
|
} |
745
|
|
|
} |
746
|
|
|
} |
747
|
|
|
|
748
|
|
|
$stmt_type = Type::getMixed(); |
749
|
|
|
|
750
|
|
|
$statements_analyzer->node_data->setType($stmt, $stmt_type); |
751
|
|
|
|
752
|
|
|
if ($var_id) { |
753
|
|
|
$context->vars_in_scope[$var_id] = $stmt_type; |
754
|
|
|
} |
755
|
|
|
|
756
|
|
|
return true; |
757
|
|
|
} |
758
|
|
|
} |
759
|
|
|
|
760
|
|
View Code Duplication |
if (!$override_property_visibility) { |
|
|
|
|
761
|
|
|
if (ClassLikeAnalyzer::checkPropertyVisibility( |
762
|
|
|
$property_id, |
763
|
|
|
$context, |
764
|
|
|
$statements_analyzer, |
765
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt), |
766
|
|
|
$statements_analyzer->getSuppressedIssues() |
767
|
|
|
) === false) { |
768
|
|
|
return false; |
769
|
|
|
} |
770
|
|
|
} |
771
|
|
|
|
772
|
|
|
$declaring_property_class = $codebase->properties->getDeclaringClassForProperty( |
773
|
|
|
$property_id, |
774
|
|
|
true, |
775
|
|
|
$statements_analyzer |
776
|
|
|
); |
777
|
|
|
|
778
|
|
|
if ($declaring_property_class === null) { |
779
|
|
|
continue; |
780
|
|
|
} |
781
|
|
|
|
782
|
|
View Code Duplication |
if ($codebase->properties_to_rename) { |
|
|
|
|
783
|
|
|
$declaring_property_id = strtolower($declaring_property_class) . '::$' . $prop_name; |
784
|
|
|
|
785
|
|
|
foreach ($codebase->properties_to_rename as $original_property_id => $new_property_name) { |
786
|
|
|
if ($declaring_property_id === $original_property_id) { |
787
|
|
|
$file_manipulations = [ |
788
|
|
|
new \Psalm\FileManipulation( |
789
|
|
|
(int) $stmt->name->getAttribute('startFilePos'), |
790
|
|
|
(int) $stmt->name->getAttribute('endFilePos') + 1, |
791
|
|
|
$new_property_name |
792
|
|
|
) |
793
|
|
|
]; |
794
|
|
|
|
795
|
|
|
\Psalm\Internal\FileManipulation\FileManipulationBuffer::add( |
796
|
|
|
$statements_analyzer->getFilePath(), |
797
|
|
|
$file_manipulations |
798
|
|
|
); |
799
|
|
|
} |
800
|
|
|
} |
801
|
|
|
} |
802
|
|
|
|
803
|
|
|
$declaring_class_storage = $codebase->classlike_storage_provider->get( |
804
|
|
|
$declaring_property_class |
805
|
|
|
); |
806
|
|
|
|
807
|
|
|
if (isset($declaring_class_storage->properties[$prop_name])) { |
808
|
|
|
$property_storage = $declaring_class_storage->properties[$prop_name]; |
809
|
|
|
|
810
|
|
|
if ($property_storage->deprecated) { |
811
|
|
|
if (IssueBuffer::accepts( |
812
|
|
|
new DeprecatedProperty( |
813
|
|
|
$property_id . ' is marked deprecated', |
814
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt), |
815
|
|
|
$property_id |
816
|
|
|
), |
817
|
|
|
$statements_analyzer->getSuppressedIssues() |
818
|
|
|
)) { |
819
|
|
|
// fall through |
820
|
|
|
} |
821
|
|
|
} |
822
|
|
|
|
823
|
|
View Code Duplication |
if ($property_storage->psalm_internal && $context->self) { |
|
|
|
|
824
|
|
|
if (! NamespaceAnalyzer::isWithin($context->self, $property_storage->psalm_internal)) { |
825
|
|
|
if (IssueBuffer::accepts( |
826
|
|
|
new InternalProperty( |
827
|
|
|
$property_id . ' is marked internal to ' . $property_storage->psalm_internal, |
828
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt), |
829
|
|
|
$property_id |
830
|
|
|
), |
831
|
|
|
$statements_analyzer->getSuppressedIssues() |
832
|
|
|
)) { |
833
|
|
|
// fall through |
834
|
|
|
} |
835
|
|
|
} |
836
|
|
|
} |
837
|
|
|
|
838
|
|
|
if ($property_storage->internal && $context->self) { |
839
|
|
|
if (! NamespaceAnalyzer::nameSpaceRootsMatch($context->self, $declaring_property_class)) { |
840
|
|
|
if (IssueBuffer::accepts( |
841
|
|
|
new InternalProperty( |
842
|
|
|
$property_id . ' is marked internal', |
843
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt), |
844
|
|
|
$property_id |
845
|
|
|
), |
846
|
|
|
$statements_analyzer->getSuppressedIssues() |
847
|
|
|
)) { |
848
|
|
|
// fall through |
849
|
|
|
} |
850
|
|
|
} |
851
|
|
|
} |
852
|
|
|
} |
853
|
|
|
|
854
|
|
|
$class_property_type = $codebase->properties->getPropertyType( |
855
|
|
|
$property_id, |
856
|
|
|
false, |
857
|
|
|
$statements_analyzer, |
858
|
|
|
$context |
859
|
|
|
); |
860
|
|
|
|
861
|
|
|
if (!$class_property_type) { |
862
|
|
View Code Duplication |
if ($declaring_class_storage->location |
|
|
|
|
863
|
|
|
&& $config->isInProjectDirs( |
864
|
|
|
$declaring_class_storage->location->file_path |
865
|
|
|
) |
866
|
|
|
) { |
867
|
|
|
if (IssueBuffer::accepts( |
868
|
|
|
new MissingPropertyType( |
869
|
|
|
'Property ' . $fq_class_name . '::$' . $prop_name |
870
|
|
|
. ' does not have a declared type', |
871
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt) |
872
|
|
|
), |
873
|
|
|
$statements_analyzer->getSuppressedIssues() |
874
|
|
|
)) { |
875
|
|
|
// fall through |
876
|
|
|
} |
877
|
|
|
} |
878
|
|
|
|
879
|
|
|
$class_property_type = Type::getMixed(); |
880
|
|
|
} else { |
881
|
|
|
$class_property_type = \Psalm\Internal\Type\TypeExpander::expandUnion( |
882
|
|
|
$codebase, |
883
|
|
|
clone $class_property_type, |
884
|
|
|
$declaring_class_storage->name, |
885
|
|
|
$declaring_class_storage->name, |
886
|
|
|
$declaring_class_storage->parent_class |
887
|
|
|
); |
888
|
|
|
|
889
|
|
|
if ($declaring_class_storage->template_types) { |
890
|
|
|
if (!$lhs_type_part instanceof TGenericObject) { |
891
|
|
|
$type_params = []; |
892
|
|
|
|
893
|
|
|
foreach ($declaring_class_storage->template_types as $type_map) { |
894
|
|
|
$type_params[] = clone array_values($type_map)[0][0]; |
895
|
|
|
} |
896
|
|
|
|
897
|
|
|
$lhs_type_part = new TGenericObject($lhs_type_part->value, $type_params); |
898
|
|
|
} |
899
|
|
|
|
900
|
|
|
$class_property_type = self::localizePropertyType( |
901
|
|
|
$codebase, |
902
|
|
|
$class_property_type, |
903
|
|
|
$lhs_type_part, |
904
|
|
|
$class_storage, |
905
|
|
|
$declaring_class_storage |
906
|
|
|
); |
907
|
|
|
} |
908
|
|
|
|
909
|
|
|
if ($lhs_type_part instanceof TGenericObject) { |
910
|
|
|
$class_property_type = self::localizePropertyType( |
911
|
|
|
$codebase, |
912
|
|
|
$class_property_type, |
913
|
|
|
$lhs_type_part, |
914
|
|
|
$class_storage, |
915
|
|
|
$declaring_class_storage |
916
|
|
|
); |
917
|
|
|
} |
918
|
|
|
} |
919
|
|
|
|
920
|
|
|
self::processTaints( |
921
|
|
|
$statements_analyzer, |
922
|
|
|
$stmt, |
923
|
|
|
$class_property_type, |
924
|
|
|
$property_id, |
925
|
|
|
$class_storage, |
926
|
|
|
$in_assignment |
927
|
|
|
); |
928
|
|
|
|
929
|
|
View Code Duplication |
if ($stmt_type = $statements_analyzer->node_data->getType($stmt)) { |
|
|
|
|
930
|
|
|
$statements_analyzer->node_data->setType( |
931
|
|
|
$stmt, |
932
|
|
|
Type::combineUnionTypes($class_property_type, $stmt_type) |
933
|
|
|
); |
934
|
|
|
} else { |
935
|
|
|
$statements_analyzer->node_data->setType($stmt, $class_property_type); |
936
|
|
|
} |
937
|
|
|
} |
938
|
|
|
|
939
|
|
|
$stmt_type = $statements_analyzer->node_data->getType($stmt); |
940
|
|
|
|
941
|
|
|
if ($stmt_var_type->isNullable() && !$context->inside_isset && $stmt_type) { |
942
|
|
|
$stmt_type->addType(new TNull); |
943
|
|
|
|
944
|
|
|
if ($stmt_var_type->ignore_nullable_issues) { |
945
|
|
|
$stmt_type->ignore_nullable_issues = true; |
946
|
|
|
} |
947
|
|
|
} |
948
|
|
|
|
949
|
|
View Code Duplication |
if ($codebase->store_node_types |
|
|
|
|
950
|
|
|
&& !$context->collect_initializations |
951
|
|
|
&& !$context->collect_mutations |
952
|
|
|
&& ($stmt_type = $statements_analyzer->node_data->getType($stmt)) |
953
|
|
|
) { |
954
|
|
|
$codebase->analyzer->addNodeType( |
955
|
|
|
$statements_analyzer->getFilePath(), |
956
|
|
|
$stmt->name, |
957
|
|
|
$stmt_type->getId() |
958
|
|
|
); |
959
|
|
|
} |
960
|
|
|
|
961
|
|
|
if ($invalid_fetch_types) { |
962
|
|
|
$lhs_type_part = $invalid_fetch_types[0]; |
963
|
|
|
|
964
|
|
View Code Duplication |
if ($has_valid_fetch_type) { |
|
|
|
|
965
|
|
|
if (IssueBuffer::accepts( |
966
|
|
|
new PossiblyInvalidPropertyFetch( |
967
|
|
|
'Cannot fetch property on possible non-object ' . $stmt_var_id . ' of type ' . $lhs_type_part, |
968
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt) |
969
|
|
|
), |
970
|
|
|
$statements_analyzer->getSuppressedIssues() |
971
|
|
|
)) { |
972
|
|
|
// fall through |
973
|
|
|
} |
974
|
|
|
} else { |
975
|
|
|
if (IssueBuffer::accepts( |
976
|
|
|
new InvalidPropertyFetch( |
977
|
|
|
'Cannot fetch property on non-object ' . $stmt_var_id . ' of type ' . $lhs_type_part, |
978
|
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt) |
979
|
|
|
), |
980
|
|
|
$statements_analyzer->getSuppressedIssues() |
981
|
|
|
)) { |
982
|
|
|
// fall through |
983
|
|
|
} |
984
|
|
|
} |
985
|
|
|
} |
986
|
|
|
|
987
|
|
|
if ($var_id) { |
988
|
|
|
$context->vars_in_scope[$var_id] = $statements_analyzer->node_data->getType($stmt) ?: Type::getMixed(); |
989
|
|
|
} |
990
|
|
|
|
991
|
|
|
return true; |
992
|
|
|
} |
993
|
|
|
|
994
|
|
|
public static function localizePropertyType( |
995
|
|
|
\Psalm\Codebase $codebase, |
996
|
|
|
Type\Union $class_property_type, |
997
|
|
|
TGenericObject $lhs_type_part, |
998
|
|
|
ClassLikeStorage $calling_class_storage, |
999
|
|
|
ClassLikeStorage $declaring_class_storage |
1000
|
|
|
) : Type\Union { |
1001
|
|
|
$template_types = CallAnalyzer::getTemplateTypesForCall( |
1002
|
|
|
$codebase, |
1003
|
|
|
$declaring_class_storage, |
1004
|
|
|
$declaring_class_storage->name, |
1005
|
|
|
$calling_class_storage, |
1006
|
|
|
$calling_class_storage->template_types ?: [] |
1007
|
|
|
); |
1008
|
|
|
|
1009
|
|
|
$extended_types = $calling_class_storage->template_type_extends; |
1010
|
|
|
|
1011
|
|
|
if ($template_types) { |
1012
|
|
|
if ($calling_class_storage->template_types) { |
1013
|
|
|
foreach ($lhs_type_part->type_params as $param_offset => $lhs_param_type) { |
1014
|
|
|
$i = -1; |
1015
|
|
|
|
1016
|
|
|
foreach ($calling_class_storage->template_types as $calling_param_name => $_) { |
1017
|
|
|
$i++; |
1018
|
|
|
|
1019
|
|
|
if ($i === $param_offset) { |
1020
|
|
|
$template_types[$calling_param_name][$calling_class_storage->name] = [ |
1021
|
|
|
$lhs_param_type, |
1022
|
|
|
0 |
1023
|
|
|
]; |
1024
|
|
|
break; |
1025
|
|
|
} |
1026
|
|
|
} |
1027
|
|
|
} |
1028
|
|
|
} |
1029
|
|
|
|
1030
|
|
|
foreach ($template_types as $type_name => $_) { |
1031
|
|
|
if (isset($extended_types[$declaring_class_storage->name][$type_name])) { |
1032
|
|
|
$mapped_type = $extended_types[$declaring_class_storage->name][$type_name]; |
1033
|
|
|
|
1034
|
|
|
foreach ($mapped_type->getAtomicTypes() as $mapped_type_atomic) { |
1035
|
|
|
if (!$mapped_type_atomic instanceof Type\Atomic\TTemplateParam) { |
1036
|
|
|
continue; |
1037
|
|
|
} |
1038
|
|
|
|
1039
|
|
|
$param_name = $mapped_type_atomic->param_name; |
1040
|
|
|
|
1041
|
|
|
$position = false; |
1042
|
|
|
|
1043
|
|
|
if (isset($calling_class_storage->template_types[$param_name])) { |
1044
|
|
|
$position = \array_search( |
1045
|
|
|
$param_name, |
1046
|
|
|
array_keys($calling_class_storage->template_types) |
1047
|
|
|
); |
1048
|
|
|
} |
1049
|
|
|
|
1050
|
|
|
if ($position !== false && isset($lhs_type_part->type_params[$position])) { |
1051
|
|
|
$template_types[$type_name][$declaring_class_storage->name] = [ |
1052
|
|
|
$lhs_type_part->type_params[$position], |
1053
|
|
|
0 |
1054
|
|
|
]; |
1055
|
|
|
} |
1056
|
|
|
} |
1057
|
|
|
} |
1058
|
|
|
} |
1059
|
|
|
|
1060
|
|
|
$class_property_type->replaceTemplateTypesWithArgTypes( |
1061
|
|
|
new TemplateResult([], $template_types), |
1062
|
|
|
$codebase |
1063
|
|
|
); |
1064
|
|
|
} |
1065
|
|
|
|
1066
|
|
|
return $class_property_type; |
1067
|
|
|
} |
1068
|
|
|
|
1069
|
|
|
private static function processTaints( |
1070
|
|
|
StatementsAnalyzer $statements_analyzer, |
1071
|
|
|
PhpParser\Node\Expr\PropertyFetch $stmt, |
1072
|
|
|
Type\Union $type, |
1073
|
|
|
string $property_id, |
1074
|
|
|
\Psalm\Storage\ClassLikeStorage $class_storage, |
1075
|
|
|
bool $in_assignment |
1076
|
|
|
) : void { |
1077
|
|
|
$codebase = $statements_analyzer->getCodebase(); |
1078
|
|
|
|
1079
|
|
|
if (!$codebase->taint |
1080
|
|
|
|| !$codebase->config->trackTaintsInPath($statements_analyzer->getFilePath()) |
1081
|
|
|
) { |
1082
|
|
|
return; |
1083
|
|
|
} |
1084
|
|
|
|
1085
|
|
|
$var_location = new CodeLocation($statements_analyzer->getSource(), $stmt->var); |
1086
|
|
|
$property_location = new CodeLocation($statements_analyzer->getSource(), $stmt); |
1087
|
|
|
|
1088
|
|
|
if ($class_storage->specialize_instance) { |
1089
|
|
|
$var_id = ExpressionIdentifier::getArrayVarId( |
1090
|
|
|
$stmt->var, |
1091
|
|
|
null, |
1092
|
|
|
$statements_analyzer |
1093
|
|
|
); |
1094
|
|
|
|
1095
|
|
|
$var_property_id = ExpressionIdentifier::getArrayVarId( |
1096
|
|
|
$stmt, |
1097
|
|
|
null, |
1098
|
|
|
$statements_analyzer |
1099
|
|
|
); |
1100
|
|
|
|
1101
|
|
|
if ($var_id) { |
1102
|
|
|
$var_type = $statements_analyzer->node_data->getType($stmt->var); |
1103
|
|
|
|
1104
|
|
|
if ($var_type && \in_array('TaintedInput', $statements_analyzer->getSuppressedIssues())) { |
1105
|
|
|
$var_type->parent_nodes = []; |
1106
|
|
|
return; |
1107
|
|
|
} |
1108
|
|
|
|
1109
|
|
|
$var_node = TaintNode::getForAssignment( |
1110
|
|
|
$var_id, |
1111
|
|
|
$var_location |
1112
|
|
|
); |
1113
|
|
|
|
1114
|
|
|
$codebase->taint->addTaintNode($var_node); |
1115
|
|
|
|
1116
|
|
|
$property_node = TaintNode::getForAssignment( |
1117
|
|
|
$var_property_id ?: $var_id . '->$property', |
1118
|
|
|
$property_location |
1119
|
|
|
); |
1120
|
|
|
|
1121
|
|
|
$codebase->taint->addTaintNode($property_node); |
1122
|
|
|
|
1123
|
|
|
$codebase->taint->addPath( |
1124
|
|
|
$var_node, |
1125
|
|
|
$property_node, |
1126
|
|
|
'property-fetch' |
1127
|
|
|
. ($stmt->name instanceof PhpParser\Node\Identifier ? '-' . $stmt->name : '') |
1128
|
|
|
); |
1129
|
|
|
|
1130
|
|
|
if ($var_type && $var_type->parent_nodes) { |
1131
|
|
|
foreach ($var_type->parent_nodes as $parent_node) { |
1132
|
|
|
$codebase->taint->addPath( |
1133
|
|
|
$parent_node, |
1134
|
|
|
$var_node, |
1135
|
|
|
'=' |
1136
|
|
|
); |
1137
|
|
|
} |
1138
|
|
|
} |
1139
|
|
|
|
1140
|
|
|
$type->parent_nodes = [$property_node]; |
1141
|
|
|
} |
1142
|
|
|
} else { |
1143
|
|
|
$code_location = new CodeLocation($statements_analyzer, $stmt->name); |
1144
|
|
|
|
1145
|
|
|
$localized_property_node = new TaintNode( |
1146
|
|
|
$property_id . '-' . $code_location->file_name . ':' . $code_location->raw_file_start, |
1147
|
|
|
$property_id, |
1148
|
|
|
$code_location, |
1149
|
|
|
null |
1150
|
|
|
); |
1151
|
|
|
|
1152
|
|
|
$codebase->taint->addTaintNode($localized_property_node); |
1153
|
|
|
|
1154
|
|
|
$property_node = new TaintNode( |
1155
|
|
|
$property_id, |
1156
|
|
|
$property_id, |
1157
|
|
|
null, |
1158
|
|
|
null |
1159
|
|
|
); |
1160
|
|
|
|
1161
|
|
|
$codebase->taint->addTaintNode($property_node); |
1162
|
|
|
|
1163
|
|
|
if ($in_assignment) { |
1164
|
|
|
$codebase->taint->addPath($localized_property_node, $property_node, 'property-assignment'); |
1165
|
|
|
} else { |
1166
|
|
|
$codebase->taint->addPath($property_node, $localized_property_node, 'property-fetch'); |
1167
|
|
|
} |
1168
|
|
|
|
1169
|
|
|
$type->parent_nodes[] = $localized_property_node; |
1170
|
|
|
} |
1171
|
|
|
} |
1172
|
|
|
} |
1173
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.