Skip to content

Commit 043a14c

Browse files
committed
RestrictedFunctionUsageExtension
1 parent ce48667 commit 043a14c

15 files changed

+484
-0
lines changed

conf/config.level0.neon

+5
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ rules:
106106
conditionalTags:
107107
PHPStan\Rules\InternalTag\RestrictedInternalClassNameUsageExtension:
108108
phpstan.restrictedClassNameUsageExtension: %featureToggles.internalTag%
109+
PHPStan\Rules\InternalTag\RestrictedInternalFunctionUsageExtension:
110+
phpstan.restrictedFunctionUsageExtension: %featureToggles.internalTag%
109111

110112
services:
111113
-
@@ -297,3 +299,6 @@ services:
297299

298300
-
299301
class: PHPStan\Rules\InternalTag\RestrictedInternalClassNameUsageExtension
302+
303+
-
304+
class: PHPStan\Rules\InternalTag\RestrictedInternalFunctionUsageExtension

conf/config.neon

+2
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ rules:
218218
- PHPStan\Rules\Debug\DumpPhpDocTypeRule
219219
- PHPStan\Rules\Debug\DumpTypeRule
220220
- PHPStan\Rules\Debug\FileAssertRule
221+
- PHPStan\Rules\RestrictedUsage\RestrictedFunctionUsageRule
222+
- PHPStan\Rules\RestrictedUsage\RestrictedFunctionCallableUsageRule
221223
- PHPStan\Rules\RestrictedUsage\RestrictedMethodUsageRule
222224
- PHPStan\Rules\RestrictedUsage\RestrictedMethodCallableUsageRule
223225
- PHPStan\Rules\RestrictedUsage\RestrictedStaticMethodUsageRule

