diff --git a/CHANGELOG.md b/CHANGELOG.md index 8af98eb37..74f44f037 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ release-by-release. ## [Unreleased] +### Added + +- **`AddSymfonyConstraintValidatorTypeDeclarationsRector`** — adds the Symfony 8 / Drupal 12 type declarations (`mixed $value` and `: void` on `validate()`, `: void` on `initialize()`) to `Symfony\Component\Validator\ConstraintValidatorInterface` implementers. Backward compatible on all supported Drupal versions, so no version gate. [#3600790] + ## [1.0.0] — 2026-07-02 ### Changed diff --git a/config/drupal-11/drupal-11.0-deprecations.php b/config/drupal-11/drupal-11.0-deprecations.php index a4d1e3ce7..97d3f2864 100644 --- a/config/drupal-11/drupal-11.0-deprecations.php +++ b/config/drupal-11/drupal-11.0-deprecations.php @@ -7,6 +7,7 @@ use DrupalRector\Drupal11\Rector\Deprecation\MigrateSqlGetMigrationPluginManagerRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveStateCacheSettingRector; use DrupalRector\Drupal11\Rector\Deprecation\StripMigrationDependenciesExpandArgRector; +use DrupalRector\Drupal12\Rector\Deprecation\AddSymfonyConstraintValidatorTypeDeclarationsRector; use DrupalRector\Rector\PHPUnit\PhpUnitTestAnnotationToAttributeRector; use DrupalRector\Rector\PHPUnit\ValueObject\PhpUnitTestAnnotationToAttributeConfiguration; use DrupalRector\Rector\ValueObject\DrupalIntroducedVersionConfiguration; @@ -59,4 +60,12 @@ new PhpUnitTestAnnotationToAttributeConfiguration('11.0.0', '12.0.0', 'depends', 'PHPUnit\Framework\Attributes\Depends'), new PhpUnitTestAnnotationToAttributeConfiguration('11.0.0', '12.0.0', 'testWith', 'PHPUnit\Framework\Attributes\TestWith'), ]); + + // Forward-compat Symfony 8 / Drupal 12 signature change (NOT an 11.0 + // deprecation). Registered here so it fires on drupal/core ^11.0 (every D11 + // install) — the change is backward compatible, so applying it while still + // on D11 is safe and prepares the module for D12. Also registered in the + // drupal-12.0 set for installs already on ^12.0. + // https://git.drupalcode.org/project/redirect/-/merge_requests/200 + $rectorConfig->rule(AddSymfonyConstraintValidatorTypeDeclarationsRector::class); }; diff --git a/config/drupal-12/drupal-12-all-deprecations.php b/config/drupal-12/drupal-12-all-deprecations.php new file mode 100644 index 000000000..8f60c677e --- /dev/null +++ b/config/drupal-12/drupal-12-all-deprecations.php @@ -0,0 +1,16 @@ +sets([ + Drupal12SetList::DRUPAL_120, + ]); + + $rectorConfig->bootstrapFiles([ + __DIR__.'/../drupal-phpunit-bootstrap-file.php', + ]); +}; diff --git a/config/drupal-12/drupal-12.0-deprecations.php b/config/drupal-12/drupal-12.0-deprecations.php new file mode 100644 index 000000000..f97c86108 --- /dev/null +++ b/config/drupal-12/drupal-12.0-deprecations.php @@ -0,0 +1,15 @@ +rule(AddSymfonyConstraintValidatorTypeDeclarationsRector::class); +}; diff --git a/src/Drupal12/Rector/Deprecation/AddSymfonyConstraintValidatorTypeDeclarationsRector.php b/src/Drupal12/Rector/Deprecation/AddSymfonyConstraintValidatorTypeDeclarationsRector.php new file mode 100644 index 000000000..dac3164d5 --- /dev/null +++ b/src/Drupal12/Rector/Deprecation/AddSymfonyConstraintValidatorTypeDeclarationsRector.php @@ -0,0 +1,127 @@ +> */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + if (!$this->isObjectType($node, new ObjectType(self::CONSTRAINT_VALIDATOR_INTERFACE))) { + return null; + } + + $changed = false; + + $validate = $node->getMethod('validate'); + if ($validate instanceof ClassMethod) { + if ($this->addMixedFirstParam($validate)) { + $changed = true; + } + if ($this->addVoidReturnType($validate)) { + $changed = true; + } + } + + $initialize = $node->getMethod('initialize'); + if ($initialize instanceof ClassMethod) { + if ($this->addVoidReturnType($initialize)) { + $changed = true; + } + } + + return $changed ? $node : null; + } + + private function addVoidReturnType(ClassMethod $method): bool + { + if ($method->returnType !== null) { + return false; + } + + $method->returnType = new Identifier('void'); + + return true; + } + + private function addMixedFirstParam(ClassMethod $method): bool + { + if (!isset($method->params[0]) || $method->params[0]->type !== null) { + return false; + } + + $method->params[0]->type = new Identifier('mixed'); + + return true; + } +} diff --git a/src/Set/Drupal12SetList.php b/src/Set/Drupal12SetList.php new file mode 100644 index 000000000..f47d7768e --- /dev/null +++ b/src/Set/Drupal12SetList.php @@ -0,0 +1,11 @@ + Drupal11SetList::DRUPAL_112, '11.3' => Drupal11SetList::DRUPAL_113, '11.4' => Drupal11SetList::DRUPAL_114, + '12.0' => Drupal12SetList::DRUPAL_120, ]; /** @@ -74,7 +75,7 @@ final class DrupalSetProvider implements SetProviderInterface * * @var string[] */ - private const MAJOR_FLOORS = ['10.0', '11.0']; + private const MAJOR_FLOORS = ['10.0', '11.0', '12.0']; /** * @return SetInterface[] diff --git a/stubs/Symfony/Component/Validator/Constraint.php b/stubs/Symfony/Component/Validator/Constraint.php new file mode 100644 index 000000000..de7ed93ac --- /dev/null +++ b/stubs/Symfony/Component/Validator/Constraint.php @@ -0,0 +1,13 @@ +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/tests/src/Drupal12/Rector/Deprecation/AddSymfonyConstraintValidatorTypeDeclarationsRector/config/configured_rule.php b/tests/src/Drupal12/Rector/Deprecation/AddSymfonyConstraintValidatorTypeDeclarationsRector/config/configured_rule.php new file mode 100644 index 000000000..97029ca02 --- /dev/null +++ b/tests/src/Drupal12/Rector/Deprecation/AddSymfonyConstraintValidatorTypeDeclarationsRector/config/configured_rule.php @@ -0,0 +1,11 @@ + diff --git a/tests/src/Drupal12/Rector/Deprecation/AddSymfonyConstraintValidatorTypeDeclarationsRector/fixture/constraint_only.php.inc b/tests/src/Drupal12/Rector/Deprecation/AddSymfonyConstraintValidatorTypeDeclarationsRector/fixture/constraint_only.php.inc new file mode 100644 index 000000000..1a2e1d8b0 --- /dev/null +++ b/tests/src/Drupal12/Rector/Deprecation/AddSymfonyConstraintValidatorTypeDeclarationsRector/fixture/constraint_only.php.inc @@ -0,0 +1,13 @@ + diff --git a/tests/src/Drupal12/Rector/Deprecation/AddSymfonyConstraintValidatorTypeDeclarationsRector/fixture/constraint_validator.php.inc b/tests/src/Drupal12/Rector/Deprecation/AddSymfonyConstraintValidatorTypeDeclarationsRector/fixture/constraint_validator.php.inc new file mode 100644 index 000000000..04f79a3c5 --- /dev/null +++ b/tests/src/Drupal12/Rector/Deprecation/AddSymfonyConstraintValidatorTypeDeclarationsRector/fixture/constraint_validator.php.inc @@ -0,0 +1,29 @@ + +----- + diff --git a/tests/src/Drupal12/Rector/Deprecation/AddSymfonyConstraintValidatorTypeDeclarationsRector/fixture/not_a_validator.php.inc b/tests/src/Drupal12/Rector/Deprecation/AddSymfonyConstraintValidatorTypeDeclarationsRector/fixture/not_a_validator.php.inc new file mode 100644 index 000000000..fc8076f7a --- /dev/null +++ b/tests/src/Drupal12/Rector/Deprecation/AddSymfonyConstraintValidatorTypeDeclarationsRector/fixture/not_a_validator.php.inc @@ -0,0 +1,11 @@ + diff --git a/tests/src/Drupal12/Rector/Deprecation/AddSymfonyConstraintValidatorTypeDeclarationsRector/fixture/self_validating_constraint.php.inc b/tests/src/Drupal12/Rector/Deprecation/AddSymfonyConstraintValidatorTypeDeclarationsRector/fixture/self_validating_constraint.php.inc new file mode 100644 index 000000000..a0f6587a1 --- /dev/null +++ b/tests/src/Drupal12/Rector/Deprecation/AddSymfonyConstraintValidatorTypeDeclarationsRector/fixture/self_validating_constraint.php.inc @@ -0,0 +1,37 @@ + +----- + diff --git a/tests/src/Set/DrupalSetProviderTest.php b/tests/src/Set/DrupalSetProviderTest.php index b95145bd1..2bec774f6 100644 --- a/tests/src/Set/DrupalSetProviderTest.php +++ b/tests/src/Set/DrupalSetProviderTest.php @@ -90,8 +90,11 @@ public static function installedVersionProvider(): array 'drupal-11.4-breaking.php', 'drupal-bootstrap.php', ]], - // A future major out of range matches nothing. - '12.0.0 matches nothing' => ['12.0.0', []], + // The next major's floor loads only its own set + bootstrap. + '12.0.0 loads only 12.0' => ['12.0.0', [ + 'drupal-12.0-deprecations.php', + 'drupal-bootstrap.php', + ]], ]; }