mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Add an error for throws in global scope
This commit is contained in:
parent
8b12751007
commit
976c2c5ef3
@ -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" />
|
||||
|
@ -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
|
||||
|
@ -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 doesn’t exist
|
||||
|
@ -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';
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -831,18 +831,7 @@ 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
|
||||
);
|
||||
|
||||
$undocumented_throws = [];
|
||||
|
||||
foreach ($context->possibly_thrown_exceptions as $possibly_thrown_exception => $_) {
|
||||
foreach ($statements_analyzer->getUncaughtThrows($context) as $possibly_thrown_exception => $_) {
|
||||
$is_expected = false;
|
||||
|
||||
foreach ($storage->throws as $expected_exception => $_) {
|
||||
@ -854,25 +843,7 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
|
||||
}
|
||||
}
|
||||
|
||||
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 ($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'
|
||||
@ -890,7 +861,6 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($add_mutations) {
|
||||
if ($this->return_vars_in_scope !== null) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ namespace Psalm\Internal\Analyzer;
|
||||
|
||||
use Psalm\Aliases;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Context;
|
||||
use Psalm\StatementsSource;
|
||||
use Psalm\Type;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
6
src/Psalm/Issue/UncaughtThrowInGlobalScope.php
Normal file
6
src/Psalm/Issue/UncaughtThrowInGlobalScope.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Psalm\Issue;
|
||||
|
||||
class UncaughtThrowInGlobalScope extends CodeIssue
|
||||
{
|
||||
}
|
@ -155,6 +155,9 @@ class DocumentationTest extends TestCase
|
||||
case 'MissingThrowsDocblock':
|
||||
continue 2;
|
||||
|
||||
case 'UncaughtThrowInGlobalScope':
|
||||
continue 2;
|
||||
|
||||
case 'InvalidStringClass':
|
||||
continue 2;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user