1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-29 20:28:59 +01:00

Add an error for throws in global scope

This commit is contained in:
bugreportuser 2019-03-24 14:17:14 -06:00 committed by Matthew Brown
parent 8b12751007
commit 976c2c5ef3
12 changed files with 132 additions and 55 deletions

View File

@ -45,6 +45,7 @@
<xs:attribute name="hoistConstants" type="xs:string" />
<xs:attribute name="addParamDefaultToDocblockType" type="xs:string" />
<xs:attribute name="checkForThrowsDocblock" type="xs:string" />
<xs:attribute name="checkForThrowsInGlobalScope" type="xs:string" />
<xs:attribute name="forbidEcho" type="xs:string" />
<xs:attribute name="errorBaseline" type="xs:string" />
<xs:attribute name="findUnusedCode" type="xs:string" />
@ -299,6 +300,7 @@
<xs:element name="TypeCoercion" type="IssueHandlerType" minOccurs="0" />
<xs:element name="TypeDoesNotContainNull" type="IssueHandlerType" minOccurs="0" />
<xs:element name="TypeDoesNotContainType" type="IssueHandlerType" minOccurs="0" />
<xs:element name="UncaughtThrowInGlobalScope" type="IssueHandlerType" minOccurs="0" />
<xs:element name="UndefinedClass" type="ClassIssueHandlerType" minOccurs="0" />
<xs:element name="UndefinedConstant" type="IssueHandlerType" minOccurs="0" />
<xs:element name="UndefinedFunction" type="FunctionIssueHandlerType" minOccurs="0" />

View File

@ -149,6 +149,14 @@ Occasionally a param default will not match up with the docblock type. By defaul
```
When `true`, Psalm will check that the developer has supplied `@throws` docblocks for every exception thrown in a given function or method. Defaults to `false`.
#### checkForThrowsInGlobalScope
```xml
<psalm
checkForThrowsDocblock="[bool]"
>
```
When `true`, Psalm will check that the developer has caught every exception in global scope. Defaults to `false`.
#### ignoreInternalFunctionFalseReturn
```xml

View File

