Skip to content

Commit 6cc7aa2

Browse files
committed
Add fnmatch() wildcard support to allowInInstanceOf and friends
Patterns are matched against the class itself and its full parent and interface hierarchy transitively, giving the same semantics as `instanceof`. All exact-name entries now go through the same traversal, removing a separate code path.
1 parent ac0f403 commit 6cc7aa2

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)