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' => [
'