@ -1957,6 +1957,20 @@ $a = "hello";
if ($a === 5) {}
```
### UncaughtThrowInGlobalScope
Emitted when a possible exception isn't caught in global scope
```php
/**
* @throws \Exception
*/
function foo() : int {
return random_int(0, 1);
}
foo();
```
### UndefinedClass
Emitted when referencing a class that doesnt exist

View File

@ -217,6 +217,11 @@ class Config
*/
public $check_for_throws_docblock = false;
/**
* @var bool
*/
public $check_for_throws_in_global_scope = false;
/**
* @var bool
*/
@ -635,6 +640,11 @@ class Config
$config->check_for_throws_docblock = $attribute_text === 'true' || $attribute_text === '1';
}
if (isset($config_xml['checkForThrowsInGlobalScope'])) {
$attribute_text = (string) $config_xml['checkForThrowsInGlobalScope'];
$config->check_for_throws_in_global_scope = $attribute_text === 'true' || $attribute_text === '1';
}
if (isset($config_xml['forbidEcho'])) {
$attribute_text = (string) $config_xml['forbidEcho'];
$config->forbid_echo = $attribute_text === 'true' || $attribute_text === '1';

View File

@ -3,9 +3,11 @@ namespace Psalm\Internal\Analyzer;
use PhpParser;
use Psalm\Codebase;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Exception\UnpreparedAnalysisException;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Issue\UncaughtThrowInGlobalScope;
use Psalm\IssueBuffer;
use Psalm\StatementsSource;
use Psalm\Type;
@ -143,6 +145,7 @@ class FileAnalyzer extends SourceAnalyzer implements StatementsSource
$this->context->is_global = true;
$this->context->defineGlobals();
$this->context->collect_exceptions = $codebase->config->check_for_throws_in_global_scope;
try {
$stmts = $codebase->getStatementsForFile($this->file_path);
@ -175,6 +178,21 @@ class FileAnalyzer extends SourceAnalyzer implements StatementsSource
$this->class_analyzers_to_analyze = [];
$this->interface_analyzers_to_analyze = [];
}
if ($codebase->config->check_for_throws_in_global_scope) {
$uncaught_throws = $statements_analyzer->getUncaughtThrows($this->context);
foreach ($uncaught_throws as $possibly_thrown_exception => $codelocation) {
if (IssueBuffer::accepts(
new UncaughtThrowInGlobalScope(
$possibly_thrown_exception . ' is thrown but not caught in global scope',
$codelocation
),
$this->getSuppressedIssues()
)) {
// fall through
}
}
}
}
/**

View File

@ -831,63 +831,33 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
}
}
if ($context->collect_exceptions) {
if ($context->possibly_thrown_exceptions) {
$ignored_exceptions = array_change_key_case(
$codebase->config->ignored_exceptions
);
$ignored_exceptions_and_descendants = array_change_key_case(
$codebase->config->ignored_exceptions_and_descendants
);
foreach ($statements_analyzer->getUncaughtThrows($context) as $possibly_thrown_exception => $_) {
$is_expected = false;
$undocumented_throws = [];
foreach ($context->possibly_thrown_exceptions as $possibly_thrown_exception => $_) {
$is_expected = false;
foreach ($storage->throws as $expected_exception => $_) {
if ($expected_exception === $possibly_thrown_exception
|| $codebase->classExtends($possibly_thrown_exception, $expected_exception)
) {
$is_expected = true;
break;
}
}
foreach ($ignored_exceptions_and_descendants as $expected_exception => $_) {
if ($expected_exception === $possibly_thrown_exception
|| $codebase->classExtends($possibly_thrown_exception, $expected_exception)
) {
$is_expected = true;
break;
}
}
if (!$is_expected) {
$undocumented_throws[$possibly_thrown_exception] = true;
}
foreach ($storage->throws as $expected_exception => $_) {
if ($expected_exception === $possibly_thrown_exception
|| $codebase->classExtends($possibly_thrown_exception, $expected_exception)
) {
$is_expected = true;
break;
}
}
foreach ($undocumented_throws as $possibly_thrown_exception => $_) {
if (isset($ignored_exceptions[strtolower($possibly_thrown_exception)])) {
continue;
}
if (IssueBuffer::accepts(
new MissingThrowsDocblock(
$possibly_thrown_exception . ' is thrown but not caught - please either catch'
. ' or add a @throws annotation',
new CodeLocation(
$this,
$this->function,
null,
true
)
),
$this->getSuppressedIssues()
)) {
// fall through
}
if (!$is_expected) {
if (IssueBuffer::accepts(
new MissingThrowsDocblock(
$possibly_thrown_exception . ' is thrown but not caught - please either catch'
. ' or add a @throws annotation',
new CodeLocation(
$this,
$this->function,
null,
true
)
),
$this->getSuppressedIssues()
)) {
// fall through
}
}
}

View File

@ -87,6 +87,7 @@ class NamespaceAnalyzer extends SourceAnalyzer implements StatementsSource
$context->collect_references = $codebase->collect_references;
$context->is_global = true;
$context->defineGlobals();
$context->collect_exceptions = $codebase->config->check_for_throws_in_global_scope;
$statements_analyzer->analyze($leftover_stmts, $context);
}
}

View File

@ -3,6 +3,7 @@ namespace Psalm\Internal\Analyzer;
use Psalm\Aliases;
use Psalm\Codebase;
use Psalm\Context;
use Psalm\StatementsSource;
use Psalm\Type;

View File

@ -1594,4 +1594,46 @@ class StatementsAnalyzer extends SourceAnalyzer implements StatementsSource
{
$this->byref_uses = $byref_uses;
}
/**
* @return array<string, CodeLocation>
*/
public function getUncaughtThrows(Context $context)
{
$uncaught_throws = [];
if ($context->collect_exceptions) {
if ($context->possibly_thrown_exceptions) {
$ignored_exceptions = array_change_key_case(
$this->codebase->config->ignored_exceptions
);
$ignored_exceptions_and_descendants = array_change_key_case(
$this->codebase->config->ignored_exceptions_and_descendants
);
foreach ($context->possibly_thrown_exceptions as $possibly_thrown_exception => $codelocation) {
if (isset($ignored_exceptions[strtolower($possibly_thrown_exception)])) {
continue;
}
$is_expected = false;
foreach ($ignored_exceptions_and_descendants as $expected_exception => $_) {
if ($expected_exception === $possibly_thrown_exception
|| $this->codebase->classExtends($possibly_thrown_exception, $expected_exception)
) {
$is_expected = true;
break;
}
}
if (!$is_expected) {
$uncaught_throws[$possibly_thrown_exception] = $codelocation;
}
}
}
}
return $uncaught_throws;
}
}

View File

@ -1717,7 +1717,9 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
$storage->suppressed_issues = $docblock_info->suppress;
if ($this->config->check_for_throws_docblock) {
if ($this->config->check_for_throws_docblock ||
$this->config->check_for_throws_in_global_scope
) {
foreach ($docblock_info->throws as $throw_class) {
$exception_fqcln = Type::getFQCLNFromString(
$throw_class,

View File

@ -0,0 +1,6 @@
<?php
namespace Psalm\Issue;
class UncaughtThrowInGlobalScope extends CodeIssue
{
}

View File

@ -155,6 +155,9 @@ class DocumentationTest extends TestCase
case 'MissingThrowsDocblock':
continue 2;
case 'UncaughtThrowInGlobalScope':
continue 2;
case 'InvalidStringClass':
continue 2;