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