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" diff --git a/Makefile b/Makefile index 304b72960c..3cb07512ee 100644 --- a/Makefile +++ b/Makefile @@ -110,6 +110,12 @@ 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-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 \ src tests install-paratest: 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" ] } 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/Annotations/AnnotationMethodReflection.php b/src/Reflection/Annotations/AnnotationMethodReflection.php index 9b1886b544..7fe7ccd14e 100644 --- a/src/Reflection/Annotations/AnnotationMethodReflection.php +++ b/src/Reflection/Annotations/AnnotationMethodReflection.php @@ -183,4 +183,9 @@ public function getAttributes(): array return []; } + public function mustUseReturnValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + } 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/Dummy/ChangedTypeMethodReflection.php b/src/Reflection/Dummy/ChangedTypeMethodReflection.php index 9a345c22cd..b5749c7a29 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 mustUseReturnValue(): TrinaryLogic + { + return $this->reflection->mustUseReturnValue(); + } + } diff --git a/src/Reflection/Dummy/DummyConstructorReflection.php b/src/Reflection/Dummy/DummyConstructorReflection.php index c48d6904ce..6652c87c47 100644 --- a/src/Reflection/Dummy/DummyConstructorReflection.php +++ b/src/Reflection/Dummy/DummyConstructorReflection.php @@ -157,4 +157,10 @@ public function getAttributes(): array return []; } + 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 dced9b6206..f81481d449 100644 --- a/src/Reflection/Dummy/DummyMethodReflection.php +++ b/src/Reflection/Dummy/DummyMethodReflection.php @@ -149,4 +149,10 @@ public function getAttributes(): array return []; } + public function mustUseReturnValue(): TrinaryLogic + { + // Align with the getAttributes() returning empty + return TrinaryLogic::createNo(); + } + } 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/ExtendedMethodReflection.php b/src/Reflection/ExtendedMethodReflection.php index 5cea392754..0cb9caeab0 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 mustUseReturnValue(): TrinaryLogic; + } diff --git a/src/Reflection/FunctionReflection.php b/src/Reflection/FunctionReflection.php index 297e4dd7d3..b99209c628 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 mustUseReturnValue(): TrinaryLogic; + } 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/Native/NativeFunctionReflection.php b/src/Reflection/Native/NativeFunctionReflection.php index 7668d51f9e..50ddbb0e9b 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 { @@ -150,4 +151,14 @@ public function getAttributes(): array return $this->attributes; } + public function mustUseReturnValue(): TrinaryLogic + { + foreach ($this->attributes as $attrib) { + if (strtolower($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..7850ed1b5d 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 mustUseReturnValue(): TrinaryLogic + { + foreach ($this->attributes as $attrib) { + if (strtolower($attrib->getName()) === 'nodiscard') { + return TrinaryLogic::createYes(); + } + } + return TrinaryLogic::createNo(); + } + } 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/Php/ClosureCallMethodReflection.php b/src/Reflection/Php/ClosureCallMethodReflection.php index aafd7b658e..3c0db2f7f3 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 mustUseReturnValue(): TrinaryLogic + { + return $this->nativeMethodReflection->mustUseReturnValue(); + } + } diff --git a/src/Reflection/Php/EnumCasesMethodReflection.php b/src/Reflection/Php/EnumCasesMethodReflection.php index ecf72e435d..91d795598b 100644 --- a/src/Reflection/Php/EnumCasesMethodReflection.php +++ b/src/Reflection/Php/EnumCasesMethodReflection.php @@ -161,4 +161,9 @@ public function getAttributes(): array return []; } + public function mustUseReturnValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/Php/ExitFunctionReflection.php b/src/Reflection/Php/ExitFunctionReflection.php index 4020bbdc09..c4ab5219df 100644 --- a/src/Reflection/Php/ExitFunctionReflection.php +++ b/src/Reflection/Php/ExitFunctionReflection.php @@ -143,4 +143,9 @@ public function getAttributes(): array return []; } + public function mustUseReturnValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index de16789678..ba4e66fa1b 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 @@ -338,4 +339,14 @@ public function getAttributes(): array return $this->attributes; } + public function mustUseReturnValue(): TrinaryLogic + { + foreach ($this->attributes as $attrib) { + if (strtolower($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..e95de465ca 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 @@ -275,4 +276,14 @@ public function getAttributes(): array return $this->attributes; } + public function mustUseReturnValue(): TrinaryLogic + { + foreach ($this->attributes as $attrib) { + if (strtolower($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..3c4aafdd60 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 mustUseReturnValue(): TrinaryLogic + { + foreach ($this->attributes as $attrib) { + if (strtolower($attrib->getName()) === 'nodiscard') { + return TrinaryLogic::createYes(); + } + } + return TrinaryLogic::createNo(); + } + } 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/ResolvedMethodReflection.php b/src/Reflection/ResolvedMethodReflection.php index 134e566eca..880fb448ac 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 mustUseReturnValue(): TrinaryLogic + { + return $this->reflection->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/Reflection/Type/IntersectionTypeMethodReflection.php b/src/Reflection/Type/IntersectionTypeMethodReflection.php index eafd314157..8d471a85b6 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 mustUseReturnValue(): TrinaryLogic + { + 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 b330b6fdad..ea6899f7b4 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 mustUseReturnValue(): TrinaryLogic + { + 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 5a9ea238cf..6fb39dd2f4 100644 --- a/src/Reflection/WrappedExtendedMethodReflection.php +++ b/src/Reflection/WrappedExtendedMethodReflection.php @@ -173,4 +173,10 @@ public function getAttributes(): array return []; } + public function mustUseReturnValue(): TrinaryLogic + { + // Align with the getAttributes() returning empty + return TrinaryLogic::createNo(); + } + } 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/src/Rules/FunctionDefinitionCheck.php b/src/Rules/FunctionDefinitionCheck.php index 626a97d8ad..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; @@ -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 $noDiscardReturnTypeMessage, ): array { $errors = []; @@ -197,6 +203,22 @@ public function checkAnonymousFunction( if ($returnTypeNode === null) { return $errors; } + 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(sprintf($noDiscardReturnTypeMessage, $returnTypeNode->toString())) + ->line($returnTypeNode->getStartLine()) + ->identifier('attribute.target') + ->build(); + break 2; + } + } + } + } if ( !$unionTypeReported @@ -266,6 +288,7 @@ public function checkClassMethod( string $unresolvableParameterTypeMessage, string $unresolvableReturnTypeMessage, string $selfOutMessage, + string $noDiscardVoidReturnMessage, ): array { $errors = $this->checkParametersAcceptor( @@ -278,6 +301,7 @@ public function checkClassMethod( $templateTypeMissingInParameterMessage, $unresolvableParameterTypeMessage, $unresolvableReturnTypeMessage, + $noDiscardVoidReturnMessage, ); $selfOutType = $methodReflection->getSelfOutType(); @@ -329,6 +353,7 @@ private function checkParametersAcceptor( string $templateTypeMissingInParameterMessage, string $unresolvableParameterTypeMessage, string $unresolvableReturnTypeMessage, + string $noDiscardReturnTypeMessage, ): array { $errors = []; @@ -473,6 +498,18 @@ private function checkParametersAcceptor( ->build(); } } + if ($parametersAcceptor->mustUseReturnValue()->yes()) { + $returnType = $parametersAcceptor->getReturnType(); + if ( + $returnType->isVoid()->yes() + || ($returnType instanceof NeverType && $returnType->isExplicit()) + ) { + $errors[] = RuleErrorBuilder::message(sprintf($noDiscardReturnTypeMessage, $returnType->describe(VerbosityLevel::typeOnly()))) + ->line($returnTypeNode->getStartLine()) + ->identifier('attribute.target') + ->build(); + } + } $returnTypeReferencedClasses = $this->getReturnTypeReferencedClasses($parametersAcceptor); diff --git a/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php b/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php new file mode 100644 index 0000000000..7ac71862e3 --- /dev/null +++ b/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php @@ -0,0 +1,82 @@ + + */ +#[RegisteredRule(level: 0)] +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 []; + } + + if ($node->expr->isFirstClassCallable()) { + return []; + } + + $funcCall = $node->expr; + 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(), + ]; + } + + $callableType = $scope->getType($funcCall->name); + if (!$callableType->isCallable()->yes()) { + return []; + } + + $mustUseReturnValue = TrinaryLogic::createNo(); + foreach ($callableType->getCallableParametersAcceptors($scope) as $callableParametersAcceptor) { + $mustUseReturnValue = $mustUseReturnValue->or($callableParametersAcceptor->mustUseReturnValue()); + } + + if (!$mustUseReturnValue->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Call to callable %s on a separate line discards return value.', + $callableType->describe(VerbosityLevel::value()), + ))->identifier('callable.resultDiscarded')->build(), + ]; + } + +} diff --git a/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php b/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php index c2a18addbc..827784dd9a 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 %s anonymous function.', )); } diff --git a/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php b/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php index a052390041..24c91a88c1 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 %s anonymous function.', ); } diff --git a/src/Rules/Functions/ExistingClassesInTypehintsRule.php b/src/Rules/Functions/ExistingClassesInTypehintsRule.php index 403ed4ec1a..66303ad26b 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 %%s function %s().', + $functionName, + ), ); } diff --git a/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php b/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php new file mode 100644 index 0000000000..e0a5d2ff4e --- /dev/null +++ b/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php @@ -0,0 +1,84 @@ + + */ +#[RegisteredRule(level: 0)] +final class CallToMethodStatementWithNoDiscardRule implements Rule +{ + + public function __construct(private RuleLevelHelper $ruleLevelHelper) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Expression::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->expr instanceof Node\Expr\MethodCall + && !$node->expr instanceof Node\Expr\NullsafeMethodCall + ) { + return []; + } + + if ($node->expr->isFirstClassCallable()) { + return []; + } + + $funcCall = $node->expr; + if (!$funcCall->name instanceof Node\Identifier) { + return []; + } + $methodName = $funcCall->name->toString(); + + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $funcCall->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->mustUseReturnValue()->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..349eeeace7 --- /dev/null +++ b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php @@ -0,0 +1,97 @@ + + */ +#[RegisteredRule(level: 0)] +final class CallToStaticMethodStatementWithNoDiscardRule implements Rule +{ + + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + 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\StaticCall) { + return []; + } + + if ($node->expr->isFirstClassCallable()) { + return []; + } + + $funcCall = $node->expr; + if (!$funcCall->name instanceof Node\Identifier) { + return []; + } + + $methodName = $funcCall->name->toString(); + if ($funcCall->class instanceof Node\Name) { + $className = $scope->resolveName($funcCall->class); + if (!$this->reflectionProvider->hasClass($className)) { + return []; + } + + $calledOnType = new ObjectType($className); + } else { + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $funcCall->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->mustUseReturnValue()->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/src/Rules/Methods/ExistingClassesInTypehintsRule.php b/src/Rules/Methods/ExistingClassesInTypehintsRule.php index 8fb0686000..11781bb2e4 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 %%s method %s::%s().', + $className, + $methodName, + ), ); } diff --git a/src/Rules/Properties/ExistingClassesInPropertyHookTypehintsRule.php b/src/Rules/Properties/ExistingClassesInPropertyHookTypehintsRule.php index 3530ee5769..96f326e17a 100644 --- a/src/Rules/Properties/ExistingClassesInPropertyHookTypehintsRule.php +++ b/src/Rules/Properties/ExistingClassesInPropertyHookTypehintsRule.php @@ -84,6 +84,13 @@ public function processNode(Node $node, Scope $scope): array $className, $propertyName, ), + // Should be impossible, property hooks do not support return types + sprintf( + 'Impossible condition: Attribute NoDiscard cannot be used on void %s hook for property %s::$%s.', + ucfirst($hookName), + $className, + $propertyName, + ), ); } diff --git a/src/Rules/Properties/PropertyHookAttributesRule.php b/src/Rules/Properties/PropertyHookAttributesRule.php index 509fe8f83e..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,12 +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', ); + + 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/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php b/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php index a2575177a5..c834dfd5b1 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 mustUseReturnValue(): TrinaryLogic + { + return $this->methodReflection->mustUseReturnValue(); + } + } 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/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 @@ + + */ +class CallToFunctionStatementWithNoDiscardRuleTest extends RuleTestCase +{ + + 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'], [ + [ + '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, + ], + [ + '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/ExistingClassesInArrowFunctionTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php index b54bf39163..0ca5ca5013 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php @@ -324,4 +324,19 @@ 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'], [ + [ + '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 b01e0f5230..1fa4bd16dc 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php @@ -353,4 +353,19 @@ public function testDeprecatedImplicitlyNullableParameterType(): void ]); } + #[RequiresPhp('>= 8.2')] + public function testNoDiscardVoid(): void + { + $this->analyse([__DIR__ . '/data/closure-typehints-nodiscard.php'], [ + [ + '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 e9ec0e10b9..79beed0ea4 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php @@ -489,4 +489,19 @@ public function testParamClosureThisClasses(): void ]); } + #[RequiresPhp('>= 8.2')] + public function testNoDiscardVoid(): void + { + $this->analyse([__DIR__ . '/data/typehints-nodiscard.php'], [ + [ + '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 new file mode 100644 index 0000000000..b8f7115d68 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/arrow-function-typehints-nodiscard.php @@ -0,0 +1,18 @@ + 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 new file mode 100644 index 0000000000..ca152e2340 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/closure-typehints-nodiscard.php @@ -0,0 +1,13 @@ += 8.1 + +namespace FunctionCallStatementResultDiscarded; + +#[\NoDiscard] +function withSideEffects(): array { + echo __FUNCTION__ . "\n"; + return [1]; +} + +withSideEffects(); + +(void)withSideEffects(); + +foreach (withSideEffects() as $num) { + var_dump($num); +} + +#[\nOdISCArD] +function differentCase(): array { + echo __FUNCTION__ . "\n"; + return [1]; +} + +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(); + +withSideEffects(...); diff --git a/tests/PHPStan/Rules/Functions/data/typehints-nodiscard.php b/tests/PHPStan/Rules/Functions/data/typehints-nodiscard.php new file mode 100644 index 0000000000..554bd8c5c8 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/typehints-nodiscard.php @@ -0,0 +1,11 @@ + + */ +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.', + 20, + ], + [ + 'Call to method MethodCallStatementResultDiscarded\ClassWithInstanceSideEffects::instanceMethod() on a separate line discards return value.', + 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 new file mode 100644 index 0000000000..5c320def63 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRuleTest.php @@ -0,0 +1,38 @@ + + */ +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.', + 19, + ], + [ + 'Call to static method MethodCallStatementResultDiscarded\ClassWithStaticSideEffects::differentCase() on a separate line discards return value.', + 27, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php index 7342558234..ee7e28cb76 100644 --- a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php @@ -599,4 +599,43 @@ 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'], [ + [ + 'Attribute NoDiscard cannot be used on void method TestMethodTypehints\Demo::nothing().', + 8, + ], + [ + '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/method-call-statement-result-discarded.php b/tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php new file mode 100644 index 0000000000..7549ebf390 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/method-call-statement-result-discarded.php @@ -0,0 +1,32 @@ +instanceMethod(); +$o?->instanceMethod(); + +(void)$o->instanceMethod(); +(void)$o?->instanceMethod(); + +foreach ($o->instanceMethod() as $num) { + var_dump($num); +} + +$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 new file mode 100644 index 0000000000..6759cdda31 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/static-method-call-statement-result-discarded.php @@ -0,0 +1,29 @@ += 8.4')] + public function testNoDiscard(): void + { + $this->analyse([__DIR__ . '/data/property-hook-attributes-nodiscard.php'], [ + [ + 'Attribute class NoDiscard cannot be used on property hooks.', + 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 { + + } + } +}