Skip to content

Commit b24661b

Browse files
authored
Can get method/function attributes for params of disallowed classes (#315)
When a disallowed classname is detected in method or function params, and it is allowed by method/function attributes, this change will allow to get the method/func attributes. It's way hacky as it seems to be not possible natively in PHPStan because `$scope->getFunction()` returns null for `FullyQualified` nodes in method/func parameter types. So we store the method name in `ClassMethod` rule, use it in `Allowed` service when set, and unset the function name in `InClassMethodNode` which is a virtual node which marks the beginning of the method body. Similar for functions in `Function_` and `InFunctionBody`. Close #314
2 parents a1eec11 + f462528 commit b24661b

11 files changed

+416
-10
lines changed

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
},
2727
"require-dev": {
2828
"nette/neon": "^3.3.1",
29-
"nikic/php-parser": "^4.13 || ^5.0",
29+
"nikic/php-parser": "^4.13.2 || ^5.0",
3030
"phpunit/phpunit": "^8.5.14 || ^10.1 || ^11.0 || ^12.0",
3131
"php-parallel-lint/php-parallel-lint": "^1.2",
3232
"php-parallel-lint/php-console-highlighter": "^1.0",
@@ -41,7 +41,7 @@
4141
},
4242
"scripts": {
4343
"lint": "vendor/bin/parallel-lint --colors src/ tests/",
44-
"lint-7.x": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/TypesEverywhere.php --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/functionCallsNamedParams.php --exclude tests/src/disallowed-allow/functionCallsNamedParams.php --exclude tests/src/disallowed/attributeUsages.php --exclude tests/src/disallowed-allow/attributeUsages.php --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php --exclude tests/src/Bar.php --exclude tests/src/Enums.php --exclude tests/src/disallowed/controlStructures.php --exclude tests/src/disallowed-allow/controlStructures.php --exclude tests/src/disallowed/firstClassCallable.php --exclude tests/src/disallowed-allow/firstClassCallable.php --exclude tests/src/disallowed/callableParameters.php --exclude tests/src/disallowed-allow/callableParameters.php",
44+
"lint-7.x": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/TypesEverywhere.php --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/functionCallsNamedParams.php --exclude tests/src/disallowed-allow/functionCallsNamedParams.php --exclude tests/src/disallowed/attributeUsages.php --exclude tests/src/disallowed-allow/attributeUsages.php --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php --exclude tests/src/AttributeClass.php --exclude tests/src/Bar.php --exclude tests/src/Enums.php --exclude tests/src/Functions.php --exclude tests/src/disallowed/controlStructures.php --exclude tests/src/disallowed-allow/controlStructures.php --exclude tests/src/disallowed/firstClassCallable.php --exclude tests/src/disallowed-allow/firstClassCallable.php --exclude tests/src/disallowed/callableParameters.php --exclude tests/src/disallowed-allow/callableParameters.php",
4545
"lint-8.0": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/TypesEverywhere.php --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php --exclude tests/src/Enums.php --exclude tests/src/disallowed/firstClassCallable.php --exclude tests/src/disallowed-allow/firstClassCallable.php",
4646
"lint-8.1": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php --exclude tests/src/disallowed/firstClassCallable.php --exclude tests/src/disallowed-allow/firstClassCallable.php",
4747
"lint-8.2": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php",

extension.neon

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,7 @@ services:
418418
- Spaze\PHPStan\Rules\Disallowed\Allowed\Allowed
419419
- Spaze\PHPStan\Rules\Disallowed\Allowed\AllowedConfigFactory
420420
- Spaze\PHPStan\Rules\Disallowed\Allowed\AllowedPath
421+
- Spaze\PHPStan\Rules\Disallowed\Allowed\GetAttributesWhenInSignature
421422
- Spaze\PHPStan\Rules\Disallowed\DisallowedAttributeFactory
422423
- Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory
423424
- Spaze\PHPStan\Rules\Disallowed\DisallowedConstantFactory
@@ -575,3 +576,19 @@ services:
575576
factory: Spaze\PHPStan\Rules\Disallowed\ControlStructures\WhileControlStructure(disallowedControlStructures: @Spaze\PHPStan\Rules\Disallowed\DisallowedControlStructureFactory::getDisallowedControlStructures(%disallowedControlStructures%))
576577
tags:
577578
- phpstan.rules.rule
579+
-
580+
factory: Spaze\PHPStan\Rules\Disallowed\HelperRules\SetCurrentClassMethodNameHelperRule
581+
tags:
582+
- phpstan.rules.rule
583+
-
584+
factory: Spaze\PHPStan\Rules\Disallowed\HelperRules\UnsetCurrentClassMethodNameHelperRule
585+
tags:
586+
- phpstan.rules.rule
587+
-
588+
factory: Spaze\PHPStan\Rules\Disallowed\HelperRules\SetCurrentFunctionNameHelperRule
589+
tags:
590+
- phpstan.rules.rule
591+
-
592+
factory: Spaze\PHPStan\Rules\Disallowed\HelperRules\UnsetCurrentFunctionNameHelperRule
593+
tags:
594+
- phpstan.rules.rule

src/Allowed/Allowed.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,22 @@ class Allowed
3232

3333
private Identifier $identifier;
3434

35+
private GetAttributesWhenInSignature $attributesWhenInSignature;
36+
3537
private AllowedPath $allowedPath;
3638

3739

3840
public function __construct(
3941
Formatter $formatter,
4042
Reflector $reflector,
4143
Identifier $identifier,
44+
GetAttributesWhenInSignature $attributesWhenInSignature,
4245
AllowedPath $allowedPath
4346
) {
4447
$this->formatter = $formatter;
4548
$this->reflector = $reflector;
4649
$this->identifier = $identifier;
50+
$this->attributesWhenInSignature = $attributesWhenInSignature;
4751
$this->allowedPath = $allowedPath;
4852
}
4953

@@ -269,6 +273,10 @@ private function getCallAttributes(?Node $node, Scope $scope): array
269273
} elseif ($node instanceof Function_) {
270274
return $this->reflector->reflectFunction($node->name->name)->getAttributes();
271275
}
276+
$attributes = $this->attributesWhenInSignature->get($scope);
277+
if ($attributes !== null) {
278+
return $attributes;
279+
}
272280
}
273281
return [];
274282
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
declare(strict_types = 1);
3+
4+
namespace Spaze\PHPStan\Rules\Disallowed\Allowed;
5+
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\BetterReflection\Reflection\Adapter\FakeReflectionAttribute;
8+
use PHPStan\BetterReflection\Reflection\Adapter\ReflectionAttribute;
9+
use PHPStan\BetterReflection\Reflection\ReflectionAttribute as BetterReflectionAttribute;
10+
use PHPStan\BetterReflection\Reflector\Reflector;
11+
12+
class GetAttributesWhenInSignature
13+
{
14+
15+
private Reflector $reflector;
16+
17+
/** @var class-string|null */
18+
private ?string $currentClass = null;
19+
20+
private ?string $currentMethod = null;
21+
22+
/** @var string|null */
23+
private ?string $currentFunction = null;
24+
25+
26+
public function __construct(Reflector $reflector)
27+
{
28+
$this->reflector = $reflector;
29+
}
30+
31+
32+
/**
33+
* Emulates the missing $scope->getMethodOrFunctionSignature().
34+
*
35+
* Because $scope->getFunction() returns null when the node, like for example a namespace node (instance of FullyQualified),
36+
* is inside the method or the function signature, it's impossible to get to the current method or function reflection using $scope to get its attributes.
37+
* The hacky solution is to store the current method name in a ClassMethod rule, read it here, and unset it in a InClassMethodNode rule,
38+
* or the function name in a Function_ and a InFunctionNode rules.
39+
*
40+
* @param Scope $scope
41+
* @return list<FakeReflectionAttribute|ReflectionAttribute|BetterReflectionAttribute>|null
42+
*/
43+
public function get(Scope $scope): ?array
44+
{
45+
if (
46+
$this->currentClass !== null
47+
&& $this->currentMethod !== null
48+
&& $scope->isInClass()
49+
&& $scope->getClassReflection()->getName() === $this->currentClass
50+
) {
51+
return $scope->getClassReflection()->getNativeReflection()->getMethod($this->currentMethod)->getAttributes();
52+
} elseif ($this->currentFunction !== null) {
53+
return $this->reflector->reflectFunction($this->currentFunction)->getAttributes();
54+
}
55+
return null;
56+
}
57+
58+
59+
/**
60+
* @param class-string $className
61+
* @param string $methodName
62+
* @return void
63+
*/
64+
public function setCurrentClassMethodName(string $className, string $methodName): void
65+
{
66+
$this->currentClass = $className;
67+
$this->currentMethod = $methodName;
68+
}
69+
70+
71+
public function unsetCurrentClassMethodName(): void
72+
{
73+
$this->currentClass = $this->currentMethod = null;
74+
}
75+
76+
77+
/**
78+
* @param string $functionName
79+
* @return void
80+
*/
81+
public function setCurrentFunctionName(string $functionName): void
82+
{
83+
$this->currentFunction = $functionName;
84+
}
85+
86+
87+
public function unsetCurrentFunctionName(): void
88+
{
89+
$this->currentFunction = null;
90+
}
91+
92+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
declare(strict_types = 1);
3+
4+
namespace Spaze\PHPStan\Rules\Disallowed\HelperRules;
5+
6+
use PhpParser\Node;
7+
use PhpParser\Node\Stmt\ClassMethod;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Rules\Rule;
10+
use Spaze\PHPStan\Rules\Disallowed\Allowed\GetAttributesWhenInSignature;
11+
12+
/**
13+
* @implements Rule<ClassMethod>
14+
*/
15+
class SetCurrentClassMethodNameHelperRule implements Rule
16+
{
17+
18+
private GetAttributesWhenInSignature $attributesWhenInSignature;
19+
20+
21+
public function __construct(GetAttributesWhenInSignature $attributesWhenInSignature)
22+
{
23+
$this->attributesWhenInSignature = $attributesWhenInSignature;
24+
}
25+
26+
27+
public function getNodeType(): string
28+
{
29+
return ClassMethod::class;
30+
}
31+
32+
33+
/**
34+
* @param ClassMethod $node
35+
* @param Scope $scope
36+
* @return array{}
37+
*/
38+
public function processNode(Node $node, Scope $scope): array
39+
{
40+
if ($scope->isInClass()) {
41+
$this->attributesWhenInSignature->setCurrentClassMethodName($scope->getClassReflection()->getName(), $node->name->name);
42+
}
43+
return [];
44+
}
45+
46+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
declare(strict_types = 1);
3+
4+
namespace Spaze\PHPStan\Rules\Disallowed\HelperRules;
5+
6+
use PhpParser\Node;
7+
use PhpParser\Node\Stmt\Function_;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Rules\Rule;
10+
use Spaze\PHPStan\Rules\Disallowed\Allowed\GetAttributesWhenInSignature;
11+
12+
/**
13+
* @implements Rule<Function_>
14+
*/
15+
class SetCurrentFunctionNameHelperRule implements Rule
16+
{
17+
18+
private GetAttributesWhenInSignature $attributesWhenInSignature;
19+
20+
21+
public function __construct(GetAttributesWhenInSignature $attributesWhenInSignature)
22+
{
23+
$this->attributesWhenInSignature = $attributesWhenInSignature;
24+
}
25+
26+
27+
public function getNodeType(): string
28+
{
29+
return Function_::class;
30+
}
31+
32+
33+
/**
34+
* @param Function_ $node
35+
* @param Scope $scope
36+
* @return array{}
37+
*/
38+
public function processNode(Node $node, Scope $scope): array
39+
{
40+
if ($node->namespacedName !== null) {
41+
$this->attributesWhenInSignature->setCurrentFunctionName($node->namespacedName->toString());
42+
}
43+
return [];
44+
}
45+
46+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
declare(strict_types = 1);
3+
4+
namespace Spaze\PHPStan\Rules\Disallowed\HelperRules;
5+
6+
use PhpParser\Node;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Node\InClassMethodNode;
9+
use PHPStan\Rules\Rule;
10+
use Spaze\PHPStan\Rules\Disallowed\Allowed\GetAttributesWhenInSignature;
11+
12+
/**
13+
* @implements Rule<InClassMethodNode>
14+
*/
15+
class UnsetCurrentClassMethodNameHelperRule implements Rule
16+
{
17+
18+
private GetAttributesWhenInSignature $attributesWhenInSignature;
19+
20+
21+
public function __construct(GetAttributesWhenInSignature $attributesWhenInSignature)
22+
{
23+
$this->attributesWhenInSignature = $attributesWhenInSignature;
24+
}
25+
26+
27+
public function getNodeType(): string
28+
{
29+
return InClassMethodNode::class;
30+
}
31+
32+
33+
/**
34+
* @param InClassMethodNode $node
35+
* @param Scope $scope
36+
* @return array{}
37+
*/
38+
public function processNode(Node $node, Scope $scope): array
39+
{
40+
$this->attributesWhenInSignature->unsetCurrentClassMethodName();
41+
return [];
42+
}
43+
44+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
declare(strict_types = 1);
3+
4+
namespace Spaze\PHPStan\Rules\Disallowed\HelperRules;
5+
6+
use PhpParser\Node;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Node\InFunctionNode;
9+
use PHPStan\Rules\Rule;
10+
use Spaze\PHPStan\Rules\Disallowed\Allowed\GetAttributesWhenInSignature;
11+
12+
/**
13+
* @implements Rule<InFunctionNode>
14+
*/
15+
class UnsetCurrentFunctionNameHelperRule implements Rule
16+
{
17+
18+
private GetAttributesWhenInSignature $attributesWhenInSignature;
19+
20+
21+
public function __construct(GetAttributesWhenInSignature $attributesWhenInSignature)
22+
{
23+
$this->attributesWhenInSignature = $attributesWhenInSignature;
24+
}
25+
26+
27+
public function getNodeType(): string
28+
{
29+
return InFunctionNode::class;
30+
}
31+
32+
33+
/**
34+
* @param InFunctionNode $node
35+
* @param Scope $scope
36+
* @return array{}
37+
*/
38+
public function processNode(Node $node, Scope $scope): array
39+
{
40+
$this->attributesWhenInSignature->unsetCurrentFunctionName();
41+
return [];
42+
}
43+
44+
}

0 commit comments

Comments
 (0)