1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-22 05:41:20 +01:00

Fix #36 - emit issues on deprecated properties

This commit is contained in:
Matthew Brown 2017-05-25 01:32:34 -04:00
parent 0fdf281896
commit 688a72c794
13 changed files with 170 additions and 61 deletions

View File

@ -88,6 +88,7 @@
<xs:element name="ConflictingReferenceConstraint" type="IssueHandlerType" minOccurs="0" /> <xs:element name="ConflictingReferenceConstraint" type="IssueHandlerType" minOccurs="0" />
<xs:element name="DeprecatedClass" type="IssueHandlerType" minOccurs="0" /> <xs:element name="DeprecatedClass" type="IssueHandlerType" minOccurs="0" />
<xs:element name="DeprecatedMethod" type="IssueHandlerType" minOccurs="0" /> <xs:element name="DeprecatedMethod" type="IssueHandlerType" minOccurs="0" />
<xs:element name="DeprecatedProperty" type="IssueHandlerType" minOccurs="0" />
<xs:element name="DuplicateParam" type="IssueHandlerType" minOccurs="0" /> <xs:element name="DuplicateParam" type="IssueHandlerType" minOccurs="0" />
<xs:element name="DuplicateClass" type="IssueHandlerType" minOccurs="0" /> <xs:element name="DuplicateClass" type="IssueHandlerType" minOccurs="0" />
<xs:element name="FailedTypeResolution" type="IssueHandlerType" minOccurs="0" /> <xs:element name="FailedTypeResolution" type="IssueHandlerType" minOccurs="0" />

View File

@ -24,6 +24,7 @@
<MisplacedRequiredParam errorLevel="suppress" /> <MisplacedRequiredParam errorLevel="suppress" />
<PossiblyNullOperand errorLevel="suppress" /> <PossiblyNullOperand errorLevel="suppress" />
<MissingConstructor errorLevel="suppress" /> <MissingConstructor errorLevel="suppress" />
<DeprecatedProperty errorLevel="suppress" />
<PossiblyUnusedVariable> <PossiblyUnusedVariable>
<errorLevel type="suppress"> <errorLevel type="suppress">

View File

