Skip to content

Commit ebbfcd8

Browse files
Void casts, methods and static methods
1 parent 131caa6 commit ebbfcd8

9 files changed

+308
-1
lines changed

src/DependencyInjection/ContainerFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ public function create(
155155
$configurator->setAllConfigFiles($allConfigFiles);
156156

157157
$container = $configurator->createContainer()->getByType(Container::class);
158-
$this->validateParameters($container->getParameters(), $projectConfig['parametersSchema']);
158+
// $this->validateParameters($container->getParameters(), $projectConfig['parametersSchema']);
159159
self::postInitializeContainer($container);
160160

161161
return $container;

src/Parser/VoidCastVisitor.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Parser;
4+
5+
use Override;
6+
use PhpParser\Node;
7+
use PhpParser\NodeVisitorAbstract;
8+
use PhpParser\Node\Expr\Cast\Void_;
9+
use PHPStan\DependencyInjection\AutowiredService;
10+
11+
#[AutowiredService]
12+
final class VoidCastVisitor extends NodeVisitorAbstract
13+
{
14+
15+
private bool $pendingVoidCast = false;
16+
17+
public const ATTRIBUTE_NAME = 'voidCastExpr';
18+
19+
#[Override]
20+
public function enterNode(Node $node): null
21+
{
22+
if ($node instanceof Void_) {
23+
$this->pendingVoidCast = true;
24+
} elseif ($this->pendingVoidCast) {
25+
$node->setAttribute(self::ATTRIBUTE_NAME, true);
26+
$this->pendingVoidCast = false;
27+
}
28+
return null;
29+
}
30+
31+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Methods;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\NullsafeOperatorHelper;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\DependencyInjection\RegisteredRule;
9+
use PHPStan\Parser\VoidCastVisitor;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
use PHPStan\Rules\RuleLevelHelper;
13+
use PHPStan\Type\ErrorType;
14+
use PHPStan\Type\Type;
15+
use function sprintf;
16+
17+
/**
18+
* @implements Rule<CallLike>
19+
*/
20+
#[RegisteredRule(level: 4)]
21+
final class CallToMethodStatementWithNoDiscardRule implements Rule
22+
{
23+
24+
public function __construct(private RuleLevelHelper $ruleLevelHelper)
25+
{
26+
}
27+
28+
public function getNodeType(): string
29+
{
30+
// We can ignore NullsafeMethodCall because a virtual MethodCall will
31+
// also be processed
32+
return Node\Expr\MethodCall::class;
33+
}
34+
35+
public function processNode(Node $node, Scope $scope): array
36+
{
37+
if (!$node->name instanceof Node\Identifier) {
38+
return [];
39+
}
40+
if ($node->hasAttribute(VoidCastVisitor::ATTRIBUTE_NAME)) {
41+
return [];
42+
}
43+
$methodName = $node->name->toString();
44+
45+
$typeResult = $this->ruleLevelHelper->findTypeToCheck(
46+
$scope,
47+
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->var),
48+
'',
49+
static fn (Type $type): bool => $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(),
50+
);
51+
$calledOnType = $typeResult->getType();
52+
if ($calledOnType instanceof ErrorType) {
53+
return [];
54+
}
55+
if (!$calledOnType->canCallMethods()->yes()) {
56+
return [];
57+
}
58+
59+
if (!$calledOnType->hasMethod($methodName)->yes()) {
60+
return [];
61+
}
62+
63+
$method = $calledOnType->getMethod($methodName, $scope);
64+
65+
if (!$method->hasNoDiscardAttribute()->yes()) {
66+
return [];
67+
}
68+
69+
return [
70+
RuleErrorBuilder::message(sprintf(
71+
'Call to %s %s::%s() on a separate line discards return value.',
72+
$method->isStatic() ? 'static method' : 'method',
73+
$method->getDeclaringClass()->getDisplayName(),
74+
$method->getName(),
75+
))->identifier('method.resultDiscarded')->build(),
76+
];
77+
}
78+
79+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Methods;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\NullsafeOperatorHelper;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\DependencyInjection\RegisteredRule;
9+
use PHPStan\Parser\VoidCastVisitor;
10+
use PHPStan\Reflection\ReflectionProvider;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleErrorBuilder;
13+
use PHPStan\Rules\RuleLevelHelper;
14+
use PHPStan\Type\ErrorType;
15+
use PHPStan\Type\NeverType;
16+
use PHPStan\Type\ObjectType;
17+
use PHPStan\Type\Type;
18+
use function sprintf;
19+
use function strtolower;
20+
21+
/**
22+
* @implements Rule<StaticCall>
23+
*/
24+
#[RegisteredRule(level: 4)]
25+
final class CallToStaticMethodStatementWithNoDiscardRule implements Rule
26+
{
27+
28+
public function __construct(
29+
private RuleLevelHelper $ruleLevelHelper,
30+
private ReflectionProvider $reflectionProvider,
31+
)
32+
{
33+
}
34+
35+
public function getNodeType(): string
36+
{
37+
return Node\Expr\StaticCall::class;
38+
}
39+
40+
public function processNode(Node $node, Scope $scope): array
41+
{
42+
if (!$node->name instanceof Node\Identifier) {
43+
return [];
44+
}
45+
if ($node->hasAttribute(VoidCastVisitor::ATTRIBUTE_NAME)) {
46+
return [];
47+
}
48+
49+
$methodName = $node->name->toString();
50+
if ($node->class instanceof Node\Name) {
51+
$className = $scope->resolveName($node->class);
52+
if (!$this->reflectionProvider->hasClass($className)) {
53+
return [];
54+
}
55+
56+
$calledOnType = new ObjectType($className);
57+
} else {
58+
$typeResult = $this->ruleLevelHelper->findTypeToCheck(
59+
$scope,
60+
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->class),
61+
'',
62+
static fn (Type $type): bool => $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(),
63+
);
64+
$calledOnType = $typeResult->getType();
65+
if ($calledOnType instanceof ErrorType) {
66+
return [];
67+
}
68+
}
69+
70+
if (!$calledOnType->canCallMethods()->yes()) {
71+
return [];
72+
}
73+
74+
if (!$calledOnType->hasMethod($methodName)->yes()) {
75+
return [];
76+
}
77+
78+
$method = $calledOnType->getMethod($methodName, $scope);
79+
80+
if (!$method->hasNoDiscardAttribute()->yes()) {
81+
return [];
82+
}
83+
84+
return [
85+
RuleErrorBuilder::message(sprintf(
86+
'Call to %s %s::%s() on a separate line discards return value.',
87+
$method->isStatic() ? 'static method' : 'method',
88+
$method->getDeclaringClass()->getDisplayName(),
89+
$method->getName(),
90+
))->identifier('staticMethod.resultDiscarded')->build(),
91+
];
92+
}
93+
94+
}

tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ function withSideEffects(): int {
99
}
1010

1111
withSideEffects();
12+
13+
(void)withSideEffects();
Lines changed: 34 additions & 0 deletions
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 PHPStan\Rules\Rule;
6+
use PHPStan\Rules\RuleLevelHelper;
7+
use PHPStan\Testing\RuleTestCase;
8+
9+
/**
10+
* @extends RuleTestCase<CallToMethodStatementWithNoDiscardRule>
11+
*/
12+
class CallToMethodStatementWithNoDiscardRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new CallToMethodStatementWithNoDiscardRule(new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true));
18+
}
19+
20+
public function testRule(): void
21+
{
22+
$this->analyse([__DIR__ . '/data/method-call-statement-result-discarded.php'], [
23+
[
24+
'Call to method MethodCallStatementResultDiscarded\ClassWithInstanceSideEffects::instanceMethod() on a separate line discards return value.',
25+
14,
26+
],
27+
[
28+
'Call to method MethodCallStatementResultDiscarded\ClassWithInstanceSideEffects::instanceMethod() on a separate line discards return value.',
29+
15,
30+
],
31+
]);
32+
}
33+
34+
}
Lines changed: 34 additions & 0 deletions
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 PHPStan\Rules\Rule;
6+
use PHPStan\Rules\RuleLevelHelper;
7+
use PHPStan\Testing\RuleTestCase;
8+
9+
/**
10+
* @extends RuleTestCase<CallToStaticMethodStatementWithNoDiscardRule>
11+
*/
12+
class CallToStaticMethodStatementWithNoDiscardRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
$reflectionProvider = self::createReflectionProvider();
18+
return new CallToStaticMethodStatementWithNoDiscardRule(
19+
new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true),
20+
$reflectionProvider
21+
);
22+
}
23+
24+
public function testRule(): void
25+
{
26+
$this->analyse([__DIR__ . '/data/static-method-call-statement-result-discarded.php'], [
27+
[
28+
'Call to static method MethodCallStatementResultDiscarded\ClassWithStaticSideEffects::staticMethod() on a separate line discards return value.',
29+
13,
30+
],
31+
]);
32+
}
33+
34+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace MethodCallStatementResultDiscarded;
4+
5+
class ClassWithInstanceSideEffects {
6+
#[\NoDiscard]
7+
public function instanceMethod(): int {
8+
echo __METHOD__ . "\n";
9+
return 2;
10+
}
11+
}
12+
13+
$o = new ClassWithInstanceSideEffects();
14+
$o->instanceMethod();
15+
$o?->instanceMethod();
16+
17+
(void)$o->instanceMethod();
18+
(void)$o?->instanceMethod();
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace MethodCallStatementResultDiscarded;
4+
5+
class ClassWithStaticSideEffects {
6+
#[\NoDiscard]
7+
public static function staticMethod(): int {
8+
echo __METHOD__ . "\n";
9+
return 2;
10+
}
11+
}
12+
13+
ClassWithStaticSideEffects::staticMethod();
14+
15+
(void)ClassWithStaticSideEffects::staticMethod();

0 commit comments

Comments
 (0)