mirror of
https://github.com/danog/psalm.git
synced 2025-01-21 21:31:13 +01:00
Fix #36 - emit issues on deprecated properties
This commit is contained in:
parent
0fdf281896
commit
688a72c794
@ -88,6 +88,7 @@
|
||||
<xs:element name="ConflictingReferenceConstraint" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="DeprecatedClass" 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="DuplicateClass" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="FailedTypeResolution" type="IssueHandlerType" minOccurs="0" />
|
||||
|
@ -24,6 +24,7 @@
|
||||
<MisplacedRequiredParam errorLevel="suppress" />
|
||||
<PossiblyNullOperand errorLevel="suppress" />
|
||||
<MissingConstructor errorLevel="suppress" />
|
||||
<DeprecatedProperty errorLevel="suppress" />
|
||||
|
||||
<PossiblyUnusedVariable>
|
||||
<errorLevel type="suppress">
|
||||
|
@ -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()) {
|
||||
|
@ -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<string, string>|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 */
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
6
src/Psalm/Issue/DeprecatedProperty.php
Normal file
6
src/Psalm/Issue/DeprecatedProperty.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Psalm\Issue;
|
||||
|
||||
class DeprecatedProperty extends CodeError
|
||||
{
|
||||
}
|
@ -40,4 +40,9 @@ class PropertyStorage
|
||||
* @var bool
|
||||
*/
|
||||
public $has_default = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $deprecated = false;
|
||||
}
|
||||
|
26
src/Psalm/VarDocblockComment.php
Normal file
26
src/Psalm/VarDocblockComment.php
Normal 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;
|
||||
}
|
@ -219,6 +219,31 @@ class AnnotationTest extends TestCase
|
||||
$a = new Foo();',
|
||||
'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' => [
|
||||
'<?php
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user