diff --git a/config.xsd b/config.xsd index b87f8c7db..232cff023 100644 --- a/config.xsd +++ b/config.xsd @@ -88,6 +88,7 @@ + diff --git a/psalm.xml b/psalm.xml index 6a6a0c1b9..f6315e348 100644 --- a/psalm.xml +++ b/psalm.xml @@ -24,6 +24,7 @@ + diff --git a/src/Psalm/Checker/ClassLikeChecker.php b/src/Psalm/Checker/ClassLikeChecker.php index 6638cf87d..9046391dc 100644 --- a/src/Psalm/Checker/ClassLikeChecker.php +++ b/src/Psalm/Checker/ClassLikeChecker.php @@ -1044,18 +1044,17 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc Config $config ) { $comment = $stmt->getDocComment(); - $type_in_comment = null; + $var_comment = null; $property_type_line_number = null; $storage = self::$storage[strtolower($this->fq_class_name)]; if ($comment && $comment->getText() && $config->use_docblock_types) { try { $property_type_line_number = $comment->getLine(); - $type_in_comment = CommentChecker::getTypeFromComment( + $var_comment = CommentChecker::getTypeFromComment( $comment->getText(), null, $this, - null, $storage->template_types, $property_type_line_number ); @@ -1071,7 +1070,7 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc } } - $property_group_type = $type_in_comment ?: null; + $property_group_type = $var_comment ? $var_comment->type : null; foreach ($stmt->props as $property) { $property_type_location = null; @@ -1098,20 +1097,21 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc $property_type = count($stmt->props) === 1 ? $property_group_type : clone $property_group_type; } - $storage->properties[$property->name] = new PropertyStorage(); - $storage->properties[$property->name]->is_static = (bool)$stmt->isStatic(); - $storage->properties[$property->name]->type = $property_type; - $storage->properties[$property->name]->location = new CodeLocation($this, $property); - $storage->properties[$property->name]->type_location = $property_type_location; - $storage->properties[$property->name]->has_default = $property->default ? true : false; - $storage->properties[$property->name]->suggested_type = $property_group_type ? null : $default_type; + $property_storage = $storage->properties[$property->name] = new PropertyStorage(); + $property_storage->is_static = (bool)$stmt->isStatic(); + $property_storage->type = $property_type; + $property_storage->location = new CodeLocation($this, $property); + $property_storage->type_location = $property_type_location; + $property_storage->has_default = $property->default ? true : false; + $property_storage->suggested_type = $property_group_type ? null : $default_type; + $property_storage->deprecated = $var_comment ? $var_comment->deprecated : false; if ($stmt->isPublic()) { - $storage->properties[$property->name]->visibility = self::VISIBILITY_PUBLIC; + $property_storage->visibility = self::VISIBILITY_PUBLIC; } elseif ($stmt->isProtected()) { - $storage->properties[$property->name]->visibility = self::VISIBILITY_PROTECTED; + $property_storage->visibility = self::VISIBILITY_PROTECTED; } elseif ($stmt->isPrivate()) { - $storage->properties[$property->name]->visibility = self::VISIBILITY_PRIVATE; + $property_storage->visibility = self::VISIBILITY_PRIVATE; } $property_id = $this->fq_class_name . '::$' . $property->name; @@ -1136,7 +1136,6 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc private function checkForMissingPropertyType(PhpParser\Node\Stmt\Property $stmt) { $comment = $stmt->getDocComment(); - $type_in_comment = null; $property_type_line_number = null; $storage = self::$storage[strtolower($this->fq_class_name)]; @@ -1194,14 +1193,14 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc Config $config ) { $comment = $stmt->getDocComment(); - $type_in_comment = null; + $var_comment = null; $storage = self::$storage[strtolower((string)$class_context->self)]; if ($comment && $config->use_docblock_types && count($stmt->consts) === 1) { - $type_in_comment = CommentChecker::getTypeFromComment((string) $comment, null, $this); + $var_comment = CommentChecker::getTypeFromComment((string) $comment, null, $this); } - $const_type = $type_in_comment ? $type_in_comment : Type::getMixed(); + $const_type = $var_comment ? $var_comment->type : Type::getMixed(); foreach ($stmt->consts as $const) { if ($stmt->isProtected()) { diff --git a/src/Psalm/Checker/CommentChecker.php b/src/Psalm/Checker/CommentChecker.php index 670c0c2e9..fac502ebf 100644 --- a/src/Psalm/Checker/CommentChecker.php +++ b/src/Psalm/Checker/CommentChecker.php @@ -8,6 +8,7 @@ use Psalm\Exception\TypeParseTreeException; use Psalm\FunctionDocblockComment; use Psalm\StatementsSource; use Psalm\Type; +use Psalm\VarDocblockComment; class CommentChecker { @@ -17,11 +18,10 @@ class CommentChecker * @param string $comment * @param Context|null $context * @param StatementsSource $source - * @param string $var_id * @param array|null $template_types * @param int|null $var_line_number * @param int|null $came_from_line_number what line number in $source that $comment came from - * @return Type\Union|null + * @return VarDocblockComment|null * @throws DocblockParseException If there was a problem parsing the docblock. * @psalm-suppress MixedArrayAccess */ @@ -29,14 +29,13 @@ class CommentChecker $comment, $context, StatementsSource $source, - $var_id = null, array $template_types = null, - &$var_line_number = null, + $var_line_number = null, $came_from_line_number = null ) { - $type_in_comments_var_id = null; + $var_id = null; - $type_in_comments = null; + $var_type_string = null; $comments = self::parseDocComment($comment, $var_line_number); @@ -60,7 +59,7 @@ class CommentChecker } if ($line_parts && $line_parts[0]) { - $type_in_comments = FunctionLikeChecker::fixUpLocalType( + $var_type_string = FunctionLikeChecker::fixUpLocalType( $line_parts[0], $source, $template_types @@ -71,7 +70,7 @@ class CommentChecker // support PHPStorm-style docblocks like // @var Type $variable if (count($line_parts) > 1 && $line_parts[1][0] === '$') { - $type_in_comments_var_id = $line_parts[1]; + $var_id = $line_parts[1]; } break; @@ -79,16 +78,16 @@ class CommentChecker } } - if (!$type_in_comments) { + if (!$var_type_string) { return null; } try { - $defined_type = Type::parseString($type_in_comments); + $defined_type = Type::parseString($var_type_string); } catch (TypeParseTreeException $e) { if (is_int($came_from_line_number)) { throw new DocblockParseException( - $type_in_comments . + $var_type_string . ' is not a valid type' . ' (from ' . $source->getCheckedFilePath() . @@ -97,18 +96,19 @@ class CommentChecker ')' ); } - throw new DocblockParseException($type_in_comments . ' is not a valid type'); + + throw new DocblockParseException($var_type_string . ' is not a valid type'); } $defined_type->setFromDocblock(); - if ($context && $type_in_comments_var_id && $type_in_comments_var_id !== $var_id) { - $context->vars_in_scope[$type_in_comments_var_id] = $defined_type; + $var_comment = new VarDocblockComment(); + $var_comment->type = $defined_type; + $var_comment->var_id = $var_id; + $var_comment->line_number = $var_line_number; + $var_comment->deprecated = isset($comments['specials']['deprecated']); - return null; - } - - return $defined_type; + return $var_comment; } /** @@ -421,7 +421,7 @@ class CommentChecker // Parse @specials. $matches = []; - $have_specials = preg_match_all('/^\s?@([\w\-:]+)\s*([^\n]*)/m', $docblock, $matches, PREG_SET_ORDER); + $have_specials = preg_match_all('/^\s?@([\w\-:]+)[\t ]*([^\n]*)/m', $docblock, $matches, PREG_SET_ORDER); if ($have_specials) { $docblock = preg_replace('/^\s?@([\w\-:]+)\s*([^\n]*)/m', '', $docblock); /** @var string[] $match */ diff --git a/src/Psalm/Checker/Statements/Block/ForeachChecker.php b/src/Psalm/Checker/Statements/Block/ForeachChecker.php index ef41032fc..dfee87045 100644 --- a/src/Psalm/Checker/Statements/Block/ForeachChecker.php +++ b/src/Psalm/Checker/Statements/Block/ForeachChecker.php @@ -222,12 +222,15 @@ class ForeachChecker $doc_comment_text = (string)$stmt->getDocComment(); if ($doc_comment_text) { - CommentChecker::getTypeFromComment( + $var_comment = CommentChecker::getTypeFromComment( $doc_comment_text, $foreach_context, - $statements_checker->getSource(), - null + $statements_checker->getSource() ); + + if ($var_comment && $var_comment->var_id) { + $context->vars_in_scope[$var_comment->var_id] = $var_comment->type; + } } $changed_vars = Context::getNewOrUpdatedVarIds($before_context, $foreach_context); diff --git a/src/Psalm/Checker/Statements/Expression/AssignmentChecker.php b/src/Psalm/Checker/Statements/Expression/AssignmentChecker.php index 25f3a24cc..30f342d98 100644 --- a/src/Psalm/Checker/Statements/Expression/AssignmentChecker.php +++ b/src/Psalm/Checker/Statements/Expression/AssignmentChecker.php @@ -14,6 +14,7 @@ use Psalm\Checker\StatementsChecker; use Psalm\Checker\TypeChecker; use Psalm\CodeLocation; use Psalm\Context; +use Psalm\Issue\DeprecatedProperty; use Psalm\Issue\FailedTypeResolution; use Psalm\Issue\InvalidArrayAssignment; use Psalm\Issue\InvalidPropertyAssignment; @@ -73,19 +74,21 @@ class AssignmentChecker $statements_checker ); + $var_comment = null; + if ($doc_comment) { - $null = null; - $type_in_comments = CommentChecker::getTypeFromComment( + $var_comment = CommentChecker::getTypeFromComment( $doc_comment, $context, $statements_checker->getSource(), - $var_id, null, - $null, + null, $came_from_line_number ); - } else { - $type_in_comments = null; + + if ($var_comment && $var_comment->var_id && $var_comment->var_id !== $var_id) { + $context->vars_in_scope[$var_comment->var_id] = $var_comment->type; + } } if ($assign_value && ExpressionChecker::analyze($statements_checker, $assign_value, $context) === false) { @@ -95,14 +98,17 @@ class AssignmentChecker } // if we're not exiting immediately, make everything mixed - $context->vars_in_scope[$var_id] = $type_in_comments ?: Type::getMixed(); + $context->vars_in_scope[$var_id] = + $var_comment && (!$var_comment->var_id || $var_comment->var_id === $var_id) + ? $var_comment->type + : Type::getMixed(); } return false; } - if ($type_in_comments) { - $assign_value_type = $type_in_comments; + if ($var_comment && (!$var_comment->var_id || $var_comment->var_id === $var_id)) { + $assign_value_type = $var_comment->type; } elseif (!$assign_value_type) { if (isset($assign_value->inferredType)) { /** @var Type\Union */ @@ -667,6 +673,18 @@ class AssignmentChecker $property_storage = ClassLikeChecker::$storage[strtolower((string)$declaring_property_class)]->properties[$stmt->name]; + if ($property_storage->deprecated) { + if (IssueBuffer::accepts( + new DeprecatedProperty( + $property_id . ' is marked deprecated', + new CodeLocation($statements_checker->getSource(), $stmt) + ), + $statements_checker->getSuppressedIssues() + )) { + // fall through + } + } + $class_property_type = $property_storage->type; if ($class_property_type === false) { diff --git a/src/Psalm/Checker/Statements/Expression/FetchChecker.php b/src/Psalm/Checker/Statements/Expression/FetchChecker.php index 57365abaf..f529ec8f4 100644 --- a/src/Psalm/Checker/Statements/Expression/FetchChecker.php +++ b/src/Psalm/Checker/Statements/Expression/FetchChecker.php @@ -11,6 +11,7 @@ use Psalm\Checker\StatementsChecker; use Psalm\Checker\TraitChecker; use Psalm\CodeLocation; use Psalm\Context; +use Psalm\Issue\DeprecatedProperty; use Psalm\Issue\InaccessibleClassConstant; use Psalm\Issue\InvalidArrayAccess; use Psalm\Issue\InvalidArrayAssignment; @@ -287,6 +288,18 @@ class FetchChecker $property_storage = $declaring_class_storage->properties[$stmt->name]; + if ($property_storage->deprecated) { + if (IssueBuffer::accepts( + new DeprecatedProperty( + $property_id . ' is marked deprecated', + new CodeLocation($statements_checker->getSource(), $stmt) + ), + $statements_checker->getSuppressedIssues() + )) { + // fall through + } + } + $class_property_type = $property_storage->type; if ($class_property_type === false) { diff --git a/src/Psalm/Checker/Statements/ExpressionChecker.php b/src/Psalm/Checker/Statements/ExpressionChecker.php index 491e900ad..c305f3db0 100644 --- a/src/Psalm/Checker/Statements/ExpressionChecker.php +++ b/src/Psalm/Checker/Statements/ExpressionChecker.php @@ -1623,14 +1623,18 @@ class ExpressionChecker ) { $doc_comment_text = (string)$stmt->getDocComment(); + $var_comment = null; + if ($doc_comment_text) { - $type_in_comments = CommentChecker::getTypeFromComment( + $var_comment = CommentChecker::getTypeFromComment( $doc_comment_text, $context, - $statements_checker->getSource() + $statements_checker ); - } else { - $type_in_comments = null; + + if ($var_comment && $var_comment->var_id) { + $context->vars_in_scope[$var_comment->var_id] = $var_comment->type; + } } if ($stmt->key) { @@ -1644,8 +1648,8 @@ class ExpressionChecker return false; } - if ($type_in_comments) { - $stmt->inferredType = $type_in_comments; + if ($var_comment && !$var_comment->var_id) { + $stmt->inferredType = $var_comment->type; } elseif (isset($stmt->value->inferredType)) { $stmt->inferredType = $stmt->value->inferredType; } else { diff --git a/src/Psalm/Checker/StatementsChecker.php b/src/Psalm/Checker/StatementsChecker.php index 2c02c5c96..259f68ab8 100644 --- a/src/Psalm/Checker/StatementsChecker.php +++ b/src/Psalm/Checker/StatementsChecker.php @@ -299,11 +299,15 @@ class StatementsChecker extends SourceChecker implements StatementsSource $class_checker->analyze(null, $global_context); } elseif ($stmt instanceof PhpParser\Node\Stmt\Nop) { if ((string)$stmt->getDocComment()) { - CommentChecker::getTypeFromComment( + $var_comment = CommentChecker::getTypeFromComment( (string)$stmt->getDocComment(), $context, $this->getSource() ); + + if ($var_comment && $var_comment->var_id) { + $context->vars_in_scope[$var_comment->var_id] = $var_comment->type; + } } } elseif ($stmt instanceof PhpParser\Node\Stmt\Goto_) { // do nothing @@ -673,14 +677,18 @@ class StatementsChecker extends SourceChecker implements StatementsSource { $doc_comment_text = (string)$stmt->getDocComment(); + $var_comment = null; + if ($doc_comment_text) { - $type_in_comments = CommentChecker::getTypeFromComment( + $var_comment = CommentChecker::getTypeFromComment( $doc_comment_text, $context, $this->source ); - } else { - $type_in_comments = null; + + if ($var_comment && $var_comment->var_id) { + $context->vars_in_scope[$var_comment->var_id] = $var_comment->type; + } } if ($stmt->expr) { @@ -688,8 +696,8 @@ class StatementsChecker extends SourceChecker implements StatementsSource return false; } - if ($type_in_comments) { - $stmt->inferredType = $type_in_comments; + if ($var_comment && !$var_comment->var_id) { + $stmt->inferredType = $var_comment->type; } elseif (isset($stmt->expr->inferredType)) { $stmt->inferredType = $stmt->expr->inferredType; } else { diff --git a/src/Psalm/Issue/DeprecatedProperty.php b/src/Psalm/Issue/DeprecatedProperty.php new file mode 100644 index 000000000..4516210da --- /dev/null +++ b/src/Psalm/Issue/DeprecatedProperty.php @@ -0,0 +1,6 @@ + 'DeprecatedClass' ], + 'deprecatedPropertyGet' => [ + 'foo;', + 'error_message' => 'DeprecatedProperty' + ], + 'deprecatedPropertySet' => [ + 'foo = 5;', + 'error_message' => 'DeprecatedProperty' + ], 'invalidDocblockParam' => [ '