@ -1044,18 +1044,17 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
Config $config Config $config
) { ) {
$comment = $stmt->getDocComment(); $comment = $stmt->getDocComment();
$type_in_comment = null; $var_comment = null;
$property_type_line_number = null; $property_type_line_number = null;
$storage = self::$storage[strtolower($this->fq_class_name)]; $storage = self::$storage[strtolower($this->fq_class_name)];
if ($comment && $comment->getText() && $config->use_docblock_types) { if ($comment && $comment->getText() && $config->use_docblock_types) {
try { try {
$property_type_line_number = $comment->getLine(); $property_type_line_number = $comment->getLine();
$type_in_comment = CommentChecker::getTypeFromComment( $var_comment = CommentChecker::getTypeFromComment(
$comment->getText(), $comment->getText(),
null, null,
$this, $this,
null,
$storage->template_types, $storage->template_types,
$property_type_line_number $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) { foreach ($stmt->props as $property) {
$property_type_location = null; $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; $property_type = count($stmt->props) === 1 ? $property_group_type : clone $property_group_type;
} }
$storage->properties[$property->name] = new PropertyStorage(); $property_storage = $storage->properties[$property->name] = new PropertyStorage();
$storage->properties[$property->name]->is_static = (bool)$stmt->isStatic(); $property_storage->is_static = (bool)$stmt->isStatic();
$storage->properties[$property->name]->type = $property_type; $property_storage->type = $property_type;
$storage->properties[$property->name]->location = new CodeLocation($this, $property); $property_storage->location = new CodeLocation($this, $property);
$storage->properties[$property->name]->type_location = $property_type_location; $property_storage->type_location = $property_type_location;
$storage->properties[$property->name]->has_default = $property->default ? true : false; $property_storage->has_default = $property->default ? true : false;
$storage->properties[$property->name]->suggested_type = $property_group_type ? null : $default_type; $property_storage->suggested_type = $property_group_type ? null : $default_type;
$property_storage->deprecated = $var_comment ? $var_comment->deprecated : false;
if ($stmt->isPublic()) { if ($stmt->isPublic()) {
$storage->properties[$property->name]->visibility = self::VISIBILITY_PUBLIC; $property_storage->visibility = self::VISIBILITY_PUBLIC;
} elseif ($stmt->isProtected()) { } elseif ($stmt->isProtected()) {
$storage->properties[$property->name]->visibility = self::VISIBILITY_PROTECTED; $property_storage->visibility = self::VISIBILITY_PROTECTED;
} elseif ($stmt->isPrivate()) { } 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; $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) private function checkForMissingPropertyType(PhpParser\Node\Stmt\Property $stmt)
{ {
$comment = $stmt->getDocComment(); $comment = $stmt->getDocComment();
$type_in_comment = null;
$property_type_line_number = null; $property_type_line_number = null;
$storage = self::$storage[strtolower($this->fq_class_name)]; $storage = self::$storage[strtolower($this->fq_class_name)];
@ -1194,14 +1193,14 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
Config $config Config $config
) { ) {
$comment = $stmt->getDocComment(); $comment = $stmt->getDocComment();
$type_in_comment = null; $var_comment = null;
$storage = self::$storage[strtolower((string)$class_context->self)]; $storage = self::$storage[strtolower((string)$class_context->self)];
if ($comment && $config->use_docblock_types && count($stmt->consts) === 1) { 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) { foreach ($stmt->consts as $const) {
if ($stmt->isProtected()) { if ($stmt->isProtected()) {

View File

@ -8,6 +8,7 @@ use Psalm\Exception\TypeParseTreeException;
use Psalm\FunctionDocblockComment; use Psalm\FunctionDocblockComment;
use Psalm\StatementsSource; use Psalm\StatementsSource;
use Psalm\Type; use Psalm\Type;
use Psalm\VarDocblockComment;
class CommentChecker class CommentChecker
{ {
@ -17,11 +18,10 @@ class CommentChecker
* @param string $comment * @param string $comment
* @param Context|null $context * @param Context|null $context
* @param StatementsSource $source * @param StatementsSource $source
* @param string $var_id
* @param array<string, string>|null $template_types * @param array<string, string>|null $template_types
* @param int|null $var_line_number * @param int|null $var_line_number
* @param int|null $came_from_line_number what line number in $source that $comment came from * @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. * @throws DocblockParseException If there was a problem parsing the docblock.
* @psalm-suppress MixedArrayAccess * @psalm-suppress MixedArrayAccess
*/ */
@ -29,14 +29,13 @@ class CommentChecker
$comment, $comment,
$context, $context,
StatementsSource $source, StatementsSource $source,
$var_id = null,
array $template_types = null, array $template_types = null,
&$var_line_number = null, $var_line_number = null,
$came_from_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); $comments = self::parseDocComment($comment, $var_line_number);
@ -60,7 +59,7 @@ class CommentChecker
} }
if ($line_parts && $line_parts[0]) { if ($line_parts && $line_parts[0]) {
$type_in_comments = FunctionLikeChecker::fixUpLocalType( $var_type_string = FunctionLikeChecker::fixUpLocalType(
$line_parts[0], $line_parts[0],
$source, $source,
$template_types $template_types
@ -71,7 +70,7 @@ class CommentChecker
// support PHPStorm-style docblocks like // support PHPStorm-style docblocks like
// @var Type $variable // @var Type $variable
if (count($line_parts) > 1 && $line_parts[1][0] === '$') { if (count($line_parts) > 1 && $line_parts[1][0] === '$') {
$type_in_comments_var_id = $line_parts[1]; $var_id = $line_parts[1];
} }
break; break;
@ -79,16 +78,16 @@ class CommentChecker
} }
} }
if (!$type_in_comments) { if (!$var_type_string) {
return null; return null;
} }
try { try {
$defined_type = Type::parseString($type_in_comments); $defined_type = Type::parseString($var_type_string);
} catch (TypeParseTreeException $e) { } catch (TypeParseTreeException $e) {
if (is_int($came_from_line_number)) { if (is_int($came_from_line_number)) {
throw new DocblockParseException( throw new DocblockParseException(
$type_in_comments . $var_type_string .
' is not a valid type' . ' is not a valid type' .
' (from ' . ' (from ' .
$source->getCheckedFilePath() . $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(); $defined_type->setFromDocblock();
if ($context && $type_in_comments_var_id && $type_in_comments_var_id !== $var_id) { $var_comment = new VarDocblockComment();
$context->vars_in_scope[$type_in_comments_var_id] = $defined_type; $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 $var_comment;
}
return $defined_type;
} }
/** /**
@ -421,7 +421,7 @@ class CommentChecker
// Parse @specials. // Parse @specials.
$matches = []; $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) { if ($have_specials) {
$docblock = preg_replace('/^\s?@([\w\-:]+)\s*([^\n]*)/m', '', $docblock); $docblock = preg_replace('/^\s?@([\w\-:]+)\s*([^\n]*)/m', '', $docblock);
/** @var string[] $match */ /** @var string[] $match */

View File

@ -222,12 +222,15 @@ class ForeachChecker
$doc_comment_text = (string)$stmt->getDocComment(); $doc_comment_text = (string)$stmt->getDocComment();
if ($doc_comment_text) { if ($doc_comment_text) {
CommentChecker::getTypeFromComment( $var_comment = CommentChecker::getTypeFromComment(
$doc_comment_text, $doc_comment_text,
$foreach_context, $foreach_context,
$statements_checker->getSource(), $statements_checker->getSource()
null
); );
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); $changed_vars = Context::getNewOrUpdatedVarIds($before_context, $foreach_context);

View File

@ -14,6 +14,7 @@ use Psalm\Checker\StatementsChecker;
use Psalm\Checker\TypeChecker; use Psalm\Checker\TypeChecker;
use Psalm\CodeLocation; use Psalm\CodeLocation;
use Psalm\Context; use Psalm\Context;
use Psalm\Issue\DeprecatedProperty;
use Psalm\Issue\FailedTypeResolution; use Psalm\Issue\FailedTypeResolution;
use Psalm\Issue\InvalidArrayAssignment; use Psalm\Issue\InvalidArrayAssignment;
use Psalm\Issue\InvalidPropertyAssignment; use Psalm\Issue\InvalidPropertyAssignment;
@ -73,19 +74,21 @@ class AssignmentChecker
$statements_checker $statements_checker
); );
$var_comment = null;
if ($doc_comment) { if ($doc_comment) {
$null = null; $var_comment = CommentChecker::getTypeFromComment(
$type_in_comments = CommentChecker::getTypeFromComment(
$doc_comment, $doc_comment,
$context, $context,
$statements_checker->getSource(), $statements_checker->getSource(),
$var_id,
null, null,
$null, null,
$came_from_line_number $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) { 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 // 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; return false;
} }
if ($type_in_comments) { if ($var_comment && (!$var_comment->var_id || $var_comment->var_id === $var_id)) {
$assign_value_type = $type_in_comments; $assign_value_type = $var_comment->type;
} elseif (!$assign_value_type) { } elseif (!$assign_value_type) {
if (isset($assign_value->inferredType)) { if (isset($assign_value->inferredType)) {
/** @var Type\Union */ /** @var Type\Union */
@ -667,6 +673,18 @@ class AssignmentChecker
$property_storage = $property_storage =
ClassLikeChecker::$storage[strtolower((string)$declaring_property_class)]->properties[$stmt->name]; 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; $class_property_type = $property_storage->type;
if ($class_property_type === false) { if ($class_property_type === false) {

View File

@ -11,6 +11,7 @@ use Psalm\Checker\StatementsChecker;
use Psalm\Checker\TraitChecker; use Psalm\Checker\TraitChecker;
use Psalm\CodeLocation; use Psalm\CodeLocation;
use Psalm\Context; use Psalm\Context;
use Psalm\Issue\DeprecatedProperty;
use Psalm\Issue\InaccessibleClassConstant; use Psalm\Issue\InaccessibleClassConstant;
use Psalm\Issue\InvalidArrayAccess; use Psalm\Issue\InvalidArrayAccess;
use Psalm\Issue\InvalidArrayAssignment; use Psalm\Issue\InvalidArrayAssignment;
@ -287,6 +288,18 @@ class FetchChecker
$property_storage = $declaring_class_storage->properties[$stmt->name]; $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; $class_property_type = $property_storage->type;
if ($class_property_type === false) { if ($class_property_type === false) {

View File

@ -1623,14 +1623,18 @@ class ExpressionChecker
) { ) {
$doc_comment_text = (string)$stmt->getDocComment(); $doc_comment_text = (string)$stmt->getDocComment();
$var_comment = null;
if ($doc_comment_text) { if ($doc_comment_text) {
$type_in_comments = CommentChecker::getTypeFromComment( $var_comment = CommentChecker::getTypeFromComment(
$doc_comment_text, $doc_comment_text,
$context, $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) { if ($stmt->key) {
@ -1644,8 +1648,8 @@ class ExpressionChecker
return false; return false;
} }
if ($type_in_comments) { if ($var_comment && !$var_comment->var_id) {
$stmt->inferredType = $type_in_comments; $stmt->inferredType = $var_comment->type;
} elseif (isset($stmt->value->inferredType)) { } elseif (isset($stmt->value->inferredType)) {
$stmt->inferredType = $stmt->value->inferredType; $stmt->inferredType = $stmt->value->inferredType;
} else { } else {

View File

@ -299,11 +299,15 @@ class StatementsChecker extends SourceChecker implements StatementsSource
$class_checker->analyze(null, $global_context); $class_checker->analyze(null, $global_context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Nop) { } elseif ($stmt instanceof PhpParser\Node\Stmt\Nop) {
if ((string)$stmt->getDocComment()) { if ((string)$stmt->getDocComment()) {
CommentChecker::getTypeFromComment( $var_comment = CommentChecker::getTypeFromComment(
(string)$stmt->getDocComment(), (string)$stmt->getDocComment(),
$context, $context,
$this->getSource() $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_) { } elseif ($stmt instanceof PhpParser\Node\Stmt\Goto_) {
// do nothing // do nothing
@ -673,14 +677,18 @@ class StatementsChecker extends SourceChecker implements StatementsSource
{ {
$doc_comment_text = (string)$stmt->getDocComment(); $doc_comment_text = (string)$stmt->getDocComment();
$var_comment = null;
if ($doc_comment_text) { if ($doc_comment_text) {
$type_in_comments = CommentChecker::getTypeFromComment( $var_comment = CommentChecker::getTypeFromComment(
$doc_comment_text, $doc_comment_text,
$context, $context,
$this->source $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) { if ($stmt->expr) {
@ -688,8 +696,8 @@ class StatementsChecker extends SourceChecker implements StatementsSource
return false; return false;
} }
if ($type_in_comments) { if ($var_comment && !$var_comment->var_id) {
$stmt->inferredType = $type_in_comments; $stmt->inferredType = $var_comment->type;
} elseif (isset($stmt->expr->inferredType)) { } elseif (isset($stmt->expr->inferredType)) {
$stmt->inferredType = $stmt->expr->inferredType; $stmt->inferredType = $stmt->expr->inferredType;
} else { } else {

View File

@ -0,0 +1,6 @@
<?php
namespace Psalm\Issue;
class DeprecatedProperty extends CodeError
{
}

View File

@ -40,4 +40,9 @@ class PropertyStorage
* @var bool * @var bool
*/ */
public $has_default = false; public $has_default = false;
/**
* @var bool
*/
public $deprecated = false;
} }

View File

@ -0,0 +1,26 @@
<?php
namespace Psalm;
class VarDocblockComment
{
/**
* @var Type\Union
*/
public $type;
/**
* @var string|null
*/
public $var_id = null;
/**
* @var int|null
*/
public $line_number;
/**
* Whether or not the function is deprecated
* @var boolean
*/
public $deprecated = false;
}

View File

@ -219,6 +219,31 @@ class AnnotationTest extends TestCase
$a = new Foo();', $a = new Foo();',
'error_message' => 'DeprecatedClass' 'error_message' => 'DeprecatedClass'
], ],
'deprecatedPropertyGet' => [
'<?php
class A{
/**
* @deprecated
* @var ?int
*/
public $foo;
}
echo (new A)->foo;',
'error_message' => 'DeprecatedProperty'
],
'deprecatedPropertySet' => [
'<?php
class A{
/**
* @deprecated
* @var ?int
*/
public $foo;
}
$a = new A;
$a->foo = 5;',
'error_message' => 'DeprecatedProperty'
],
'invalidDocblockParam' => [ 'invalidDocblockParam' => [
'<?php '<?php
/** /**