Skip to content

Commit

Permalink
Fix #1897 - add support for unions in @psalm-assert annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
muglug committed Jul 4, 2019
1 parent df3d7e1 commit efe096c
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 54 deletions.
4 changes: 2 additions & 2 deletions src/Psalm/Internal/PluginManager/ComposerLock.php
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ public function __construct(array $file_names)


/** /**
* @param mixed $package * @param mixed $package
* @psalm-assert-if-true array{type:'psalm-plugin',name:string,extra:array{psalm:array{pluginClass:string}}} * @psalm-assert-if-true array $package
* $package
*/ */
public function isPlugin($package): bool public function isPlugin($package): bool
{ {
Expand Down Expand Up @@ -79,6 +78,7 @@ private function getAllPluginPackages(): array
/** @psalm-suppress MixedAssignment */ /** @psalm-suppress MixedAssignment */
foreach ($packages as $package) { foreach ($packages as $package) {
if ($this->isPlugin($package)) { if ($this->isPlugin($package)) {
/** @var array{type:'psalm-plugin',name:string,extra:array{psalm:array{pluginClass:string}}} */
$ret[] = $package; $ret[] = $package;
} }
} }
Expand Down
141 changes: 90 additions & 51 deletions src/Psalm/Internal/Visitor/ReflectorVisitor.php
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -2080,108 +2080,79 @@ private function registerFunctionLike(PhpParser\Node\FunctionLike $stmt, $fake_m
$storage->assertions = []; $storage->assertions = [];


foreach ($docblock_info->assertions as $assertion) { foreach ($docblock_info->assertions as $assertion) {
$assertion_type = $assertion['type']; $assertion_type_parts = $this->getAssertionParts($assertion['type'], $stmt, $template_types);

if (strpos($assertion_type, '|') !== false) {
if (IssueBuffer::accepts(
new InvalidDocblock(
'Docblock assertions cannot contain | characters',
new CodeLocation($this->file_scanner, $stmt, null, true)
)
)) {
}

continue;
}

if (strpos($assertion_type, '\'') !== false || strpos($assertion_type, '"') !== false) {
if (IssueBuffer::accepts(
new InvalidDocblock(
'Docblock assertions cannot contain quotes',
new CodeLocation($this->file_scanner, $stmt, null, true)
)
)) {
}


if (!$assertion_type_parts) {
continue; continue;
} }


$prefix = '';
if ($assertion_type[0] === '!') {
$prefix = '!';
$assertion_type = substr($assertion_type, 1);
}
if ($assertion_type[0] === '~') {
$prefix .= '~';
$assertion_type = substr($assertion_type, 1);
}
if ($assertion_type[0] === '=') {
$prefix .= '=';
$assertion_type = substr($assertion_type, 1);
}

if ($assertion_type !== 'falsy'
&& !isset($template_types[$assertion_type])
&& !isset(Type::PSALM_RESERVED_WORDS[$assertion_type])
) {
$assertion_type = Type::getFQCLNFromString($assertion_type, $this->aliases);
}

foreach ($storage->params as $i => $param) { foreach ($storage->params as $i => $param) {
if ($param->name === $assertion['param_name']) { if ($param->name === $assertion['param_name']) {
$storage->assertions[] = new \Psalm\Storage\Assertion( $storage->assertions[] = new \Psalm\Storage\Assertion(
$i, $i,
[[$prefix . $assertion_type]] [$assertion_type_parts]
); );
continue 2; continue 2;
} }
} }


$storage->assertions[] = new \Psalm\Storage\Assertion( $storage->assertions[] = new \Psalm\Storage\Assertion(
'$' . $assertion['param_name'], '$' . $assertion['param_name'],
[[$prefix . $assertion_type]] [$assertion_type_parts]
); );
} }
} }


if ($docblock_info->if_true_assertions) { if ($docblock_info->if_true_assertions) {
$storage->assertions = []; $storage->if_true_assertions = [];


foreach ($docblock_info->if_true_assertions as $assertion) { foreach ($docblock_info->if_true_assertions as $assertion) {
$assertion_type_parts = $this->getAssertionParts($assertion['type'], $stmt, $template_types);

if (!$assertion_type_parts) {
continue;
}

foreach ($storage->params as $i => $param) { foreach ($storage->params as $i => $param) {
if ($param->name === $assertion['param_name']) { if ($param->name === $assertion['param_name']) {
$storage->if_true_assertions[] = new \Psalm\Storage\Assertion( $storage->if_true_assertions[] = new \Psalm\Storage\Assertion(
$i, $i,
[[$assertion['type']]] [$assertion_type_parts]
); );
continue 2; continue 2;
} }
} }


$storage->if_true_assertions[] = new \Psalm\Storage\Assertion( $storage->if_true_assertions[] = new \Psalm\Storage\Assertion(
'$' . $assertion['param_name'], '$' . $assertion['param_name'],
[[$assertion['type']]] [$assertion_type_parts]
); );
} }
} }


if ($docblock_info->if_false_assertions) { if ($docblock_info->if_false_assertions) {
$storage->assertions = []; $storage->if_false_assertions = [];


foreach ($docblock_info->if_false_assertions as $assertion) { foreach ($docblock_info->if_false_assertions as $assertion) {
$assertion_type_parts = $this->getAssertionParts($assertion['type'], $stmt, $template_types);

if (!$assertion_type_parts) {
continue;
}

foreach ($storage->params as $i => $param) { foreach ($storage->params as $i => $param) {
if ($param->name === $assertion['param_name']) { if ($param->name === $assertion['param_name']) {
$storage->if_false_assertions[] = new \Psalm\Storage\Assertion( $storage->if_false_assertions[] = new \Psalm\Storage\Assertion(
$i, $i,
[[$assertion['type']]] [$assertion_type_parts]
); );
continue 2; continue 2;
} }
} }


$storage->if_false_assertions[] = new \Psalm\Storage\Assertion( $storage->if_false_assertions[] = new \Psalm\Storage\Assertion(
'$' . $assertion['param_name'], '$' . $assertion['param_name'],
[[$assertion['type']]] [$assertion_type_parts]
); );
} }
} }
Expand Down Expand Up @@ -2410,6 +2381,74 @@ function (FunctionLikeParameter $p) {
return $storage; return $storage;
} }


