Skip to content

Commit 63956f7

Browse files
committed
Moved illegalConstructorMethodCall rules from phpstan to phpstan-strict-rules
1 parent ad53bd9 commit 63956f7

8 files changed

+378
-2
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ parameters:
7979
switchConditionsMatchingType: false
8080
noVariableVariables: false
8181
strictArrayFilter: false
82+
illegalConstructorMethodCall: false
8283
```
8384

8485
Aside from introducing new custom rules, phpstan-strict-rules also [change the default values of some configuration parameters](https://github.com/phpstan/phpstan-strict-rules/blob/1.6.x/rules.neon#L1) that are present in PHPStan itself. These parameters are [documented on phpstan.org](https://phpstan.org/config-reference#stricter-analysis).

rules.neon

+12-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ parameters:
1010
reportStaticMethodSignatures: true
1111
reportMaybesInPropertyPhpDocTypes: true
1212
reportWrongPhpDocTypeInVarTag: true
13-
featureToggles:
14-
illegalConstructorMethodCall: true
1513
strictRules:
1614
allRules: true
1715
disallowedLooseComparison: %strictRules.allRules%
@@ -31,6 +29,7 @@ parameters:
3129
switchConditionsMatchingType: %strictRules.allRules%
3230
noVariableVariables: %strictRules.allRules%
3331
strictArrayFilter: %strictRules.allRules%
32+
illegalConstructorMethodCall: %strictRules.allRules%
3433

3534
parametersSchema:
3635
strictRules: structure([
@@ -52,6 +51,7 @@ parametersSchema:
5251
switchConditionsMatchingType: anyOf(bool(), arrayOf(bool()))
5352
noVariableVariables: anyOf(bool(), arrayOf(bool()))
5453
strictArrayFilter: anyOf(bool(), arrayOf(bool()))
54+
illegalConstructorMethodCall: anyOf(bool(), arrayOf(bool()))
5555
])
5656

5757
conditionalTags:
@@ -133,6 +133,10 @@ conditionalTags:
133133
phpstan.rules.rule: %strictRules.noVariableVariables%
134134
PHPStan\Rules\VariableVariables\VariablePropertyFetchRule:
135135
phpstan.rules.rule: %strictRules.noVariableVariables%
136+
PHPStan\Rules\Methods\IllegalConstructorMethodCallRule:
137+
phpstan.rules.rule: %strictRules.illegalConstructorMethodCall%
138+
PHPStan\Rules\Methods\IllegalConstructorStaticCallRule:
139+
phpstan.rules.rule: %strictRules.illegalConstructorMethodCall%
136140

137141
services:
138142
-
@@ -207,6 +211,12 @@ services:
207211
-
208212
class: PHPStan\Rules\Methods\WrongCaseOfInheritedMethodRule
209213

214+
-
215+
class: PHPStan\Rules\Methods\IllegalConstructorMethodCallRule
216+
217+
-
218+
class: PHPStan\Rules\Methods\IllegalConstructorStaticCallRule
219+
210220
-
211221
class: PHPStan\Rules\Operators\OperandInArithmeticPostDecrementRule
212222

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Methods;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Rules\Rule;
8+
use PHPStan\Rules\RuleErrorBuilder;
9+
10+
/**
11+
* @implements Rule<Node\Expr\MethodCall>
12+
*/
13+
final class IllegalConstructorMethodCallRule implements Rule
14+
{
15+
16+
public function getNodeType(): string
17+
{
18+
return Node\Expr\MethodCall::class;
19+
}
20+
21+
public function processNode(Node $node, Scope $scope): array
22+
{
23+
if (!$node->name instanceof Node\Identifier || $node->name->toLowerString() !== '__construct') {
24+
return [];
25+
}
26+
27+
return [
28+
RuleErrorBuilder::message('Call to __construct() on an existing object is not allowed.')
29+
->identifier('constructor.call')
30+
->build(),
31+
];
32+
}
33+
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Methods;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Rules\Rule;
8+
use PHPStan\Rules\RuleErrorBuilder;
9+
use function array_key_exists;
10+
use function array_map;
11+
use function in_array;
12+
use function sprintf;
13+
use function strtolower;
14+
15+
/**
16+
* @implements Rule<Node\Expr\StaticCall>
17+
*/
18+
final class IllegalConstructorStaticCallRule implements Rule
19+
{
20+
21+
public function getNodeType(): string
22+
{
23+
return Node\Expr\StaticCall::class;
24+
}
25+
26+
public function processNode(Node $node, Scope $scope): array
27+
{
28+
if (!$node->name instanceof Node\Identifier || $node->name->toLowerString() !== '__construct') {
29+
return [];
30+
}
31+
32+
if ($this->isCollectCallingConstructor($node, $scope)) {
33+
return [];
34+
}
35+
36+
return [
37+
RuleErrorBuilder::message('Static call to __construct() is only allowed on a parent class in the constructor.')
38+
->identifier('constructor.call')
39+
->build(),
40+
];
41+
}
42+
43+
private function isCollectCallingConstructor(Node\Expr\StaticCall $node, Scope $scope): bool
44+
{
45+
// __construct should be called from inside constructor
46+
if ($scope->getFunction() === null) {
47+
return false;
48+
}
49+
50+
if ($scope->getFunction()->getName() !== '__construct') {
51+
if (!$this->isInRenamedTraitConstructor($scope)) {
52+
return false;
53+
}
54+
}
55+
56+
if (!$scope->isInClass()) {
57+
return false;
58+
}
59+
60+
if (!$node->class instanceof Node\Name) {
61+
return false;
62+
}
63+
64+
$parentClasses = array_map(static fn (string $name) => strtolower($name), $scope->getClassReflection()->getParentClassesNames());
65+
66+
return in_array(strtolower($scope->resolveName($node->class)), $parentClasses, true);
67+
}
68+
69+
private function isInRenamedTraitConstructor(Scope $scope): bool
70+
{
71+
if (!$scope->isInClass()) {
72+
return false;
73+
}
74+
75+
if (!$scope->isInTrait()) {
76+
return false;
77+
}
78+
79+
if ($scope->getFunction() === null) {
80+
return false;
81+
}
82+
83+
$traitAliases = $scope->getClassReflection()->getNativeReflection()->getTraitAliases();
84+
$functionName = $scope->getFunction()->getName();
85+
if (!array_key_exists($functionName, $traitAliases)) {
86+
return false;
87+
}
88+
89+
return $traitAliases[$functionName] === sprintf('%s::%s', $scope->getTraitReflection()->getName(), '__construct');
90+
}
91+
92+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Methods;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<IllegalConstructorMethodCallRule>
10+
*/
11+
class IllegalConstructorMethodCallRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new IllegalConstructorMethodCallRule();
17+
}
18+
19+
public function testMethods(): void
20+
{
21+
$this->analyse([__DIR__ . '/data/illegal-constructor-call-rule-test.php'], [
22+
[
23+
'Call to __construct() on an existing object is not allowed.',
24+
13,
25+
],
26+
[
27+
'Call to __construct() on an existing object is not allowed.',
28+
18,
29+
],
30+
[
31+
'Call to __construct() on an existing object is not allowed.',
32+
60,
33+
],
34+
]);
35+
}
36+
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Methods;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use const PHP_VERSION_ID;
8+
9+
/**
10+
* @extends RuleTestCase<IllegalConstructorStaticCallRule>
11+
*/
12+
class IllegalConstructorStaticCallRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new IllegalConstructorStaticCallRule();
18+
}
19+
20+
public function testMethods(): void
21+
{
22+
$this->analyse([__DIR__ . '/data/illegal-constructor-call-rule-test.php'], [
23+
[
24+
'Static call to __construct() is only allowed on a parent class in the constructor.',
25+
31,
26+
],
27+
[
28+
'Static call to __construct() is only allowed on a parent class in the constructor.',
29+
43,
30+
],
31+
[
32+
'Static call to __construct() is only allowed on a parent class in the constructor.',
33+
44,
34+
],
35+
[
36+
'Static call to __construct() is only allowed on a parent class in the constructor.',
37+
49,
38+
],
39+
[
40+
'Static call to __construct() is only allowed on a parent class in the constructor.',
41+
50,
42+
],
43+
[
44+
'Static call to __construct() is only allowed on a parent class in the constructor.',
45+
100,
46+
],
47+
]);
48+
}
49+
50+
public function testBug9577(): void
51+
{
52+
if (PHP_VERSION_ID < 80100) {
53+
self::markTestSkipped('Test requires PHP 8.1.');
54+
}
55+
56+
$this->analyse([__DIR__ . '/data/bug-9577.php'], []);
57+
}
58+
59+
}

tests/Rules/Methods/data/bug-9577.php

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php // lint >= 8.1
2+
3+
namespace Bug9577IllegalConstructorStaticCall;
4+
5+
trait StringableMessageTrait
6+
{
7+
public function __construct(
8+
private readonly \Stringable $StringableMessage,
9+
int $code = 0,
10+
?\Throwable $previous = null,
11+
) {
12+
parent::__construct((string) $StringableMessage, $code, $previous);
13+
}
14+
15+
public function getStringableMessage(): \Stringable
16+
{
17+
return $this->StringableMessage;
18+
}
19+
}
20+
21+
class SpecializedException extends \RuntimeException
22+
{
23+
use StringableMessageTrait {
24+
StringableMessageTrait::__construct as __traitConstruct;
25+
}
26+
27+
public function __construct(
28+
private readonly object $aService,
29+
\Stringable $StringableMessage,
30+
int $code = 0,
31+
?\Throwable $previous = null,
32+
) {
33+
$this->__traitConstruct($StringableMessage, $code, $previous);
34+
}
35+
36+
public function getService(): object
37+
{
38+
return $this->aService;
39+
}
40+
}

0 commit comments

Comments
 (0)