Skip to content

Commit 725c15b

Browse files
authored
ForbidCheckedExceptionInCallableRule (#137)
1 parent 122df5f commit 725c15b

16 files changed

+1364
-22
lines changed

README.md

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ShipMonk PHPStan rules
22
About **30 super-strict rules** we found useful in ShipMonk.
3-
We tend to have PHPStan set up as strict as possible (bleedingEdge, strict-rules, checkUninitializedProperties, ...), but that still was not strict enough for us.
3+
We tend to have PHPStan set up as strict as possible ([bleedingEdge](https://phpstan.org/blog/what-is-bleeding-edge), [strict-rules](https://github.com/phpstan/phpstan-strict-rules), [checkUninitializedProperties](https://phpstan.org/config-reference#checkuninitializedproperties), ...), but that still was not strict enough for us.
44
This set of rules should fill the missing gaps we found.
55

66
If you find some rules opinionated, you can easily disable them.
@@ -44,6 +44,28 @@ parameters:
4444
forbidCast:
4545
enabled: true
4646
blacklist: ['(array)', '(object)', '(unset)']
47+
forbidCheckedExceptionInCallable:
48+
enabled: true
49+
immediatelyCalledCallables:
50+
array_reduce: 1
51+
array_intersect_ukey: 2
52+
array_uintersect: 2
53+
array_uintersect_assoc: 2
54+
array_intersect_uassoc: 2
55+
array_uintersect_uassoc: [2, 3]
56+
array_diff_ukey: 2
57+
array_udiff: 2
58+
array_udiff_assoc: 2
59+
array_diff_uassoc: 2
60+
array_udiff_uassoc: [2, 3]
61+
array_filter: 1
62+
array_map: 0
63+
array_walk_recursive: 1
64+
array_walk: 1
65+
uasort: 1
66+
uksort: 1
67+
usort: 1
68+
allowedCheckedExceptionCallables: []
4769
forbidCheckedExceptionInYieldingMethod:
4870
enabled: true
4971
forbidCustomFunctions:
@@ -335,9 +357,69 @@ parameters:
335357
blacklist!: ['(array)', '(object)', '(unset)']
336358
```
337359

360+
### forbidCheckedExceptionInCallable
361+
- Denies throwing [checked exception](https://phpstan.org/blog/bring-your-exceptions-under-control) in callables (Closures and First class callables) as those cannot be tracked as checked by PHPStan analysis, because it is unknown when the callable is about to be called
362+
- It allows configuration of functions/methods, where the callable is called immediately, those cases are allowed and are also added to [dynamic throw type extension](https://phpstan.org/developing-extensions/dynamic-throw-type-extensions) which causes those exceptions to be tracked properly in your codebase (!)
363+
- By default, native functions like `array_map` are present. So it is recommended not to overwrite the defaults here (by `!` char).
364+
- It allows configuration of functions/methods, where the callable is handling all thrown exceptions and it is safe to throw anything from there; this basically makes such calls ignored by this rule
365+
- It ignores [implicitly thrown Throwable](https://phpstan.org/blog/bring-your-exceptions-under-control#what-does-absent-%40throws-above-a-function-mean%3F)
366+
367+
```neon
368+
parameters:
369+
shipmonkRules:
370+
forbidCheckedExceptionInCallable:
371+
immediatellyCalledCallables:
372+
'Doctrine\ORM\EntityManager::transactional': 0 # 0 is argument index where the closure appears, you can use list if needed
373+
'Symfony\Contracts\Cache\CacheInterface::get': 1
374+
'Acme\my_custom_function': 0
375+
allowedCheckedExceptionCallables:
376+
'Symfony\Component\Console\Question::setValidator': 0 # symfony automatically converts all thrown exceptions to error output, so it is safe to throw anything here
377+
```
378+
379+
- We recommend using following config for checked exceptions:
380+
- Also, [bleedingEdge](https://phpstan.org/blog/what-is-bleeding-edge) enables proper analysis of dead types in multi-catch, so we recommend enabling even that
381+
382+
```neon
383+
parameters:
384+
exceptions:
385+
check:
386+
missingCheckedExceptionInThrows: true # enforce checked exceptions to be stated in @throws
387+
tooWideThrowType: true # report invalid @throws (exceptions that are not actually thrown in annotated method)
388+
implicitThrows: false # no @throws means nothing is thrown (otherwise Throwable is thrown)
389+
checkedExceptionClasses:
390+
- YourApp\TopLevelRuntimeException # track only your exceptions (children of some, typically RuntimeException)
391+
```
392+
393+
394+
```php
395+
class UserEditFacade
396+
{
397+
/**
398+
* @throws UserNotFoundException
399+
* ^ This throws would normally be reported as never thrown in native phpstan, but we know the closure is immediately called
400+
*/
401+
public function updateUserEmail(UserId $userId, Email $email): void
402+
{
403+
$this->entityManager->transactional(function () use ($userId, $email) {
404+
$user = $this->userRepository->get($userId); // throws checked UserNotFoundException
405+
$user->updateEmail($email);
406+
})
407+
}
408+
409+
public function getUpdateEmailCallback(UserId $userId, Email $email): callable
410+
{
411+
return function () use ($userId, $email) {
412+
$user = $this->userRepository->get($userId); // this usage is denied, it throws checked exception, but you don't know when, thus it cannot be tracked by phpstan
413+
$user->updateEmail($email);
414+
};
415+
}
416+
}
417+
```
418+
338419
### forbidCheckedExceptionInYieldingMethod
339420
- Denies throwing [checked exception](https://phpstan.org/blog/bring-your-exceptions-under-control) within yielding methods as those exceptions are not throw upon method call, but when generator gets iterated.
340421
- This behaviour cannot be easily reflected within PHPStan exception analysis and may cause [false negatives](https://phpstan.org/r/d07ac0f0-a49d-4f82-b1dd-1939058bbeed).
422+
- Make sure you have enabled checked exceptions, otherwise, this rule does nothing
341423

342424
```php
343425
class Provider {

composer.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
"require": {
1313
"php": "^7.4 || ^8.0",
1414
"nikic/php-parser": "^4.14.0",
15-
"phpstan/phpstan": "^1.10.0"
15+
"phpstan/phpstan": "^1.10.30"
1616
},
1717
"require-dev": {
1818
"editorconfig-checker/editorconfig-checker": "^10.3.0",
1919
"ergebnis/composer-normalize": "^2.28",
20+
"nette/neon": "^3.3.1",
2021
"phpstan/phpstan-phpunit": "^1.1.1",
2122
"phpstan/phpstan-strict-rules": "^1.2.3",
2223
"phpunit/phpunit": "^9.5.20",
@@ -33,7 +34,8 @@
3334
"ShipMonk\\PHPStan\\": "tests/"
3435
},
3536
"classmap": [
36-
"tests/Rule/data"
37+
"tests/Rule/data",
38+
"tests/Extension/data"
3739
]
3840
},
3941
"config": {

composer.lock

Lines changed: 74 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

phpcs.xml.dist

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<file>src/</file>
1919
<file>tests/</file>
2020

21-
<exclude-pattern>tests/Rule/data/*</exclude-pattern>
21+
<exclude-pattern>tests/*/data/*</exclude-pattern>
2222

2323
<config name="installed_paths" value="../../slevomat/coding-standard"/>
2424

@@ -310,16 +310,8 @@
310310
<rule ref="SlevomatCodingStandard.Exceptions.DisallowNonCapturingCatch"/>
311311
<rule ref="SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly"/>
312312
<rule ref="SlevomatCodingStandard.Functions.DisallowNamedArguments"/>
313-
<rule ref="SlevomatCodingStandard.Functions.DisallowTrailingCommaInClosureUse">
314-
<properties>
315-
<property name="onlySingleLine" value="true"/>
316-
</properties>
317-
</rule>
318-
<rule ref="SlevomatCodingStandard.Functions.DisallowTrailingCommaInDeclaration">
319-
<properties>
320-
<property name="onlySingleLine" value="true"/>
321-
</properties>
322-
</rule>
313+
<rule ref="SlevomatCodingStandard.Functions.DisallowTrailingCommaInClosureUse"/><!-- add onlySingleLine once PHP is bumped -->
314+
<rule ref="SlevomatCodingStandard.Functions.DisallowTrailingCommaInDeclaration"/><!-- add onlySingleLine once PHP is bumped -->
323315
<rule ref="SlevomatCodingStandard.Functions.DisallowTrailingCommaInCall">
324316
<properties>
325317
<property name="onlySingleLine" value="true"/>
@@ -404,6 +396,8 @@
404396
Doctrine\Common\Collections\Collection
405397
"/>
406398
</properties>
399+
<exclude name="SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingTraversableTypeHintSpecification"/><!-- this has problems with vendor libs, PHPStan checks this much more reliably -->
400+
<exclude name="SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint"/><!-- this has problems with vendor libs, PHPStan checks this much more reliably -->
407401
</rule>
408402
<rule ref="SlevomatCodingStandard.TypeHints.ReturnTypeHint">
409403
<properties>

phpstan.neon.dist

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,18 @@ parameters:
1212
- tests
1313
excludePaths:
1414
analyseAndScan:
15-
- tests/Rule/data/*
15+
- tests/*/data/*
1616
tmpDir: cache/phpstan/
1717
checkMissingCallableSignature: true
1818
checkUninitializedProperties: true
1919
checkTooWideReturnTypesInProtectedAndPublicMethods: true
20+
exceptions:
21+
check:
22+
missingCheckedExceptionInThrows: true
23+
tooWideThrowType: true
24+
implicitThrows: false
25+
uncheckedExceptionClasses:
26+
- LogicException
2027

2128
shipmonkRules:
2229
classSuffixNaming:
@@ -30,3 +37,7 @@ parameters:
3037
message: "#Class BackedEnum not found\\.#"
3138
path: src/Rule/BackedEnumGenericsRule.php
3239
reportUnmatched: false # fails only for PHP < 8 https://github.com/phpstan/phpstan/issues/6290
40+
41+
-
42+
message: "#but it's missing from the PHPDoc @throws tag\\.$#" # allow uncatched exceptions in tests
43+
path: tests/*

0 commit comments

Comments
 (0)