From 4c12eab8adc786e5381cfde95176f40554fe6930 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 23 Dec 2022 13:56:15 +0100 Subject: [PATCH 1/2] EnforceNativeReturnTypehintRule: try narrowing enforcement --- src/Rule/EnforceNativeReturnTypehintRule.php | 138 +++++++++++++----- .../EnforceNativeReturnTypehintRuleTest.php | 1 + .../code-81.php | 82 ++++++++++- 3 files changed, 181 insertions(+), 40 deletions(-) diff --git a/src/Rule/EnforceNativeReturnTypehintRule.php b/src/Rule/EnforceNativeReturnTypehintRule.php index 33a255e..6df8518 100644 --- a/src/Rule/EnforceNativeReturnTypehintRule.php +++ b/src/Rule/EnforceNativeReturnTypehintRule.php @@ -3,10 +3,13 @@ namespace ShipMonk\PHPStan\Rule; use Generator; +use Grpc\Call; use LogicException; use PhpParser\Node; +use PhpParser\Node\Stmt\Function_; use PhpParser\Node\Stmt\Throw_; use PHPStan\Analyser\Scope; +use PHPStan\Node\ClassMethod; use PHPStan\Node\ClosureReturnStatementsNode; use PHPStan\Node\FunctionReturnStatementsNode; use PHPStan\Node\MethodReturnStatementsNode; @@ -23,6 +26,7 @@ use PHPStan\Type\IterableType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StaticType; @@ -31,10 +35,12 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use PHPStan\Type\VoidType; +use function _PHPStan_532094bc1\RingCentral\Psr7\str; use function count; +use ReflectionClass; use function implode; use function in_array; -use function sprintf; /** * @implements Rule @@ -48,15 +54,19 @@ class EnforceNativeReturnTypehintRule implements Rule private bool $treatPhpDocTypesAsCertain; + private bool $enforceNarrowestTypehint; + public function __construct( FileTypeMapper $fileTypeMapper, PhpVersion $phpVersion, - bool $treatPhpDocTypesAsCertain + bool $treatPhpDocTypesAsCertain, + bool $enforceNarrowestTypehint = true ) { $this->fileTypeMapper = $fileTypeMapper; $this->phpVersion = $phpVersion; $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; + $this->enforceNarrowestTypehint = $enforceNarrowestTypehint; } public function getNodeType(): string @@ -74,10 +84,6 @@ public function processNode(Node $node, Scope $scope): array return []; } - if ($this->hasNativeReturnTypehint($node)) { - return []; - } - if (!$scope->isInAnonymousFunction() && in_array($scope->getFunctionName(), ['__construct', '__destruct', '__clone'], true)) { return []; } @@ -86,6 +92,12 @@ public function processNode(Node $node, Scope $scope): array return []; // return may easily differ for each usage } + $hasNativeReturnType = $this->hasNativeReturnTypehint($node); + + if ($hasNativeReturnType && !$this->enforceNarrowestTypehint) { + return []; + } + $phpDocReturnType = $this->getPhpDocReturnType($node, $scope); $returnType = $phpDocReturnType ?? $this->getTypeOfReturnStatements($node); $alwaysThrows = $this->alwaysThrowsException($node); @@ -96,9 +108,20 @@ public function processNode(Node $node, Scope $scope): array return []; } - return [ - sprintf('Missing native return typehint %s', $typeHint), - ]; + if (!$hasNativeReturnType) { + return ["Missing native return typehint {$this->toTypehint($typeHint)}"]; + } + + if ($this->enforceNarrowestTypehint) { + $nativeReturnType = $this->getNativeReturnTypehint($node, $scope); + $typeHintFromNativeTypehint = $this->getTypehintByType($nativeReturnType, $scope, $phpDocReturnType !== null, $alwaysThrows, true); + + if ($typeHintFromNativeTypehint !== null && $typeHintFromNativeTypehint->isSuperTypeOf($typeHint)->yes() && !$typeHintFromNativeTypehint->equals($typeHint)) { + return ["Native return typehint is {$this->toTypehint($typeHintFromNativeTypehint)}, but can be narrowed to {$this->toTypehint($typeHint)}"]; + } + } + + return []; } private function getTypehintByType( @@ -107,27 +130,27 @@ private function getTypehintByType( bool $typeFromPhpDoc, bool $alwaysThrowsException, bool $topLevel - ): ?string + ): ?Type { if ($type instanceof MixedType) { - return $this->phpVersion->getVersionId() >= 80_000 ? 'mixed' : null; + return $this->phpVersion->getVersionId() >= 80_000 ? new MixedType() : null; } if ($type->isVoid()->yes()) { - return 'void'; + return new VoidType(); } if ($type instanceof NeverType) { if (($typeFromPhpDoc || $alwaysThrowsException) && $this->phpVersion->getVersionId() >= 80_100) { - return 'never'; + return new NeverType(); } - return 'void'; + return new VoidType(); } if ($type->isNull()->yes()) { if (!$topLevel || $this->phpVersion->getVersionId() >= 80_200) { - return 'null'; + return new NullType(); } return null; @@ -140,44 +163,44 @@ private function getTypehintByType( if (($typeWithoutNull->isTrue()->yes() || $typeWithoutNull->isFalse()->yes()) && $this->phpVersion->getVersionId() >= 80_200) { $typeHint = $typeWithoutNull->describe(VerbosityLevel::typeOnly()); } else { - $typeHint = 'bool'; + $typeHint = new BooleanType(); } } elseif ((new IntegerType())->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) { - $typeHint = 'int'; + $typeHint = new IntegerType(); } elseif ((new FloatType())->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) { - $typeHint = 'float'; + $typeHint = new FloatType(); } elseif ((new ArrayType(new MixedType(), new MixedType()))->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) { - $typeHint = 'array'; + $typeHint = new ArrayType(new MixedType(), new MixedType()); } elseif ((new StringType())->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) { - $typeHint = 'string'; + $typeHint = new StringType(); } elseif ($typeWithoutNull instanceof StaticType) { if ($this->phpVersion->getVersionId() < 80_000) { - $typeHint = 'self'; + $typeHint = new StaticType($typeWithoutNull->getClassReflection()); // TODO self } else { - $typeHint = 'static'; + $typeHint = new StaticType($typeWithoutNull->getClassReflection()); // TODO static } } elseif (count($typeWithoutNull->getObjectClassNames()) === 1) { $className = $typeWithoutNull->getObjectClassNames()[0]; if ($className === $this->getClassName($scope)) { - $typeHint = 'self'; + $typeHint = new ObjectType('\\' . $className); // TODO self } else { - $typeHint = '\\' . $className; + $typeHint = new ObjectType('\\' . $className); } } elseif ((new CallableType())->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) { - $typeHint = 'callable'; + $typeHint = new CallableType(); } elseif ((new IterableType(new MixedType(), new MixedType()))->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) { - $typeHint = 'iterable'; + $typeHint = new IterableType(new MixedType(), new MixedType()); } elseif ($this->getUnionTypehint($type, $scope, $typeFromPhpDoc, $alwaysThrowsException) !== null) { return $this->getUnionTypehint($type, $scope, $typeFromPhpDoc, $alwaysThrowsException); } elseif ($this->getIntersectionTypehint($type, $scope, $typeFromPhpDoc, $alwaysThrowsException) !== null) { return $this->getIntersectionTypehint($type, $scope, $typeFromPhpDoc, $alwaysThrowsException); } elseif ((new ObjectWithoutClassType())->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) { - $typeHint = 'object'; + $typeHint = new ObjectWithoutClassType(); } if ($typeHint !== null && TypeCombinator::containsNull($type)) { - $typeHint = '?' . $typeHint; + $typeHint = TypeCombinator::addNull($typeHint); } return $typeHint; @@ -202,6 +225,37 @@ private function getTypeOfReturnStatements(ReturnStatementsNode $node): Type return TypeCombinator::union(...$types); } + private function getNativeReturnTypehint(ReturnStatementsNode $node, Scope $scope): Type + { + $reflection = new ReflectionClass($node); + + if ($node instanceof MethodReturnStatementsNode) { // @phpstan-ignore-line ignore bc warning + $classMethodReflection = $reflection->getProperty('classMethod'); + $classMethodReflection->setAccessible(true); + /** @var ClassMethod $classMethod */ + $classMethod = $classMethodReflection->getValue($node); + return $scope->getFunctionType($classMethod->returnType, $classMethod->returnType === null, false); + } + + if ($node instanceof FunctionReturnStatementsNode) { // @phpstan-ignore-line ignore bc warning + $functionReflection = $reflection->getProperty('function'); + $functionReflection->setAccessible(true); + /** @var Function_ $function */ + $function = $functionReflection->getValue($node); + return $scope->getFunctionType($function->returnType, $function->returnType === null, false); + } + + if ($node instanceof ClosureReturnStatementsNode) { // @phpstan-ignore-line ignore bc warning + $closureReflection = $reflection->getProperty('closureExpr'); + $closureReflection->setAccessible(true); + /** @var Closure $closure */ + $closure = $closureReflection->getValue($node); + return $scope->getFunctionType($closure->returnType, $closure->returnType === null, false); + } + + throw new LogicException('Unexpected subtype'); + } + /** * To be removed once we bump phpstan version to 1.9.5+ (https://github.com/phpstan/phpstan-src/pull/2141) */ @@ -261,7 +315,7 @@ private function getUnionTypehint( Scope $scope, bool $typeFromPhpDoc, bool $alwaysThrowsException - ): ?string + ): ?Type { if (!$type instanceof UnionType) { return null; @@ -294,10 +348,10 @@ private function getUnionTypehint( continue; } - $typehintParts[] = $wrap ? "($subtypeHint)" : $subtypeHint; + $typehintParts[] = $subtypeHint; } - return implode('|', $typehintParts); + return new UnionType($typehintParts); } private function getIntersectionTypehint( @@ -305,7 +359,7 @@ private function getIntersectionTypehint( Scope $scope, bool $typeFromPhpDoc, bool $alwaysThrowsException - ): ?string + ): ?Type { if (!$type instanceof IntersectionType) { // @phpstan-ignore-line ignore instanceof intersection return null; @@ -338,10 +392,10 @@ private function getIntersectionTypehint( continue; } - $typehintParts[] = $wrap ? "($subtypeHint)" : $subtypeHint; + $typehintParts[] = $subtypeHint; } - return implode('&', $typehintParts); + return new IntersectionType($typehintParts); } private function alwaysThrowsException(ReturnStatementsNode $node): bool @@ -357,4 +411,20 @@ private function alwaysThrowsException(ReturnStatementsNode $node): bool return $exitPoints !== []; } + private function toTypehint(Type $type): string + { + if (TypeCombinator::containsNull($type) && $type instanceof UnionType && count($type->getTypes()) === 2) { + $typeWithoutNull = TypeCombinator::removeNull($type); + return '?' . $typeWithoutNull->toPhpDocNode(); + } + + $typeHint = str_replace(' ', '', (string) $type->toPhpDocNode()); + + if ($typeHint[0] === '(' && $typeHint[strlen($typeHint) - 1] === ')') { + return substr($typeHint, 1, strlen($typeHint) - 2); + } + + return $typeHint; + } + } diff --git a/tests/Rule/EnforceNativeReturnTypehintRuleTest.php b/tests/Rule/EnforceNativeReturnTypehintRuleTest.php index 2f5eb6b..ffac915 100644 --- a/tests/Rule/EnforceNativeReturnTypehintRuleTest.php +++ b/tests/Rule/EnforceNativeReturnTypehintRuleTest.php @@ -24,6 +24,7 @@ protected function getRule(): Rule self::getContainer()->getByType(FileTypeMapper::class), $this->phpVersion, true, + true, ); } diff --git a/tests/Rule/data/EnforceNativeReturnTypehintRule/code-81.php b/tests/Rule/data/EnforceNativeReturnTypehintRule/code-81.php index a8613ec..52c33c4 100644 --- a/tests/Rule/data/EnforceNativeReturnTypehintRule/code-81.php +++ b/tests/Rule/data/EnforceNativeReturnTypehintRule/code-81.php @@ -18,14 +18,11 @@ class DeductFromPhpDocs { public function doNotReportWithTypehint1(): array {} /** @return int */ - public function doNotReportWithTypehint2(): never {} + public function doNotReportWithTypehint2(): int {} - /** @return int */ + /** @return mixed */ public function doNotReportWithTypehint3(): mixed {} - /** @return float */ - public function doNotReportWithTypehint4(): int {} - /** @return list */ public function requireArray() {} // error: Missing native return typehint array @@ -84,7 +81,7 @@ public function requireMixed4() {} // error: Missing native return typehint mixe public function requireVoid() {} // error: Missing native return typehint void /** @return null */ - public function requireNullVoid() {} + public function requireNull() {} /** @return never */ public function requireNever() {} // error: Missing native return typehint never @@ -227,6 +224,79 @@ function () { // error: Missing native return typehint static } +class EnforceNarrowerTypehint { + + public function requireIterableObject(): iterable // error: Native return typehint is iterable, but can be narrowed to \ArrayObject + { + return new \ArrayObject(); + } + + public function requireCallableObject(): callable // error: Native return typehint is callable, but can be narrowed to \EnforceNativeReturnTypehintRule81\CallableObject + { + return new CallableObject(); + } + + public function requireString(): mixed // error: Native return typehint is mixed, but can be narrowed to string + { + return self::class; + } + + public function requireClosure(): callable // error: Native return typehint is callable, but can be narrowed to \Closure + { + return function (): int { + return 1; + }; + } + + public function requireChild(): \Throwable // error: Native return typehint is \Throwable, but can be narrowed to \LogicException + { + return new \LogicException(); + } + + /** + * @return \LogicException|\RuntimeException + */ + public function requireUnion(): object // error: Native return typehint is object, but can be narrowed to \LogicException|\RuntimeException + { + + } + + /** + * @return \LogicException|\RuntimeException + */ + public function requireUnion2(): \Throwable // error: Native return typehint is \Throwable, but can be narrowed to \LogicException|\RuntimeException + { + + } + + public function requireNever(): void // error: Native return typehint is void, but can be narrowed to never + { + throw new \LogicException(); + } + + public function returnThis(): self // error: Native return typehint is self, but can be narrowed to static + { + return $this; + } + + + public function requireNullableString2(?string $out): mixed // error: Native return typehint is mixed, but can be narrowed to ?string + { + return $out; + } + + public function dontRequireMixed(mixed $out): string + { + return $out; + } + + public function dontRequireWiderType(mixed $out): string + { + return $out; + } + +} + /** @return int */ function functionWithPhpDoc() { // error: Missing native return typehint int From 53c245506c5d1825695d5e3124054fc17135c483 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 2 Jun 2023 17:58:19 +0200 Subject: [PATCH 2/2] Some fixes and simplifications --- src/Rule/EnforceNativeReturnTypehintRule.php | 73 +++++++++-------- .../code-81.php | 82 ++++++++++++++----- .../code-82.php | 7 ++ .../code-enum.php | 3 +- 4 files changed, 107 insertions(+), 58 deletions(-) diff --git a/src/Rule/EnforceNativeReturnTypehintRule.php b/src/Rule/EnforceNativeReturnTypehintRule.php index 6df8518..8d7ed0b 100644 --- a/src/Rule/EnforceNativeReturnTypehintRule.php +++ b/src/Rule/EnforceNativeReturnTypehintRule.php @@ -3,7 +3,6 @@ namespace ShipMonk\PHPStan\Rule; use Generator; -use Grpc\Call; use LogicException; use PhpParser\Node; use PhpParser\Node\Stmt\Function_; @@ -19,6 +18,7 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\CallableType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; @@ -29,18 +29,17 @@ use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\StaticType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; -use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; -use function _PHPStan_532094bc1\RingCentral\Psr7\str; -use function count; use ReflectionClass; -use function implode; +use function count; use function in_array; +use function str_replace; +use function strlen; +use function substr; /** * @implements Rule @@ -113,11 +112,19 @@ public function processNode(Node $node, Scope $scope): array } if ($this->enforceNarrowestTypehint) { + $actualReturnType = $this->getTypeOfReturnStatements($node); $nativeReturnType = $this->getNativeReturnTypehint($node, $scope); + + $typeHintFromActualReturns = $this->getTypehintByType($actualReturnType, $scope, $phpDocReturnType !== null, $alwaysThrows, true); $typeHintFromNativeTypehint = $this->getTypehintByType($nativeReturnType, $scope, $phpDocReturnType !== null, $alwaysThrows, true); - if ($typeHintFromNativeTypehint !== null && $typeHintFromNativeTypehint->isSuperTypeOf($typeHint)->yes() && !$typeHintFromNativeTypehint->equals($typeHint)) { - return ["Native return typehint is {$this->toTypehint($typeHintFromNativeTypehint)}, but can be narrowed to {$this->toTypehint($typeHint)}"]; + if ( + $typeHintFromActualReturns !== null + && $typeHintFromNativeTypehint !== null + && $typeHintFromNativeTypehint->isSuperTypeOf($typeHintFromActualReturns)->yes() + && !$typeHintFromNativeTypehint->equals($typeHintFromActualReturns) + ) { + return ["Native return typehint is {$this->toTypehint($typeHintFromNativeTypehint)}, but can be narrowed to {$this->toTypehint($typeHintFromActualReturns)}"]; } } @@ -160,41 +167,43 @@ private function getTypehintByType( $typeHint = null; if ((new BooleanType())->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) { - if (($typeWithoutNull->isTrue()->yes() || $typeWithoutNull->isFalse()->yes()) && $this->phpVersion->getVersionId() >= 80_200) { - $typeHint = $typeWithoutNull->describe(VerbosityLevel::typeOnly()); + $supportsStandaloneTrue = $this->phpVersion->getVersionId() >= 80_200; + + if ($supportsStandaloneTrue && $typeWithoutNull->isTrue()) { + $typeHint = new ConstantBooleanType(true); + } elseif ($supportsStandaloneTrue && $typeWithoutNull->isFalse()) { + $typeHint = new ConstantBooleanType(false); } else { $typeHint = new BooleanType(); } } elseif ((new IntegerType())->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) { $typeHint = new IntegerType(); + } elseif ((new FloatType())->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) { $typeHint = new FloatType(); + } elseif ((new ArrayType(new MixedType(), new MixedType()))->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) { $typeHint = new ArrayType(new MixedType(), new MixedType()); + } elseif ((new StringType())->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) { $typeHint = new StringType(); - } elseif ($typeWithoutNull instanceof StaticType) { - if ($this->phpVersion->getVersionId() < 80_000) { - $typeHint = new StaticType($typeWithoutNull->getClassReflection()); // TODO self - } else { - $typeHint = new StaticType($typeWithoutNull->getClassReflection()); // TODO static - } + } elseif (count($typeWithoutNull->getObjectClassNames()) === 1) { $className = $typeWithoutNull->getObjectClassNames()[0]; + $typeHint = new ObjectType('\\' . $className); - if ($className === $this->getClassName($scope)) { - $typeHint = new ObjectType('\\' . $className); // TODO self - } else { - $typeHint = new ObjectType('\\' . $className); - } } elseif ((new CallableType())->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) { $typeHint = new CallableType(); + } elseif ((new IterableType(new MixedType(), new MixedType()))->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) { $typeHint = new IterableType(new MixedType(), new MixedType()); + } elseif ($this->getUnionTypehint($type, $scope, $typeFromPhpDoc, $alwaysThrowsException) !== null) { return $this->getUnionTypehint($type, $scope, $typeFromPhpDoc, $alwaysThrowsException); + } elseif ($this->getIntersectionTypehint($type, $scope, $typeFromPhpDoc, $alwaysThrowsException) !== null) { return $this->getIntersectionTypehint($type, $scope, $typeFromPhpDoc, $alwaysThrowsException); + } elseif ((new ObjectWithoutClassType())->accepts($typeWithoutNull, $scope->isDeclareStrictTypes())->yes()) { $typeHint = new ObjectWithoutClassType(); } @@ -328,14 +337,10 @@ private function getUnionTypehint( $typehintParts = []; foreach ($type->getTypes() as $subtype) { - $wrap = false; - if ($subtype instanceof IntersectionType) { // @phpstan-ignore-line ignore instanceof intersection if ($this->phpVersion->getVersionId() < 80_200) { // DNF return null; } - - $wrap = true; } $subtypeHint = $this->getTypehintByType($subtype, $scope, $typeFromPhpDoc, $alwaysThrowsException, false); @@ -351,7 +356,7 @@ private function getUnionTypehint( $typehintParts[] = $subtypeHint; } - return new UnionType($typehintParts); + return TypeCombinator::union(...$typehintParts); } private function getIntersectionTypehint( @@ -372,14 +377,10 @@ private function getIntersectionTypehint( $typehintParts = []; foreach ($type->getTypes() as $subtype) { - $wrap = false; - if ($subtype instanceof UnionType) { if ($this->phpVersion->getVersionId() < 80_200) { // DNF return null; } - - $wrap = true; } $subtypeHint = $this->getTypehintByType($subtype, $scope, $typeFromPhpDoc, $alwaysThrowsException, false); @@ -395,20 +396,22 @@ private function getIntersectionTypehint( $typehintParts[] = $subtypeHint; } - return new IntersectionType($typehintParts); + return TypeCombinator::intersect(...$typehintParts); } private function alwaysThrowsException(ReturnStatementsNode $node): bool { - $exitPoints = $node->getStatementResult()->getExitPoints(); + if (count($node->getReturnStatements()) > 0) { + return false; + } - foreach ($exitPoints as $exitPoint) { - if (!$exitPoint->getStatement() instanceof Throw_) { + foreach ($node->getExecutionEnds() as $executionEnd) { + if (!$executionEnd->getNode() instanceof Throw_) { return false; } } - return $exitPoints !== []; + return $node->getExecutionEnds() !== []; } private function toTypehint(Type $type): string diff --git a/tests/Rule/data/EnforceNativeReturnTypehintRule/code-81.php b/tests/Rule/data/EnforceNativeReturnTypehintRule/code-81.php index 52c33c4..a1acf56 100644 --- a/tests/Rule/data/EnforceNativeReturnTypehintRule/code-81.php +++ b/tests/Rule/data/EnforceNativeReturnTypehintRule/code-81.php @@ -15,13 +15,19 @@ public function __invoke(): void { class DeductFromPhpDocs { /** @return list */ - public function doNotReportWithTypehint1(): array {} + public function doNotReportWithTypehint1(): array { + return []; + } /** @return int */ - public function doNotReportWithTypehint2(): int {} + public function doNotReportWithTypehint2(): int { + return 1; + } /** @return mixed */ - public function doNotReportWithTypehint3(): mixed {} + public function doNotReportWithTypehint3($a): mixed { + return $a; + } /** @return list */ public function requireArray() {} // error: Missing native return typehint array @@ -48,13 +54,13 @@ public function requireIterable() {} // error: Missing native return typehint it public function requireCallable() {} // error: Missing native return typehint callable /** @return string|int */ - public function requireUnionOfScalars() {} // error: Missing native return typehint string|int + public function requireUnionOfScalars() {} // error: Missing native return typehint int|string /** @return string|int|null */ - public function requireUnionOfScalarsWithNull() {} // error: Missing native return typehint string|int|null + public function requireUnionOfScalarsWithNull() {} // error: Missing native return typehint int|string|null /** @return I&J&A */ - public function requireIntersection() {} // error: Missing native return typehint \EnforceNativeReturnTypehintRule81\I&\EnforceNativeReturnTypehintRule81\J&\EnforceNativeReturnTypehintRule81\A + public function requireIntersection() {} // error: Missing native return typehint \EnforceNativeReturnTypehintRule81\A&\EnforceNativeReturnTypehintRule81\I&\EnforceNativeReturnTypehintRule81\J /** @return A|B|int */ public function requireMixedUnion1() {} // error: Missing native return typehint \EnforceNativeReturnTypehintRule81\A|\EnforceNativeReturnTypehintRule81\B|int @@ -102,13 +108,13 @@ public function requireDnf() {} // error: Missing native return typehint object public function requireDnfWithScalarIncluded() {} /** @return static */ - public function returnStatic() {} // error: Missing native return typehint static + public function returnStatic() {} // error: Missing native return typehint \EnforceNativeReturnTypehintRule81\DeductFromPhpDocs /** @return $this */ - public function returnStatic2() {} // error: Missing native return typehint static + public function returnStatic2() {} // error: Missing native return typehint \EnforceNativeReturnTypehintRule81\DeductFromPhpDocs /** @return self */ - public function returnSelf() {} // error: Missing native return typehint self + public function returnSelf() {} // error: Missing native return typehint \EnforceNativeReturnTypehintRule81\DeductFromPhpDocs /** @return \Traversable */ public function returnTraversable() {} // error: Missing native return typehint \Traversable @@ -143,7 +149,7 @@ public function __destruct() } - public function requireUnionOfScalars(bool $bool) // error: Missing native return typehint string|int + public function requireUnionOfScalars(bool $bool) // error: Missing native return typehint int|string { if ($bool) { return ''; @@ -165,12 +171,29 @@ public function requireNever() // error: Missing native return typehint never throw new \LogicException(); } - public function returnNewSelf() // error: Missing native return typehint self + + public function notRequireNever(bool $decide) // error: Missing native return typehint void + { + if ($decide) { + return; + } + + throw new \LogicException(); + } + + public function notRequireNever2(bool $decide) // error: Missing native return typehint void + { + if ($decide) { + throw new \LogicException(); + } + } + + public function returnNewSelf() // error: Missing native return typehint \EnforceNativeReturnTypehintRule81\DeductFromReturnStatements { return new self; } - public function returnThis() // error: Missing native return typehint static + public function returnThis() // error: Missing native return typehint \EnforceNativeReturnTypehintRule81\DeductFromReturnStatements { return $this; } @@ -213,7 +236,7 @@ public function requireString() // error: Missing native return typehint string public function testClosureWithoutReturn(): \Closure { - function () { // error: Missing native return typehint static + function () { // error: Missing native return typehint \EnforceNativeReturnTypehintRule81\DeductFromReturnStatements return $this; }; @@ -253,20 +276,14 @@ public function requireChild(): \Throwable // error: Native return typehint is \ return new \LogicException(); } - /** - * @return \LogicException|\RuntimeException - */ public function requireUnion(): object // error: Native return typehint is object, but can be narrowed to \LogicException|\RuntimeException { - + return rand(0, 1) ? new \LogicException : new \RuntimeException; } - /** - * @return \LogicException|\RuntimeException - */ public function requireUnion2(): \Throwable // error: Native return typehint is \Throwable, but can be narrowed to \LogicException|\RuntimeException { - + return rand(0, 1) ? new \LogicException : new \RuntimeException; } public function requireNever(): void // error: Native return typehint is void, but can be narrowed to never @@ -274,7 +291,28 @@ public function requireNever(): void // error: Native return typehint is void, b throw new \LogicException(); } - public function returnThis(): self // error: Native return typehint is self, but can be narrowed to static + /** + * @return mixed[] + */ + public static function ignorePhpDocWhenNarrowing(): iterable // error: Native return typehint is iterable, but can be narrowed to \Generator + { + yield 1 => 1; + } + + public function returnArrayFalse(int $page): array|bool + { + if ($page === 1) { + return [1, 2, 3, 4]; + } + + if ($page === 2) { + return [5, 6]; + } + + return false; + } + + public function returnThis(): self { return $this; } diff --git a/tests/Rule/data/EnforceNativeReturnTypehintRule/code-82.php b/tests/Rule/data/EnforceNativeReturnTypehintRule/code-82.php index d7a0c08..11bdf28 100644 --- a/tests/Rule/data/EnforceNativeReturnTypehintRule/code-82.php +++ b/tests/Rule/data/EnforceNativeReturnTypehintRule/code-82.php @@ -176,6 +176,13 @@ public function notRequireNever(bool $decide) // error: Missing native return ty throw new \LogicException(); } + public function notRequireNever2(bool $decide) // error: Missing native return typehint void + { + if ($decide) { + throw new \LogicException(); + } + } + public function returnNewSelf() // error: Missing native return typehint self { return new self; diff --git a/tests/Rule/data/EnforceNativeReturnTypehintRule/code-enum.php b/tests/Rule/data/EnforceNativeReturnTypehintRule/code-enum.php index 22690c0..818217d 100644 --- a/tests/Rule/data/EnforceNativeReturnTypehintRule/code-enum.php +++ b/tests/Rule/data/EnforceNativeReturnTypehintRule/code-enum.php @@ -7,8 +7,9 @@ enum MyBoolean: string case True = 'true'; case False = 'false'; - public static function createFromBool(bool $boolValue) // error: Missing native return typehint self + public static function createFromBool(bool $boolValue) // error: Missing native return typehint \EnforceNativeReturnTypehintRule\MyBoolean { return $boolValue ? self::True : self::False; } + }