diff --git a/src/Psalm/Checker/ClassLikeChecker.php b/src/Psalm/Checker/ClassLikeChecker.php index c512f0b5e..5d7c13bb1 100644 --- a/src/Psalm/Checker/ClassLikeChecker.php +++ b/src/Psalm/Checker/ClassLikeChecker.php @@ -335,6 +335,19 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc } } } + + if ($docblock_info->properties) { + foreach ($docblock_info->properties as $property) { + $pseudo_property_type = Type::parseString( + FunctionLikeChecker::fixUpLocalType( + $property['type'], + $this + ) + ); + + $storage->pseudo_instance_properties[$property['name']] = $pseudo_property_type; + } + } } } diff --git a/src/Psalm/Checker/CommentChecker.php b/src/Psalm/Checker/CommentChecker.php index 0e7cc3b64..c777afdb6 100644 --- a/src/Psalm/Checker/CommentChecker.php +++ b/src/Psalm/Checker/CommentChecker.php @@ -261,6 +261,48 @@ class CommentChecker } } + if (isset($comments['specials']['deprecated'])) { + $info->deprecated = true; + } + + if (isset($comments['specials']['property'])) { + /** @var string $property */ + foreach ($comments['specials']['property'] as $line_number => $property) { + try { + $line_parts = self::splitDocLine($property); + } catch (DocblockParseException $e) { + throw $e; + } + + if (count($line_parts) === 1 && $line_parts[0][0] === '$') { + array_unshift($line_parts, 'mixed'); + } + + if (count($line_parts) > 1) { + if (preg_match('/^' . self::TYPE_REGEX . '$/', $line_parts[0]) + && !preg_match('/\[[^\]]+\]/', $line_parts[0]) + && preg_match('/^(\.\.\.)?&?\$[A-Za-z0-9_]+,?$/', $line_parts[1]) + && !strpos($line_parts[0], '::') + && $line_parts[0][0] !== '{' + ) { + if ($line_parts[1][0] === '&') { + $line_parts[1] = substr($line_parts[1], 1); + } + + $line_parts[1] = preg_replace('/,$/', '', $line_parts[1]); + + $info->properties[] = [ + 'name' => $line_parts[1], + 'type' => $line_parts[0], + 'line_number' => $line_number + ]; + } + } else { + throw new DocblockParseException('Badly-formatted @param'); + } + } + } + return $info; } diff --git a/src/Psalm/Checker/Statements/Expression/AssignmentChecker.php b/src/Psalm/Checker/Statements/Expression/AssignmentChecker.php index 3f70ac7bc..2480456a4 100644 --- a/src/Psalm/Checker/Statements/Expression/AssignmentChecker.php +++ b/src/Psalm/Checker/Statements/Expression/AssignmentChecker.php @@ -608,6 +608,14 @@ class AssignmentChecker if ($lhs_var_id !== '$this' && MethodChecker::methodExists($lhs_type_part . '::__set', $file_checker)) { if ($var_id) { + $class_storage = ClassLikeChecker::$storage[strtolower((string)$lhs_type_part)]; + + if (isset($class_storage->pseudo_instance_properties['$' . $prop_name])) { + $class_property_types[] = clone $class_storage->pseudo_instance_properties['$' . $prop_name]; + $has_regular_setter = true; + continue; + } + $context->vars_in_scope[$var_id] = Type::getMixed(); } continue; diff --git a/src/Psalm/Checker/Statements/Expression/FetchChecker.php b/src/Psalm/Checker/Statements/Expression/FetchChecker.php index d33089264..e72c67d3a 100644 --- a/src/Psalm/Checker/Statements/Expression/FetchChecker.php +++ b/src/Psalm/Checker/Statements/Expression/FetchChecker.php @@ -240,7 +240,14 @@ class FetchChecker if ($stmt_var_id !== '$this' && MethodChecker::methodExists($lhs_type_part->value . '::__get', $file_checker) ) { - $stmt->inferredType = Type::getMixed(); + $class_storage = ClassLikeChecker::$storage[strtolower((string)$lhs_type_part)]; + + if (isset($class_storage->pseudo_instance_properties['$' . $stmt->name])) { + $stmt->inferredType = clone $class_storage->pseudo_instance_properties['$' . $stmt->name]; + } else { + $stmt->inferredType = Type::getMixed(); + } + continue; } diff --git a/src/Psalm/ClassLikeDocblockComment.php b/src/Psalm/ClassLikeDocblockComment.php index cfdfb273c..03a45cb7a 100644 --- a/src/Psalm/ClassLikeDocblockComment.php +++ b/src/Psalm/ClassLikeDocblockComment.php @@ -13,4 +13,9 @@ class ClassLikeDocblockComment * @var array> */ public $template_types = []; + + /** + * @var array + */ + public $properties = []; } diff --git a/src/Psalm/Storage/ClassLikeStorage.php b/src/Psalm/Storage/ClassLikeStorage.php index 24e3de4b4..4958a4dcf 100644 --- a/src/Psalm/Storage/ClassLikeStorage.php +++ b/src/Psalm/Storage/ClassLikeStorage.php @@ -120,6 +120,11 @@ class ClassLikeStorage */ public $properties = []; + /** + * @var array + */ + public $pseudo_instance_properties = []; + /** * @var array */ diff --git a/tests/AnnotationTest.php b/tests/AnnotationTest.php index 49752adc3..85db43e8a 100644 --- a/tests/AnnotationTest.php +++ b/tests/AnnotationTest.php @@ -50,14 +50,14 @@ class AnnotationTest extends TestCase function fooFoo() : string { return "boop"; } - + /** * @return array */ function foo2() : array { return ["hello"]; } - + /** * @return array */ @@ -112,7 +112,7 @@ class AnnotationTest extends TestCase 'goodDocblockInNamespace' => [ ' [ + 'foo = "hello";' + ], ]; } @@ -190,7 +209,27 @@ class AnnotationTest extends TestCase function fooFoo() : void { }', 'error_message' => 'InvalidDocblock' - ] + ], + 'propertyDocblockInvalidAssignment' => [ + 'foo = 5;', + 'error_message' => 'InvalidPropertyAssignment', + ], ]; } }