From 73d5925170affbd45c67def42dca47803b43bf44 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Thu, 21 Aug 2025 09:57:05 +0300 Subject: [PATCH 01/34] Add support for PHP 8.5 `#[\NoDiscard]` on functions --- ...llToFunctionStatementWithNoDiscardRule.php | 66 +++++++++++++++++++ ...FunctionStatementWithNoDiscardRuleTest.php | 29 ++++++++ ...nction-call-statement-result-discarded.php | 11 ++++ 3 files changed, 106 insertions(+) create mode 100644 src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php create mode 100644 tests/PHPStan/Rules/Functions/CallToFunctionStatementWithNoDiscardRuleTest.php create mode 100644 tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php diff --git a/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php b/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php new file mode 100644 index 0000000000..c35a7b038b --- /dev/null +++ b/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php @@ -0,0 +1,66 @@ + + */ +#[RegisteredRule(level: 4)] +final class CallToFunctionStatementWithNoDiscardRule implements Rule +{ + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Expression::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->expr instanceof Node\Expr\FuncCall) { + return []; + } + + $funcCall = $node->expr; + if (!($funcCall->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($funcCall->name, $scope)) { + return []; + } + + $function = $this->reflectionProvider->getFunction($funcCall->name, $scope); + + $attributes = $function->getAttributes(); + $hasNoDiscard = false; + foreach ($attributes as $attrib) { + if ($attrib->getName() === 'NoDiscard') { + $hasNoDiscard = true; + break; + } + } + if (!$hasNoDiscard) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Call to function %s() on a separate line discards return value.', + $function->getName(), + ))->identifier('function.resultDiscarded')->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithNoDiscardRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithNoDiscardRuleTest.php new file mode 100644 index 0000000000..6553cbb866 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithNoDiscardRuleTest.php @@ -0,0 +1,29 @@ + + */ +class CallToFunctionStatementWithNoDiscardRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToFunctionStatementWithNoDiscardRule(self::createReflectionProvider()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/function-call-statement-result-discarded.php'], [ + [ + 'Call to function FunctionCallStatementResultDiscarded\withSideEffects() on a separate line discards return value.', + 11, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php b/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php new file mode 100644 index 0000000000..54ca2390a5 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php @@ -0,0 +1,11 @@ + Date: Thu, 28 Aug 2025 23:05:15 +0300 Subject: [PATCH 02/34] Trinary logic migration --- .../Annotations/AnnotationMethodReflection.php | 5 +++++ src/Reflection/Dummy/ChangedTypeMethodReflection.php | 5 +++++ src/Reflection/Dummy/DummyConstructorReflection.php | 6 ++++++ src/Reflection/Dummy/DummyMethodReflection.php | 6 ++++++ src/Reflection/ExtendedMethodReflection.php | 7 +++++++ src/Reflection/FunctionReflection.php | 7 +++++++ src/Reflection/Native/NativeFunctionReflection.php | 10 ++++++++++ src/Reflection/Native/NativeMethodReflection.php | 10 ++++++++++ src/Reflection/Php/ClosureCallMethodReflection.php | 5 +++++ src/Reflection/Php/EnumCasesMethodReflection.php | 5 +++++ src/Reflection/Php/ExitFunctionReflection.php | 5 +++++ .../Php/PhpFunctionFromParserNodeReflection.php | 10 ++++++++++ src/Reflection/Php/PhpFunctionReflection.php | 10 ++++++++++ src/Reflection/Php/PhpMethodReflection.php | 10 ++++++++++ src/Reflection/ResolvedMethodReflection.php | 5 +++++ .../Type/IntersectionTypeMethodReflection.php | 5 +++++ src/Reflection/Type/UnionTypeMethodReflection.php | 5 +++++ src/Reflection/WrappedExtendedMethodReflection.php | 6 ++++++ .../CallToFunctionStatementWithNoDiscardRule.php | 10 +--------- .../RewrittenDeclaringClassMethodReflection.php | 5 +++++ 20 files changed, 128 insertions(+), 9 deletions(-) diff --git a/src/Reflection/Annotations/AnnotationMethodReflection.php b/src/Reflection/Annotations/AnnotationMethodReflection.php index 9b1886b544..eff496c0d7 100644 --- a/src/Reflection/Annotations/AnnotationMethodReflection.php +++ b/src/Reflection/Annotations/AnnotationMethodReflection.php @@ -183,4 +183,9 @@ public function getAttributes(): array return []; } + public function hasNoDiscardAttribute(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/Dummy/ChangedTypeMethodReflection.php b/src/Reflection/Dummy/ChangedTypeMethodReflection.php index 9a345c22cd..2869c4a9ce 100644 --- a/src/Reflection/Dummy/ChangedTypeMethodReflection.php +++ b/src/Reflection/Dummy/ChangedTypeMethodReflection.php @@ -172,4 +172,9 @@ public function getAttributes(): array return $this->reflection->getAttributes(); } + public function hasNoDiscardAttribute(): TrinaryLogic + { + return $this->reflection->hasNoDiscardAttribute(); + } + } diff --git a/src/Reflection/Dummy/DummyConstructorReflection.php b/src/Reflection/Dummy/DummyConstructorReflection.php index c48d6904ce..c424fcfbbe 100644 --- a/src/Reflection/Dummy/DummyConstructorReflection.php +++ b/src/Reflection/Dummy/DummyConstructorReflection.php @@ -157,4 +157,10 @@ public function getAttributes(): array return []; } + public function hasNoDiscardAttribute(): TrinaryLogic + { + // Align with the getAttributes() returning empty + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/Dummy/DummyMethodReflection.php b/src/Reflection/Dummy/DummyMethodReflection.php index dced9b6206..3fba88c555 100644 --- a/src/Reflection/Dummy/DummyMethodReflection.php +++ b/src/Reflection/Dummy/DummyMethodReflection.php @@ -149,4 +149,10 @@ public function getAttributes(): array return []; } + public function hasNoDiscardAttribute(): TrinaryLogic + { + // Align with the getAttributes() returning empty + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/ExtendedMethodReflection.php b/src/Reflection/ExtendedMethodReflection.php index 5cea392754..771a232692 100644 --- a/src/Reflection/ExtendedMethodReflection.php +++ b/src/Reflection/ExtendedMethodReflection.php @@ -65,4 +65,11 @@ public function isPure(): TrinaryLogic; */ public function getAttributes(): array; + /** + * Has the #[\NoDiscard] attribute - on PHP 8.5+ if the function's return + * value is unused at runtime a warning is emitted, phpstan will emit the + * warning during analysis and on older PHP versions too + */ + public function hasNoDiscardAttribute(): TrinaryLogic; + } diff --git a/src/Reflection/FunctionReflection.php b/src/Reflection/FunctionReflection.php index 297e4dd7d3..fdf479b796 100644 --- a/src/Reflection/FunctionReflection.php +++ b/src/Reflection/FunctionReflection.php @@ -62,4 +62,11 @@ public function isPure(): TrinaryLogic; */ public function getAttributes(): array; + /** + * Has the #[\NoDiscard] attribute - on PHP 8.5+ if the function's return + * value is unused at runtime a warning is emitted, phpstan will emit the + * warning during analysis and on older PHP versions too + */ + public function hasNoDiscardAttribute(): TrinaryLogic; + } diff --git a/src/Reflection/Native/NativeFunctionReflection.php b/src/Reflection/Native/NativeFunctionReflection.php index 7668d51f9e..eccfe480e5 100644 --- a/src/Reflection/Native/NativeFunctionReflection.php +++ b/src/Reflection/Native/NativeFunctionReflection.php @@ -150,4 +150,14 @@ public function getAttributes(): array return $this->attributes; } + public function hasNoDiscardAttribute(): TrinaryLogic + { + foreach ($this->attributes as $attrib) { + if ($attrib->getName() === 'NoDiscard') { + return TrinaryLogic::createYes(); + } + } + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/Native/NativeMethodReflection.php b/src/Reflection/Native/NativeMethodReflection.php index 91fc46229d..4da14d64f0 100644 --- a/src/Reflection/Native/NativeMethodReflection.php +++ b/src/Reflection/Native/NativeMethodReflection.php @@ -227,4 +227,14 @@ public function getAttributes(): array return $this->attributes; } + public function hasNoDiscardAttribute(): TrinaryLogic + { + foreach ($this->attributes as $attrib) { + if ($attrib->getName() === 'NoDiscard') { + return TrinaryLogic::createYes(); + } + } + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/Php/ClosureCallMethodReflection.php b/src/Reflection/Php/ClosureCallMethodReflection.php index aafd7b658e..a3a97d97c5 100644 --- a/src/Reflection/Php/ClosureCallMethodReflection.php +++ b/src/Reflection/Php/ClosureCallMethodReflection.php @@ -202,4 +202,9 @@ public function getAttributes(): array return $this->nativeMethodReflection->getAttributes(); } + public function hasNoDiscardAttribute(): TrinaryLogic + { + return $this->nativeMethodReflection->hasNoDiscardAttribute(); + } + } diff --git a/src/Reflection/Php/EnumCasesMethodReflection.php b/src/Reflection/Php/EnumCasesMethodReflection.php index ecf72e435d..15bcf3ebda 100644 --- a/src/Reflection/Php/EnumCasesMethodReflection.php +++ b/src/Reflection/Php/EnumCasesMethodReflection.php @@ -161,4 +161,9 @@ public function getAttributes(): array return []; } + public function hasNoDiscardAttribute(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + } diff --git a/src/Reflection/Php/ExitFunctionReflection.php b/src/Reflection/Php/ExitFunctionReflection.php index 4020bbdc09..f482a2dd73 100644 --- a/src/Reflection/Php/ExitFunctionReflection.php +++ b/src/Reflection/Php/ExitFunctionReflection.php @@ -143,4 +143,9 @@ public function getAttributes(): array return []; } + public function hasNoDiscardAttribute(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index de16789678..17bf02ae91 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -338,4 +338,14 @@ public function getAttributes(): array return $this->attributes; } + public function hasNoDiscardAttribute(): TrinaryLogic + { + foreach ($this->attributes as $attrib) { + if ($attrib->getName() === 'NoDiscard') { + return TrinaryLogic::createYes(); + } + } + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/Php/PhpFunctionReflection.php b/src/Reflection/Php/PhpFunctionReflection.php index 38604e8761..2a156d5d3e 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -275,4 +275,14 @@ public function getAttributes(): array return $this->attributes; } + public function hasNoDiscardAttribute(): TrinaryLogic + { + foreach ($this->attributes as $attrib) { + if ($attrib->getName() === 'NoDiscard') { + return TrinaryLogic::createYes(); + } + } + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index 22982ca1f3..cf942d27f4 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -520,4 +520,14 @@ public function getAttributes(): array return $this->attributes; } + public function hasNoDiscardAttribute(): TrinaryLogic + { + foreach ($this->attributes as $attrib) { + if ($attrib->getName() === 'NoDiscard') { + return TrinaryLogic::createYes(); + } + } + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/ResolvedMethodReflection.php b/src/Reflection/ResolvedMethodReflection.php index 134e566eca..c45071b998 100644 --- a/src/Reflection/ResolvedMethodReflection.php +++ b/src/Reflection/ResolvedMethodReflection.php @@ -229,4 +229,9 @@ public function getAttributes(): array return $this->reflection->getAttributes(); } + public function hasNoDiscardAttribute(): TrinaryLogic + { + return $this->reflection->hasNoDiscardAttribute(); + } + } diff --git a/src/Reflection/Type/IntersectionTypeMethodReflection.php b/src/Reflection/Type/IntersectionTypeMethodReflection.php index eafd314157..e90a8f4a57 100644 --- a/src/Reflection/Type/IntersectionTypeMethodReflection.php +++ b/src/Reflection/Type/IntersectionTypeMethodReflection.php @@ -228,4 +228,9 @@ public function getAttributes(): array return $this->methods[0]->getAttributes(); } + public function hasNoDiscardAttribute(): TrinaryLogic + { + return TrinaryLogic::lazyMaxMin($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->hasNoDiscardAttribute()); + } + } diff --git a/src/Reflection/Type/UnionTypeMethodReflection.php b/src/Reflection/Type/UnionTypeMethodReflection.php index b330b6fdad..ffd75362ba 100644 --- a/src/Reflection/Type/UnionTypeMethodReflection.php +++ b/src/Reflection/Type/UnionTypeMethodReflection.php @@ -205,4 +205,9 @@ public function getAttributes(): array return $this->methods[0]->getAttributes(); } + public function hasNoDiscardAttribute(): TrinaryLogic + { + return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->hasNoDiscardAttribute()); + } + } diff --git a/src/Reflection/WrappedExtendedMethodReflection.php b/src/Reflection/WrappedExtendedMethodReflection.php index 5a9ea238cf..00102a9447 100644 --- a/src/Reflection/WrappedExtendedMethodReflection.php +++ b/src/Reflection/WrappedExtendedMethodReflection.php @@ -173,4 +173,10 @@ public function getAttributes(): array return []; } + public function hasNoDiscardAttribute(): TrinaryLogic + { + // Align with the getAttributes() returning empty + return TrinaryLogic::createNo(); + } + } diff --git a/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php b/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php index c35a7b038b..13d4d77f22 100644 --- a/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php +++ b/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php @@ -43,15 +43,7 @@ public function processNode(Node $node, Scope $scope): array $function = $this->reflectionProvider->getFunction($funcCall->name, $scope); - $attributes = $function->getAttributes(); - $hasNoDiscard = false; - foreach ($attributes as $attrib) { - if ($attrib->getName() === 'NoDiscard') { - $hasNoDiscard = true; - break; - } - } - if (!$hasNoDiscard) { + if (!$function->hasNoDiscardAttribute()->yes()) { return []; } diff --git a/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php b/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php index a2575177a5..d8383f0cad 100644 --- a/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php +++ b/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php @@ -145,4 +145,9 @@ public function hasSideEffects(): TrinaryLogic return $this->methodReflection->hasSideEffects(); } + public function hasNoDiscardAttribute(): TrinaryLogic + { + return $this->methodReflection->hasNoDiscardAttribute(); + } + } From 477b4b69d1147d5c0c80ca9e4d2b563fc5f0ddc3 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Fri, 29 Aug 2025 23:08:59 +0300 Subject: [PATCH 03/34] Void casts, methods and static methods --- src/DependencyInjection/ContainerFactory.php | 2 +- src/Parser/VoidCastVisitor.php | 31 ++++++ ...CallToMethodStatementWithNoDiscardRule.php | 79 ++++++++++++++++ ...StaticMethodStatementWithNoDiscardRule.php | 94 +++++++++++++++++++ ...nction-call-statement-result-discarded.php | 2 + ...ToMethodStatementWithNoDiscardRuleTest.php | 34 +++++++ ...icMethodStatementWithNoDiscardRuleTest.php | 34 +++++++ ...method-call-statement-result-discarded.php | 18 ++++ ...method-call-statement-result-discarded.php | 15 +++ 9 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 src/Parser/VoidCastVisitor.php create mode 100644 src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php create mode 100644 src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php create mode 100644 tests/PHPStan/Rules/Methods/CallToMethodStatementWithNoDiscardRuleTest.php create mode 100644 tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRuleTest.php create mode 100644 tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php create mode 100644 tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php diff --git a/src/DependencyInjection/ContainerFactory.php b/src/DependencyInjection/ContainerFactory.php index 379b1fb741..0582abc57f 100644 --- a/src/DependencyInjection/ContainerFactory.php +++ b/src/DependencyInjection/ContainerFactory.php @@ -159,7 +159,7 @@ public function create( $configurator->setAllConfigFiles($allConfigFiles); $container = $configurator->createContainer()->getByType(Container::class); - $this->validateParameters($container->getParameters(), $projectConfig['parametersSchema']); + // $this->validateParameters($container->getParameters(), $projectConfig['parametersSchema']); self::postInitializeContainer($container); return $container; diff --git a/src/Parser/VoidCastVisitor.php b/src/Parser/VoidCastVisitor.php new file mode 100644 index 0000000000..45f0f85962 --- /dev/null +++ b/src/Parser/VoidCastVisitor.php @@ -0,0 +1,31 @@ +pendingVoidCast = true; + } elseif ($this->pendingVoidCast) { + $node->setAttribute(self::ATTRIBUTE_NAME, true); + $this->pendingVoidCast = false; + } + return null; + } + +} diff --git a/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php b/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php new file mode 100644 index 0000000000..578a5f7d8e --- /dev/null +++ b/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php @@ -0,0 +1,79 @@ + + */ +#[RegisteredRule(level: 4)] +final class CallToMethodStatementWithNoDiscardRule implements Rule +{ + + public function __construct(private RuleLevelHelper $ruleLevelHelper) + { + } + + public function getNodeType(): string + { + // We can ignore NullsafeMethodCall because a virtual MethodCall will + // also be processed + return Node\Expr\MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Identifier) { + return []; + } + if ($node->hasAttribute(VoidCastVisitor::ATTRIBUTE_NAME)) { + return []; + } + $methodName = $node->name->toString(); + + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->var), + '', + static fn (Type $type): bool => $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(), + ); + $calledOnType = $typeResult->getType(); + if ($calledOnType instanceof ErrorType) { + return []; + } + if (!$calledOnType->canCallMethods()->yes()) { + return []; + } + + if (!$calledOnType->hasMethod($methodName)->yes()) { + return []; + } + + $method = $calledOnType->getMethod($methodName, $scope); + + if (!$method->hasNoDiscardAttribute()->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Call to %s %s::%s() on a separate line discards return value.', + $method->isStatic() ? 'static method' : 'method', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->identifier('method.resultDiscarded')->build(), + ]; + } + +} diff --git a/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php new file mode 100644 index 0000000000..550afcd66b --- /dev/null +++ b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php @@ -0,0 +1,94 @@ + + */ +#[RegisteredRule(level: 4)] +final class CallToStaticMethodStatementWithNoDiscardRule implements Rule +{ + + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return Node\Expr\StaticCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Identifier) { + return []; + } + if ($node->hasAttribute(VoidCastVisitor::ATTRIBUTE_NAME)) { + return []; + } + + $methodName = $node->name->toString(); + if ($node->class instanceof Node\Name) { + $className = $scope->resolveName($node->class); + if (!$this->reflectionProvider->hasClass($className)) { + return []; + } + + $calledOnType = new ObjectType($className); + } else { + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->class), + '', + static fn (Type $type): bool => $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(), + ); + $calledOnType = $typeResult->getType(); + if ($calledOnType instanceof ErrorType) { + return []; + } + } + + if (!$calledOnType->canCallMethods()->yes()) { + return []; + } + + if (!$calledOnType->hasMethod($methodName)->yes()) { + return []; + } + + $method = $calledOnType->getMethod($methodName, $scope); + + if (!$method->hasNoDiscardAttribute()->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Call to %s %s::%s() on a separate line discards return value.', + $method->isStatic() ? 'static method' : 'method', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->identifier('staticMethod.resultDiscarded')->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php b/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php index 54ca2390a5..9617818b42 100644 --- a/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php +++ b/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php @@ -9,3 +9,5 @@ function withSideEffects(): int { } withSideEffects(); + +(void)withSideEffects(); diff --git a/tests/PHPStan/Rules/Methods/CallToMethodStatementWithNoDiscardRuleTest.php b/tests/PHPStan/Rules/Methods/CallToMethodStatementWithNoDiscardRuleTest.php new file mode 100644 index 0000000000..841d394346 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/CallToMethodStatementWithNoDiscardRuleTest.php @@ -0,0 +1,34 @@ + + */ +class CallToMethodStatementWithNoDiscardRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallToMethodStatementWithNoDiscardRule(new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true)); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/method-call-statement-result-discarded.php'], [ + [ + 'Call to method MethodCallStatementResultDiscarded\ClassWithInstanceSideEffects::instanceMethod() on a separate line discards return value.', + 14, + ], + [ + 'Call to method MethodCallStatementResultDiscarded\ClassWithInstanceSideEffects::instanceMethod() on a separate line discards return value.', + 15, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRuleTest.php b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRuleTest.php new file mode 100644 index 0000000000..2fd842d9d9 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRuleTest.php @@ -0,0 +1,34 @@ + + */ +class CallToStaticMethodStatementWithNoDiscardRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::createReflectionProvider(); + return new CallToStaticMethodStatementWithNoDiscardRule( + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), + $reflectionProvider + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/static-method-call-statement-result-discarded.php'], [ + [ + 'Call to static method MethodCallStatementResultDiscarded\ClassWithStaticSideEffects::staticMethod() on a separate line discards return value.', + 13, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php b/tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php new file mode 100644 index 0000000000..7e02149927 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php @@ -0,0 +1,18 @@ +instanceMethod(); +$o?->instanceMethod(); + +(void)$o->instanceMethod(); +(void)$o?->instanceMethod(); diff --git a/tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php b/tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php new file mode 100644 index 0000000000..bfd91891e0 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php @@ -0,0 +1,15 @@ + Date: Fri, 29 Aug 2025 23:13:55 +0300 Subject: [PATCH 04/34] fixes --- src/DependencyInjection/ContainerFactory.php | 2 +- src/Parser/VoidCastVisitor.php | 4 ++-- .../Methods/CallToStaticMethodStatementWithNoDiscardRule.php | 2 -- .../CallToStaticMethodStatementWithNoDiscardRuleTest.php | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/DependencyInjection/ContainerFactory.php b/src/DependencyInjection/ContainerFactory.php index 0582abc57f..379b1fb741 100644 --- a/src/DependencyInjection/ContainerFactory.php +++ b/src/DependencyInjection/ContainerFactory.php @@ -159,7 +159,7 @@ public function create( $configurator->setAllConfigFiles($allConfigFiles); $container = $configurator->createContainer()->getByType(Container::class); - // $this->validateParameters($container->getParameters(), $projectConfig['parametersSchema']); + $this->validateParameters($container->getParameters(), $projectConfig['parametersSchema']); self::postInitializeContainer($container); return $container; diff --git a/src/Parser/VoidCastVisitor.php b/src/Parser/VoidCastVisitor.php index 45f0f85962..5a31241db3 100644 --- a/src/Parser/VoidCastVisitor.php +++ b/src/Parser/VoidCastVisitor.php @@ -4,8 +4,8 @@ use Override; use PhpParser\Node; -use PhpParser\NodeVisitorAbstract; use PhpParser\Node\Expr\Cast\Void_; +use PhpParser\NodeVisitorAbstract; use PHPStan\DependencyInjection\AutowiredService; #[AutowiredService] @@ -17,7 +17,7 @@ final class VoidCastVisitor extends NodeVisitorAbstract public const ATTRIBUTE_NAME = 'voidCastExpr'; #[Override] - public function enterNode(Node $node): null + public function enterNode(Node $node): ?Node { if ($node instanceof Void_) { $this->pendingVoidCast = true; diff --git a/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php index 550afcd66b..4b6fb7b7da 100644 --- a/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php +++ b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php @@ -12,11 +12,9 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; -use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use function sprintf; -use function strtolower; /** * @implements Rule diff --git a/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRuleTest.php b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRuleTest.php index 2fd842d9d9..a3fed3a39b 100644 --- a/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRuleTest.php @@ -17,7 +17,7 @@ protected function getRule(): Rule $reflectionProvider = self::createReflectionProvider(); return new CallToStaticMethodStatementWithNoDiscardRule( new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), - $reflectionProvider + $reflectionProvider, ); } From b9e0c624e7ca51cd54179376ff1e0d872f284731 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Fri, 29 Aug 2025 23:17:04 +0300 Subject: [PATCH 05/34] parse errors --- build/collision-detector.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build/collision-detector.json b/build/collision-detector.json index a687cd3ea4..bafed03e0c 100644 --- a/build/collision-detector.json +++ b/build/collision-detector.json @@ -15,6 +15,9 @@ "../tests/PHPStan/Rules/Functions/data/define-bug-3349.php", "../tests/PHPStan/Levels/data/stubs/function.php", "../tests/PHPStan/Rules/Properties/data/abstract-final-property-hook-parse-error.php", - "../tests/PHPStan/Rules/Properties/data/final-property-hooks.php" + "../tests/PHPStan/Rules/Properties/data/final-property-hooks.php", + "../tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php", + "../tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php", + "../tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php" ] } From 4379a759e52585f99e5e1b2711352e007c978c9a Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Fri, 29 Aug 2025 23:21:55 +0300 Subject: [PATCH 06/34] missing use --- .../Methods/CallToStaticMethodStatementWithNoDiscardRule.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php index 4b6fb7b7da..7394c5ac80 100644 --- a/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php +++ b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Methods; use PhpParser\Node; +use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; From dfbb5a692c0979e9afdf7a274ddd8b562a52aa97 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Fri, 29 Aug 2025 23:22:27 +0300 Subject: [PATCH 07/34] another --- src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php b/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php index 578a5f7d8e..7e9cac9586 100644 --- a/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php +++ b/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Methods; use PhpParser\Node; +use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; @@ -15,7 +16,7 @@ use function sprintf; /** - * @implements Rule + * @implements Rule */ #[RegisteredRule(level: 4)] final class CallToMethodStatementWithNoDiscardRule implements Rule @@ -29,7 +30,7 @@ public function getNodeType(): string { // We can ignore NullsafeMethodCall because a virtual MethodCall will // also be processed - return Node\Expr\MethodCall::class; + return MethodCall::class; } public function processNode(Node $node, Scope $scope): array From 065712db12290724bda8977e1121d685c34887e1 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Sat, 30 Aug 2025 17:56:46 +0300 Subject: [PATCH 08/34] New VoidCastRule --- src/Rules/Cast/VoidCastRule.php | 41 +++++++++++++++++++ tests/PHPStan/Rules/Cast/VoidCastRuleTest.php | 37 +++++++++++++++++ tests/PHPStan/Rules/Cast/data/void-cast.php | 7 ++++ 3 files changed, 85 insertions(+) create mode 100644 src/Rules/Cast/VoidCastRule.php create mode 100644 tests/PHPStan/Rules/Cast/VoidCastRuleTest.php create mode 100644 tests/PHPStan/Rules/Cast/data/void-cast.php diff --git a/src/Rules/Cast/VoidCastRule.php b/src/Rules/Cast/VoidCastRule.php new file mode 100644 index 0000000000..9e01ab6c83 --- /dev/null +++ b/src/Rules/Cast/VoidCastRule.php @@ -0,0 +1,41 @@ + + */ +#[RegisteredRule(level: 0)] +final class VoidCastRule implements Rule +{ + + public function __construct() + { + } + + public function getNodeType(): string + { + return Node\Expr\Cast\Void_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($scope->isInFirstLevelStatement()) { + return []; + } + + return [ + RuleErrorBuilder::message('The (void) cast cannot be used within an expression.') + ->identifier('cast.void') + ->nonIgnorable() + ->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/Cast/VoidCastRuleTest.php b/tests/PHPStan/Rules/Cast/VoidCastRuleTest.php new file mode 100644 index 0000000000..f9143c3ca7 --- /dev/null +++ b/tests/PHPStan/Rules/Cast/VoidCastRuleTest.php @@ -0,0 +1,37 @@ + + */ +class VoidCastRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new VoidCastRule(); + } + + public function testPrintRule(): void + { + $this->analyse([__DIR__ . '/data/void-cast.php'], [ + [ + 'The (void) cast cannot be used within an expression.', + 5, + ], + [ + 'The (void) cast cannot be used within an expression.', + 6, + ], + [ + 'The (void) cast cannot be used within an expression.', + 7, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Cast/data/void-cast.php b/tests/PHPStan/Rules/Cast/data/void-cast.php new file mode 100644 index 0000000000..7c8c342055 --- /dev/null +++ b/tests/PHPStan/Rules/Cast/data/void-cast.php @@ -0,0 +1,7 @@ + Date: Sat, 30 Aug 2025 18:12:04 +0300 Subject: [PATCH 09/34] NoDiscard on hooks --- src/Rules/AttributesCheck.php | 7 +++++++ src/Rules/Properties/PropertyHookAttributesRule.php | 1 + .../Properties/PropertyHookAttributesRuleTest.php | 4 ++++ .../Properties/data/property-hook-attributes.php | 11 +++++++++++ 4 files changed, 23 insertions(+) diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index 36b3c6faa6..4fa9ecc4bf 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -40,6 +40,7 @@ public function check( array $attrGroups, int $requiredTarget, string $targetName, + bool $isPropertyHook = false, ): array { $errors = []; @@ -81,6 +82,12 @@ public function check( ->identifier('attribute.target') ->line($attribute->getStartLine()) ->build(); + } elseif ($isPropertyHook && strtolower($name) === "nodiscard") { + // #[\NoDiscard] cannot be used on property hooks + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s cannot be used on property hooks.', $name)) + ->identifier('attribute.target') + ->line($attribute->getStartLine()) + ->build(); } if (($flags & Attribute::IS_REPEATABLE) === 0) { diff --git a/src/Rules/Properties/PropertyHookAttributesRule.php b/src/Rules/Properties/PropertyHookAttributesRule.php index 509fe8f83e..db5ede4e1d 100644 --- a/src/Rules/Properties/PropertyHookAttributesRule.php +++ b/src/Rules/Properties/PropertyHookAttributesRule.php @@ -33,6 +33,7 @@ public function processNode(Node $node, Scope $scope): array $node->getOriginalNode()->attrGroups, Attribute::TARGET_METHOD, 'method', + true, ); } diff --git a/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php index ef9b16df15..cb89c3bacb 100644 --- a/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php @@ -55,6 +55,10 @@ public function testRule(): void 'Attribute class PropertyHookAttributes\Foo does not have the method target.', 27, ], + [ + 'Attribute class NoDiscard cannot be used on property hooks.', + 63, + ], ]); } diff --git a/tests/PHPStan/Rules/Properties/data/property-hook-attributes.php b/tests/PHPStan/Rules/Properties/data/property-hook-attributes.php index 495cc793b0..3390cec241 100644 --- a/tests/PHPStan/Rules/Properties/data/property-hook-attributes.php +++ b/tests/PHPStan/Rules/Properties/data/property-hook-attributes.php @@ -55,3 +55,14 @@ class Dolor } } + +class Sit +{ + + public int $i { + #[\NoDiscard] + get { + + } + } +} From d87ffc7b88658fc030e4911ebf6a40d14f965daf Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Sat, 30 Aug 2025 18:38:34 +0300 Subject: [PATCH 10/34] Lint exclusions, unify implementations --- Makefile | 2 ++ build/collision-detector.json | 3 ++- ...CallToMethodStatementWithNoDiscardRule.php | 22 +++++++++++-------- ...StaticMethodStatementWithNoDiscardRule.php | 22 +++++++++++-------- ...nction-call-statement-result-discarded.php | 8 +++++-- ...method-call-statement-result-discarded.php | 8 +++++-- ...method-call-statement-result-discarded.php | 8 +++++-- 7 files changed, 48 insertions(+), 25 deletions(-) diff --git a/Makefile b/Makefile index 304b72960c..487f00a56d 100644 --- a/Makefile +++ b/Makefile @@ -110,6 +110,8 @@ lint: --exclude tests/PHPStan/Rules/Classes/data/enum-cannot-be-attribute.php \ --exclude tests/PHPStan/Rules/Classes/data/class-attributes.php \ --exclude tests/PHPStan/Rules/Classes/data/enum-attributes.php \ + --exclude tests/PHPStan/Rules/Cast/data/void-cast.php \ + --exclude tests/PHPStan/Rules/Properties/data/property-hook-attributes.php \ src tests install-paratest: diff --git a/build/collision-detector.json b/build/collision-detector.json index bafed03e0c..17ab26239c 100644 --- a/build/collision-detector.json +++ b/build/collision-detector.json @@ -18,6 +18,7 @@ "../tests/PHPStan/Rules/Properties/data/final-property-hooks.php", "../tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php", "../tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php", - "../tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php" + "../tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php", + "../tests/PHPStan/Rules/Properties/data/property-hook-attributes.php" ] } diff --git a/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php b/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php index 7e9cac9586..968faf0711 100644 --- a/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php +++ b/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php @@ -3,7 +3,6 @@ namespace PHPStan\Rules\Methods; use PhpParser\Node; -use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; @@ -16,7 +15,7 @@ use function sprintf; /** - * @implements Rule + * @implements Rule */ #[RegisteredRule(level: 4)] final class CallToMethodStatementWithNoDiscardRule implements Rule @@ -28,24 +27,29 @@ public function __construct(private RuleLevelHelper $ruleLevelHelper) public function getNodeType(): string { - // We can ignore NullsafeMethodCall because a virtual MethodCall will - // also be processed - return MethodCall::class; + return Node\Stmt\Expression::class; } public function processNode(Node $node, Scope $scope): array { - if (!$node->name instanceof Node\Identifier) { + if (!$node->expr instanceof Node\Expr\MethodCall + && !$node->expr instanceof Node\Expr\NullsafeMethodCall + ) { return []; } - if ($node->hasAttribute(VoidCastVisitor::ATTRIBUTE_NAME)) { + + $funcCall = $node->expr; + if (!$funcCall->name instanceof Node\Identifier) { + return []; + } + if ($funcCall->hasAttribute(VoidCastVisitor::ATTRIBUTE_NAME)) { return []; } - $methodName = $node->name->toString(); + $methodName = $funcCall->name->toString(); $typeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->var), + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $funcCall->var), '', static fn (Type $type): bool => $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(), ); diff --git a/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php index 7394c5ac80..fc8b04fe3f 100644 --- a/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php +++ b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php @@ -3,7 +3,6 @@ namespace PHPStan\Rules\Methods; use PhpParser\Node; -use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; @@ -18,7 +17,7 @@ use function sprintf; /** - * @implements Rule + * @implements Rule */ #[RegisteredRule(level: 4)] final class CallToStaticMethodStatementWithNoDiscardRule implements Rule @@ -33,21 +32,26 @@ public function __construct( public function getNodeType(): string { - return Node\Expr\StaticCall::class; + return Node\Stmt\Expression::class; } public function processNode(Node $node, Scope $scope): array { - if (!$node->name instanceof Node\Identifier) { + if (!$node->expr instanceof Node\Expr\StaticCall) { return []; } - if ($node->hasAttribute(VoidCastVisitor::ATTRIBUTE_NAME)) { + + $funcCall = $node->expr; + if (!$funcCall->name instanceof Node\Identifier) { + return []; + } + if ($funcCall->hasAttribute(VoidCastVisitor::ATTRIBUTE_NAME)) { return []; } - $methodName = $node->name->toString(); - if ($node->class instanceof Node\Name) { - $className = $scope->resolveName($node->class); + $methodName = $funcCall->name->toString(); + if ($funcCall->class instanceof Node\Name) { + $className = $scope->resolveName($funcCall->class); if (!$this->reflectionProvider->hasClass($className)) { return []; } @@ -56,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array } else { $typeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->class), + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $funcCall->class), '', static fn (Type $type): bool => $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(), ); diff --git a/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php b/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php index 9617818b42..80c525060d 100644 --- a/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php +++ b/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php @@ -3,11 +3,15 @@ namespace FunctionCallStatementResultDiscarded; #[\NoDiscard] -function withSideEffects(): int { +function withSideEffects(): array { echo __FUNCTION__ . "\n"; - return 1; + return [1]; } withSideEffects(); (void)withSideEffects(); + +foreach (withSideEffects() as $num) { + var_dump($num); +} diff --git a/tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php b/tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php index 7e02149927..4fb606c1b5 100644 --- a/tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php +++ b/tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php @@ -4,9 +4,9 @@ class ClassWithInstanceSideEffects { #[\NoDiscard] - public function instanceMethod(): int { + public function instanceMethod(): array { echo __METHOD__ . "\n"; - return 2; + return [2]; } } @@ -16,3 +16,7 @@ public function instanceMethod(): int { (void)$o->instanceMethod(); (void)$o?->instanceMethod(); + +foreach ($o->instanceMethod() as $num) { + var_dump($num); +} diff --git a/tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php b/tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php index bfd91891e0..f5685cbdfc 100644 --- a/tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php +++ b/tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php @@ -4,12 +4,16 @@ class ClassWithStaticSideEffects { #[\NoDiscard] - public static function staticMethod(): int { + public static function staticMethod(): array { echo __METHOD__ . "\n"; - return 2; + return [2]; } } ClassWithStaticSideEffects::staticMethod(); (void)ClassWithStaticSideEffects::staticMethod(); + +foreach (ClassWithStaticSideEffects::staticMethod() as $num) { + var_dump($num); +} From 76a00d48f3b9c519756d87d0da65b975bad3c24c Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Sat, 30 Aug 2025 18:46:43 +0300 Subject: [PATCH 11/34] fixes --- Makefile | 2 +- build/collision-detector.json | 3 ++- src/Rules/AttributesCheck.php | 2 +- .../Properties/PropertyHookAttributesRuleTest.php | 9 ++++++++- .../data/property-hook-attributes-nodiscard.php | 14 ++++++++++++++ .../Properties/data/property-hook-attributes.php | 11 ----------- 6 files changed, 26 insertions(+), 15 deletions(-) create mode 100644 tests/PHPStan/Rules/Properties/data/property-hook-attributes-nodiscard.php diff --git a/Makefile b/Makefile index 487f00a56d..1bde3401a1 100644 --- a/Makefile +++ b/Makefile @@ -111,7 +111,7 @@ lint: --exclude tests/PHPStan/Rules/Classes/data/class-attributes.php \ --exclude tests/PHPStan/Rules/Classes/data/enum-attributes.php \ --exclude tests/PHPStan/Rules/Cast/data/void-cast.php \ - --exclude tests/PHPStan/Rules/Properties/data/property-hook-attributes.php \ + --exclude tests/PHPStan/Rules/Properties/data/property-hook-attributes-nodiscard.php \ src tests install-paratest: diff --git a/build/collision-detector.json b/build/collision-detector.json index 17ab26239c..1188d514b1 100644 --- a/build/collision-detector.json +++ b/build/collision-detector.json @@ -19,6 +19,7 @@ "../tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php", "../tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php", "../tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php", - "../tests/PHPStan/Rules/Properties/data/property-hook-attributes.php" + "../tests/PHPStan/Rules/Properties/data/property-hook-attributes-nodiscard.php", + "../tests/PHPStan/Rules/Cast/data/void-cast.php" ] } diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index 4fa9ecc4bf..874611e9c7 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -82,7 +82,7 @@ public function check( ->identifier('attribute.target') ->line($attribute->getStartLine()) ->build(); - } elseif ($isPropertyHook && strtolower($name) === "nodiscard") { + } elseif ($isPropertyHook && strtolower($name) === 'nodiscard') { // #[\NoDiscard] cannot be used on property hooks $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s cannot be used on property hooks.', $name)) ->identifier('attribute.target') diff --git a/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php index cb89c3bacb..e195cd5533 100644 --- a/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php @@ -55,9 +55,16 @@ public function testRule(): void 'Attribute class PropertyHookAttributes\Foo does not have the method target.', 27, ], + ]); + } + + #[RequiresPhp('>= 8.5')] + public function testNoDiscard(): void + { + $this->analyse([__DIR__ . '/data/property-hook-attributes-nodiscard.php'], [ [ 'Attribute class NoDiscard cannot be used on property hooks.', - 63, + 9, ], ]); } diff --git a/tests/PHPStan/Rules/Properties/data/property-hook-attributes-nodiscard.php b/tests/PHPStan/Rules/Properties/data/property-hook-attributes-nodiscard.php new file mode 100644 index 0000000000..d0c7d0c88e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-hook-attributes-nodiscard.php @@ -0,0 +1,14 @@ += 8.5 + +namespace PropertyHookAttributes; + +class Sit +{ + + public int $i { + #[\NoDiscard] + get { + + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/property-hook-attributes.php b/tests/PHPStan/Rules/Properties/data/property-hook-attributes.php index 3390cec241..495cc793b0 100644 --- a/tests/PHPStan/Rules/Properties/data/property-hook-attributes.php +++ b/tests/PHPStan/Rules/Properties/data/property-hook-attributes.php @@ -55,14 +55,3 @@ class Dolor } } - -class Sit -{ - - public int $i { - #[\NoDiscard] - get { - - } - } -} From 59646cc6a44d75ee1fa5b2afcd5a646e52a2f6cf Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Mon, 15 Sep 2025 12:42:15 -0700 Subject: [PATCH 12/34] Fixes Case insensitive attribute handling, mandatory parameter, enum cases --- src/Reflection/Native/NativeFunctionReflection.php | 3 ++- src/Reflection/Native/NativeMethodReflection.php | 2 +- src/Reflection/Php/EnumCasesMethodReflection.php | 2 +- .../Php/PhpFunctionFromParserNodeReflection.php | 3 ++- src/Reflection/Php/PhpFunctionReflection.php | 3 ++- src/Reflection/Php/PhpMethodReflection.php | 2 +- src/Rules/AttributesCheck.php | 2 +- src/Rules/Classes/ClassAttributesRule.php | 1 + src/Rules/Classes/ClassConstantAttributesRule.php | 1 + src/Rules/EnumCases/EnumCaseAttributesRule.php | 1 + src/Rules/Functions/ArrowFunctionAttributesRule.php | 1 + src/Rules/Functions/ClosureAttributesRule.php | 1 + src/Rules/Functions/FunctionAttributesRule.php | 1 + src/Rules/Functions/ParamAttributesRule.php | 1 + src/Rules/Methods/MethodAttributesRule.php | 1 + src/Rules/Properties/PropertyAttributesRule.php | 1 + src/Rules/Traits/TraitAttributesRule.php | 1 + .../CallToFunctionStatementWithNoDiscardRuleTest.php | 4 ++++ .../data/function-call-statement-result-discarded.php | 8 ++++++++ .../CallToMethodStatementWithNoDiscardRuleTest.php | 8 ++++++-- .../CallToStaticMethodStatementWithNoDiscardRuleTest.php | 6 +++++- .../data/method-call-statement-result-discarded.php | 8 ++++++++ .../static-method-call-statement-result-discarded.php | 8 ++++++++ 23 files changed, 59 insertions(+), 10 deletions(-) diff --git a/src/Reflection/Native/NativeFunctionReflection.php b/src/Reflection/Native/NativeFunctionReflection.php index eccfe480e5..612cfbcc24 100644 --- a/src/Reflection/Native/NativeFunctionReflection.php +++ b/src/Reflection/Native/NativeFunctionReflection.php @@ -10,6 +10,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use function count; +use function strtolower; final class NativeFunctionReflection implements FunctionReflection { @@ -153,7 +154,7 @@ public function getAttributes(): array public function hasNoDiscardAttribute(): TrinaryLogic { foreach ($this->attributes as $attrib) { - if ($attrib->getName() === 'NoDiscard') { + if (strtolower($attrib->getName()) === 'nodiscard') { return TrinaryLogic::createYes(); } } diff --git a/src/Reflection/Native/NativeMethodReflection.php b/src/Reflection/Native/NativeMethodReflection.php index 4da14d64f0..a0310a060c 100644 --- a/src/Reflection/Native/NativeMethodReflection.php +++ b/src/Reflection/Native/NativeMethodReflection.php @@ -230,7 +230,7 @@ public function getAttributes(): array public function hasNoDiscardAttribute(): TrinaryLogic { foreach ($this->attributes as $attrib) { - if ($attrib->getName() === 'NoDiscard') { + if (strtolower($attrib->getName()) === 'nodiscard') { return TrinaryLogic::createYes(); } } diff --git a/src/Reflection/Php/EnumCasesMethodReflection.php b/src/Reflection/Php/EnumCasesMethodReflection.php index 15bcf3ebda..3aa8e757b8 100644 --- a/src/Reflection/Php/EnumCasesMethodReflection.php +++ b/src/Reflection/Php/EnumCasesMethodReflection.php @@ -163,7 +163,7 @@ public function getAttributes(): array public function hasNoDiscardAttribute(): TrinaryLogic { - return TrinaryLogic::createYes(); + return TrinaryLogic::createNo(); } } diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index 17bf02ae91..8cc39c84df 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -24,6 +24,7 @@ use function array_reverse; use function is_array; use function is_string; +use function strtolower; /** * @api @@ -341,7 +342,7 @@ public function getAttributes(): array public function hasNoDiscardAttribute(): TrinaryLogic { foreach ($this->attributes as $attrib) { - if ($attrib->getName() === 'NoDiscard') { + if (strtolower($attrib->getName()) === 'nodiscard') { return TrinaryLogic::createYes(); } } diff --git a/src/Reflection/Php/PhpFunctionReflection.php b/src/Reflection/Php/PhpFunctionReflection.php index 2a156d5d3e..063baea8fe 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -29,6 +29,7 @@ use function count; use function is_array; use function is_file; +use function strtolower; #[GenerateFactory(interface: FunctionReflectionFactory::class)] final class PhpFunctionReflection implements FunctionReflection @@ -278,7 +279,7 @@ public function getAttributes(): array public function hasNoDiscardAttribute(): TrinaryLogic { foreach ($this->attributes as $attrib) { - if ($attrib->getName() === 'NoDiscard') { + if (strtolower($attrib->getName()) === 'nodiscard') { return TrinaryLogic::createYes(); } } diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index cf942d27f4..dfe1feeb7c 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -523,7 +523,7 @@ public function getAttributes(): array public function hasNoDiscardAttribute(): TrinaryLogic { foreach ($this->attributes as $attrib) { - if ($attrib->getName() === 'NoDiscard') { + if (strtolower($attrib->getName()) === 'nodiscard') { return TrinaryLogic::createYes(); } } diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index 874611e9c7..44fa19e0a3 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -40,7 +40,7 @@ public function check( array $attrGroups, int $requiredTarget, string $targetName, - bool $isPropertyHook = false, + bool $isPropertyHook, ): array { $errors = []; diff --git a/src/Rules/Classes/ClassAttributesRule.php b/src/Rules/Classes/ClassAttributesRule.php index 7bce7b065c..87079edfb3 100644 --- a/src/Rules/Classes/ClassAttributesRule.php +++ b/src/Rules/Classes/ClassAttributesRule.php @@ -38,6 +38,7 @@ public function processNode(Node $node, Scope $scope): array $classLikeNode->attrGroups, Attribute::TARGET_CLASS, 'class', + false, ); $classReflection = $node->getClassReflection(); diff --git a/src/Rules/Classes/ClassConstantAttributesRule.php b/src/Rules/Classes/ClassConstantAttributesRule.php index 3beaf3d0ea..639d81376b 100644 --- a/src/Rules/Classes/ClassConstantAttributesRule.php +++ b/src/Rules/Classes/ClassConstantAttributesRule.php @@ -32,6 +32,7 @@ public function processNode(Node $node, Scope $scope): array $node->attrGroups, Attribute::TARGET_CLASS_CONSTANT, 'class constant', + false, ); } diff --git a/src/Rules/EnumCases/EnumCaseAttributesRule.php b/src/Rules/EnumCases/EnumCaseAttributesRule.php index f6489f2e87..407cd6a6a8 100644 --- a/src/Rules/EnumCases/EnumCaseAttributesRule.php +++ b/src/Rules/EnumCases/EnumCaseAttributesRule.php @@ -32,6 +32,7 @@ public function processNode(Node $node, Scope $scope): array $node->attrGroups, Attribute::TARGET_CLASS_CONSTANT, 'class constant', + false, ); } diff --git a/src/Rules/Functions/ArrowFunctionAttributesRule.php b/src/Rules/Functions/ArrowFunctionAttributesRule.php index 092758eaad..b4e47f530c 100644 --- a/src/Rules/Functions/ArrowFunctionAttributesRule.php +++ b/src/Rules/Functions/ArrowFunctionAttributesRule.php @@ -33,6 +33,7 @@ public function processNode(Node $node, Scope $scope): array $node->getOriginalNode()->attrGroups, Attribute::TARGET_FUNCTION, 'function', + false, ); } diff --git a/src/Rules/Functions/ClosureAttributesRule.php b/src/Rules/Functions/ClosureAttributesRule.php index d9dd348f9c..cb91bdddbc 100644 --- a/src/Rules/Functions/ClosureAttributesRule.php +++ b/src/Rules/Functions/ClosureAttributesRule.php @@ -33,6 +33,7 @@ public function processNode(Node $node, Scope $scope): array $node->getOriginalNode()->attrGroups, Attribute::TARGET_FUNCTION, 'function', + false, ); } diff --git a/src/Rules/Functions/FunctionAttributesRule.php b/src/Rules/Functions/FunctionAttributesRule.php index a7b6547cb0..4bc818d429 100644 --- a/src/Rules/Functions/FunctionAttributesRule.php +++ b/src/Rules/Functions/FunctionAttributesRule.php @@ -33,6 +33,7 @@ public function processNode(Node $node, Scope $scope): array $node->getOriginalNode()->attrGroups, Attribute::TARGET_FUNCTION, 'function', + false, ); } diff --git a/src/Rules/Functions/ParamAttributesRule.php b/src/Rules/Functions/ParamAttributesRule.php index ad67abb22b..49459aed40 100644 --- a/src/Rules/Functions/ParamAttributesRule.php +++ b/src/Rules/Functions/ParamAttributesRule.php @@ -39,6 +39,7 @@ public function processNode(Node $node, Scope $scope): array $node->attrGroups, $targetType, $targetName, + false, ); } diff --git a/src/Rules/Methods/MethodAttributesRule.php b/src/Rules/Methods/MethodAttributesRule.php index 56bb6016a1..90af959867 100644 --- a/src/Rules/Methods/MethodAttributesRule.php +++ b/src/Rules/Methods/MethodAttributesRule.php @@ -33,6 +33,7 @@ public function processNode(Node $node, Scope $scope): array $node->getOriginalNode()->attrGroups, Attribute::TARGET_METHOD, 'method', + false, ); } diff --git a/src/Rules/Properties/PropertyAttributesRule.php b/src/Rules/Properties/PropertyAttributesRule.php index 2a77c1e01a..d7c071df54 100644 --- a/src/Rules/Properties/PropertyAttributesRule.php +++ b/src/Rules/Properties/PropertyAttributesRule.php @@ -32,6 +32,7 @@ public function processNode(Node $node, Scope $scope): array $node->attrGroups, Attribute::TARGET_PROPERTY, 'property', + false, ); } diff --git a/src/Rules/Traits/TraitAttributesRule.php b/src/Rules/Traits/TraitAttributesRule.php index d2601de8ad..7f99a1f751 100644 --- a/src/Rules/Traits/TraitAttributesRule.php +++ b/src/Rules/Traits/TraitAttributesRule.php @@ -38,6 +38,7 @@ public function processNode(Node $node, Scope $scope): array $originalNode->attrGroups, Attribute::TARGET_CLASS, 'class', + false, ); if (count($node->getTraitReflection()->getNativeReflection()->getAttributes('AllowDynamicProperties')) > 0) { diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithNoDiscardRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithNoDiscardRuleTest.php index 6553cbb866..0d36700dd8 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithNoDiscardRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithNoDiscardRuleTest.php @@ -23,6 +23,10 @@ public function testRule(): void 'Call to function FunctionCallStatementResultDiscarded\withSideEffects() on a separate line discards return value.', 11, ], + [ + 'Call to function FunctionCallStatementResultDiscarded\differentCase() on a separate line discards return value.', + 25, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php b/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php index 80c525060d..a1f5fa71dd 100644 --- a/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php +++ b/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php @@ -15,3 +15,11 @@ function withSideEffects(): array { foreach (withSideEffects() as $num) { var_dump($num); } + +#[\nOdISCArD] +function differentCase(): array { + echo __FUNCTION__ . "\n"; + return [1]; +} + +differentCase(); diff --git a/tests/PHPStan/Rules/Methods/CallToMethodStatementWithNoDiscardRuleTest.php b/tests/PHPStan/Rules/Methods/CallToMethodStatementWithNoDiscardRuleTest.php index 841d394346..fb3e55dee4 100644 --- a/tests/PHPStan/Rules/Methods/CallToMethodStatementWithNoDiscardRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToMethodStatementWithNoDiscardRuleTest.php @@ -22,11 +22,15 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/method-call-statement-result-discarded.php'], [ [ 'Call to method MethodCallStatementResultDiscarded\ClassWithInstanceSideEffects::instanceMethod() on a separate line discards return value.', - 14, + 20, ], [ 'Call to method MethodCallStatementResultDiscarded\ClassWithInstanceSideEffects::instanceMethod() on a separate line discards return value.', - 15, + 21, + ], + [ + 'Call to method MethodCallStatementResultDiscarded\ClassWithInstanceSideEffects::differentCase() on a separate line discards return value.', + 30, ], ]); } diff --git a/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRuleTest.php b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRuleTest.php index a3fed3a39b..5c320def63 100644 --- a/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRuleTest.php @@ -26,7 +26,11 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/static-method-call-statement-result-discarded.php'], [ [ 'Call to static method MethodCallStatementResultDiscarded\ClassWithStaticSideEffects::staticMethod() on a separate line discards return value.', - 13, + 19, + ], + [ + 'Call to static method MethodCallStatementResultDiscarded\ClassWithStaticSideEffects::differentCase() on a separate line discards return value.', + 27, ], ]); } diff --git a/tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php b/tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php index 4fb606c1b5..df48d076fe 100644 --- a/tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php +++ b/tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php @@ -8,6 +8,12 @@ public function instanceMethod(): array { echo __METHOD__ . "\n"; return [2]; } + + #[\nOdISCArD] + public function differentCase(): array { + echo __METHOD__ . "\n"; + return [2]; + } } $o = new ClassWithInstanceSideEffects(); @@ -20,3 +26,5 @@ public function instanceMethod(): array { foreach ($o->instanceMethod() as $num) { var_dump($num); } + +$o->differentCase(); diff --git a/tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php b/tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php index f5685cbdfc..6449fd72a2 100644 --- a/tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php +++ b/tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php @@ -8,6 +8,12 @@ public static function staticMethod(): array { echo __METHOD__ . "\n"; return [2]; } + + #[\nOdISCArD] + public static function differentCase(): array { + echo __METHOD__ . "\n"; + return [2]; + } } ClassWithStaticSideEffects::staticMethod(); @@ -17,3 +23,5 @@ public static function staticMethod(): array { foreach (ClassWithStaticSideEffects::staticMethod() as $num) { var_dump($num); } + +ClassWithStaticSideEffects::differentCase(); From 53ff4161df37168e8f4e41e38d6e110d5f91a6e5 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Mon, 15 Sep 2025 13:57:54 -0700 Subject: [PATCH 13/34] try validating void phpunit is too slow locally --- src/Rules/FunctionDefinitionCheck.php | 33 +++++++++++++++++++ ...ingClassesInArrowFunctionTypehintsRule.php | 2 ++ .../ExistingClassesInClosureTypehintsRule.php | 2 ++ .../ExistingClassesInTypehintsRule.php | 4 +++ .../ExistingClassesInTypehintsRule.php | 5 +++ ...tingClassesInPropertyHookTypehintsRule.php | 6 ++++ ...lassesInArrowFunctionTypehintsRuleTest.php | 14 ++++++++ ...stingClassesInClosureTypehintsRuleTest.php | 10 ++++++ .../ExistingClassesInTypehintsRuleTest.php | 10 ++++++ .../arrow-function-typehints-nodiscard.php | 13 ++++++++ .../data/closure-typehints-nodiscard.php | 8 +++++ .../Functions/data/typehints-nodiscard.php | 7 ++++ .../ExistingClassesInTypehintsRuleTest.php | 14 ++++++++ .../Methods/data/typehints-nodiscard.php | 14 ++++++++ ...ClassesInPropertyHookTypehintsRuleTest.php | 15 +++++++++ .../data/property-hooks-nodiscard.php | 17 ++++++++++ 16 files changed, 174 insertions(+) create mode 100644 tests/PHPStan/Rules/Functions/data/arrow-function-typehints-nodiscard.php create mode 100644 tests/PHPStan/Rules/Functions/data/closure-typehints-nodiscard.php create mode 100644 tests/PHPStan/Rules/Functions/data/typehints-nodiscard.php create mode 100644 tests/PHPStan/Rules/Methods/data/typehints-nodiscard.php create mode 100644 tests/PHPStan/Rules/Properties/data/property-hooks-nodiscard.php diff --git a/src/Rules/FunctionDefinitionCheck.php b/src/Rules/FunctionDefinitionCheck.php index 626a97d8ad..995b59d9f7 100644 --- a/src/Rules/FunctionDefinitionCheck.php +++ b/src/Rules/FunctionDefinitionCheck.php @@ -10,6 +10,7 @@ use PhpParser\Node\Identifier; use PhpParser\Node\IntersectionType; use PhpParser\Node\Name; +use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\NullableType; use PhpParser\Node\Param; use PhpParser\Node\Stmt\ClassMethod; @@ -76,6 +77,7 @@ public function checkFunction( string $templateTypeMissingInParameterMessage, string $unresolvableParameterTypeMessage, string $unresolvableReturnTypeMessage, + string $noDiscardVoidReturnMessage, ): array { return $this->checkParametersAcceptor( @@ -88,23 +90,27 @@ public function checkFunction( $templateTypeMissingInParameterMessage, $unresolvableParameterTypeMessage, $unresolvableReturnTypeMessage, + $noDiscardVoidReturnMessage, ); } /** * @param Node\Param[] $parameters * @param Node\Identifier|Node\Name|Node\ComplexType|null $returnTypeNode + * @param Node\AttributeGroup[] $attribGroups * @return list */ public function checkAnonymousFunction( Scope $scope, array $parameters, $returnTypeNode, + array $attribGroups, string $parameterMessage, string $returnMessage, string $unionTypesMessage, string $unresolvableParameterTypeMessage, string $unresolvableReturnTypeMessage, + string $noDiscardVoidReturnMessage, ): array { $errors = []; @@ -197,6 +203,19 @@ public function checkAnonymousFunction( if ($returnTypeNode === null) { return $errors; } + if ($returnTypeNode instanceof FullyQualified && $returnTypeNode->name === 'void') { + foreach ($attribGroupss as $attribGroup) { + foreach ($attribGroup->attrs as $attrib) { + if (strtolower($attrib->name) === 'nodiscard') { + $errors[] = RuleErrorBuilder::message($noDiscardVoidReturnMessage) + ->line($returnTypeNode->getStartLine()) + ->identifier('attribute.target') + ->build(); + break 2; + } + } + } + } if ( !$unionTypeReported @@ -266,6 +285,7 @@ public function checkClassMethod( string $unresolvableParameterTypeMessage, string $unresolvableReturnTypeMessage, string $selfOutMessage, + string $noDiscardVoidReturnMessage, ): array { $errors = $this->checkParametersAcceptor( @@ -278,6 +298,7 @@ public function checkClassMethod( $templateTypeMissingInParameterMessage, $unresolvableParameterTypeMessage, $unresolvableReturnTypeMessage, + $noDiscardVoidReturnMessage, ); $selfOutType = $methodReflection->getSelfOutType(); @@ -329,6 +350,7 @@ private function checkParametersAcceptor( string $templateTypeMissingInParameterMessage, string $unresolvableParameterTypeMessage, string $unresolvableReturnTypeMessage, + string $noDiscardVoidReturnMessage, ): array { $errors = []; @@ -473,6 +495,17 @@ private function checkParametersAcceptor( ->build(); } } + if ($parametersAcceptor->hasNoDiscardAttribute()) { + $returnType = $functionNode->getReturnType(); + if ($returnType instanceof FullyQualified + && $returnType->name === 'void' + ) { + $errors[] = RuleErrorBuilder::message($noDiscardVoidReturnMessage) + ->line($returnTypeNode->getStartLine()) + ->identifier('attribute.target') + ->build(); + } + } $returnTypeReferencedClasses = $this->getReturnTypeReferencedClasses($parametersAcceptor); diff --git a/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php b/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php index c2a18addbc..342ca81fa3 100644 --- a/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php @@ -46,11 +46,13 @@ public function processNode(Node $node, Scope $scope): array $scope, $node->getParams(), $node->getReturnType(), + $node->getAttrGroups(), 'Parameter $%s of anonymous function has invalid type %s.', 'Anonymous function has invalid return type %s.', 'Anonymous function uses native union types but they\'re supported only on PHP 8.0 and later.', 'Parameter $%s of anonymous function has unresolvable native type.', 'Anonymous function has unresolvable native return type.', + 'Attribute NoDiscard cannot be used on void anonymous function.', )); } diff --git a/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php b/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php index a052390041..d471ebdb0d 100644 --- a/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php @@ -31,11 +31,13 @@ public function processNode(Node $node, Scope $scope): array $scope, $node->getParams(), $node->getReturnType(), + $node->getAttrGroups(), 'Parameter $%s of anonymous function has invalid type %s.', 'Anonymous function has invalid return type %s.', 'Anonymous function uses native union types but they\'re supported only on PHP 8.0 and later.', 'Parameter $%s of anonymous function has unresolvable native type.', 'Anonymous function has unresolvable native return type.', + 'Attribute NoDiscard cannot be used on void anonymous function.', ); } diff --git a/src/Rules/Functions/ExistingClassesInTypehintsRule.php b/src/Rules/Functions/ExistingClassesInTypehintsRule.php index 403ed4ec1a..5b66bbd9c9 100644 --- a/src/Rules/Functions/ExistingClassesInTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInTypehintsRule.php @@ -53,6 +53,10 @@ public function processNode(Node $node, Scope $scope): array 'Function %s() has unresolvable native return type.', $functionName, ), + sprintf( + 'Attribute NoDiscard cannot be used on void function %s().', + $functionName + ), ); } diff --git a/src/Rules/Methods/ExistingClassesInTypehintsRule.php b/src/Rules/Methods/ExistingClassesInTypehintsRule.php index 8fb0686000..3cb07984ec 100644 --- a/src/Rules/Methods/ExistingClassesInTypehintsRule.php +++ b/src/Rules/Methods/ExistingClassesInTypehintsRule.php @@ -64,6 +64,11 @@ public function processNode(Node $node, Scope $scope): array $className, $methodName, ), + sprintf( + 'Attribute NoDiscard cannot be used on void method %s::%s().', + $className, + $methodName, + ), ); } diff --git a/src/Rules/Properties/ExistingClassesInPropertyHookTypehintsRule.php b/src/Rules/Properties/ExistingClassesInPropertyHookTypehintsRule.php index 3530ee5769..db70003287 100644 --- a/src/Rules/Properties/ExistingClassesInPropertyHookTypehintsRule.php +++ b/src/Rules/Properties/ExistingClassesInPropertyHookTypehintsRule.php @@ -84,6 +84,12 @@ public function processNode(Node $node, Scope $scope): array $className, $propertyName, ), + sprintf( + 'Attribute NoDiscard cannot be used on void %s hook for property %s::$%s.', + ucfirst($hookName), + $className, + $propertyName, + ), ); } diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php index b54bf39163..388c2f0f33 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php @@ -54,6 +54,10 @@ public function testRule(): void 'Anonymous function has invalid return type ArrowFunctionExistingClassesInTypehints\Baz.', 10, ], + [ + 'Attribute NoDiscard cannot be used on void anonymous function.', + 12, + ], ]); } @@ -324,4 +328,14 @@ public function testBug5206(): void $this->analyse([__DIR__ . '/data/bug-5206.php'], $errors); } + public function testNoDiscardVoid(): void + { + $this->analyse([__DIR__ . '/data/arrow-function-typehints-nodiscard.php'], [ + [ + 'Attribute NoDiscard cannot be used on void anonymous function.', + 10, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php index b01e0f5230..1a24bdf9a1 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php @@ -353,4 +353,14 @@ public function testDeprecatedImplicitlyNullableParameterType(): void ]); } + public function testNoDiscardVoid(): void + { + $this->analyse([__DIR__ . '/data/closure-typehints-nodiscard.php'], [ + [ + 'Attribute NoDiscard cannot be used on void anonymous function.', + 17, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php index e9ec0e10b9..76fcf59fd4 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php @@ -489,4 +489,14 @@ public function testParamClosureThisClasses(): void ]); } + public function testNoDiscardVoid(): void + { + $this->analyse([__DIR__ . '/data/typehints-nodiscard.php'], [ + [ + 'Attribute NoDiscard cannot be used on void function nothing().', + 6, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/arrow-function-typehints-nodiscard.php b/tests/PHPStan/Rules/Functions/data/arrow-function-typehints-nodiscard.php new file mode 100644 index 0000000000..c6d6db74b4 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/arrow-function-typehints-nodiscard.php @@ -0,0 +1,13 @@ + true; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/closure-typehints-nodiscard.php b/tests/PHPStan/Rules/Functions/data/closure-typehints-nodiscard.php new file mode 100644 index 0000000000..0b473bf6f0 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/closure-typehints-nodiscard.php @@ -0,0 +1,8 @@ +analyse([__DIR__ . '/data/bug-12501.php'], []); } + public function testNoDiscardVoid(): void + { + $this->analyse([__DIR__ . '/data/typehints-nodiscard.php'], [ + [ + 'Attribute NoDiscard cannot be used on void method TestMethodTypehints\Demo::nothing().', + 8, + ], + [ + 'Attribute NoDiscard cannot be used on void method TestMethodTypehints\Demo::alsoNothing().', + 12, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/typehints-nodiscard.php b/tests/PHPStan/Rules/Methods/data/typehints-nodiscard.php new file mode 100644 index 0000000000..6def386caf --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/typehints-nodiscard.php @@ -0,0 +1,14 @@ += 8.4')] + public function testNoDiscardVoid(): void + { + $this->analyse([__DIR__ . '/data/property-hooks-nodiscard.php'], [ + [ + 'Attribute NoDiscard cannot be used on void get hook for property ExistingClassesPropertyHooks\Demo::$foo.', + 9, + ], + [ + 'Attribute NoDiscard cannot be used on void set hook for property ExistingClassesPropertyHooks\Demo::$set.', + 14, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/property-hooks-nodiscard.php b/tests/PHPStan/Rules/Properties/data/property-hooks-nodiscard.php new file mode 100644 index 0000000000..e3becc2c9d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-hooks-nodiscard.php @@ -0,0 +1,17 @@ += 8.4 + +namespace ExistingClassesPropertyHooks; + +class Demo { + + public mixed $get { + #[\NoDiscard] + get => true; + } + + public mixed $set { + #[\NoDiscard] + set => false; + } + +} From 0bcf3b97dc2a6a08db103bb4d2e443ee2cb2837d Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Mon, 15 Sep 2025 14:01:33 -0700 Subject: [PATCH 14/34] lint exclusions --- Makefile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Makefile b/Makefile index 1bde3401a1..c51d497047 100644 --- a/Makefile +++ b/Makefile @@ -112,6 +112,11 @@ lint: --exclude tests/PHPStan/Rules/Classes/data/enum-attributes.php \ --exclude tests/PHPStan/Rules/Cast/data/void-cast.php \ --exclude tests/PHPStan/Rules/Properties/data/property-hook-attributes-nodiscard.php \ + --exclude tests/PHPStan/Rules/Functions/data/arrow-function-typehints-nodiscard.php \ + --exclude tests/PHPStan/Rules/Functions/data/closure-typehints-nodiscard.php \ + --exclude tests/PHPStan/Rules/Functions/data/typehints-nodiscard.php \ + --exclude tests/PHPStan/Rules/Methods/data/typehints-nodiscard.php \ + --exclude tests/PHPStan/Rules/Properties/data/property-hooks-nodiscard.php \ src tests install-paratest: From 3dacefed6b013971c30effc931b40b1b48da5de2 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Mon, 15 Sep 2025 14:01:54 -0700 Subject: [PATCH 15/34] , --- src/Rules/Functions/ExistingClassesInTypehintsRule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Rules/Functions/ExistingClassesInTypehintsRule.php b/src/Rules/Functions/ExistingClassesInTypehintsRule.php index 5b66bbd9c9..735a4f316a 100644 --- a/src/Rules/Functions/ExistingClassesInTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInTypehintsRule.php @@ -55,7 +55,7 @@ public function processNode(Node $node, Scope $scope): array ), sprintf( 'Attribute NoDiscard cannot be used on void function %s().', - $functionName + $functionName, ), ); } From 097dee7b79b43862a1b351a79dd51aecde5a74b3 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Mon, 15 Sep 2025 14:03:29 -0700 Subject: [PATCH 16/34] s/s/, ->yes --- src/Rules/FunctionDefinitionCheck.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Rules/FunctionDefinitionCheck.php b/src/Rules/FunctionDefinitionCheck.php index 995b59d9f7..da21670727 100644 --- a/src/Rules/FunctionDefinitionCheck.php +++ b/src/Rules/FunctionDefinitionCheck.php @@ -204,7 +204,7 @@ public function checkAnonymousFunction( return $errors; } if ($returnTypeNode instanceof FullyQualified && $returnTypeNode->name === 'void') { - foreach ($attribGroupss as $attribGroup) { + foreach ($attribGroups as $attribGroup) { foreach ($attribGroup->attrs as $attrib) { if (strtolower($attrib->name) === 'nodiscard') { $errors[] = RuleErrorBuilder::message($noDiscardVoidReturnMessage) @@ -495,7 +495,7 @@ private function checkParametersAcceptor( ->build(); } } - if ($parametersAcceptor->hasNoDiscardAttribute()) { + if ($parametersAcceptor->hasNoDiscardAttribute()->yes()) { $returnType = $functionNode->getReturnType(); if ($returnType instanceof FullyQualified && $returnType->name === 'void' From df77b1c72659df749dddeb3d1c707fab7651ae4e Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Mon, 15 Sep 2025 14:10:31 -0700 Subject: [PATCH 17/34] ->name --- src/Rules/FunctionDefinitionCheck.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Rules/FunctionDefinitionCheck.php b/src/Rules/FunctionDefinitionCheck.php index da21670727..159ab9e7e7 100644 --- a/src/Rules/FunctionDefinitionCheck.php +++ b/src/Rules/FunctionDefinitionCheck.php @@ -206,7 +206,7 @@ public function checkAnonymousFunction( if ($returnTypeNode instanceof FullyQualified && $returnTypeNode->name === 'void') { foreach ($attribGroups as $attribGroup) { foreach ($attribGroup->attrs as $attrib) { - if (strtolower($attrib->name) === 'nodiscard') { + if (strtolower($attrib->name->name) === 'nodiscard') { $errors[] = RuleErrorBuilder::message($noDiscardVoidReturnMessage) ->line($returnTypeNode->getStartLine()) ->identifier('attribute.target') From e9c974c846fa3e68b00ca1d76294538a245328f0 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Mon, 15 Sep 2025 14:24:03 -0700 Subject: [PATCH 18/34] identifier --- src/Rules/FunctionDefinitionCheck.php | 5 ++--- .../Functions/ExistingClassesInClosureTypehintsRuleTest.php | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Rules/FunctionDefinitionCheck.php b/src/Rules/FunctionDefinitionCheck.php index 159ab9e7e7..4daf4d0a87 100644 --- a/src/Rules/FunctionDefinitionCheck.php +++ b/src/Rules/FunctionDefinitionCheck.php @@ -10,7 +10,6 @@ use PhpParser\Node\Identifier; use PhpParser\Node\IntersectionType; use PhpParser\Node\Name; -use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\NullableType; use PhpParser\Node\Param; use PhpParser\Node\Stmt\ClassMethod; @@ -203,7 +202,7 @@ public function checkAnonymousFunction( if ($returnTypeNode === null) { return $errors; } - if ($returnTypeNode instanceof FullyQualified && $returnTypeNode->name === 'void') { + if ($returnTypeNode instanceof Identifier && $returnTypeNode->name === 'void') { foreach ($attribGroups as $attribGroup) { foreach ($attribGroup->attrs as $attrib) { if (strtolower($attrib->name->name) === 'nodiscard') { @@ -497,7 +496,7 @@ private function checkParametersAcceptor( } if ($parametersAcceptor->hasNoDiscardAttribute()->yes()) { $returnType = $functionNode->getReturnType(); - if ($returnType instanceof FullyQualified + if ($returnType instanceof Identifier && $returnType->name === 'void' ) { $errors[] = RuleErrorBuilder::message($noDiscardVoidReturnMessage) diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php index 1a24bdf9a1..e4c50ec9b1 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php @@ -358,7 +358,7 @@ public function testNoDiscardVoid(): void $this->analyse([__DIR__ . '/data/closure-typehints-nodiscard.php'], [ [ 'Attribute NoDiscard cannot be used on void anonymous function.', - 17, + 5, ], ]); } From 79a5f1f730e54a5042e50d17347388d391a888a1 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Mon, 15 Sep 2025 14:27:49 -0700 Subject: [PATCH 19/34] namespace --- .../Rules/Functions/ExistingClassesInTypehintsRuleTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php index 76fcf59fd4..1c685ff2d7 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php @@ -493,7 +493,7 @@ public function testNoDiscardVoid(): void { $this->analyse([__DIR__ . '/data/typehints-nodiscard.php'], [ [ - 'Attribute NoDiscard cannot be used on void function nothing().', + 'Attribute NoDiscard cannot be used on void function TestFunctionTypehints\nothing().', 6, ], ]); From fb0e6aeb1fae7cdb385a64411be97237269996a9 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Mon, 15 Sep 2025 14:42:04 -0700 Subject: [PATCH 20/34] misplaced --- .../ExistingClassesInArrowFunctionTypehintsRuleTest.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php index 388c2f0f33..abd9602099 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php @@ -54,10 +54,6 @@ public function testRule(): void 'Anonymous function has invalid return type ArrowFunctionExistingClassesInTypehints\Baz.', 10, ], - [ - 'Attribute NoDiscard cannot be used on void anonymous function.', - 12, - ], ]); } From 07ec1f707aec7d12e25a0d9bfa9606c3020b0e94 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Mon, 15 Sep 2025 14:53:58 -0700 Subject: [PATCH 21/34] impossible --- ...istingClassesInPropertyHookTypehintsRule.php | 3 ++- ...ngClassesInPropertyHookTypehintsRuleTest.php | 15 --------------- .../data/property-hooks-nodiscard.php | 17 ----------------- 3 files changed, 2 insertions(+), 33 deletions(-) delete mode 100644 tests/PHPStan/Rules/Properties/data/property-hooks-nodiscard.php diff --git a/src/Rules/Properties/ExistingClassesInPropertyHookTypehintsRule.php b/src/Rules/Properties/ExistingClassesInPropertyHookTypehintsRule.php index db70003287..96f326e17a 100644 --- a/src/Rules/Properties/ExistingClassesInPropertyHookTypehintsRule.php +++ b/src/Rules/Properties/ExistingClassesInPropertyHookTypehintsRule.php @@ -84,8 +84,9 @@ public function processNode(Node $node, Scope $scope): array $className, $propertyName, ), + // Should be impossible, property hooks do not support return types sprintf( - 'Attribute NoDiscard cannot be used on void %s hook for property %s::$%s.', + 'Impossible condition: Attribute NoDiscard cannot be used on void %s hook for property %s::$%s.', ucfirst($hookName), $className, $propertyName, diff --git a/tests/PHPStan/Rules/Properties/ExistingClassesInPropertyHookTypehintsRuleTest.php b/tests/PHPStan/Rules/Properties/ExistingClassesInPropertyHookTypehintsRuleTest.php index 80399cd4be..05610e0482 100644 --- a/tests/PHPStan/Rules/Properties/ExistingClassesInPropertyHookTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ExistingClassesInPropertyHookTypehintsRuleTest.php @@ -62,19 +62,4 @@ public function testRule(): void ]); } - #[RequiresPhp('>= 8.4')] - public function testNoDiscardVoid(): void - { - $this->analyse([__DIR__ . '/data/property-hooks-nodiscard.php'], [ - [ - 'Attribute NoDiscard cannot be used on void get hook for property ExistingClassesPropertyHooks\Demo::$foo.', - 9, - ], - [ - 'Attribute NoDiscard cannot be used on void set hook for property ExistingClassesPropertyHooks\Demo::$set.', - 14, - ], - ]); - } - } diff --git a/tests/PHPStan/Rules/Properties/data/property-hooks-nodiscard.php b/tests/PHPStan/Rules/Properties/data/property-hooks-nodiscard.php deleted file mode 100644 index e3becc2c9d..0000000000 --- a/tests/PHPStan/Rules/Properties/data/property-hooks-nodiscard.php +++ /dev/null @@ -1,17 +0,0 @@ -= 8.4 - -namespace ExistingClassesPropertyHooks; - -class Demo { - - public mixed $get { - #[\NoDiscard] - get => true; - } - - public mixed $set { - #[\NoDiscard] - set => false; - } - -} From e2fdc31aaff976cb03375a4996c7edd12d152ccf Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Sep 2025 08:47:21 +0200 Subject: [PATCH 22/34] Remove the visitor, not sure what it is for --- src/Parser/VoidCastVisitor.php | 31 ------------------- ...CallToMethodStatementWithNoDiscardRule.php | 4 --- ...StaticMethodStatementWithNoDiscardRule.php | 4 --- 3 files changed, 39 deletions(-) delete mode 100644 src/Parser/VoidCastVisitor.php diff --git a/src/Parser/VoidCastVisitor.php b/src/Parser/VoidCastVisitor.php deleted file mode 100644 index 5a31241db3..0000000000 --- a/src/Parser/VoidCastVisitor.php +++ /dev/null @@ -1,31 +0,0 @@ -pendingVoidCast = true; - } elseif ($this->pendingVoidCast) { - $node->setAttribute(self::ATTRIBUTE_NAME, true); - $this->pendingVoidCast = false; - } - return null; - } - -} diff --git a/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php b/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php index 968faf0711..91d198e0ce 100644 --- a/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php +++ b/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; -use PHPStan\Parser\VoidCastVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -42,9 +41,6 @@ public function processNode(Node $node, Scope $scope): array if (!$funcCall->name instanceof Node\Identifier) { return []; } - if ($funcCall->hasAttribute(VoidCastVisitor::ATTRIBUTE_NAME)) { - return []; - } $methodName = $funcCall->name->toString(); $typeResult = $this->ruleLevelHelper->findTypeToCheck( diff --git a/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php index fc8b04fe3f..0921056501 100644 --- a/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php +++ b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; -use PHPStan\Parser\VoidCastVisitor; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -45,9 +44,6 @@ public function processNode(Node $node, Scope $scope): array if (!$funcCall->name instanceof Node\Identifier) { return []; } - if ($funcCall->hasAttribute(VoidCastVisitor::ATTRIBUTE_NAME)) { - return []; - } $methodName = $funcCall->name->toString(); if ($funcCall->class instanceof Node\Name) { From 35918ccf65c36c7de2a0c163a4124a88f78d4701 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Sep 2025 08:52:19 +0200 Subject: [PATCH 23/34] Remove changes to collision detector config --- build/collision-detector.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/build/collision-detector.json b/build/collision-detector.json index 1188d514b1..a687cd3ea4 100644 --- a/build/collision-detector.json +++ b/build/collision-detector.json @@ -15,11 +15,6 @@ "../tests/PHPStan/Rules/Functions/data/define-bug-3349.php", "../tests/PHPStan/Levels/data/stubs/function.php", "../tests/PHPStan/Rules/Properties/data/abstract-final-property-hook-parse-error.php", - "../tests/PHPStan/Rules/Properties/data/final-property-hooks.php", - "../tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php", - "../tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php", - "../tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php", - "../tests/PHPStan/Rules/Properties/data/property-hook-attributes-nodiscard.php", - "../tests/PHPStan/Rules/Cast/data/void-cast.php" + "../tests/PHPStan/Rules/Properties/data/final-property-hooks.php" ] } From 6604dc6599d6c9547ccff592526f769fd2eec9d3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Sep 2025 08:52:42 +0200 Subject: [PATCH 24/34] Run Collision Detector on PHP 8.5 --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2f1fb9f465..b9441c7ae5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -135,7 +135,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.4" + php-version: "8.5" - name: "Install dependencies" run: "composer install --no-interaction --no-progress" From 374fc6fab1a6b3b134f5885cc384f5f3c38bf074 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Sep 2025 08:57:51 +0200 Subject: [PATCH 25/34] Rework property hook check --- src/Rules/AttributesCheck.php | 7 ------ src/Rules/Classes/ClassAttributesRule.php | 1 - .../Classes/ClassConstantAttributesRule.php | 1 - .../EnumCases/EnumCaseAttributesRule.php | 1 - .../Functions/ArrowFunctionAttributesRule.php | 1 - src/Rules/Functions/ClosureAttributesRule.php | 1 - .../Functions/FunctionAttributesRule.php | 1 - src/Rules/Functions/ParamAttributesRule.php | 1 - src/Rules/Methods/MethodAttributesRule.php | 1 - .../Properties/PropertyAttributesRule.php | 1 - .../Properties/PropertyHookAttributesRule.php | 25 ++++++++++++++++--- src/Rules/Traits/TraitAttributesRule.php | 1 - 12 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index 44fa19e0a3..36b3c6faa6 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -40,7 +40,6 @@ public function check( array $attrGroups, int $requiredTarget, string $targetName, - bool $isPropertyHook, ): array { $errors = []; @@ -82,12 +81,6 @@ public function check( ->identifier('attribute.target') ->line($attribute->getStartLine()) ->build(); - } elseif ($isPropertyHook && strtolower($name) === 'nodiscard') { - // #[\NoDiscard] cannot be used on property hooks - $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s cannot be used on property hooks.', $name)) - ->identifier('attribute.target') - ->line($attribute->getStartLine()) - ->build(); } if (($flags & Attribute::IS_REPEATABLE) === 0) { diff --git a/src/Rules/Classes/ClassAttributesRule.php b/src/Rules/Classes/ClassAttributesRule.php index 87079edfb3..7bce7b065c 100644 --- a/src/Rules/Classes/ClassAttributesRule.php +++ b/src/Rules/Classes/ClassAttributesRule.php @@ -38,7 +38,6 @@ public function processNode(Node $node, Scope $scope): array $classLikeNode->attrGroups, Attribute::TARGET_CLASS, 'class', - false, ); $classReflection = $node->getClassReflection(); diff --git a/src/Rules/Classes/ClassConstantAttributesRule.php b/src/Rules/Classes/ClassConstantAttributesRule.php index 639d81376b..3beaf3d0ea 100644 --- a/src/Rules/Classes/ClassConstantAttributesRule.php +++ b/src/Rules/Classes/ClassConstantAttributesRule.php @@ -32,7 +32,6 @@ public function processNode(Node $node, Scope $scope): array $node->attrGroups, Attribute::TARGET_CLASS_CONSTANT, 'class constant', - false, ); } diff --git a/src/Rules/EnumCases/EnumCaseAttributesRule.php b/src/Rules/EnumCases/EnumCaseAttributesRule.php index 407cd6a6a8..f6489f2e87 100644 --- a/src/Rules/EnumCases/EnumCaseAttributesRule.php +++ b/src/Rules/EnumCases/EnumCaseAttributesRule.php @@ -32,7 +32,6 @@ public function processNode(Node $node, Scope $scope): array $node->attrGroups, Attribute::TARGET_CLASS_CONSTANT, 'class constant', - false, ); } diff --git a/src/Rules/Functions/ArrowFunctionAttributesRule.php b/src/Rules/Functions/ArrowFunctionAttributesRule.php index b4e47f530c..092758eaad 100644 --- a/src/Rules/Functions/ArrowFunctionAttributesRule.php +++ b/src/Rules/Functions/ArrowFunctionAttributesRule.php @@ -33,7 +33,6 @@ public function processNode(Node $node, Scope $scope): array $node->getOriginalNode()->attrGroups, Attribute::TARGET_FUNCTION, 'function', - false, ); } diff --git a/src/Rules/Functions/ClosureAttributesRule.php b/src/Rules/Functions/ClosureAttributesRule.php index cb91bdddbc..d9dd348f9c 100644 --- a/src/Rules/Functions/ClosureAttributesRule.php +++ b/src/Rules/Functions/ClosureAttributesRule.php @@ -33,7 +33,6 @@ public function processNode(Node $node, Scope $scope): array $node->getOriginalNode()->attrGroups, Attribute::TARGET_FUNCTION, 'function', - false, ); } diff --git a/src/Rules/Functions/FunctionAttributesRule.php b/src/Rules/Functions/FunctionAttributesRule.php index 4bc818d429..a7b6547cb0 100644 --- a/src/Rules/Functions/FunctionAttributesRule.php +++ b/src/Rules/Functions/FunctionAttributesRule.php @@ -33,7 +33,6 @@ public function processNode(Node $node, Scope $scope): array $node->getOriginalNode()->attrGroups, Attribute::TARGET_FUNCTION, 'function', - false, ); } diff --git a/src/Rules/Functions/ParamAttributesRule.php b/src/Rules/Functions/ParamAttributesRule.php index 49459aed40..ad67abb22b 100644 --- a/src/Rules/Functions/ParamAttributesRule.php +++ b/src/Rules/Functions/ParamAttributesRule.php @@ -39,7 +39,6 @@ public function processNode(Node $node, Scope $scope): array $node->attrGroups, $targetType, $targetName, - false, ); } diff --git a/src/Rules/Methods/MethodAttributesRule.php b/src/Rules/Methods/MethodAttributesRule.php index 90af959867..56bb6016a1 100644 --- a/src/Rules/Methods/MethodAttributesRule.php +++ b/src/Rules/Methods/MethodAttributesRule.php @@ -33,7 +33,6 @@ public function processNode(Node $node, Scope $scope): array $node->getOriginalNode()->attrGroups, Attribute::TARGET_METHOD, 'method', - false, ); } diff --git a/src/Rules/Properties/PropertyAttributesRule.php b/src/Rules/Properties/PropertyAttributesRule.php index d7c071df54..2a77c1e01a 100644 --- a/src/Rules/Properties/PropertyAttributesRule.php +++ b/src/Rules/Properties/PropertyAttributesRule.php @@ -32,7 +32,6 @@ public function processNode(Node $node, Scope $scope): array $node->attrGroups, Attribute::TARGET_PROPERTY, 'property', - false, ); } diff --git a/src/Rules/Properties/PropertyHookAttributesRule.php b/src/Rules/Properties/PropertyHookAttributesRule.php index db5ede4e1d..2eb1c11f60 100644 --- a/src/Rules/Properties/PropertyHookAttributesRule.php +++ b/src/Rules/Properties/PropertyHookAttributesRule.php @@ -9,6 +9,9 @@ use PHPStan\Node\InPropertyHookNode; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; +use function strtolower; /** * @implements Rule @@ -28,13 +31,29 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - return $this->attributesCheck->check( + $attrGroups = $node->getOriginalNode()->attrGroups; + $errors = $this->attributesCheck->check( $scope, - $node->getOriginalNode()->attrGroups, + $attrGroups, Attribute::TARGET_METHOD, 'method', - true, ); + + foreach ($attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attribute) { + $name = $attribute->name->toString(); + if (strtolower($name) === 'nodiscard') { + $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s cannot be used on property hooks.', $name)) + ->identifier('attribute.target') + ->line($attribute->getStartLine()) + ->nonIgnorable() + ->build(); + break; + } + } + } + + return $errors; } } diff --git a/src/Rules/Traits/TraitAttributesRule.php b/src/Rules/Traits/TraitAttributesRule.php index 7f99a1f751..d2601de8ad 100644 --- a/src/Rules/Traits/TraitAttributesRule.php +++ b/src/Rules/Traits/TraitAttributesRule.php @@ -38,7 +38,6 @@ public function processNode(Node $node, Scope $scope): array $originalNode->attrGroups, Attribute::TARGET_CLASS, 'class', - false, ); if (count($node->getTraitReflection()->getNativeReflection()->getAttributes('AllowDynamicProperties')) > 0) { From d12b87d1c7bdedf2c7d08cbeadb2d9c483a2d6fb Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Sep 2025 09:00:46 +0200 Subject: [PATCH 26/34] Fix build --- build/collision-detector.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/collision-detector.json b/build/collision-detector.json index a687cd3ea4..c5171fcc96 100644 --- a/build/collision-detector.json +++ b/build/collision-detector.json @@ -15,6 +15,7 @@ "../tests/PHPStan/Rules/Functions/data/define-bug-3349.php", "../tests/PHPStan/Levels/data/stubs/function.php", "../tests/PHPStan/Rules/Properties/data/abstract-final-property-hook-parse-error.php", - "../tests/PHPStan/Rules/Properties/data/final-property-hooks.php" + "../tests/PHPStan/Rules/Properties/data/final-property-hooks.php", + "../tests/PHPStan/Rules/Cast/data/void-cast.php" ] } From 0a3b7348c5fd92bc790600b6667cb75a4aabbf30 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Sep 2025 09:07:51 +0200 Subject: [PATCH 27/34] Test works on older PHP versions --- .../PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php index e195cd5533..e59a22028d 100644 --- a/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php @@ -58,7 +58,7 @@ public function testRule(): void ]); } - #[RequiresPhp('>= 8.5')] + #[RequiresPhp('>= 8.4')] public function testNoDiscard(): void { $this->analyse([__DIR__ . '/data/property-hook-attributes-nodiscard.php'], [ From 6c947cbf0d5874a008f439c26e6e1d5430f4d6f8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Sep 2025 09:09:12 +0200 Subject: [PATCH 28/34] This file does not exist --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index c51d497047..3cb07512ee 100644 --- a/Makefile +++ b/Makefile @@ -116,7 +116,6 @@ lint: --exclude tests/PHPStan/Rules/Functions/data/closure-typehints-nodiscard.php \ --exclude tests/PHPStan/Rules/Functions/data/typehints-nodiscard.php \ --exclude tests/PHPStan/Rules/Methods/data/typehints-nodiscard.php \ - --exclude tests/PHPStan/Rules/Properties/data/property-hooks-nodiscard.php \ src tests install-paratest: From 560895e1826b4325ed29ad8ae0aba7e743ca0e69 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Sep 2025 09:10:55 +0200 Subject: [PATCH 29/34] Rename method --- src/Reflection/Annotations/AnnotationMethodReflection.php | 2 +- src/Reflection/Dummy/ChangedTypeMethodReflection.php | 4 ++-- src/Reflection/Dummy/DummyConstructorReflection.php | 2 +- src/Reflection/Dummy/DummyMethodReflection.php | 2 +- src/Reflection/ExtendedMethodReflection.php | 4 ++-- src/Reflection/FunctionReflection.php | 4 ++-- src/Reflection/Native/NativeFunctionReflection.php | 2 +- src/Reflection/Native/NativeMethodReflection.php | 2 +- src/Reflection/Php/ClosureCallMethodReflection.php | 4 ++-- src/Reflection/Php/EnumCasesMethodReflection.php | 2 +- src/Reflection/Php/ExitFunctionReflection.php | 2 +- src/Reflection/Php/PhpFunctionFromParserNodeReflection.php | 2 +- src/Reflection/Php/PhpFunctionReflection.php | 2 +- src/Reflection/Php/PhpMethodReflection.php | 2 +- src/Reflection/ResolvedMethodReflection.php | 4 ++-- src/Reflection/Type/IntersectionTypeMethodReflection.php | 4 ++-- src/Reflection/Type/UnionTypeMethodReflection.php | 4 ++-- src/Reflection/WrappedExtendedMethodReflection.php | 2 +- src/Rules/FunctionDefinitionCheck.php | 2 +- .../Functions/CallToFunctionStatementWithNoDiscardRule.php | 2 +- src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php | 2 +- .../Methods/CallToStaticMethodStatementWithNoDiscardRule.php | 2 +- .../RewrittenDeclaringClassMethodReflection.php | 4 ++-- 23 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/Reflection/Annotations/AnnotationMethodReflection.php b/src/Reflection/Annotations/AnnotationMethodReflection.php index eff496c0d7..7fe7ccd14e 100644 --- a/src/Reflection/Annotations/AnnotationMethodReflection.php +++ b/src/Reflection/Annotations/AnnotationMethodReflection.php @@ -183,7 +183,7 @@ public function getAttributes(): array return []; } - public function hasNoDiscardAttribute(): TrinaryLogic + public function mustUseReturnValue(): TrinaryLogic { return TrinaryLogic::createNo(); } diff --git a/src/Reflection/Dummy/ChangedTypeMethodReflection.php b/src/Reflection/Dummy/ChangedTypeMethodReflection.php index 2869c4a9ce..b5749c7a29 100644 --- a/src/Reflection/Dummy/ChangedTypeMethodReflection.php +++ b/src/Reflection/Dummy/ChangedTypeMethodReflection.php @@ -172,9 +172,9 @@ public function getAttributes(): array return $this->reflection->getAttributes(); } - public function hasNoDiscardAttribute(): TrinaryLogic + public function mustUseReturnValue(): TrinaryLogic { - return $this->reflection->hasNoDiscardAttribute(); + return $this->reflection->mustUseReturnValue(); } } diff --git a/src/Reflection/Dummy/DummyConstructorReflection.php b/src/Reflection/Dummy/DummyConstructorReflection.php index c424fcfbbe..6652c87c47 100644 --- a/src/Reflection/Dummy/DummyConstructorReflection.php +++ b/src/Reflection/Dummy/DummyConstructorReflection.php @@ -157,7 +157,7 @@ public function getAttributes(): array return []; } - public function hasNoDiscardAttribute(): TrinaryLogic + public function mustUseReturnValue(): TrinaryLogic { // Align with the getAttributes() returning empty return TrinaryLogic::createNo(); diff --git a/src/Reflection/Dummy/DummyMethodReflection.php b/src/Reflection/Dummy/DummyMethodReflection.php index 3fba88c555..f81481d449 100644 --- a/src/Reflection/Dummy/DummyMethodReflection.php +++ b/src/Reflection/Dummy/DummyMethodReflection.php @@ -149,7 +149,7 @@ public function getAttributes(): array return []; } - public function hasNoDiscardAttribute(): TrinaryLogic + public function mustUseReturnValue(): TrinaryLogic { // Align with the getAttributes() returning empty return TrinaryLogic::createNo(); diff --git a/src/Reflection/ExtendedMethodReflection.php b/src/Reflection/ExtendedMethodReflection.php index 771a232692..0cb9caeab0 100644 --- a/src/Reflection/ExtendedMethodReflection.php +++ b/src/Reflection/ExtendedMethodReflection.php @@ -67,9 +67,9 @@ public function getAttributes(): array; /** * Has the #[\NoDiscard] attribute - on PHP 8.5+ if the function's return - * value is unused at runtime a warning is emitted, phpstan will emit the + * value is unused at runtime a warning is emitted, PHPStan will emit the * warning during analysis and on older PHP versions too */ - public function hasNoDiscardAttribute(): TrinaryLogic; + public function mustUseReturnValue(): TrinaryLogic; } diff --git a/src/Reflection/FunctionReflection.php b/src/Reflection/FunctionReflection.php index fdf479b796..b99209c628 100644 --- a/src/Reflection/FunctionReflection.php +++ b/src/Reflection/FunctionReflection.php @@ -64,9 +64,9 @@ public function getAttributes(): array; /** * Has the #[\NoDiscard] attribute - on PHP 8.5+ if the function's return - * value is unused at runtime a warning is emitted, phpstan will emit the + * value is unused at runtime a warning is emitted, PHPStan will emit the * warning during analysis and on older PHP versions too */ - public function hasNoDiscardAttribute(): TrinaryLogic; + public function mustUseReturnValue(): TrinaryLogic; } diff --git a/src/Reflection/Native/NativeFunctionReflection.php b/src/Reflection/Native/NativeFunctionReflection.php index 612cfbcc24..50ddbb0e9b 100644 --- a/src/Reflection/Native/NativeFunctionReflection.php +++ b/src/Reflection/Native/NativeFunctionReflection.php @@ -151,7 +151,7 @@ public function getAttributes(): array return $this->attributes; } - public function hasNoDiscardAttribute(): TrinaryLogic + public function mustUseReturnValue(): TrinaryLogic { foreach ($this->attributes as $attrib) { if (strtolower($attrib->getName()) === 'nodiscard') { diff --git a/src/Reflection/Native/NativeMethodReflection.php b/src/Reflection/Native/NativeMethodReflection.php index a0310a060c..7850ed1b5d 100644 --- a/src/Reflection/Native/NativeMethodReflection.php +++ b/src/Reflection/Native/NativeMethodReflection.php @@ -227,7 +227,7 @@ public function getAttributes(): array return $this->attributes; } - public function hasNoDiscardAttribute(): TrinaryLogic + public function mustUseReturnValue(): TrinaryLogic { foreach ($this->attributes as $attrib) { if (strtolower($attrib->getName()) === 'nodiscard') { diff --git a/src/Reflection/Php/ClosureCallMethodReflection.php b/src/Reflection/Php/ClosureCallMethodReflection.php index a3a97d97c5..3c0db2f7f3 100644 --- a/src/Reflection/Php/ClosureCallMethodReflection.php +++ b/src/Reflection/Php/ClosureCallMethodReflection.php @@ -202,9 +202,9 @@ public function getAttributes(): array return $this->nativeMethodReflection->getAttributes(); } - public function hasNoDiscardAttribute(): TrinaryLogic + public function mustUseReturnValue(): TrinaryLogic { - return $this->nativeMethodReflection->hasNoDiscardAttribute(); + return $this->nativeMethodReflection->mustUseReturnValue(); } } diff --git a/src/Reflection/Php/EnumCasesMethodReflection.php b/src/Reflection/Php/EnumCasesMethodReflection.php index 3aa8e757b8..91d795598b 100644 --- a/src/Reflection/Php/EnumCasesMethodReflection.php +++ b/src/Reflection/Php/EnumCasesMethodReflection.php @@ -161,7 +161,7 @@ public function getAttributes(): array return []; } - public function hasNoDiscardAttribute(): TrinaryLogic + public function mustUseReturnValue(): TrinaryLogic { return TrinaryLogic::createNo(); } diff --git a/src/Reflection/Php/ExitFunctionReflection.php b/src/Reflection/Php/ExitFunctionReflection.php index f482a2dd73..c4ab5219df 100644 --- a/src/Reflection/Php/ExitFunctionReflection.php +++ b/src/Reflection/Php/ExitFunctionReflection.php @@ -143,7 +143,7 @@ public function getAttributes(): array return []; } - public function hasNoDiscardAttribute(): TrinaryLogic + public function mustUseReturnValue(): TrinaryLogic { return TrinaryLogic::createNo(); } diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index 8cc39c84df..ba4e66fa1b 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -339,7 +339,7 @@ public function getAttributes(): array return $this->attributes; } - public function hasNoDiscardAttribute(): TrinaryLogic + public function mustUseReturnValue(): TrinaryLogic { foreach ($this->attributes as $attrib) { if (strtolower($attrib->getName()) === 'nodiscard') { diff --git a/src/Reflection/Php/PhpFunctionReflection.php b/src/Reflection/Php/PhpFunctionReflection.php index 063baea8fe..e95de465ca 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -276,7 +276,7 @@ public function getAttributes(): array return $this->attributes; } - public function hasNoDiscardAttribute(): TrinaryLogic + public function mustUseReturnValue(): TrinaryLogic { foreach ($this->attributes as $attrib) { if (strtolower($attrib->getName()) === 'nodiscard') { diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index dfe1feeb7c..3c4aafdd60 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -520,7 +520,7 @@ public function getAttributes(): array return $this->attributes; } - public function hasNoDiscardAttribute(): TrinaryLogic + public function mustUseReturnValue(): TrinaryLogic { foreach ($this->attributes as $attrib) { if (strtolower($attrib->getName()) === 'nodiscard') { diff --git a/src/Reflection/ResolvedMethodReflection.php b/src/Reflection/ResolvedMethodReflection.php index c45071b998..880fb448ac 100644 --- a/src/Reflection/ResolvedMethodReflection.php +++ b/src/Reflection/ResolvedMethodReflection.php @@ -229,9 +229,9 @@ public function getAttributes(): array return $this->reflection->getAttributes(); } - public function hasNoDiscardAttribute(): TrinaryLogic + public function mustUseReturnValue(): TrinaryLogic { - return $this->reflection->hasNoDiscardAttribute(); + return $this->reflection->mustUseReturnValue(); } } diff --git a/src/Reflection/Type/IntersectionTypeMethodReflection.php b/src/Reflection/Type/IntersectionTypeMethodReflection.php index e90a8f4a57..8d471a85b6 100644 --- a/src/Reflection/Type/IntersectionTypeMethodReflection.php +++ b/src/Reflection/Type/IntersectionTypeMethodReflection.php @@ -228,9 +228,9 @@ public function getAttributes(): array return $this->methods[0]->getAttributes(); } - public function hasNoDiscardAttribute(): TrinaryLogic + public function mustUseReturnValue(): TrinaryLogic { - return TrinaryLogic::lazyMaxMin($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->hasNoDiscardAttribute()); + return TrinaryLogic::lazyMaxMin($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->mustUseReturnValue()); } } diff --git a/src/Reflection/Type/UnionTypeMethodReflection.php b/src/Reflection/Type/UnionTypeMethodReflection.php index ffd75362ba..ea6899f7b4 100644 --- a/src/Reflection/Type/UnionTypeMethodReflection.php +++ b/src/Reflection/Type/UnionTypeMethodReflection.php @@ -205,9 +205,9 @@ public function getAttributes(): array return $this->methods[0]->getAttributes(); } - public function hasNoDiscardAttribute(): TrinaryLogic + public function mustUseReturnValue(): TrinaryLogic { - return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->hasNoDiscardAttribute()); + return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->mustUseReturnValue()); } } diff --git a/src/Reflection/WrappedExtendedMethodReflection.php b/src/Reflection/WrappedExtendedMethodReflection.php index 00102a9447..6fb39dd2f4 100644 --- a/src/Reflection/WrappedExtendedMethodReflection.php +++ b/src/Reflection/WrappedExtendedMethodReflection.php @@ -173,7 +173,7 @@ public function getAttributes(): array return []; } - public function hasNoDiscardAttribute(): TrinaryLogic + public function mustUseReturnValue(): TrinaryLogic { // Align with the getAttributes() returning empty return TrinaryLogic::createNo(); diff --git a/src/Rules/FunctionDefinitionCheck.php b/src/Rules/FunctionDefinitionCheck.php index 4daf4d0a87..a349303529 100644 --- a/src/Rules/FunctionDefinitionCheck.php +++ b/src/Rules/FunctionDefinitionCheck.php @@ -494,7 +494,7 @@ private function checkParametersAcceptor( ->build(); } } - if ($parametersAcceptor->hasNoDiscardAttribute()->yes()) { + if ($parametersAcceptor->mustUseReturnValue()->yes()) { $returnType = $functionNode->getReturnType(); if ($returnType instanceof Identifier && $returnType->name === 'void' diff --git a/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php b/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php index 13d4d77f22..6f5b820e22 100644 --- a/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php +++ b/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php @@ -43,7 +43,7 @@ public function processNode(Node $node, Scope $scope): array $function = $this->reflectionProvider->getFunction($funcCall->name, $scope); - if (!$function->hasNoDiscardAttribute()->yes()) { + if (!$function->mustUseReturnValue()->yes()) { return []; } diff --git a/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php b/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php index 91d198e0ce..d4365693db 100644 --- a/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php +++ b/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php @@ -63,7 +63,7 @@ public function processNode(Node $node, Scope $scope): array $method = $calledOnType->getMethod($methodName, $scope); - if (!$method->hasNoDiscardAttribute()->yes()) { + if (!$method->mustUseReturnValue()->yes()) { return []; } diff --git a/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php index 0921056501..07224be09b 100644 --- a/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php +++ b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php @@ -76,7 +76,7 @@ public function processNode(Node $node, Scope $scope): array $method = $calledOnType->getMethod($methodName, $scope); - if (!$method->hasNoDiscardAttribute()->yes()) { + if (!$method->mustUseReturnValue()->yes()) { return []; } diff --git a/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php b/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php index d8383f0cad..c834dfd5b1 100644 --- a/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php +++ b/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php @@ -145,9 +145,9 @@ public function hasSideEffects(): TrinaryLogic return $this->methodReflection->hasSideEffects(); } - public function hasNoDiscardAttribute(): TrinaryLogic + public function mustUseReturnValue(): TrinaryLogic { - return $this->methodReflection->hasNoDiscardAttribute(); + return $this->methodReflection->mustUseReturnValue(); } } From 257076b9e530991dc3c4d2a599cb597f7ef68729 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Sep 2025 09:42:30 +0200 Subject: [PATCH 30/34] Shift rules to level 0 --- .../Functions/CallToFunctionStatementWithNoDiscardRule.php | 2 +- src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php | 2 +- .../Methods/CallToStaticMethodStatementWithNoDiscardRule.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php b/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php index 6f5b820e22..f2a7ee73c8 100644 --- a/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php +++ b/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php @@ -13,7 +13,7 @@ /** * @implements Rule */ -#[RegisteredRule(level: 4)] +#[RegisteredRule(level: 0)] final class CallToFunctionStatementWithNoDiscardRule implements Rule { diff --git a/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php b/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php index d4365693db..76ab41c902 100644 --- a/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php +++ b/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php @@ -16,7 +16,7 @@ /** * @implements Rule */ -#[RegisteredRule(level: 4)] +#[RegisteredRule(level: 0)] final class CallToMethodStatementWithNoDiscardRule implements Rule { diff --git a/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php index 07224be09b..9965da60f3 100644 --- a/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php +++ b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php @@ -18,7 +18,7 @@ /** * @implements Rule */ -#[RegisteredRule(level: 4)] +#[RegisteredRule(level: 0)] final class CallToStaticMethodStatementWithNoDiscardRule implements Rule { From 9bf8476b7a402208daff8d029899c4bbb144e7d8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Sep 2025 09:24:08 +0200 Subject: [PATCH 31/34] Check callables too --- src/Analyser/MutatingScope.php | 26 ++++++++++++++ src/PhpDoc/TypeNodeResolver.php | 2 +- .../Callables/CallableParametersAcceptor.php | 7 ++++ .../Callables/FunctionCallableVariant.php | 5 +++ .../ExtendedCallableFunctionVariant.php | 6 ++++ .../GenericParametersAcceptorResolver.php | 1 + src/Reflection/InaccessibleMethod.php | 5 +++ src/Reflection/ParametersAcceptorSelector.php | 4 +++ .../ResolvedFunctionVariantWithCallable.php | 6 ++++ src/Reflection/TrivialParametersAcceptor.php | 5 +++ ...llToFunctionStatementWithNoDiscardRule.php | 36 ++++++++++++++----- src/Rules/RuleLevelHelper.php | 1 + src/Type/CallableType.php | 5 +++ src/Type/ClosureType.php | 16 +++++++++ ...FromCallableDynamicReturnTypeExtension.php | 1 + ...FunctionStatementWithNoDiscardRuleTest.php | 18 ++++++++++ ...nction-call-statement-result-discarded.php | 22 +++++++++++- 17 files changed, 156 insertions(+), 10 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 411ad6faee..6a073dd1a3 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1442,6 +1442,16 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu if (array_key_exists($cacheKey, $cachedTypes)) { $cachedClosureData = $cachedTypes[$cacheKey]; + $mustUseReturnValue = TrinaryLogic::createNo(); + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toLowerString() === 'nodiscard') { + $mustUseReturnValue = TrinaryLogic::createYes(); + break; + } + } + } + return new ClosureType( $parameters, $cachedClosureData['returnType'], @@ -1454,6 +1464,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu invalidateExpressions: $cachedClosureData['invalidateExpressions'], usedVariables: $cachedClosureData['usedVariables'], acceptsNamedArguments: TrinaryLogic::createYes(), + mustUseReturnValue: $mustUseReturnValue, ); } if (self::$resolveClosureTypeDepth >= 2) { @@ -1656,6 +1667,16 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu ]; $node->setAttribute('phpstanCachedTypes', $cachedTypes); + $mustUseReturnValue = TrinaryLogic::createNo(); + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toLowerString() === 'nodiscard') { + $mustUseReturnValue = TrinaryLogic::createYes(); + break; + } + } + } + return new ClosureType( $parameters, $returnType, @@ -1668,6 +1689,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu invalidateExpressions: $invalidateExpressions, usedVariables: $usedVariables, acceptsNamedArguments: TrinaryLogic::createYes(), + mustUseReturnValue: $mustUseReturnValue, ); } elseif ($node instanceof New_) { if ($node->class instanceof Name) { @@ -2716,10 +2738,12 @@ private function createFirstClassCallable( $throwPoints = []; $impurePoints = []; $acceptsNamedArguments = TrinaryLogic::createYes(); + $mustUseReturnValue = TrinaryLogic::createMaybe(); if ($variant instanceof CallableParametersAcceptor) { $throwPoints = $variant->getThrowPoints(); $impurePoints = $variant->getImpurePoints(); $acceptsNamedArguments = $variant->acceptsNamedArguments(); + $mustUseReturnValue = $variant->mustUseReturnValue(); } elseif ($function !== null) { $returnTypeForThrow = $variant->getReturnType(); $throwType = $function->getThrowType(); @@ -2745,6 +2769,7 @@ private function createFirstClassCallable( } $acceptsNamedArguments = $function->acceptsNamedArguments(); + $mustUseReturnValue = $function->mustUseReturnValue(); } $parameters = $variant->getParameters(); @@ -2759,6 +2784,7 @@ private function createFirstClassCallable( $throwPoints, $impurePoints, acceptsNamedArguments: $acceptsNamedArguments, + mustUseReturnValue: $mustUseReturnValue, ); } diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index ced0b7804b..ea29d7c088 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -1032,7 +1032,7 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi ), ]); } elseif ($mainType instanceof ClosureType) { - $closure = new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, templateTags: $templateTags, impurePoints: $mainType->getImpurePoints(), invalidateExpressions: $mainType->getInvalidateExpressions(), usedVariables: $mainType->getUsedVariables(), acceptsNamedArguments: $mainType->acceptsNamedArguments()); + $closure = new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, templateTags: $templateTags, impurePoints: $mainType->getImpurePoints(), invalidateExpressions: $mainType->getInvalidateExpressions(), usedVariables: $mainType->getUsedVariables(), acceptsNamedArguments: $mainType->acceptsNamedArguments(), mustUseReturnValue: $mainType->mustUseReturnValue()); if ($closure->isPure()->yes() && $returnType->isVoid()->yes()) { return new ErrorType(); } diff --git a/src/Reflection/Callables/CallableParametersAcceptor.php b/src/Reflection/Callables/CallableParametersAcceptor.php index 25ff7d770c..11a22e40cf 100644 --- a/src/Reflection/Callables/CallableParametersAcceptor.php +++ b/src/Reflection/Callables/CallableParametersAcceptor.php @@ -36,4 +36,11 @@ public function getInvalidateExpressions(): array; */ public function getUsedVariables(): array; + /** + * Has the #[\NoDiscard] attribute - on PHP 8.5+ if the function's return + * value is unused at runtime a warning is emitted, PHPStan will emit the + * warning during analysis and on older PHP versions too + */ + public function mustUseReturnValue(): TrinaryLogic; + } diff --git a/src/Reflection/Callables/FunctionCallableVariant.php b/src/Reflection/Callables/FunctionCallableVariant.php index 71ea905c52..5e385e4480 100644 --- a/src/Reflection/Callables/FunctionCallableVariant.php +++ b/src/Reflection/Callables/FunctionCallableVariant.php @@ -168,4 +168,9 @@ public function acceptsNamedArguments(): TrinaryLogic return $this->function->acceptsNamedArguments(); } + public function mustUseReturnValue(): TrinaryLogic + { + return $this->function->mustUseReturnValue(); + } + } diff --git a/src/Reflection/ExtendedCallableFunctionVariant.php b/src/Reflection/ExtendedCallableFunctionVariant.php index 5e2d3a9c10..4cac48123d 100644 --- a/src/Reflection/ExtendedCallableFunctionVariant.php +++ b/src/Reflection/ExtendedCallableFunctionVariant.php @@ -36,6 +36,7 @@ public function __construct( private array $invalidateExpressions, private array $usedVariables, private TrinaryLogic $acceptsNamedArguments, + private TrinaryLogic $mustUseReturnValue, ) { parent::__construct( @@ -80,4 +81,9 @@ public function acceptsNamedArguments(): TrinaryLogic return $this->acceptsNamedArguments; } + public function mustUseReturnValue(): TrinaryLogic + { + return $this->mustUseReturnValue; + } + } diff --git a/src/Reflection/GenericParametersAcceptorResolver.php b/src/Reflection/GenericParametersAcceptorResolver.php index 35f70bf37d..784b1427d1 100644 --- a/src/Reflection/GenericParametersAcceptorResolver.php +++ b/src/Reflection/GenericParametersAcceptorResolver.php @@ -128,6 +128,7 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc $originalParametersAcceptor->getInvalidateExpressions(), $originalParametersAcceptor->getUsedVariables(), $originalParametersAcceptor->acceptsNamedArguments(), + $originalParametersAcceptor->mustUseReturnValue(), ); } diff --git a/src/Reflection/InaccessibleMethod.php b/src/Reflection/InaccessibleMethod.php index 037f4e8137..f340cf3127 100644 --- a/src/Reflection/InaccessibleMethod.php +++ b/src/Reflection/InaccessibleMethod.php @@ -88,4 +88,9 @@ public function acceptsNamedArguments(): TrinaryLogic return $this->methodReflection->acceptsNamedArguments(); } + public function mustUseReturnValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + } diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 7e167dcfc5..6b47480d73 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -639,6 +639,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc $invalidateExpressions = []; $usedVariables = []; $acceptsNamedArguments = TrinaryLogic::createNo(); + $mustUseReturnValue = TrinaryLogic::createMaybe(); foreach ($acceptors as $acceptor) { $returnTypes[] = $acceptor->getReturnType(); @@ -655,6 +656,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc $invalidateExpressions = array_merge($invalidateExpressions, $acceptor->getInvalidateExpressions()); $usedVariables = array_merge($usedVariables, $acceptor->getUsedVariables()); $acceptsNamedArguments = $acceptsNamedArguments->or($acceptor->acceptsNamedArguments()); + $mustUseReturnValue = $mustUseReturnValue->or($acceptor->mustUseReturnValue()); } $isVariadic = $isVariadic || $acceptor->isVariadic(); @@ -761,6 +763,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc $invalidateExpressions, $usedVariables, $acceptsNamedArguments, + $mustUseReturnValue, ); } @@ -797,6 +800,7 @@ private static function wrapAcceptor(ParametersAcceptor $acceptor): ExtendedPara $acceptor->getInvalidateExpressions(), $acceptor->getUsedVariables(), $acceptor->acceptsNamedArguments(), + $acceptor->mustUseReturnValue(), ); } diff --git a/src/Reflection/ResolvedFunctionVariantWithCallable.php b/src/Reflection/ResolvedFunctionVariantWithCallable.php index 59c9cf1bec..17574ec30e 100644 --- a/src/Reflection/ResolvedFunctionVariantWithCallable.php +++ b/src/Reflection/ResolvedFunctionVariantWithCallable.php @@ -28,6 +28,7 @@ public function __construct( private array $invalidateExpressions, private array $usedVariables, private TrinaryLogic $acceptsNamedArguments, + private TrinaryLogic $mustUseReturnValue, ) { } @@ -112,4 +113,9 @@ public function acceptsNamedArguments(): TrinaryLogic return $this->acceptsNamedArguments; } + public function mustUseReturnValue(): TrinaryLogic + { + return $this->mustUseReturnValue; + } + } diff --git a/src/Reflection/TrivialParametersAcceptor.php b/src/Reflection/TrivialParametersAcceptor.php index ea6b278145..2863ff83cd 100644 --- a/src/Reflection/TrivialParametersAcceptor.php +++ b/src/Reflection/TrivialParametersAcceptor.php @@ -98,4 +98,9 @@ public function acceptsNamedArguments(): TrinaryLogic return TrinaryLogic::createYes(); } + public function mustUseReturnValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + } diff --git a/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php b/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php index f2a7ee73c8..de222fc5e8 100644 --- a/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php +++ b/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php @@ -8,6 +8,8 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\TrinaryLogic; +use PHPStan\Type\VerbosityLevel; use function sprintf; /** @@ -33,25 +35,43 @@ public function processNode(Node $node, Scope $scope): array } $funcCall = $node->expr; - if (!($funcCall->name instanceof Node\Name)) { - return []; + if ($funcCall->name instanceof Node\Name) { + if (!$this->reflectionProvider->hasFunction($funcCall->name, $scope)) { + return []; + } + + $function = $this->reflectionProvider->getFunction($funcCall->name, $scope); + if (!$function->mustUseReturnValue()->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Call to function %s() on a separate line discards return value.', + $function->getName(), + ))->identifier('function.resultDiscarded')->build(), + ]; } - if (!$this->reflectionProvider->hasFunction($funcCall->name, $scope)) { + $callableType = $scope->getType($funcCall->name); + if (!$callableType->isCallable()->yes()) { return []; } - $function = $this->reflectionProvider->getFunction($funcCall->name, $scope); + $mustUseReturnValue = TrinaryLogic::createNo(); + foreach ($callableType->getCallableParametersAcceptors($scope) as $callableParametersAcceptor) { + $mustUseReturnValue = $mustUseReturnValue->or($callableParametersAcceptor->mustUseReturnValue()); + } - if (!$function->mustUseReturnValue()->yes()) { + if (!$mustUseReturnValue->yes()) { return []; } return [ RuleErrorBuilder::message(sprintf( - 'Call to function %s() on a separate line discards return value.', - $function->getName(), - ))->identifier('function.resultDiscarded')->build(), + 'Call to callable %s on a separate line discards return value.', + $callableType->describe(VerbosityLevel::value()), + ))->identifier('callable.resultDiscarded')->build(), ]; } diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index d82ff0745b..1b46ccfbe7 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -122,6 +122,7 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType): $acceptedType->getInvalidateExpressions(), $acceptedType->getUsedVariables(), $acceptedType->acceptsNamedArguments(), + $acceptedType->mustUseReturnValue(), ); } diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index ddb857fc99..079afc8c1e 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -295,6 +295,11 @@ public function acceptsNamedArguments(): TrinaryLogic return TrinaryLogic::createYes(); } + public function mustUseReturnValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function toNumber(): Type { return new ErrorType(); diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index 74c804e887..49bbfe8c1b 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -80,6 +80,8 @@ class ClosureType implements TypeWithClassName, CallableParametersAcceptor private TrinaryLogic $acceptsNamedArguments; + private TrinaryLogic $mustUseReturnValue; + /** * @api * @param list|null $parameters @@ -102,12 +104,17 @@ public function __construct( private array $invalidateExpressions = [], private array $usedVariables = [], ?TrinaryLogic $acceptsNamedArguments = null, + ?TrinaryLogic $mustUseReturnValue = null, ) { if ($acceptsNamedArguments === null) { $acceptsNamedArguments = TrinaryLogic::createYes(); } $this->acceptsNamedArguments = $acceptsNamedArguments; + if ($mustUseReturnValue === null) { + $mustUseReturnValue = TrinaryLogic::createMaybe(); + } + $this->mustUseReturnValue = $mustUseReturnValue; $this->parameters = $parameters ?? []; $this->returnType = $returnType ?? new MixedType(); @@ -269,6 +276,8 @@ function (): string { $this->impurePoints, $this->invalidateExpressions, $this->usedVariables, + $this->acceptsNamedArguments, + $this->mustUseReturnValue, ); return $printer->print($selfWithoutParameterNames->toPhpDocNode()); @@ -448,6 +457,11 @@ public function acceptsNamedArguments(): TrinaryLogic return $this->acceptsNamedArguments; } + public function mustUseReturnValue(): TrinaryLogic + { + return $this->mustUseReturnValue; + } + public function isCloneable(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -627,6 +641,7 @@ public function traverse(callable $cb): Type $this->invalidateExpressions, $this->usedVariables, $this->acceptsNamedArguments, + $this->mustUseReturnValue, ); } @@ -677,6 +692,7 @@ public function traverseSimultaneously(Type $right, callable $cb): Type $this->invalidateExpressions, $this->usedVariables, $this->acceptsNamedArguments, + $this->mustUseReturnValue, ); } diff --git a/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php b/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php index 6674b65374..df0e2d54aa 100644 --- a/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php @@ -55,6 +55,7 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, invalidateExpressions: $variant->getInvalidateExpressions(), usedVariables: $variant->getUsedVariables(), acceptsNamedArguments: $variant->acceptsNamedArguments(), + mustUseReturnValue: $variant->mustUseReturnValue(), ); } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithNoDiscardRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithNoDiscardRuleTest.php index 0d36700dd8..8c1abccdad 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithNoDiscardRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithNoDiscardRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -16,6 +17,7 @@ protected function getRule(): Rule return new CallToFunctionStatementWithNoDiscardRule(self::createReflectionProvider()); } + #[RequiresPhp('>= 8.1')] public function testRule(): void { $this->analyse([__DIR__ . '/data/function-call-statement-result-discarded.php'], [ @@ -27,6 +29,22 @@ public function testRule(): void 'Call to function FunctionCallStatementResultDiscarded\differentCase() on a separate line discards return value.', 25, ], + [ + 'Call to callable \'FunctionCallStateme…\' on a separate line discards return value.', + 30, + ], + [ + 'Call to callable Closure(): array on a separate line discards return value.', + 35, + ], + [ + 'Call to callable Closure(): 1 on a separate line discards return value.', + 40, + ], + [ + 'Call to callable Closure(): 1 on a separate line discards return value.', + 45, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php b/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php index a1f5fa71dd..e576d8adc3 100644 --- a/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php +++ b/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php @@ -1,4 +1,4 @@ -= 8.1 namespace FunctionCallStatementResultDiscarded; @@ -23,3 +23,23 @@ function differentCase(): array { } differentCase(); + +$callable = 'FunctionCallStatementResultDiscarded\\withSideEffects'; +$callableResult = $callable(); + +$callable(); + +$firstClassCallable = withSideEffects(...); +$firstClasCallableResult = $firstClassCallable(); + +$firstClassCallable(); + +$closureWithNoDiscard = #[\NoDiscard] function () { return 1; }; +$a = $closureWithNoDiscard(); + +$closureWithNoDiscard(); + +$arrowWithNoDiscard = #[\NoDiscard] fn () => 1; +$b = $arrowWithNoDiscard(); + +$arrowWithNoDiscard(); From 96e3a0d05d043e80fad295e7d0b6c7b55f7f372e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Sep 2025 16:11:33 +0200 Subject: [PATCH 32/34] Do not report first class callables --- .../Functions/CallToFunctionStatementWithNoDiscardRule.php | 4 ++++ src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php | 4 ++++ .../Methods/CallToStaticMethodStatementWithNoDiscardRule.php | 4 ++++ .../data/function-call-statement-result-discarded.php | 2 ++ .../Methods/data/method-call-statement-result-discarded.php | 2 ++ .../data/static-method-call-statement-result-discarded.php | 2 ++ 6 files changed, 18 insertions(+) diff --git a/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php b/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php index de222fc5e8..7ac71862e3 100644 --- a/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php +++ b/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php @@ -34,6 +34,10 @@ public function processNode(Node $node, Scope $scope): array return []; } + if ($node->expr->isFirstClassCallable()) { + return []; + } + $funcCall = $node->expr; if ($funcCall->name instanceof Node\Name) { if (!$this->reflectionProvider->hasFunction($funcCall->name, $scope)) { diff --git a/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php b/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php index 76ab41c902..e0a5d2ff4e 100644 --- a/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php +++ b/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php @@ -37,6 +37,10 @@ public function processNode(Node $node, Scope $scope): array return []; } + if ($node->expr->isFirstClassCallable()) { + return []; + } + $funcCall = $node->expr; if (!$funcCall->name instanceof Node\Identifier) { return []; diff --git a/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php index 9965da60f3..349eeeace7 100644 --- a/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php +++ b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php @@ -40,6 +40,10 @@ public function processNode(Node $node, Scope $scope): array return []; } + if ($node->expr->isFirstClassCallable()) { + return []; + } + $funcCall = $node->expr; if (!$funcCall->name instanceof Node\Identifier) { return []; diff --git a/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php b/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php index e576d8adc3..e167b4f4b9 100644 --- a/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php +++ b/tests/PHPStan/Rules/Functions/data/function-call-statement-result-discarded.php @@ -43,3 +43,5 @@ function differentCase(): array { $b = $arrowWithNoDiscard(); $arrowWithNoDiscard(); + +withSideEffects(...); diff --git a/tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php b/tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php index df48d076fe..7549ebf390 100644 --- a/tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php +++ b/tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php @@ -28,3 +28,5 @@ public function differentCase(): array { } $o->differentCase(); + +$o->instanceMethod(...); diff --git a/tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php b/tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php index 6449fd72a2..6759cdda31 100644 --- a/tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php +++ b/tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php @@ -25,3 +25,5 @@ public static function differentCase(): array { } ClassWithStaticSideEffects::differentCase(); + +ClassWithStaticSideEffects::staticMethod(...); From 1d2157d6b06587d6f889b4965bf6e843c112a6d8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Sep 2025 16:21:35 +0200 Subject: [PATCH 33/34] Report NoDiscard above never return type + implicit void methods reported too --- src/Rules/FunctionDefinitionCheck.php | 21 +++++++----- ...ingClassesInArrowFunctionTypehintsRule.php | 2 +- .../ExistingClassesInClosureTypehintsRule.php | 2 +- .../ExistingClassesInTypehintsRule.php | 2 +- .../ExistingClassesInTypehintsRule.php | 2 +- ...lassesInArrowFunctionTypehintsRuleTest.php | 4 +++ ...stingClassesInClosureTypehintsRuleTest.php | 4 +++ .../ExistingClassesInTypehintsRuleTest.php | 4 +++ .../arrow-function-typehints-nodiscard.php | 5 +++ .../data/closure-typehints-nodiscard.php | 5 +++ .../Functions/data/typehints-nodiscard.php | 4 +++ .../ExistingClassesInTypehintsRuleTest.php | 24 +++++++++++++ .../Methods/data/typehints-nodiscard.php | 34 +++++++++++++++++++ 13 files changed, 101 insertions(+), 12 deletions(-) diff --git a/src/Rules/FunctionDefinitionCheck.php b/src/Rules/FunctionDefinitionCheck.php index a349303529..6c749bca6b 100644 --- a/src/Rules/FunctionDefinitionCheck.php +++ b/src/Rules/FunctionDefinitionCheck.php @@ -31,6 +31,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\Generic\TemplateType; +use PHPStan\Type\NeverType; use PHPStan\Type\NonexistentParentClassType; use PHPStan\Type\ParserNodeTypeToPHPStanType; use PHPStan\Type\Type; @@ -109,7 +110,7 @@ public function checkAnonymousFunction( string $unionTypesMessage, string $unresolvableParameterTypeMessage, string $unresolvableReturnTypeMessage, - string $noDiscardVoidReturnMessage, + string $noDiscardReturnTypeMessage, ): array { $errors = []; @@ -202,11 +203,14 @@ public function checkAnonymousFunction( if ($returnTypeNode === null) { return $errors; } - if ($returnTypeNode instanceof Identifier && $returnTypeNode->name === 'void') { + if ( + $returnTypeNode instanceof Identifier + && in_array($returnTypeNode->toLowerString(), ['void', 'never'], true) + ) { foreach ($attribGroups as $attribGroup) { foreach ($attribGroup->attrs as $attrib) { if (strtolower($attrib->name->name) === 'nodiscard') { - $errors[] = RuleErrorBuilder::message($noDiscardVoidReturnMessage) + $errors[] = RuleErrorBuilder::message(sprintf($noDiscardReturnTypeMessage, $returnTypeNode->toString())) ->line($returnTypeNode->getStartLine()) ->identifier('attribute.target') ->build(); @@ -349,7 +353,7 @@ private function checkParametersAcceptor( string $templateTypeMissingInParameterMessage, string $unresolvableParameterTypeMessage, string $unresolvableReturnTypeMessage, - string $noDiscardVoidReturnMessage, + string $noDiscardReturnTypeMessage, ): array { $errors = []; @@ -495,11 +499,12 @@ private function checkParametersAcceptor( } } if ($parametersAcceptor->mustUseReturnValue()->yes()) { - $returnType = $functionNode->getReturnType(); - if ($returnType instanceof Identifier - && $returnType->name === 'void' + $returnType = $parametersAcceptor->getReturnType(); + if ( + $returnType->isVoid()->yes() + || ($returnType instanceof NeverType && $returnType->isExplicit()) ) { - $errors[] = RuleErrorBuilder::message($noDiscardVoidReturnMessage) + $errors[] = RuleErrorBuilder::message(sprintf($noDiscardReturnTypeMessage, $returnType->describe(VerbosityLevel::typeOnly()))) ->line($returnTypeNode->getStartLine()) ->identifier('attribute.target') ->build(); diff --git a/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php b/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php index 342ca81fa3..827784dd9a 100644 --- a/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php @@ -52,7 +52,7 @@ public function processNode(Node $node, Scope $scope): array 'Anonymous function uses native union types but they\'re supported only on PHP 8.0 and later.', 'Parameter $%s of anonymous function has unresolvable native type.', 'Anonymous function has unresolvable native return type.', - 'Attribute NoDiscard cannot be used on void anonymous function.', + 'Attribute NoDiscard cannot be used on %s anonymous function.', )); } diff --git a/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php b/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php index d471ebdb0d..24c91a88c1 100644 --- a/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php @@ -37,7 +37,7 @@ public function processNode(Node $node, Scope $scope): array 'Anonymous function uses native union types but they\'re supported only on PHP 8.0 and later.', 'Parameter $%s of anonymous function has unresolvable native type.', 'Anonymous function has unresolvable native return type.', - 'Attribute NoDiscard cannot be used on void anonymous function.', + 'Attribute NoDiscard cannot be used on %s anonymous function.', ); } diff --git a/src/Rules/Functions/ExistingClassesInTypehintsRule.php b/src/Rules/Functions/ExistingClassesInTypehintsRule.php index 735a4f316a..66303ad26b 100644 --- a/src/Rules/Functions/ExistingClassesInTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInTypehintsRule.php @@ -54,7 +54,7 @@ public function processNode(Node $node, Scope $scope): array $functionName, ), sprintf( - 'Attribute NoDiscard cannot be used on void function %s().', + 'Attribute NoDiscard cannot be used on %%s function %s().', $functionName, ), ); diff --git a/src/Rules/Methods/ExistingClassesInTypehintsRule.php b/src/Rules/Methods/ExistingClassesInTypehintsRule.php index 3cb07984ec..11781bb2e4 100644 --- a/src/Rules/Methods/ExistingClassesInTypehintsRule.php +++ b/src/Rules/Methods/ExistingClassesInTypehintsRule.php @@ -65,7 +65,7 @@ public function processNode(Node $node, Scope $scope): array $methodName, ), sprintf( - 'Attribute NoDiscard cannot be used on void method %s::%s().', + 'Attribute NoDiscard cannot be used on %%s method %s::%s().', $className, $methodName, ), diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php index abd9602099..7afdf74dad 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php @@ -331,6 +331,10 @@ public function testNoDiscardVoid(): void 'Attribute NoDiscard cannot be used on void anonymous function.', 10, ], + [ + 'Attribute NoDiscard cannot be used on never anonymous function.', + 15, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php index e4c50ec9b1..9ad551ac0f 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php @@ -360,6 +360,10 @@ public function testNoDiscardVoid(): void 'Attribute NoDiscard cannot be used on void anonymous function.', 5, ], + [ + 'Attribute NoDiscard cannot be used on never anonymous function.', + 10, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php index 1c685ff2d7..19035aa930 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php @@ -496,6 +496,10 @@ public function testNoDiscardVoid(): void 'Attribute NoDiscard cannot be used on void function TestFunctionTypehints\nothing().', 6, ], + [ + 'Attribute NoDiscard cannot be used on never function TestFunctionTypehints\returnNever().', + 10, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/arrow-function-typehints-nodiscard.php b/tests/PHPStan/Rules/Functions/data/arrow-function-typehints-nodiscard.php index c6d6db74b4..b8f7115d68 100644 --- a/tests/PHPStan/Rules/Functions/data/arrow-function-typehints-nodiscard.php +++ b/tests/PHPStan/Rules/Functions/data/arrow-function-typehints-nodiscard.php @@ -10,4 +10,9 @@ public function doFoo() #[\NoDiscard] fn(): void => true; } + public function doBar() + { + #[\NoDiscard] fn(): never => true; + } + } diff --git a/tests/PHPStan/Rules/Functions/data/closure-typehints-nodiscard.php b/tests/PHPStan/Rules/Functions/data/closure-typehints-nodiscard.php index 0b473bf6f0..ca152e2340 100644 --- a/tests/PHPStan/Rules/Functions/data/closure-typehints-nodiscard.php +++ b/tests/PHPStan/Rules/Functions/data/closure-typehints-nodiscard.php @@ -6,3 +6,8 @@ { }; + +$callbackNever = #[\NoDiscard] function (): never +{ + +}; diff --git a/tests/PHPStan/Rules/Functions/data/typehints-nodiscard.php b/tests/PHPStan/Rules/Functions/data/typehints-nodiscard.php index 86219deca3..554bd8c5c8 100644 --- a/tests/PHPStan/Rules/Functions/data/typehints-nodiscard.php +++ b/tests/PHPStan/Rules/Functions/data/typehints-nodiscard.php @@ -5,3 +5,7 @@ #[\NoDiscard] function nothing(): void { } + +#[\NoDiscard] +function returnNever(): never { +} diff --git a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php index e35cf9707c..8f1aba12e6 100644 --- a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php @@ -610,6 +610,30 @@ public function testNoDiscardVoid(): void 'Attribute NoDiscard cannot be used on void method TestMethodTypehints\Demo::alsoNothing().', 12, ], + [ + 'Attribute NoDiscard cannot be used on never method TestMethodTypehints\Demo::returnNever().', + 16, + ], + [ + 'Attribute NoDiscard cannot be used on void method TestMethodTypehints\Demo::__construct().', + 19, + ], + [ + 'Attribute NoDiscard cannot be used on void method TestMethodTypehints\Demo::__destruct().', + 25, + ], + [ + 'Attribute NoDiscard cannot be used on void method TestMethodTypehints\Demo::__unset().', + 31, + ], + [ + 'Attribute NoDiscard cannot be used on void method TestMethodTypehints\Demo::__wakeup().', + 37, + ], + [ + 'Attribute NoDiscard cannot be used on void method TestMethodTypehints\Demo::__clone().', + 43, + ], ]); } diff --git a/tests/PHPStan/Rules/Methods/data/typehints-nodiscard.php b/tests/PHPStan/Rules/Methods/data/typehints-nodiscard.php index 6def386caf..5211ed9f3f 100644 --- a/tests/PHPStan/Rules/Methods/data/typehints-nodiscard.php +++ b/tests/PHPStan/Rules/Methods/data/typehints-nodiscard.php @@ -11,4 +11,38 @@ public function nothing(): void { #[\NoDiscard] public static function alsoNothing(): void { } + + #[\NoDiscard] + public static function returnNever(): never { + } + + #[\NoDiscard] + public function __construct() + { + + } + + #[\NoDiscard] + public function __destruct() + { + + } + + #[\NoDiscard] + public function __unset() + { + + } + + #[\NoDiscard] + public function __wakeup() + { + + } + + #[\NoDiscard] + public function __clone() + { + + } } From 5db8f0d1fdb251bb7f5829801408614f7a56fa6c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Sep 2025 22:49:51 +0200 Subject: [PATCH 34/34] Fix tests --- .../ExistingClassesInArrowFunctionTypehintsRuleTest.php | 1 + .../Functions/ExistingClassesInClosureTypehintsRuleTest.php | 1 + .../Rules/Functions/ExistingClassesInTypehintsRuleTest.php | 1 + .../PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php | 1 + 4 files changed, 4 insertions(+) diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php index 7afdf74dad..0ca5ca5013 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php @@ -324,6 +324,7 @@ public function testBug5206(): void $this->analyse([__DIR__ . '/data/bug-5206.php'], $errors); } + #[RequiresPhp('>= 8.2')] public function testNoDiscardVoid(): void { $this->analyse([__DIR__ . '/data/arrow-function-typehints-nodiscard.php'], [ diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php index 9ad551ac0f..1fa4bd16dc 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php @@ -353,6 +353,7 @@ public function testDeprecatedImplicitlyNullableParameterType(): void ]); } + #[RequiresPhp('>= 8.2')] public function testNoDiscardVoid(): void { $this->analyse([__DIR__ . '/data/closure-typehints-nodiscard.php'], [ diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php index 19035aa930..79beed0ea4 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php @@ -489,6 +489,7 @@ public function testParamClosureThisClasses(): void ]); } + #[RequiresPhp('>= 8.2')] public function testNoDiscardVoid(): void { $this->analyse([__DIR__ . '/data/typehints-nodiscard.php'], [ diff --git a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php index 8f1aba12e6..ee7e28cb76 100644 --- a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php @@ -599,6 +599,7 @@ public function testBug12501(): void $this->analyse([__DIR__ . '/data/bug-12501.php'], []); } + #[RequiresPhp('>= 8.2')] public function testNoDiscardVoid(): void { $this->analyse([__DIR__ . '/data/typehints-nodiscard.php'], [