mirror of
https://github.com/danog/psalm.git
synced 2025-01-21 21:31:13 +01:00
Support @property-read and @property-write as new tags. (#317)
In combination with `@psalm-seal-properties`, this can be used to have in-depth checking of magic properties.
This commit is contained in:
parent
52c414f1d2
commit
136d48f77c
@ -307,51 +307,66 @@ class CommentChecker
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if ($line_parts[0][0] === '$' && $line_parts[0] !== '$this') {
|
||||
throw new IncorrectDocblockException('Misplaced variable');
|
||||
}
|
||||
|
||||
$line_parts[1] = preg_replace('/,$/', '', $line_parts[1]);
|
||||
|
||||
$info->properties[] = [
|
||||
'name' => $line_parts[1],
|
||||
'type' => $line_parts[0],
|
||||
'line_number' => (int)$line_number,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
throw new DocblockParseException('Badly-formatted @param');
|
||||
}
|
||||
}
|
||||
}
|
||||
self::addMagicPropertyToInfo($info, $comments['specials'], 'property');
|
||||
self::addMagicPropertyToInfo($info, $comments['specials'], 'property-read');
|
||||
self::addMagicPropertyToInfo($info, $comments['specials'], 'property-write');
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ClassLikeDocblockComment $info
|
||||
* @param array<string,array<mixed,string>> $specials
|
||||
* @param string $property_tag ('property', 'property-read', or 'property-write')
|
||||
* @return void
|
||||
* @throws DocblockParseException
|
||||
*/
|
||||
protected static function addMagicPropertyToInfo(ClassLikeDocblockComment $info, array $specials, $property_tag)
|
||||
{
|
||||
$magic_property_comments = isset($specials[$property_tag]) ? $specials[$property_tag] : [];
|
||||
foreach ($magic_property_comments 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);
|
||||
}
|
||||
|
||||
if ($line_parts[0][0] === '$' && $line_parts[0] !== '$this') {
|
||||
throw new IncorrectDocblockException('Misplaced variable');
|
||||
}
|
||||
|
||||
$line_parts[1] = preg_replace('/,$/', '', $line_parts[1]);
|
||||
|
||||
$info->properties[] = [
|
||||
'name' => $line_parts[1],
|
||||
'type' => $line_parts[0],
|
||||
'line_number' => $line_number,
|
||||
'tag' => $property_tag,
|
||||
];
|
||||
} else {
|
||||
throw new DocblockParseException('Badly-formatted @property');
|
||||
}
|
||||
} else {
|
||||
throw new DocblockParseException('Badly-formatted @property');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $return_block
|
||||
*
|
||||
|
@ -16,7 +16,7 @@ class ClassLikeDocblockComment
|
||||
public $template_types = [];
|
||||
|
||||
/**
|
||||
* @var array<int, array{name:string, type:string, line_number: int}>
|
||||
* @var array<int, array{name:string, type:string, tag:string, line_number:int}>
|
||||
*/
|
||||
public $properties = [];
|
||||
|
||||
|
@ -222,8 +222,12 @@ class DependencyFinderVisitor extends PhpParser\NodeVisitorAbstract implements P
|
||||
$pseudo_property_type = Type::parseString($property['type']);
|
||||
$pseudo_property_type->setFromDocblock();
|
||||
|
||||
$storage->pseudo_property_set_types[$property['name']] = $pseudo_property_type;
|
||||
$storage->pseudo_property_get_types[$property['name']] = $pseudo_property_type;
|
||||
if ($property['tag'] !== 'property-read') {
|
||||
$storage->pseudo_property_set_types[$property['name']] = $pseudo_property_type;
|
||||
}
|
||||
if ($property['tag'] !== 'property-write') {
|
||||
$storage->pseudo_property_get_types[$property['name']] = $pseudo_property_type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -445,6 +445,27 @@ class AnnotationTest extends TestCase
|
||||
$a->foo = 5;',
|
||||
'error_message' => 'InvalidPropertyAssignment',
|
||||
],
|
||||
'propertyWriteDocblockInvalidAssignment' => [
|
||||
'<?php
|
||||
/**
|
||||
* @property-write string $foo
|
||||
*/
|
||||
class A {
|
||||
public function __get(string $name) : ?string {
|
||||
if ($name === "foo") {
|
||||
return "hello";
|
||||
}
|
||||
}
|
||||
|
||||
/** @param mixed $value */
|
||||
public function __set(string $name, $value) : void {
|
||||
}
|
||||
}
|
||||
|
||||
$a = new A();
|
||||
$a->foo = 5;',
|
||||
'error_message' => 'InvalidPropertyAssignment',
|
||||
],
|
||||
'propertySealedDocblockUndefinedPropertyAssignment' => [
|
||||
'<?php
|
||||
/**
|
||||
@ -489,6 +510,24 @@ class AnnotationTest extends TestCase
|
||||
$a->foo = 5;',
|
||||
'error_message' => 'InvalidPropertyAssignment',
|
||||
],
|
||||
'propertyReadInvalidFetch' => [
|
||||
'<?php
|
||||
/**
|
||||
* @property-read string $foo
|
||||
*/
|
||||
class A {
|
||||
/** @return mixed */
|
||||
public function __get(string $name) {
|
||||
if ($name === "foo") {
|
||||
return "hello";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$a = new A();
|
||||
echo count($a->foo);',
|
||||
'error_message' => 'InvalidArgument',
|
||||
],
|
||||
'propertySealedDocblockUndefinedPropertyFetch' => [
|
||||
'<?php
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user