1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 12:55:26 +01:00
psalm/tests/TryCatchTest.php
Daniil Gentili 1986c8b4a8
Add support for strict arrays, fix type alias intersection, fix array_is_list assertion on non-lists (#8395)
* Immutable CodeLocation

* Remove excess clones

* Remove external clones

* Remove leftover clones

* Fix final clone issue

* Immutable storages

* Refactoring

* Fixes

* Fixes

* Fix

* Fix

* Fixes

* Simplify

* Fixes

* Fix

* Fixes

* Update

* Fix

* Cache global types

* Fix

* Update

* Update

* Fixes

* Fixes

* Refactor

* Fixes

* Fix

* Fix

* More caching

* Fix

* Fix

* Update

* Update

* Fix

* Fixes

* Update

* Refactor

* Update

* Fixes

* Break one more test

* Fix

* FIx

* Fix

* Fix

* Fix

* Fix

* Improve performance and readability

* Equivalent logic

* Fixes

* Revert

* Revert "Revert"

This reverts commit f9175100c8452c80559234200663fd4c4f4dd889.

* Fix

* Fix reference bug

* Make default TypeVisitor immutable

* Bugfix

* Remove clones

* Partial refactoring

* Refactoring

* Fixes

* Fix

* Fixes

* Fixes

* cs-fix

* Fix final bugs

* Add test

* Misc fixes

* Update

* Fixes

* Experiment with removing different property

* revert "Experiment with removing different property"

This reverts commit ac1156e077fc4ea633530d51096d27b6e88bfdf9.

* Uniform naming

* Uniform naming

* Hack hotfix

* Clean up $_FILES ref #8621

* Undo hack, try fixing properly

* Helper method

* Remove redundant call

* Partially fix bugs

* Cleanup

* Change defaults

* Fix bug

* Fix (?, hope this doesn't break anything else)

* cs-fix

* Review fixes

* Bugfix

* Bugfix

* Improve logic

* Add support for list{} and callable-list{} types, properly implement array_is_list assertions (fixes #8389)

* Default to sealed arrays

* Fix array_merge bug

* Fixes

* Fix

* Sealed type checks

* Properly infer properties-of and get_object_vars on final classes

* Fix array_map zipping

* Fix tests

* Fixes

* Fixes

* Fix more stuff

* Recursively resolve type aliases

* Fix typo

* Fixes

* Fix array_is_list assertion on keyed array

* Add BC docs

* Fixes

* fix

* Update

* Update

* Update

* Update

* Seal arrays with count assertions

* Fix #8528

* Fix

* Update

* Improve sealed array foreach logic

* get_object_vars on template properties

* Fix sealed array assertion reconciler logic

* Improved reconciler

* Add tests

* Single source of truth for test types

* Fix tests

* Fixup tests

* Fixup tests

* Fixup tests

* Update

* Fix tests

* Fix tests

* Final fixes

* Fixes

* Use list syntax only when needed

* Fix tests

* Cs-fix

* Update docs

* Update docs

* Update docs

* Update docs

* Update docs

* Document missing types

* Update docs

* Improve class-string-map docs

* Update

* Update

* I love working on psalm :)

* Keep arrays unsealed by default

* Fixup tests

* Fix syntax mistake

* cs-fix

* Fix typo

* Re-import missing types

* Keep strict types only in return types

* argc/argv fixes

* argc/argv fixes

* Fix test

* Comment-out valinor code, pinging @romm pls merge https://github.com/CuyZ/Valinor/pull/246 so we can add valinor to the psalm docs :)
2022-11-05 22:34:42 +01:00

639 lines
21 KiB
PHP

<?php
namespace Psalm\Tests;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
class TryCatchTest extends TestCase
{
use ValidCodeAnalysisTestTrait;
use InvalidCodeAnalysisTestTrait;
/**
*
*/
public function providerValidCodeParse(): iterable
{
return [
'addThrowableInterfaceType' => [
'code' => '<?php
interface CustomThrowable {}
class CustomException extends Exception implements CustomThrowable {}
/** @psalm-suppress InvalidCatch */
try {
throw new CustomException("Bad");
} catch (CustomThrowable $e) {
echo $e->getMessage();
}',
],
'rethrowInterfaceExceptionWithoutInvalidThrow' => [
'code' => '<?php
interface CustomThrowable {}
class CustomException extends Exception implements CustomThrowable {}
/** @psalm-suppress InvalidCatch */
try {
throw new CustomException("Bad");
} catch (CustomThrowable $e) {
throw $e;
}',
],
'tryCatchVar' => [
'code' => '<?php
try {
$worked = true;
}
catch (\Exception $e) {
$worked = false;
}',
'assertions' => [
'$worked' => 'bool',
],
],
'alwaysReturnsBecauseCatchDoesNothing' => [
'code' => '<?php
function throws(): void {
throw new Exception("bad");
}
function foo(): string {
try {
throws();
} catch (Exception $e) {
// do nothing
}
return "hello";
}',
],
'wheresTheCatch' => [
'code' => '<?php
function foo() : bool {
try {
return true;
} finally {
}
}
function bar() : bool {
try {
// do nothing
} finally {
return true;
}
}',
],
'catchWithNoReturnButFinallyReturns' => [
'code' => '<?php
function foo() : bool {
try {
if (rand(0, 1)) throw new Exception("bad");
} catch (Exception $e) {
echo $e->getMessage();
// do nothing here either
} finally {
return true;
}
}',
],
'stopAnalysisAfterBadTryIssue' => [
'code' => '<?php
$foo = true;
try {
$a->bar();
} catch (\TypeError $e) {
$foo = false;
}
if (!$foo) {}',
'assertions' => [],
'ignored_issues' => [
'UndefinedGlobalVariable',
'MixedMethodCall',
],
],
'issetAfterTryCatchWithoutAssignmentInCatch' => [
'code' => '<?php
function test(): string {
throw new Exception("bad");
}
$a = "foo";
try {
$var = test();
} catch (Exception $e) {
echo "bad";
}
if (isset($var)) {}
echo $a;',
],
'issetAfterTryCatchWithoutAssignmentInCatchButReturn' => [
'code' => '<?php
function test(): string {
throw new Exception("bad");
}
$a = "foo";
try {
$var = test();
} catch (Exception $e) {
return;
}
echo $var;
echo $a;',
],
'issetAfterTryCatchWithAssignmentInCatch' => [
'code' => '<?php
function test(): string {
throw new Exception("bad");
}
$a = "foo";
try {
$var = test();
} catch (Exception $e) {
$var = "bad";
}
echo $var;
echo $a;',
],
'issetAfterTryCatchWithIfInCatch' => [
'code' => '<?php
function test(): string {
throw new Exception("bad");
}
function foo() : void {
$a = null;
$params = null;
try {
$a = test();
$params = $a;
} catch (\Exception $exception) {
$params = "hello";
}
echo $params;
}',
],
'noRedundantConditionsInFinally' => [
'code' => '<?php
function doThings(): void {}
function message(): string { return "message"; }
$errors = [];
try {
doThings();
} catch (RuntimeException $e) {
$errors["field"] = message();
} catch (LengthException $e) {
$errors[rand(0,1) ? "field" : "field2"] = message();
} finally {
if (!empty($errors)) {
return $errors;
}
}',
],
'typeDoesNotContainTypeInCatch' => [
'code' => '<?php
function foo(bool $test, callable $bar): string {
try {
$bar();
if ($test) {
return "moo";
}
return "miau";
} catch (\Exception $exception) {
if ($test) {
return "moo";
}
return "miau";
}
}',
],
'notAlwaysUndefinedVarInFinally' => [
'code' => '<?php
function maybeThrows() : void {
if (rand(0, 1)) {
throw new UnexpectedValueException();
}
}
function doTry() : void {
try {
maybeThrows();
return;
} catch (Exception $exception) {
throw $exception;
} finally {
if (isset($exception)) {
echo "here";
}
}
}',
],
'noReturnInsideCatch' => [
'code' => '<?php
/**
* @return never-returns
*/
function example() : void {
throw new Exception();
}
try {
$str = "a";
} catch (Exception $e) {
example();
}
ord($str);',
],
'varSetInOnlyCatch' => [
'code' => '<?php
try {
if (rand(0, 1)) {
throw new \Exception("Gotcha!");
}
exit;
} catch (\Exception $e) {
$lastException = $e;
}
echo $lastException->getMessage();'
],
'varSetInOnlyCatchWithNull' => [
'code' => '<?php
$lastException = null;
try {
if (rand(0, 1)) {
throw new \Exception("Gotcha!");
}
exit;
} catch (\Exception $e) {
$lastException = $e;
}
echo $lastException->getMessage();'
],
'allowDoubleNestedLoop' => [
'code' => '<?php
function foo() : void {
do {
try {
do {
$count = rand(0, 10);
} while ($count === 5);
} catch (Exception $e) {}
} while (rand(0, 1));
}'
],
'aliasException' => [
'code' => '<?php
namespace UserException;
class UserException extends \Exception {
}
namespace Alias\UserException;
use function class_alias;
class_alias(
\UserException\UserException::class,
\Alias\UserException\UserExceptionAlias::class
);
namespace Client;
try {
throw new \Alias\UserException\UserExceptionAlias();
} catch (\Alias\UserException\UserExceptionAlias $e) {
// do nothing
}'
],
'aliasAnotherException' => [
'code' => '<?php
namespace UserException;
class UserException extends \Exception {
}
namespace Alias\UserException;
use function class_alias;
class_alias(
\UserException\UserException::class,
\'\Alias\UserException\UserExceptionAlias\'
);
namespace Client;
try {
throw new \Alias\UserException\UserExceptionAlias();
} catch (\Alias\UserException\UserExceptionAlias $e) {
// do nothing
}'
],
'notRedundantVarCheckInFinally' => [
'code' => '<?php
$var = "a";
try {
if (rand(0, 1)) {
throw new \Exception();
}
$var = "b";
} finally {
if ($var === "a") {
echo $var;
}
}'
],
'suppressUndefinedVarInFinally' => [
'code' => '<?php
try {} finally {
/** @psalm-suppress UndefinedGlobalVariable, MixedPropertyAssignment */
$event->end = null;
}',
],
'returnsInTry' => [
'code' => '<?php
final class A
{
private ?string $property = null;
public function handle(string $arg): string
{
if (null !== $this->property) {
return $arg;
}
try {
return $arg;
} finally {
}
}
}'
],
'finallyArgMaybeUndefined' => [
'code' => '<?php
class TestMe {
private function startTransaction(): void {
}
private function endTransaction(bool $commit): void {
echo $commit ? "Committing" : "Rolling back";
}
public function doWork(): void {
$this->startTransaction();
try {
$this->workThatMayOrMayNotThrow();
$success = true;
} finally {
$this->endTransaction($success ?? false);
}
}
private function workThatMayOrMayNotThrow(): void {}
}'
],
'finallyArgIsNotUndefinedIfSet' => [
'code' => '<?php
function fooFunction (): string {
try{
$foo = "foo";
} finally {
/** @psalm-suppress PossiblyUndefinedVariable */
echo $foo;
$foo = "bar";
}
return $foo;
}'
],
'allowReturningPossiblyUndefinedFromTry' => [
'code' => '<?php
function fooFunction (): string {
try{
$foo = "foo";
} finally {
/** @psalm-suppress PossiblyUndefinedVariable */
echo $foo;
}
return $foo;
}'
],
'mixedNotUndefinedAfterTry' => [
'code' => '<?php
/**
* @return array<int, mixed>
* @psalm-suppress MixedAssignment
*/
function fetchFromCache(mixed $m)
{
$data = [];
try {
$value = $m;
} catch (Throwable $e) {
$value = $m;
}
$data[] = $value;
return $data;
}',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.0'
],
'issetInCatch' => [
'code' => '<?php
function foo() : void {
try {
$a = 0;
} catch (Exception $e) {
echo isset($a) ? $a : 1;
}
}'
],
'issetExceptionInFinally' => [
'code' => '<?php
try {
if (rand(0, 1)) {
throw new \Exception("bad");
}
} catch (Throwable $exception) {
//throw $exception;
} finally {
if (isset($exception)) {}
}'
],
];
}
/**
*
*/
public function providerInvalidCodeParse(): iterable
{
return [
'invalidCatchClass' => [
'code' => '<?php
class A {}
try {
$worked = true;
}
catch (A $e) {}',
'error_message' => 'InvalidCatch',
],
'invalidThrowClass' => [
'code' => '<?php
class A {}
throw new A();',
'error_message' => 'InvalidThrow',
],
'theresNoCatch' => [
'code' => '<?php
function missing_return() : bool {
try {
} finally {
}
}',
'error_message' => 'InvalidReturnType',
],
'catchDoesNotReturn' => [
'code' => '<?php
function missing_return() : bool {
try {
} finally {
}
}',
'error_message' => 'InvalidReturnType',
],
'catchWithNoReturnAndFinallyDoesNotReturn' => [
'code' => '<?php
function foo() : bool {
try {
if (rand(0, 1)) throw new Exception("bad");
return true;
} catch (Exception $e) {
echo $e->getMessage();
// do nothing here either
} finally {
}
}',
'error_message' => 'InvalidReturnType',
],
'catchWithNoReturnAndNoFinally' => [
'code' => '<?php
function foo() : bool {
try {
if (rand(0, 1)) throw new Exception("bad");
return true;
} catch (Exception $e) {
echo $e->getMessage();
// do nothing here either
}
}',
'error_message' => 'InvalidReturnType',
],
'preventPossiblyUndefinedVarInTry' => [
'code' => '<?php
class Foo {
public static function possiblyThrows(): bool {
$result = (bool)rand(0, 1);
if (!$result) {
throw new \Exception("BOOM");
}
return true;
}
}
try {
$result = Foo::possiblyThrows();
$a = "ACME";
if ($result) {
echo $a;
}
} catch (\Exception $e) {
echo $a;
}',
'error_message' => 'PossiblyUndefinedGlobalVariable',
],
'possiblyNullReturnInTry' => [
'code' => '<?php
function foo() : string {
$a = null;
try {
$a = dangerous();
} catch (Exception $e) {
return $a;
}
return $a;
}
function dangerous() : string {
if (rand(0, 1)) {
throw new \Exception("bad");
}
return "hello";
}',
'error_message' => 'NullableReturnStatement'
],
'isAlwaysDefinedInFinally' => [
'code' => '<?php
function maybeThrows() : void {
if (rand(0, 1)) {
throw new UnexpectedValueException();
}
}
function doTry() : void {
$exception = new \Exception();
try {
maybeThrows();
return;
} catch (Exception $exception) {
throw $exception;
} finally {
if ($exception) {
echo "here";
}
}
}',
'error_message' => 'RedundantCondition'
],
];
}
}