/**
* @return ?array<int, string>
*/
private function getAssertionParts(
string $assertion_type,
PhpParser\Node\FunctionLike $stmt,
?array $template_types
) : ?array {
$is_union = false;

if (strpos($assertion_type, '|') !== false) {
$is_union = true;
}

if (strpos($assertion_type, '\'') !== false || strpos($assertion_type, '"') !== false) {
if (IssueBuffer::accepts(
new InvalidDocblock(
'Docblock assertions cannot contain quotes',
new CodeLocation($this->file_scanner, $stmt, null, true)
)
)) {
}

return null;
}

$prefix = '';
if ($assertion_type[0] === '!') {
$prefix = '!';
$assertion_type = substr($assertion_type, 1);
}
if ($assertion_type[0] === '~') {
$prefix .= '~';
$assertion_type = substr($assertion_type, 1);
}
if ($assertion_type[0] === '=') {
$prefix .= '=';
$assertion_type = substr($assertion_type, 1);
}

if ($prefix && $is_union) {
if (IssueBuffer::accepts(
new InvalidDocblock(
'Docblock assertions cannot contain | characters together with ' . $prefix,
new CodeLocation($this->file_scanner, $stmt, null, true)
)
)) {
}

return null;
}

$assertion_type_parts = explode('|', $assertion_type);

foreach ($assertion_type_parts as $i => $assertion_type_part) {
if ($assertion_type_part !== 'falsy'
&& !isset($template_types[$assertion_type_part])
&& !isset(Type::PSALM_RESERVED_WORDS[$assertion_type_part])
) {
$assertion_type_parts[$i] = $prefix . Type::getFQCLNFromString($assertion_type_part, $this->aliases);
} else {
$assertion_type_parts[$i] = $prefix . $assertion_type_part;
}
}

return $assertion_type_parts;
}

/** /**
* @param PhpParser\Node\Param $param * @param PhpParser\Node\Param $param
* *
Expand Down
2 changes: 1 addition & 1 deletion src/Psalm/Storage/Assertion.php
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Assertion
* @param string|int $var_id * @param string|int $var_id
* @param array<int, array<int, string>> $rule * @param array<int, array<int, string>> $rule
*/ */
public function __construct($var_id, $rule) public function __construct($var_id, array $rule)
{ {
$this->rule = $rule; $this->rule = $rule;
$this->var_id = $var_id; $this->var_id = $var_id;
Expand Down
23 changes: 23 additions & 0 deletions tests/AssertTest.php
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -885,6 +885,29 @@ function test(?string $in) : void {
} }
}', }',
], ],
'assertUnion' => [
'<?php
class Foo{
public function bar() : void {}
}
/**
* @param mixed $b
* @psalm-assert int|Foo $b
*/
function assertIntOrFoo($b) : void {
if (!is_int($b) && !(is_object($b) && $b instanceof Foo)) {
throw new \Exception("bad");
}
}
/** @psalm-suppress MixedAssignment */
$a = $_GET["a"];
assertIntOrFoo($a);
if (!is_int($a)) $a->bar();',
],
]; ];
} }


Expand Down

0 comments on commit efe096c

Please sign in to comment.