diff --git a/composer.json b/composer.json index b8f6b6e1b63..f07f10f4a45 100644 --- a/composer.json +++ b/composer.json @@ -99,6 +99,7 @@ "tests/debug_functions.php", "rules-tests/Transform/Rector/FuncCall/FuncCallToMethodCallRector/Source/some_view_function.php", "rules-tests/TypeDeclaration/Rector/ClassMethod/ParamTypeByMethodCallTypeRector/Source/FunctionTyped.php", + "rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Source/functions.php", "rules-tests/Php70/Rector/ClassMethod/Php4ConstructorRector/Source/ParentClass.php", "rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Source/functions.php" ] diff --git a/e2e/parallel with space/src/Test.php b/e2e/parallel with space/src/Test.php index 09037ce3e74..58dd6ff4c24 100644 --- a/e2e/parallel with space/src/Test.php +++ b/e2e/parallel with space/src/Test.php @@ -1,5 +1,7 @@ 42; + +$arrowWithParam = fn (int $x): int => $x + 1; + +$arrowString = fn (): string => 'hello'; + +?> +----- + 42; + +$arrowWithParam = fn (int $x): int => $x + 1; + +$arrowString = fn (): string => 'hello'; diff --git a/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/closure_return.php.inc b/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/closure_return.php.inc new file mode 100644 index 00000000000..0eec9312681 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/closure_return.php.inc @@ -0,0 +1,27 @@ + +----- + +----- + +----- + +----- + +----- +add(1, 2); +} + +?> +----- +add(1, 2); +} diff --git a/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/named_arguments.php.inc b/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/named_arguments.php.inc new file mode 100644 index 00000000000..b4657652b67 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/named_arguments.php.inc @@ -0,0 +1,29 @@ + +----- + +----- + +----- +count = 123; +} + +?> +----- +count = 123; +} diff --git a/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/return_object.php.inc b/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/return_object.php.inc new file mode 100644 index 00000000000..e46058dbfec --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/return_object.php.inc @@ -0,0 +1,25 @@ + +----- + 'not an int'; diff --git a/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_attribute_wrong_type.php.inc b/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_attribute_wrong_type.php.inc new file mode 100644 index 00000000000..e0c90008470 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_attribute_wrong_type.php.inc @@ -0,0 +1,10 @@ +count = '123'; +} diff --git a/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_static_property_wrong_type.php.inc b/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_static_property_wrong_type.php.inc new file mode 100644 index 00000000000..ea1d33d2366 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_static_property_wrong_type.php.inc @@ -0,0 +1,10 @@ + +----- + +----- + +----- + +----- + +----- + +----- + +----- +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Source/AnotherClass.php b/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Source/AnotherClass.php new file mode 100644 index 00000000000..1b24c81cc21 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Source/AnotherClass.php @@ -0,0 +1,12 @@ +name; + } + + public function process(self $other): void + { + } + + public static function format(string $input): string + { + return trim($input); + } +} diff --git a/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Source/functions.php b/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Source/functions.php new file mode 100644 index 00000000000..2a05a34cc2c --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Source/functions.php @@ -0,0 +1,34 @@ +withRules([SafeDeclareStrictTypesRector::class]); diff --git a/rules/TypeDeclaration/NodeAnalyzer/StrictTypeSafetyChecker.php b/rules/TypeDeclaration/NodeAnalyzer/StrictTypeSafetyChecker.php new file mode 100644 index 00000000000..66f1e83a177 --- /dev/null +++ b/rules/TypeDeclaration/NodeAnalyzer/StrictTypeSafetyChecker.php @@ -0,0 +1,202 @@ +betterNodeFinder->findInstanceOf($fileNode->stmts, CallLike::class); + foreach ($callLikes as $callLike) { + if (! $this->isCallLikeSafe($callLike)) { + return false; + } + } + + $attributes = $this->betterNodeFinder->findInstanceOf($fileNode->stmts, Attribute::class); + foreach ($attributes as $attribute) { + if (! $this->isAttributeSafe($attribute)) { + return false; + } + } + + $functionLikes = $this->betterNodeFinder->findInstanceOf($fileNode->stmts, FunctionLike::class); + foreach ($functionLikes as $functionLike) { + if (! $this->areFunctionReturnsTypeSafe($functionLike)) { + return false; + } + } + + $assigns = $this->betterNodeFinder->findInstanceOf($fileNode->stmts, Assign::class); + foreach ($assigns as $assign) { + if (! $this->isPropertyAssignSafe($assign)) { + return false; + } + } + + return true; + } + + private function isCallLikeSafe(CallLike $callLike): bool + { + if ($callLike->isFirstClassCallable()) { + return true; + } + + $reflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($callLike); + if (! $reflection instanceof FunctionReflection && ! $reflection instanceof MethodReflection) { + return false; + } + + $scope = ScopeFetcher::fetch($callLike); + $parameters = ParametersAcceptorSelectorVariantsWrapper::select($reflection, $callLike, $scope) + ->getParameters(); + + return $this->areArgsSafe($callLike->getArgs(), $parameters); + } + + private function isAttributeSafe(Attribute $attribute): bool + { + $reflection = $this->reflectionResolver->resolveConstructorReflectionFromAttribute($attribute); + if (! $reflection instanceof MethodReflection) { + return false; + } + + $parameters = ParametersAcceptorSelector::combineAcceptors($reflection->getVariants())->getParameters(); + + return $this->areArgsSafe($attribute->args, $parameters); + } + + /** + * @param Arg[] $args + * @param ParameterReflection[] $parameters + */ + private function areArgsSafe(array $args, array $parameters): bool + { + foreach ($args as $position => $arg) { + if ($arg->unpack) { + return false; + } + + $parameterReflection = null; + + if ($arg->name !== null) { + foreach ($parameters as $parameter) { + if ($parameter->getName() === $arg->name->name) { + $parameterReflection = $parameter; + break; + } + } + } elseif (isset($parameters[$position])) { + $parameterReflection = $parameters[$position]; + } else { + $lastParameter = end($parameters); + if ($lastParameter !== false && $lastParameter->isVariadic()) { + $parameterReflection = $lastParameter; + } + } + + if ($parameterReflection === null) { + return false; + } + + $parameterType = $parameterReflection->getType(); + $argType = $this->nodeTypeResolver->getNativeType($arg->value); + + if (! $this->isTypeSafeForStrictMode($parameterType, $argType)) { + return false; + } + } + + return true; + } + + private function areFunctionReturnsTypeSafe(FunctionLike $functionLike): bool + { + if ($functionLike->getReturnType() === null) { + return true; + } + + $declaredReturnType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($functionLike->getReturnType()); + + if ( + $declaredReturnType instanceof MixedType + || $declaredReturnType instanceof VoidType + || $declaredReturnType instanceof NeverType + ) { + return true; + } + + $returns = $this->betterNodeFinder->findReturnsScoped($functionLike); + + foreach ($returns as $return) { + if ($return->expr === null) { + continue; + } + + $returnExprType = $this->nodeTypeResolver->getNativeType($return->expr); + + if (! $this->isTypeSafeForStrictMode($declaredReturnType, $returnExprType)) { + return false; + } + } + + return true; + } + + private function isPropertyAssignSafe(Assign $assign): bool + { + if (! $assign->var instanceof PropertyFetch && ! $assign->var instanceof StaticPropertyFetch) { + return true; + } + + $propertyReflection = $this->reflectionResolver->resolvePropertyReflectionFromPropertyFetch($assign->var); + if ($propertyReflection === null) { + return false; + } + + $propertyType = $propertyReflection->getNativeType(); + $assignedType = $this->nodeTypeResolver->getNativeType($assign->expr); + + return $this->isTypeSafeForStrictMode($propertyType, $assignedType); + } + + private function isTypeSafeForStrictMode(Type $declaredType, Type $valueType): bool + { + return $declaredType->accepts($valueType, strictTypes: true) + ->yes(); + } +} diff --git a/rules/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector.php b/rules/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector.php new file mode 100644 index 00000000000..bb64e7469ef --- /dev/null +++ b/rules/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector.php @@ -0,0 +1,89 @@ +declareStrictTypeFinder->hasDeclareStrictTypes($node)) { + return null; + } + + if (! $this->strictTypeSafetyChecker->isFileStrictTypeSafe($node)) { + return null; + } + + $declaresStrictType = $this->nodeFactory->createDeclaresStrictType(); + $node->stmts = array_merge([$declaresStrictType, new Nop()], $node->stmts); + + return $node; + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [FileNode::class]; + } + + public function provideMinPhpVersion(): int + { + return PhpVersion::PHP_70; + } +} diff --git a/src/Config/Level/CodeQualityLevel.php b/src/Config/Level/CodeQualityLevel.php index b2aa1c8a9e6..699d823676a 100644 --- a/src/Config/Level/CodeQualityLevel.php +++ b/src/Config/Level/CodeQualityLevel.php @@ -84,6 +84,7 @@ use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector; use Rector\Renaming\Rector\FuncCall\RenameFunctionRector; use Rector\Strict\Rector\Empty_\DisallowedEmptyRuleFixerRector; +use Rector\TypeDeclaration\Rector\StmtsAwareInterface\SafeDeclareStrictTypesRector; /** * Key 0 = level 0 @@ -183,6 +184,7 @@ final class CodeQualityLevel SortCallLikeNamedArgsRector::class, SortAttributeNamedArgsRector::class, RemoveReadonlyPropertyVisibilityOnReadonlyClassRector::class, + SafeDeclareStrictTypesRector::class, ]; /**