Skip to content

Commit 725b0fa

Browse files
authored
Add fnmatch() wildcard support to allowInInstanceOf and friends (#399)
`allowInInstanceOf`, `allowExceptInInstanceOf`, and `disallowInInstanceOf` now accept fnmatch() patterns (e.g. `App\Wrappers\*`). Pattern is matched against the class name and all parent classes and interfaces transitively, consistent with `instanceof` semantics
2 parents ac0f403 + 6cc7aa2 commit 725b0fa

4 files changed

Lines changed: 156 additions & 1 deletion

File tree

docs/allow-in-instance-of.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ Use `allowInInstanceOf`, named after the PHP's `instanceof` operator, if you wan
55
- a class that inherits from a class of given name
66
- a class that implements given interface
77

8+
The class names support [fnmatch()](https://www.php.net/fnmatch) patterns. When a wildcard pattern is used, it is matched against the current class name as well as all its parent class and interface names transitively, so the same instanceof semantics apply.
9+
10+
For example, to allow a function call in any class inside the `App\Wrappers` namespace and its subclasses:
11+
12+
```neon
13+
parameters:
14+
disallowedFunctionCalls:
15+
-
16+
function: 'someFunction()'
17+
allowInInstanceOf:
18+
- 'App\Wrappers\*'
19+
```
20+
821
This is useful for example when you want to allow properties or parameters of class `ClassName` in all classes that extend `ParentClass`:
922

1023
```neon

src/Allowed/Allowed.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PhpParser\Node\Stmt\Function_;
1010
use PHPStan\Analyser\Scope;
1111
use PHPStan\BetterReflection\Reflector\Reflector;
12+
use PHPStan\Reflection\ClassReflection;
1213
use PHPStan\Reflection\FunctionReflection;
1314
use PHPStan\Reflection\MethodReflection;
1415
use PHPStan\Type\Type;
@@ -150,8 +151,31 @@ private function callMatches(Scope $scope, string $call): bool
150151
*/
151152
private function isInstanceOf(Scope $scope, array $allowConfig): bool
152153
{
154+
if (!$scope->isInClass()) {
155+
return false;
156+
}
157+
$classReflection = $scope->getClassReflection();
153158
foreach ($allowConfig as $allowInstanceOf) {
154-
if ($scope->isInClass() && $scope->getClassReflection()->is($allowInstanceOf)) {
159+
if ($this->classOrAncestorMatches($classReflection, $allowInstanceOf)) {
160+
return true;
161+
}
162+
}
163+
return false;
164+
}
165+
166+
167+
private function classOrAncestorMatches(ClassReflection $classReflection, string $pattern): bool
168+
{
169+
if (fnmatch($pattern, $classReflection->getName(), FNM_NOESCAPE | FNM_CASEFOLD)) {
170+
return true;
171+
}
172+
foreach ($classReflection->getParentClassesNames() as $name) {
173+
if (fnmatch($pattern, $name, FNM_NOESCAPE | FNM_CASEFOLD)) {
174+
return true;
175+
}
176+
}
177+
foreach (array_keys($classReflection->getInterfaces()) as $name) {
178+
if (fnmatch($pattern, $name, FNM_NOESCAPE | FNM_CASEFOLD)) {
155179
return true;
156180
}
157181
}

tests/Calls/FunctionCallsTest.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,19 @@ protected function getRule(): Rule
225225
Stringable::class,
226226
],
227227
],
228+
// test allowed instances with wildcards, intentionally wrong case to test FNM_CASEFOLD
229+
[
230+
'function' => 'str_pad()',
231+
'allowInInstanceOf' => [
232+
'waldo\foo\wild*',
233+
],
234+
],
235+
[
236+
'function' => 'str_repeat()',
237+
'disallowInInstanceOf' => [
238+
'Waldo\Foo\Wild*',
239+
],
240+
],
228241
]
229242
);
230243
}
@@ -423,6 +436,37 @@ public function testAllowInInstanceOf(): void
423436
}
424437

425438

439+
public function testAllowInInstanceOfWildcard(): void
440+
{
441+
$this->analyse([__DIR__ . '/../src/BarWildcard.php'], [
442+
[
443+
'Calling str_repeat() is forbidden.',
444+
16,
445+
],
446+
[
447+
'Calling str_repeat() is forbidden.',
448+
27,
449+
],
450+
[
451+
'Calling str_repeat() is forbidden.',
452+
38,
453+
],
454+
[
455+
'Calling str_repeat() is forbidden.',
456+
49,
457+
],
458+
[
459+
'Calling str_repeat() is forbidden.',
460+
60,
461+
],
462+
[
463+
'Calling str_pad() is forbidden.',
464+
70,
465+
],
466+
]);
467+
}
468+
469+
426470
public static function getAdditionalConfigFiles(): array
427471
{
428472
return [

tests/src/BarWildcard.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
declare(strict_types = 1);
3+
4+
namespace Waldo\Foo;
5+
6+
interface WildInterface
7+
{
8+
}
9+
10+
class WildBase
11+
{
12+
13+
public function inWild(): void
14+
{
15+
str_pad('foo', 10);
16+
str_repeat('bar', 3);
17+
}
18+
19+
}
20+
21+
class ChildOfBase extends WildBase
22+
{
23+
24+
public function inWild(): void
25+
{
26+
str_pad('foo', 10);
27+
str_repeat('bar', 3);
28+
}
29+
30+
}
31+
32+
class GrandChildOfBase extends ChildOfBase
33+
{
34+
35+
public function inWild(): void
36+
{
37+
str_pad('foo', 10);
38+
str_repeat('bar', 3);
39+
}
40+
41+
}
42+
43+
class ImplementsWildInterface implements WildInterface
44+
{
45+
46+
public function inWild(): void
47+
{
48+
str_pad('foo', 10);
49+
str_repeat('bar', 3);
50+
}
51+
52+
}
53+
54+
class InheritsWildInterface extends ImplementsWildInterface
55+
{
56+
57+
public function inWild(): void
58+
{
59+
str_pad('foo', 10);
60+
str_repeat('bar', 3);
61+
}
62+
63+
}
64+
65+
class NotWild
66+
{
67+
68+
public function inWild(): void
69+
{
70+
str_pad('foo', 10);
71+
str_repeat('bar', 3);
72+
}
73+
74+
}

0 commit comments

Comments
 (0)