Skip to content

Commit

Permalink
Merge pull request #7994 from aszenz/4.x
Browse files Browse the repository at this point in the history
Adds support for fixing missing throws doc block
  • Loading branch information
orklah committed Jul 12, 2022
2 parents 7f3d55d + e3f46d9 commit 416b597
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 0 deletions.
15 changes: 15 additions & 0 deletions src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
Expand Up @@ -65,6 +65,8 @@
use function array_merge;
use function array_search;
use function array_values;
use function assert;
use function class_exists;
use function count;
use function end;
use function in_array;
Expand Down Expand Up @@ -706,6 +708,7 @@ public function analyze(
}
}

$missingThrowsDocblockErrors = [];
foreach ($statements_analyzer->getUncaughtThrows($context) as $possibly_thrown_exception => $codelocations) {
$is_expected = false;

Expand All @@ -719,6 +722,7 @@ public function analyze(
}

if (!$is_expected) {
$missingThrowsDocblockErrors[] = $possibly_thrown_exception;
foreach ($codelocations as $codelocation) {
// issues are suppressed in ThrowAnalyzer, CallAnalyzer, etc.
IssueBuffer::maybeAdd(
Expand All @@ -732,6 +736,17 @@ public function analyze(
}
}

if ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['MissingThrowsDocblock'])
) {
$manipulator = FunctionDocblockManipulator::getForFunction(
$project_analyzer,
$this->source->getFilePath(),
$this->function
);
$manipulator->addThrowsDocblock($missingThrowsDocblockErrors);
}

if ($codebase->taint_flow_graph
&& $this->function instanceof ClassMethod
&& $cased_method_id
Expand Down
1 change: 1 addition & 0 deletions src/Psalm/Internal/Analyzer/ProjectAnalyzer.php
Expand Up @@ -1323,6 +1323,7 @@ public function setIssuesToFix(array $issues): void

$supported_issues_to_fix[] = 'MissingImmutableAnnotation';
$supported_issues_to_fix[] = 'MissingPureAnnotation';
$supported_issues_to_fix[] = 'MissingThrowsDocblock';

$unsupportedIssues = array_diff(array_keys($issues), $supported_issues_to_fix);

Expand Down
Expand Up @@ -14,7 +14,9 @@
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Internal\Scanner\ParsedDocblock;

use function array_key_exists;
use function array_merge;
use function array_reduce;
use function count;
use function is_string;
use function ltrim;
Expand Down Expand Up @@ -96,6 +98,9 @@ class FunctionDocblockManipulator
/** @var bool */
private $is_pure = false;

/** @var list<string> */
private $throwsExceptions = [];

/**
* @param Closure|Function_|ClassMethod|ArrowFunction $stmt
*/
Expand Down Expand Up @@ -395,6 +400,21 @@ private function getDocblock(): string
$modified_docblock = true;
$parsed_docblock->tags['psalm-pure'] = [''];
}
if (count($this->throwsExceptions) > 0) {
$modified_docblock = true;
$inferredThrowsClause = array_reduce(
$this->throwsExceptions,
function (string $throwsClause, string $exception) {
return $throwsClause === '' ? $exception : $throwsClause.'|'.$exception;
},
''
);
if (array_key_exists('throws', $parsed_docblock->tags)) {
$parsed_docblock->tags['throws'][] = $inferredThrowsClause;
} else {
$parsed_docblock->tags['throws'] = [$inferredThrowsClause];
}
}


if ($this->new_phpdoc_return_type && $this->new_phpdoc_return_type !== $old_phpdoc_return_type) {
Expand Down Expand Up @@ -528,6 +548,14 @@ public function makePure(): void
$this->is_pure = true;
}

/**
* @param list<string> $exceptions
*/
public function addThrowsDocblock(array $exceptions): void
{
$this->throwsExceptions = $exceptions;
}

public static function clearCache(): void
{
self::$manipulators = [];
Expand Down
1 change: 1 addition & 0 deletions tests/FileManipulation/FileManipulationTestCase.php
Expand Up @@ -86,6 +86,7 @@ public function testValidCode(
$safe_types
);
$this->project_analyzer->getCodebase()->allow_backwards_incompatible_changes = $allow_backwards_incompatible_changes;
$this->project_analyzer->getConfig()->check_for_throws_docblock = true;

if (strpos(static::class, 'Unused') || strpos(static::class, 'Unnecessary')) {
$this->project_analyzer->getCodebase()->reportUnusedCode();
Expand Down
123 changes: 123 additions & 0 deletions tests/FileManipulation/ThrowsBlockAdditionTest.php
@@ -0,0 +1,123 @@
<?php

namespace Psalm\Tests\FileManipulation;

class ThrowsBlockAdditionTest extends FileManipulationTestCase
{
/**
* @return array<string,array{string,string,string,string[],bool}>
*/
public function providerValidCodeParse(): array
{
return [
'addThrowsAnnotationToFunction' => [
'<?php
function foo(string $s): string {
if("" === $s) {
throw new \InvalidArgumentException();
}
return $s;
}',
'<?php
/**
* @throws InvalidArgumentException
*/
function foo(string $s): string {
if("" === $s) {
throw new \InvalidArgumentException();
}
return $s;
}',
'7.4',
['MissingThrowsDocblock'],
true,
],
'addMultipleThrowsAnnotationToFunction' => [
'<?php
function foo(string $s): string {
if("" === $s) {
throw new \InvalidArgumentException();
}
if("" === \trim($s)) {
throw new \DomainException();
}
return $s;
}',
'<?php
/**
* @throws InvalidArgumentException|DomainException
*/
function foo(string $s): string {
if("" === $s) {
throw new \InvalidArgumentException();
}
if("" === \trim($s)) {
throw new \DomainException();
}
return $s;
}',
'7.4',
['MissingThrowsDocblock'],
true,
],
'preservesExistingThrowsAnnotationToFunction' => [
'<?php
/**
* @throws InvalidArgumentException|DomainException
*/
function foo(string $s): string {
if("" === $s) {
throw new \Exception();
}
return $s;
}',
'<?php
/**
* @throws InvalidArgumentException|DomainException
* @throws Exception
*/
function foo(string $s): string {
if("" === $s) {
throw new \Exception();
}
return $s;
}',
'7.4',
['MissingThrowsDocblock'],
true,
],
'doesNotAddDuplicateThrows' => [
'<?php
/**
* @throws InvalidArgumentException
*/
function foo(string $s): string {
if("" === $s) {
throw new \InvalidArgumentException();
}
if("" === \trim($s)) {
throw new \DomainException();
}
return $s;
}',
'<?php
/**
* @throws InvalidArgumentException
* @throws DomainException
*/
function foo(string $s): string {
if("" === $s) {
throw new \InvalidArgumentException();
}
if("" === \trim($s)) {
throw new \DomainException();
}
return $s;
}',
'7.4',
['MissingThrowsDocblock'],
true,
],
];
}
}

0 comments on commit 416b597

Please sign in to comment.