1
0
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:
Tyson Andre 2017-12-14 11:22:27 -08:00 committed by Matthew Brown
parent 52c414f1d2
commit 136d48f77c
4 changed files with 102 additions and 44 deletions

View File

@ -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
*

View File

@ -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 = [];

View File

@ -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;
}
}
}

View File

@ -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
/**