From c6065897be0c2a957aab6436d08922ff137dd63b Mon Sep 17 00:00:00 2001 From: Caleb White Date: Thu, 8 Jan 2026 06:35:06 -0600 Subject: [PATCH 1/2] feat: create new SafeDeclareStrictTypesRector --- composer.json | 1 + e2e/parallel with space/src/Test.php | 2 + .../Fixture/arrow_function_return.php.inc | 23 ++ .../Fixture/closure_return.php.inc | 27 +++ .../Fixture/constructor_calls.php.inc | 19 ++ .../Fixture/explicit_cast.php.inc | 25 +++ .../Fixture/first_class_callable.php.inc | 19 ++ .../Fixture/int_to_float_parameter.php.inc | 19 ++ .../Fixture/method_calls.php.inc | 25 +++ .../Fixture/named_arguments.php.inc | 29 +++ .../Fixture/nullable_types.php.inc | 27 +++ .../Fixture/object_types.php.inc | 33 +++ .../Fixture/property_assignment.php.inc | 25 +++ .../Fixture/return_object.php.inc | 25 +++ .../skip_arrow_function_wrong_return.php.inc | 5 + .../Fixture/skip_attribute_wrong_type.php.inc | 10 + .../Fixture/skip_closure_wrong_return.php.inc | 7 + .../Fixture/skip_dynamic_call.php.inc | 8 + .../Fixture/skip_int_return_as_string.php.inc | 8 + .../skip_named_argument_wrong_type.php.inc | 12 ++ ...kip_property_assignment_wrong_type.php.inc | 10 + .../skip_static_property_wrong_type.php.inc | 10 + .../skip_string_to_int_coercion.php.inc | 7 + .../skip_union_return_mismatch.php.inc | 8 + .../Fixture/skip_union_type_mismatch.php.inc | 11 + .../Fixture/skip_unpack.php.inc | 13 ++ .../Fixture/skip_variadic_wrong_type.php.inc | 10 + .../Fixture/skip_wrong_object_type.php.inc | 16 ++ .../Fixture/skip_wrong_return_type.php.inc | 11 + .../Fixture/static_calls.php.inc | 19 ++ .../static_property_assignment.php.inc | 25 +++ .../Fixture/typed_calls.php.inc | 19 ++ .../Fixture/union_return_type.php.inc | 27 +++ .../Fixture/union_types.php.inc | 27 +++ .../Fixture/variadic_function.php.inc | 25 +++ .../Fixture/without_namespace.php.inc | 15 ++ .../SafeDeclareStrictTypesRectorTest.php | 28 +++ .../Source/AnotherClass.php | 12 ++ .../Source/TypedAttribute.php | 16 ++ .../Source/TypedClass.php | 37 ++++ .../Source/functions.php | 34 +++ .../config/configured_rule.php | 9 + .../NodeAnalyzer/StrictTypeSafetyChecker.php | 202 ++++++++++++++++++ .../SafeDeclareStrictTypesRector.php | 89 ++++++++ src/Config/Level/CodeQualityLevel.php | 2 + 45 files changed, 1031 insertions(+) create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/arrow_function_return.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/closure_return.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/constructor_calls.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/explicit_cast.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/first_class_callable.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/int_to_float_parameter.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/method_calls.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/named_arguments.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/nullable_types.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/object_types.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/property_assignment.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/return_object.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_arrow_function_wrong_return.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_attribute_wrong_type.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_closure_wrong_return.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_dynamic_call.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_int_return_as_string.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_named_argument_wrong_type.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_property_assignment_wrong_type.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_static_property_wrong_type.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_string_to_int_coercion.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_union_return_mismatch.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_union_type_mismatch.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_unpack.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_variadic_wrong_type.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_wrong_object_type.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/skip_wrong_return_type.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/static_calls.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/static_property_assignment.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/typed_calls.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/union_return_type.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/union_types.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/variadic_function.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/without_namespace.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/SafeDeclareStrictTypesRectorTest.php create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Source/AnotherClass.php create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Source/TypedAttribute.php create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Source/TypedClass.php create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Source/functions.php create mode 100644 rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/config/configured_rule.php create mode 100644 rules/TypeDeclaration/NodeAnalyzer/StrictTypeSafetyChecker.php create mode 100644 rules/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector.php diff --git a/composer.json b/composer.json index 0685b56efa7..0a361dc723e 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 d895def1c81..aae830a863f 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, ]; /** From 6f90d8b4f1779c7d5eba43d601d9afedc4d9cb03 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Mon, 19 Jan 2026 16:22:57 -0600 Subject: [PATCH 2/2] tests: add mixed type example --- .../Fixture/closure_return.php.inc | 8 ++++++++ 1 file changed, 8 insertions(+) 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 index 0eec9312681..8a29c38fab5 100644 --- a/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/closure_return.php.inc +++ b/rules-tests/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector/Fixture/closure_return.php.inc @@ -10,6 +10,10 @@ $closureWithParam = function (int $x): int { return $x + 1; }; +$closureWithMixedReturn = function (int $x): mixed { + return $x + 1; +}; + ?> -----