1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Fix #2019 - allow union in @throws

This commit is contained in:
Brown 2019-08-13 15:44:18 -04:00
parent d6e084ec9c
commit 2b2988b072
6 changed files with 67 additions and 29 deletions

View File

@ -523,14 +523,18 @@ class CommentAnalyzer
}
if (isset($parsed_docblock['specials']['throws'])) {
foreach ($parsed_docblock['specials']['throws'] as $throws_entry) {
foreach ($parsed_docblock['specials']['throws'] as $offset => $throws_entry) {
$throws_class = preg_split('/[\s]+/', $throws_entry)[0];
if (!$throws_class) {
throw new IncorrectDocblockException('Unexpectedly empty @throws');
}
$info->throws[] = $throws_class;
$info->throws[] = [
$throws_class,
$offset + $comment->getFilePos(),
$comment->getLine() + substr_count($comment->getText(), "\n", 0, $offset)
];
}
}

View File

@ -1020,32 +1020,32 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
}
foreach ($storage->throws as $expected_exception => $_) {
if ($storage->location
&& ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
if (isset($storage->throw_locations[$expected_exception])) {
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
$statements_analyzer,
$expected_exception,
$storage->location,
$storage->throw_locations[$expected_exception],
$statements_analyzer->getSuppressedIssues(),
false,
false,
true,
true
)
) {
$input_type = new Type\Union([new TNamedObject($expected_exception)]);
$container_type = new Type\Union([new TNamedObject('Exception'), new TNamedObject('Throwable')]);
)) {
$input_type = new Type\Union([new TNamedObject($expected_exception)]);
$container_type = new Type\Union([new TNamedObject('Exception'), new TNamedObject('Throwable')]);
if (!TypeAnalyzer::isContainedBy($codebase, $input_type, $container_type)) {
if (IssueBuffer::accepts(
new \Psalm\Issue\InvalidThrow(
'Class supplied for @throws ' . $expected_exception
. ' does not implement Throwable',
$storage->location,
$expected_exception
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
if (!TypeAnalyzer::isContainedBy($codebase, $input_type, $container_type)) {
if (IssueBuffer::accepts(
new \Psalm\Issue\InvalidThrow(
'Class supplied for @throws ' . $expected_exception
. ' does not implement Throwable',
$storage->throw_locations[$expected_exception],
$expected_exception
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
}
}

View File

@ -111,7 +111,7 @@ class FunctionDocblockComment
public $suppress = [];
/**
* @var array<int, string>
* @var array<int, array{0: string, 1: int, 2: int}>
*/
public $throws = [];

View File

@ -2034,16 +2034,25 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
$storage->suppressed_issues = $docblock_info->suppress;
foreach ($docblock_info->throws as $throw_class) {
$exception_fqcln = Type::getFQCLNFromString(
$throw_class,
$this->aliases
foreach ($docblock_info->throws as [$throw, $offset, $line]) {
$throw_location = new CodeLocation\DocblockTypeLocation(
$this->file_scanner,
$offset,
$offset + \strlen($throw),
$line
);
$this->codebase->scanner->queueClassLikeForScanning($exception_fqcln, $this->file_path);
$this->file_storage->referenced_classlikes[strtolower($exception_fqcln)] = $exception_fqcln;
foreach (\array_map('trim', explode('|', $throw)) as $throw_class) {
$exception_fqcln = Type::getFQCLNFromString(
$throw_class,
$this->aliases
);
$storage->throws[$exception_fqcln] = true;
$this->codebase->scanner->queueClassLikeForScanning($exception_fqcln, $this->file_path);
$this->file_storage->referenced_classlikes[strtolower($exception_fqcln)] = $exception_fqcln;
$storage->throws[$exception_fqcln] = true;
$storage->throw_locations[$exception_fqcln] = $throw_location;
}
}
if (!$this->config->use_docblock_types) {

View File

@ -151,6 +151,11 @@ class FunctionLikeStorage
*/
public $throws = [];
/**
* @var array<string, CodeLocation>
*/
public $throw_locations = [];
/**
* @var bool
*/

View File

@ -8,7 +8,7 @@ class ThrowsAnnotationTest extends TestCase
{
public function testUndefinedClassAsThrows() : void
{
$this->expectExceptionMessage('UndefinedDocblockClass');
$this->expectExceptionMessage('UndefinedDocblockClass - somefile.php:3:28');
$this->expectException(\Psalm\Exception\CodeException::class);
$this->addFile(
@ -46,6 +46,26 @@ class ThrowsAnnotationTest extends TestCase
$this->analyzeFile('somefile.php', $context);
}
public function testInheritedThrowableClassAsThrows() : void
{
$this->addFile(
'somefile.php',
'<?php
class MyException extends Exception {}
class Foo {
/**
* @throws MyException|Throwable
*/
public function bar() : void {}
}'
);
$context = new Context();
$this->analyzeFile('somefile.php', $context);
}
/**
* @return void
*/