src/DependencyInjection/ConditionalTagsExtension.php

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use PHPStan\Rules\LazyRegistry;
2727
use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
2828
use PHPStan\Rules\RestrictedUsage\RestrictedClassNameUsageExtension;
29+
use PHPStan\Rules\RestrictedUsage\RestrictedFunctionUsageExtension;
2930
use PHPStan\Rules\RestrictedUsage\RestrictedMethodUsageExtension;
3031
use PHPStan\ShouldNotHappenException;
3132
use function array_reduce;
@@ -77,6 +78,7 @@ public function getConfigSchema(): Nette\Schema\Schema
7778
PropertyDeprecationExtension::PROPERTY_EXTENSION_TAG => $bool,
7879
RestrictedMethodUsageExtension::METHOD_EXTENSION_TAG => $bool,
7980
RestrictedClassNameUsageExtension::CLASS_NAME_EXTENSION_TAG => $bool,
81+
RestrictedFunctionUsageExtension::FUNCTION_EXTENSION_TAG => $bool,
8082
])->min(1));
8183
}
8284

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\InternalTag;
4+
5+
use PHPStan\Analyser\Scope;
6+
use PHPStan\Reflection\FunctionReflection;
7+
use PHPStan\Rules\RestrictedUsage\RestrictedFunctionUsageExtension;
8+
use PHPStan\Rules\RestrictedUsage\RestrictedUsage;
9+
use function array_slice;
10+
use function explode;
11+
use function sprintf;
12+
13+
final class RestrictedInternalFunctionUsageExtension implements RestrictedFunctionUsageExtension
14+
{
15+
16+
public function __construct(private RestrictedInternalUsageHelper $helper)
17+
{
18+
}
19+
20+
public function isRestrictedFunctionUsage(FunctionReflection $functionReflection, Scope $scope): ?RestrictedUsage
21+
{
22+
if (!$functionReflection->isInternal()->yes()) {
23+
return null;
24+
}
25+
26+
if (!$this->helper->shouldBeReported($scope, $functionReflection->getName())) {
27+
return null;
28+
}
29+
30+
$namespace = array_slice(explode('\\', $functionReflection->getName()), 0, -1)[0] ?? null;
31+
if ($namespace === null) {
32+
return RestrictedUsage::create(
33+
sprintf(
34+
'Call to internal function %s().',
35+
$functionReflection->getName(),
36+
),
37+
'function.internal',
38+
);
39+
}
40+
41+
return RestrictedUsage::create(
42+
sprintf(
43+
'Call to internal function %s() from outside its root namespace %s.',
44+
$functionReflection->getName(),
45+
$namespace,
46+
),
47+
'function.internal',
48+
);
49+
}
50+
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\RestrictedUsage;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Name;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\DependencyInjection\Container;
9+
use PHPStan\Node\FunctionCallableNode;
10+
use PHPStan\Reflection\ReflectionProvider;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleErrorBuilder;
13+
14+
/**
15+
* @implements Rule<FunctionCallableNode>
16+
*/
17+
final class RestrictedFunctionCallableUsageRule implements Rule
18+
{
19+
20+
public function __construct(
21+
private Container $container,
22+
private ReflectionProvider $reflectionProvider,
23+
)
24+
{
25+
}
26+
27+
public function getNodeType(): string
28+
{
29+
return FunctionCallableNode::class;
30+
}
31+
32+
/**
33+
* @api
34+
*/
35+
public function processNode(Node $node, Scope $scope): array
36+
{
37+
if (!($node->getName() instanceof Name)) {
38+
return [];
39+
}
40+
41+
if (!$this->reflectionProvider->hasFunction($node->getName(), $scope)) {
42+
return [];
43+
}
44+
45+
$functionReflection = $this->reflectionProvider->getFunction($node->getName(), $scope);
46+
47+
/** @var RestrictedFunctionUsageExtension[] $extensions */
48+
$extensions = $this->container->getServicesByTag(RestrictedFunctionUsageExtension::FUNCTION_EXTENSION_TAG);
49+
$errors = [];
50+
51+
foreach ($extensions as $extension) {
52+
$restrictedUsage = $extension->isRestrictedFunctionUsage($functionReflection, $scope);
53+
if ($restrictedUsage === null) {
54+
continue;
55+
}
56+
57+
$errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage)
58+
->identifier($restrictedUsage->identifier)
59+
->build();
60+
}
61+
62+
return $errors;
63+
}
64+
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\RestrictedUsage;
4+
5+
use PHPStan\Analyser\Scope;
6+
use PHPStan\Reflection\FunctionReflection;
7+
8+
/**
9+
* Extensions implementing this interface are called for each analysed function call.
10+
*
11+
* Extension can decide to create RestrictedUsage object
12+
* with error message & error identifier to be reported for this function call.
13+
*
14+
* Typical usage is to report errors for functions marked as @-deprecated or @-internal.
15+
*
16+
* To register it in the configuration file use the following tag:
17+
*
18+
* ```
19+
* services:
20+
* -
21+
* class: App\PHPStan\MyExtension
22+
* tags:
23+
* - phpstan.restrictedFunctionUsageExtension
24+
* ```
25+
*
26+
* @api
27+
*/
28+
interface RestrictedFunctionUsageExtension
29+
{
30+
31+
public const FUNCTION_EXTENSION_TAG = 'phpstan.restrictedFunctionUsageExtension';
32+
33+
public function isRestrictedFunctionUsage(
34+
FunctionReflection $functionReflection,
35+
Scope $scope,
36+
): ?RestrictedUsage;
37+
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\RestrictedUsage;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Name;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\DependencyInjection\Container;
9+
use PHPStan\Reflection\ReflectionProvider;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
13+
/**
14+
* @implements Rule<Node\Expr\FuncCall>
15+
*/
16+
final class RestrictedFunctionUsageRule implements Rule
17+
{
18+
19+
public function __construct(
20+
private Container $container,
21+
private ReflectionProvider $reflectionProvider,
22+
)
23+
{
24+
}
25+
26+
public function getNodeType(): string
27+
{
28+
return Node\Expr\FuncCall::class;
29+
}
30+
31+
/**
32+
* @api
33+
*/
34+
public function processNode(Node $node, Scope $scope): array
35+
{
36+
if (!($node->name instanceof Name)) {
37+
return [];
38+
}
39+
40+
if (!$this->reflectionProvider->hasFunction($node->name, $scope)) {
41+
return [];
42+
}
43+
44+
$functionReflection = $this->reflectionProvider->getFunction($node->name, $scope);
45+
46+
/** @var RestrictedFunctionUsageExtension[] $extensions */
47+
$extensions = $this->container->getServicesByTag(RestrictedFunctionUsageExtension::FUNCTION_EXTENSION_TAG);
48+
$errors = [];
49+
50+
foreach ($extensions as $extension) {
51+
$restrictedUsage = $extension->isRestrictedFunctionUsage($functionReflection, $scope);
52+
if ($restrictedUsage === null) {
53+
continue;
54+
}
55+
56+
$errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage)
57+
->identifier($restrictedUsage->identifier)
58+
->build();
59+
}
60+
61+
return $errors;
62+
}
63+
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\InternalTag;
4+
5+
use PHPStan\Rules\RestrictedUsage\RestrictedFunctionUsageRule;
6+
use PHPStan\Rules\Rule;
7+
use PHPStan\Testing\RuleTestCase;
8+
9+
/**
10+
* @extends RuleTestCase<RestrictedFunctionUsageRule>
11+
*/
12+
class RestrictedInternalFunctionUsageExtensionTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return self::getContainer()->getByType(RestrictedFunctionUsageRule::class);
18+
}
19+
20+
public function testRule(): void
21+
{
22+
$this->analyse([__DIR__ . '/data/function-internal-tag.php'], [
23+
[
24+
'Call to internal function FunctionInternalTagOne\doInternal() from outside its root namespace FunctionInternalTagOne.',
25+
35,
26+
],
27+
[
28+
'Call to internal function FunctionInternalTagOne\doInternal() from outside its root namespace FunctionInternalTagOne.',
29+
44,
30+
],
31+
[
32+
'Call to internal function doInternalWithoutNamespace().',
33+
60,
34+
],
35+
[
36+
'Call to internal function doInternalWithoutNamespace().',
37+
69,
38+
],
39+
]);
40+
}
41+
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
namespace FunctionInternalTagOne {
4+
5+
/** @internal */
6+
function doInternal()
7+
{
8+
9+
}
10+
11+
function doNotInternal()
12+
{
13+
14+
}
15+
16+
function (): void {
17+
doInternal();
18+
doNotInternal();
19+
};
20+
21+
}
22+
23+
namespace FunctionInternalTagOne\Test {
24+
25+
function (): void {
26+
\FunctionInternalTagOne\doInternal();
27+
\FunctionInternalTagOne\doNotInternal();
28+
};
29+
30+
}
31+
32+
namespace FunctionInternalTagTwo {
33+
34+
function (): void {
35+
\FunctionInternalTagOne\doInternal();
36+
\FunctionInternalTagOne\doNotInternal();
37+
};
38+
39+
}
40+
41+
namespace {
42+
43+
function (): void {
44+
\FunctionInternalTagOne\doInternal();
45+
\FunctionInternalTagOne\doNotInternal();
46+
};
47+
48+
/** @internal */
49+
function doInternalWithoutNamespace()
50+
{
51+
52+
}
53+
54+
function doNotInternalWithoutNamespace()
55+
{
56+
57+
}
58+
59+
function (): void {
60+
doInternalWithoutNamespace();
61+
doNotInternalWithoutNamespace();
62+
};
63+
64+
}
65+
66+
namespace SomeNamespace {
67+
68+
function (): void {
69+
\doInternalWithoutNamespace();
70+
\doNotInternalWithoutNamespace();
71+
};
72+
73+
}

0 commit comments

Comments
 (0)