From 208ff05786ab07614cf9406213a6cb2076527184 Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 25 May 2026 18:40:51 +0200 Subject: [PATCH 01/23] docs: fixup cr/issue links for rules --- config/drupal-11/drupal-11.0-deprecations.php | 4 +- config/drupal-11/drupal-11.1-deprecations.php | 13 +++-- config/drupal-11/drupal-11.2-deprecations.php | 32 +++++++++---- config/drupal-11/drupal-11.3-deprecations.php | 25 +++++++--- config/drupal-11/drupal-11.4-deprecations.php | 47 +++++++++++++++---- 5 files changed, 90 insertions(+), 31 deletions(-) diff --git a/config/drupal-11/drupal-11.0-deprecations.php b/config/drupal-11/drupal-11.0-deprecations.php index 1bfda948d..e85c68df5 100644 --- a/config/drupal-11/drupal-11.0-deprecations.php +++ b/config/drupal-11/drupal-11.0-deprecations.php @@ -17,12 +17,14 @@ $rectorConfig->rule(GetNameToNameRector::class); // https://www.drupal.org/node/3436954 - // https://www.drupal.org/node/2575105 (change record) + // https://www.drupal.org/node/3443018 (change record) + // https://www.drupal.org/node/2575105 (related) // $settings['state_cache'] deprecated in drupal:11.0.0. // State caching is now permanently enabled and the setting has no effect. $rectorConfig->rule(RemoveStateCacheSettingRector::class); // https://www.drupal.org/node/3395986 + // https://www.drupal.org/node/3395991 (change record) // REQUEST_TIME constant deprecated in drupal:8.3.0, removed in drupal:11.0.0. // Replaced by \Drupal::time()->getRequestTime(). $rectorConfig->ruleWithConfiguration(ReplaceRequestTimeConstantRector::class, [ diff --git a/config/drupal-11/drupal-11.1-deprecations.php b/config/drupal-11/drupal-11.1-deprecations.php index f10884043..6b9440a94 100644 --- a/config/drupal-11/drupal-11.1-deprecations.php +++ b/config/drupal-11/drupal-11.1-deprecations.php @@ -19,14 +19,16 @@ return static function (RectorConfig $rectorConfig): void { // https://www.drupal.org/node/3459533 - // https://www.drupal.org/node/2946122 (change record) + // https://www.drupal.org/node/3459535 (change record) + // https://www.drupal.org/node/2946122 (related) // PluginBase::isConfigurable() deprecated in drupal:11.1.0, removed in drupal:12.0.0. // Replaced by instanceof \Drupal\Component\Plugin\ConfigurableInterface. $rectorConfig->ruleWithConfiguration(PluginBaseIsConfigurableRector::class, [ new DrupalIntroducedVersionConfiguration('11.1.0'), ]); - // https://www.drupal.org/node/3467559 + // https://www.drupal.org/node/3151086 + // https://www.drupal.org/node/3467559 (change record) // AliasWhitelist and AliasWhitelistInterface deprecated in drupal:11.1.0, removed in drupal:12.0.0. // Replaced by AliasPrefixList and AliasPrefixListInterface. // AliasManager::pathAliasWhitelistRebuild() deprecated in drupal:11.1.0, removed in drupal:12.0.0. @@ -41,7 +43,8 @@ ]); // https://www.drupal.org/node/3442009 - // https://www.drupal.org/node/3368812 (change record) + // https://www.drupal.org/node/3442349 (change record) + // https://www.drupal.org/node/3368812 (related) // ModuleHandlerInterface::writeCache() deprecated in drupal:11.1.0, removed in drupal:12.0.0. No replacement needed. // ModuleHandlerInterface::getHookInfo() deprecated in drupal:11.1.0, removed in drupal:12.0.0. Replaced by []. $rectorConfig->rule(RemoveModuleHandlerDeprecatedMethodsRector::class); @@ -81,9 +84,11 @@ ]); // https://www.drupal.org/node/3488176 + // https://www.drupal.org/node/3488470 (change record) // drupal_common_theme() removed in drupal:11.1.0. // Replaced by \Drupal\Core\Theme\ThemeCommonElements::commonElements(). - // https://www.drupal.org/node/3268441 + // https://www.drupal.org/node/2350849 + // https://www.drupal.org/node/3268441 (change record) // image_filter_keyword() deprecated in drupal:11.1.0, removed in drupal:12.0.0. // Replaced by \Drupal\Component\Utility\Image::getKeywordOffset(). $rectorConfig->ruleWithConfiguration(FunctionToStaticRector::class, [ diff --git a/config/drupal-11/drupal-11.2-deprecations.php b/config/drupal-11/drupal-11.2-deprecations.php index e8dac77f2..46c94502a 100644 --- a/config/drupal-11/drupal-11.2-deprecations.php +++ b/config/drupal-11/drupal-11.2-deprecations.php @@ -43,14 +43,16 @@ new DrupalIntroducedVersionConfiguration('11.2.0'), ]); - // https://www.drupal.org/node/3500622 + // https://www.drupal.org/node/3498947 + // https://www.drupal.org/node/3500622 (change record) // CacheBackendInterface::invalidateAll() deprecated in drupal:11.2.0, removed in drupal:12.0.0. // Replaced by deleteAll(). $rectorConfig->ruleWithConfiguration(MethodToMethodWithCheckRector::class, [ new MethodToMethodWithCheckConfiguration('Drupal\Core\Cache\CacheBackendInterface', 'invalidateAll', 'deleteAll'), ]); - // https://www.drupal.org/node/3504125 + // https://www.drupal.org/node/3501136 + // https://www.drupal.org/node/3504125 (change record) // template_preprocess_*() functions deprecated in drupal:11.2.0, removed in drupal:12.0.0. // Replaced by ThemePreprocess and DatePreprocess service methods. $rectorConfig->ruleWithConfiguration(FunctionToServiceRector::class, [ @@ -64,8 +66,10 @@ ]); // https://www.drupal.org/node/3501136 + // https://www.drupal.org/node/3504125 (change record) // template_preprocess() deprecated in drupal:11.2.0, removed in drupal:12.0.0. - // https://www.drupal.org/node/3522119 + // https://www.drupal.org/node/3521059 + // https://www.drupal.org/node/3522119 (change record) // update_clear_update_disk_cache(), update_delete_file_if_stale(), // _update_manager_cache_directory(), _update_manager_extract_directory(), // and _update_manager_unique_identifier() deprecated in drupal:11.2.0, removed in drupal:13.0.0. @@ -91,6 +95,7 @@ $rectorConfig->rule(RemoveHandlerBaseDefineExtraOptionsRector::class); // https://www.drupal.org/node/3410938 + // https://www.drupal.org/node/3410939 (change record) // drupal_requirements_severity() deprecated in drupal:11.2.0, removed in drupal:12.0.0. // Replaced by RequirementSeverity::maxSeverityFromRequirements(). // https://www.drupal.org/node/3495966 @@ -104,9 +109,11 @@ ]); // https://www.drupal.org/node/3489415 + // https://www.drupal.org/node/3489502 (change record) // views_field_default_views_data() and _views_field_get_entity_type_storage() deprecated in drupal:11.2.0, removed in drupal:12.0.0. // Replaced by views.field_data_provider service methods. - // https://www.drupal.org/node/3489411 + // https://www.drupal.org/node/3069442 + // https://www.drupal.org/node/3489411 (change record) // views_entity_field_label() deprecated in drupal:11.2.0, removed in drupal:12.0.0. // Replaced by entity_field.manager::getFieldLabels(). $rectorConfig->ruleWithConfiguration(FunctionToServiceRector::class, [ @@ -118,7 +125,8 @@ // https://www.drupal.org/node/3575841 // REQUIREMENT_INFO/OK/WARNING/ERROR global constants deprecated in drupal:11.2.0, removed in drupal:12.0.0. // Replaced by RequirementSeverity enum cases. - // https://www.drupal.org/node/3488133 + // https://www.drupal.org/node/3477277 + // https://www.drupal.org/node/3488133 (change record) // LOCALE_TRANSLATION_DEFAULT_SERVER_PATTERN deprecated in drupal:11.2.0, removed in drupal:12.0.0. // Replaced by \Drupal::TRANSLATION_DEFAULT_SERVER_PATTERN. $rectorConfig->ruleWithConfiguration(ConstantToClassConstantRector::class, [ @@ -130,7 +138,7 @@ ]); // https://www.drupal.org/node/3473440 - // https://www.drupal.org/node/3474692 (change record) + // https://www.drupal.org/node/3474692 (related) // TwigNodeTrans 6th $tag constructor argument deprecated in twig/twig 3.12, removed in drupal:11.2.0. // Drop the argument. $rectorConfig->ruleWithConfiguration(RemoveTwigNodeTransTagArgumentRector::class, [ @@ -170,7 +178,8 @@ new DrupalIntroducedVersionConfiguration('11.2.0'), ]); - // https://www.drupal.org/node/3494172 + // https://www.drupal.org/node/3494126 + // https://www.drupal.org/node/3494172 (change record) // file_get_content_headers($file) deprecated in drupal:11.2.0, removed in drupal:12.0.0. // Replaced by $file->getDownloadHeaders(). $rectorConfig->ruleWithConfiguration(FunctionToFirstArgMethodRector::class, [ @@ -201,15 +210,19 @@ ]); // https://www.drupal.org/node/3495943 + // https://www.drupal.org/node/3496491 (change record) // #[StopProceduralHookScan] attribute renamed to #[ProceduralHookScanStop] in drupal:11.2.0. $rectorConfig->rule(RenameStopProceduralHookScanRector::class); // https://www.drupal.org/node/3488572 + // https://www.drupal.org/node/3488580 (change record) // Drupal\Core\Entity\Query\Sql\pgsql\* deprecated in drupal:11.2.0, removed in drupal:12.0.0. // Moved to Drupal\pgsql\EntityQuery\*. // https://www.drupal.org/node/3472008 + // https://www.drupal.org/node/3478687 (change record) // Drupal\jsonapi\EventSubscriber\ResourceResponseValidator moved to jsonapi_response_validator submodule. - // https://www.drupal.org/node/3498916 + // https://www.drupal.org/node/3498915 + // https://www.drupal.org/node/3498916 (change record) // Drupal\migrate_drupal\Plugin\migrate\source\ContentEntity/ContentEntityDeriver deprecated in drupal:11.2.0, // removed in drupal:12.0.0. Moved to Drupal\migrate namespace. $rectorConfig->ruleWithConfiguration(RenameClassRector::class, [ @@ -231,7 +244,8 @@ // Pass NULL explicitly instead of the root path argument. $rectorConfig->rule(RemoveRootFromCreateConnectionOptionsFromUrlRector::class); - // https://www.drupal.org/node/3410939 + // https://www.drupal.org/node/3410938 + // https://www.drupal.org/node/3410939 (change record) // SystemManager::REQUIREMENT_* deprecated in drupal:11.2.0, removed in drupal:12.0.0. // Replaced by \Drupal\Core\Extension\Requirement\RequirementSeverity enum cases. $rectorConfig->ruleWithConfiguration(ClassConstantToClassConstantRector::class, [ diff --git a/config/drupal-11/drupal-11.3-deprecations.php b/config/drupal-11/drupal-11.3-deprecations.php index 2401edcc1..40ac7b70b 100644 --- a/config/drupal-11/drupal-11.3-deprecations.php +++ b/config/drupal-11/drupal-11.3-deprecations.php @@ -52,9 +52,11 @@ $rectorConfig->rule(NodeStorageDeprecatedMethodsRector::class); // https://www.drupal.org/node/3533083 + // https://www.drupal.org/node/3533315 (change record) // node_mass_update() deprecated in drupal:11.3.0, removed in drupal:13.0.0. // Replaced by \Drupal\node\NodeBulkUpdate::process(). - // https://www.drupal.org/node/3547356 + // https://www.drupal.org/node/1685492 + // https://www.drupal.org/node/3547356 (change record) // twig_render_template() deprecated in drupal:11.3.0, removed in drupal:12.0.0. // Replaced by \Drupal::service(TwigThemeEngine::class)->renderTemplate(). // twig_extension() is handled by ReplaceTwigExtensionRector below. @@ -63,7 +65,8 @@ new FunctionToServiceConfiguration('11.3.0', 'twig_render_template', 'Drupal\Core\Template\TwigThemeEngine', 'renderTemplate'), ]); - // https://www.drupal.org/node/3504125 + // https://www.drupal.org/node/3501136 + // https://www.drupal.org/node/3504125 (change record) // template_preprocess_layout() deprecated in drupal:11.3.0, removed in drupal:12.0.0. // Replaced by \Drupal\layout_discovery\Hook\LayoutDiscoveryThemeHooks::preprocessLayout(). $rectorConfig->ruleWithConfiguration(FunctionToServiceRector::class, [ @@ -71,6 +74,7 @@ ]); // https://www.drupal.org/node/1685492 + // https://www.drupal.org/node/3547356 (change record) // twig_extension() deprecated in drupal:11.3.0, removed in drupal:12.0.0. // Replaced by the '.html.twig' string literal. $rectorConfig->ruleWithConfiguration(ReplaceTwigExtensionRector::class, [ @@ -80,7 +84,8 @@ new DrupalIntroducedVersionConfiguration('11.3.0'), ]); - // https://www.drupal.org/node/3535528 + // https://www.drupal.org/node/3535526 + // https://www.drupal.org/node/3535528 (change record) // block_content_add_body_field() deprecated in drupal:11.3.0, removed in drupal:13.0.0. // The body field is now added via config. $rectorConfig->ruleWithConfiguration(FunctionCallRemovalRector::class, [ @@ -88,9 +93,11 @@ ]); // https://www.drupal.org/node/2010202 + // https://www.drupal.org/node/3384294 (change record) // comment_uri($comment) deprecated in drupal:11.3.0, removed in drupal:12.0.0. // Replaced by $comment->permalink(). - // https://www.drupal.org/node/3531945 + // https://www.drupal.org/node/3531944 + // https://www.drupal.org/node/3531945 (change record) // node_type_get_description($node_type) deprecated in drupal:11.3.0, removed in drupal:12.0.0. // Replaced by $node_type->getDescription(). $rectorConfig->ruleWithConfiguration(FunctionToFirstArgMethodRector::class, [ @@ -107,7 +114,8 @@ new DrupalIntroducedVersionConfiguration('11.3.0'), ]); - // https://www.drupal.org/node/3548329 + // https://www.drupal.org/node/3548326 + // https://www.drupal.org/node/3548329 (change record) // responsive_image_* functions deprecated in drupal:11.3.0, removed in drupal:12.0.0. // Replaced by \Drupal::service(ResponsiveImageBuilder::class)->method() calls. $rectorConfig->ruleWithConfiguration(FunctionToServiceRector::class, [ @@ -134,6 +142,7 @@ ]); // https://www.drupal.org/node/3534092 + // https://www.drupal.org/node/3534099 (change record) // file_system_settings_submit() deprecated in drupal:11.3.0, removed in drupal:13.0.0. // Replaced by \Drupal\file\Hook\FileHooks::settingsSubmit(). // https://www.drupal.org/node/3534089 @@ -153,7 +162,8 @@ new DrupalIntroducedVersionConfiguration('11.3.0'), ]); - // https://www.drupal.org/node/3495601 + // https://www.drupal.org/node/3495600 + // https://www.drupal.org/node/3495601 (change record) // JSONAPI_FILTER_AMONG_* global constants deprecated in drupal:11.3.0, removed in drupal:13.0.0. // Replaced by \Drupal\jsonapi\JsonApiFilter::AMONG_* class constants. $rectorConfig->ruleWithConfiguration(ConstantToClassConstantRector::class, [ @@ -203,7 +213,8 @@ new DrupalIntroducedVersionConfiguration('11.3.0'), ]); - // https://www.drupal.org/node/3551450 + // https://www.drupal.org/node/3551446 + // https://www.drupal.org/node/3551450 (change record) // workspaces.association service and WorkspaceAssociationInterface renamed in drupal:11.3.0. // Replaced by workspaces.tracker and WorkspaceTrackerInterface. $rectorConfig->ruleWithConfiguration(RenameClassRector::class, [ diff --git a/config/drupal-11/drupal-11.4-deprecations.php b/config/drupal-11/drupal-11.4-deprecations.php index a4b52fdde..4e248e17e 100644 --- a/config/drupal-11/drupal-11.4-deprecations.php +++ b/config/drupal-11/drupal-11.4-deprecations.php @@ -48,7 +48,8 @@ new DrupalIntroducedVersionConfiguration('11.4.0'), ]); - // https://www.drupal.org/node/3578055 + // https://www.drupal.org/node/2473041 + // https://www.drupal.org/node/3578055 (change record) // node_access_grants() deprecated in drupal:11.4.0, removed in drupal:13.0.0. // Replaced by \Drupal\node\NodeGrantsHelper::nodeAccessGrants(). $rectorConfig->ruleWithConfiguration(FunctionToServiceRector::class, [ @@ -89,7 +90,8 @@ new DrupalIntroducedVersionConfiguration('11.4.0'), ]); - // https://www.drupal.org/node/3570851 + // https://www.drupal.org/node/3570849 + // https://www.drupal.org/node/3570851 (change record) // SessionManager::delete() deprecated in drupal:11.4.0, removed in drupal:12.0.0. // Replaced by \Drupal\Core\Session\UserSessionRepositoryInterface::deleteAll(). $rectorConfig->ruleWithConfiguration(ReplaceSessionManagerDeleteRector::class, [ @@ -97,9 +99,11 @@ ]); // https://www.drupal.org/node/3550054 + // https://www.drupal.org/node/3550055 (change record) // CommentItemInterface::FORM_BELOW and FORM_SEPARATE_PAGE deprecated in 11.4.0, // removed in 13.0.0. Replaced by FormLocation enum cases. - // https://www.drupal.org/node/3547352 + // https://www.drupal.org/node/3547349 + // https://www.drupal.org/node/3547352 (change record) // CommentItemInterface::HIDDEN/CLOSED/OPEN and CommentInterface::ANONYMOUS_* // deprecated in 11.4.0, removed in 13.0.0. Replaced by CommentingStatus and // AnonymousContact enum cases. @@ -155,31 +159,41 @@ ]); // https://www.drupal.org/node/3574727 + // https://www.drupal.org/node/3566774 (change record) // language_configuration_element_submit() deprecated in 11.4.0, removed in 13.0.0. // Replaced by LanguageConfiguration::submit(). // language_process_language_select() deprecated in 11.4.0, removed in 12.0.0. // Replaced by LanguageHooks::processLanguageSelect() via the service container. // https://www.drupal.org/node/3566792 + // https://www.drupal.org/node/3566774 (change record) // ckeditor5_filter_format_edit_form_submit() and _update_ckeditor5_html_filter() // deprecated in 11.4.0, removed in 12.0.0. Replaced by Ckeditor5Hooks service. // https://www.drupal.org/node/3560398 + // https://www.drupal.org/node/3560399 (change record) // _dblog_get_message_types() and dblog_filters() deprecated in 11.4.0, // removed in 13.0.0. Replaced by DbLogFilters service. // https://www.drupal.org/node/3566888 + // https://www.drupal.org/node/3566774 (change record) // contact_user_profile_form_submit() and contact_form_user_admin_settings_submit() // deprecated in 11.4.0, removed in 12.0.0. Replaced by ContactFormHooks service. // https://www.drupal.org/node/3548571 + // https://www.drupal.org/node/3548573 (change record) // content_translation_* functions deprecated in 11.4.0, removed in 12.0.0/13.0.0. // https://www.drupal.org/node/3572339 + // https://www.drupal.org/node/3572345 (change record) // locale_translation_batch_update_build() and locale_translation_batch_fetch_build() // deprecated in 11.4.0, removed in 13.0.0. Replaced by LocaleFetch service. // https://www.drupal.org/node/3569328 + // https://www.drupal.org/node/3569330 (change record) // locale.translation.inc functions deprecated in 11.4.0, removed in 13.0.0. // https://www.drupal.org/node/3571400 + // https://www.drupal.org/node/3571402 (change record) // menu_ui.module procedural functions deprecated in 11.4.0, removed in 12.0.0/13.0.0. // https://www.drupal.org/node/3568387 + // https://www.drupal.org/node/3568389 (change record) // text_summary() deprecated in 11.4.0, removed in 13.0.0. Replaced by TextSummary service. - // https://www.drupal.org/node/3582107 + // https://www.drupal.org/node/3582106 + // https://www.drupal.org/node/3582107 (change record) // user_form_process_password_confirm() deprecated in 11.4.0, removed in 13.0.0. $rectorConfig->ruleWithConfiguration(FunctionToServiceRector::class, [ new FunctionToServiceConfiguration('11.4.0', 'language_process_language_select', 'Drupal\language\Hook\LanguageHooks', 'processLanguageSelect'), @@ -220,13 +234,15 @@ ]); // https://www.drupal.org/node/3035340 + // https://www.drupal.org/node/3040111 (change record) // views_ui_contextual_links_suppress*() deprecated in drupal:11.4.0, removed in drupal:13.0.0. // These are no-ops and can be removed. // https://www.drupal.org/node/3566768 // https://www.drupal.org/node/3566774 (change record) // automated_cron_settings_submit() deprecated in drupal:11.4.0, removed in drupal:13.0.0. // Config saving is now handled automatically via #config_target on the interval element. - // https://www.drupal.org/node/3566783 + // https://www.drupal.org/node/3566782 + // https://www.drupal.org/node/3566783 (change record) // block_theme_initialize() deprecated in drupal:11.4.0, removed in drupal:13.0.0. // Logic moved to protected BlockHooks::themeInitialize(); external callers must drop the call. $rectorConfig->ruleWithConfiguration(FunctionCallRemovalRector::class, [ @@ -324,15 +340,18 @@ ]); // https://www.drupal.org/node/3568144 + // https://www.drupal.org/node/3568146 (change record) // editor_filter_xss() deprecated in drupal:11.4.0, removed in drupal:13.0.0. // Replaced by \Drupal::service('element.editor')->filterXss(). // https://www.drupal.org/node/3570917 + // https://www.drupal.org/node/3570919 (change record) // editor_image_upload_settings_form() deprecated in drupal:11.4.0, removed in drupal:13.0.0. // Replaced by \Drupal::service(EditorImageUploadSettings::class)->getForm(). // https://www.drupal.org/node/2907780 + // https://www.drupal.org/node/3494023 (change record) // field_purge_batch() deprecated in drupal:11.4.0, removed in drupal:13.0.0. // Replaced by \Drupal::service(FieldPurger::class)->purgeBatch(). - // https://www.drupal.org/node/3566774 + // https://www.drupal.org/node/3566774 (change record) // _media_library_media_type_form_submit() and _media_library_views_form_media_library_after_build() // deprecated in drupal:11.4.0, removed in drupal:12.0.0. Replaced by MediaLibraryHooks service. $rectorConfig->ruleWithConfiguration(FunctionToServiceRector::class, [ @@ -343,7 +362,7 @@ new FunctionToServiceConfiguration('11.4.0', '_media_library_views_form_media_library_after_build', 'Drupal\media_library\Hook\MediaLibraryHooks', 'viewsFormAfterBuild'), ]); - // https://www.drupal.org/node/3566774 + // https://www.drupal.org/node/3566774 (change record) // _media_library_configure_form_display() and _media_library_configure_view_display() // deprecated in drupal:11.4.0, removed in drupal:12.0.0. // Replaced by MediaLibraryDisplayManager static methods. @@ -353,9 +372,11 @@ ]); // https://www.drupal.org/node/3574727 + // https://www.drupal.org/node/3566774 (change record) // language_configuration_element_submit() deprecated in 11.4.0, removed in 13.0.0. // Replaced by LanguageConfiguration::submit(). - // https://www.drupal.org/node/3566774 + // https://www.drupal.org/node/3035340 + // https://www.drupal.org/node/3566774 (change record) // views_ui/admin.inc static trait functions deprecated in 11.4.0, removed in 13.0.0. $rectorConfig->ruleWithConfiguration(FunctionToStaticRector::class, [ new FunctionToStaticConfiguration('11.4.0', 'language_configuration_element_submit', 'Drupal\language\Element\LanguageConfiguration', 'submit'), @@ -366,12 +387,15 @@ ]); // https://www.drupal.org/node/3568087 + // https://www.drupal.org/node/3568088 (change record) // contextual_links_to_id() and contextual_id_to_links() deprecated in drupal:11.4.0, removed in drupal:13.0.0. // Replaced by ContextualLinksSerializer service. // https://www.drupal.org/node/3567618 + // https://www.drupal.org/node/3567619 (change record) // image_path_flush() and image_style_options() deprecated in drupal:11.4.0, removed in drupal:13.0.0. // Replaced by ImageDerivativeUtilities service. - // https://www.drupal.org/node/3577675 + // https://www.drupal.org/node/3577671 + // https://www.drupal.org/node/3577675 (change record) // locale_translate_get_interface_translation_files() deprecated in drupal:11.4.0, removed in drupal:13.0.0. // Replaced by LocaleFileManager::getInterfaceTranslationFiles(). $rectorConfig->ruleWithConfiguration(FunctionToServiceRector::class, [ @@ -393,7 +417,8 @@ new FunctionToServiceConfiguration('11.4.0', 'views_add_contextual_links', 'Drupal\views\ContextualLinksHelper', 'addLinks', true), ]); - // https://www.drupal.org/node/3567619 + // https://www.drupal.org/node/3567618 + // https://www.drupal.org/node/3567619 (change record) // IMAGE_DERIVATIVE_TOKEN deprecated in drupal:11.4.0, removed in drupal:13.0.0. // Replaced by \Drupal\image\ImageStyleInterface::TOKEN. $rectorConfig->ruleWithConfiguration(ConstantToClassConstantRector::class, [ @@ -408,6 +433,7 @@ ]); // https://www.drupal.org/node/3015812 + // https://www.drupal.org/node/3015925 (change record) // system_region_list() and system_default_region() deprecated in drupal:11.4.0, removed in drupal:13.0.0. // Replaced by Theme object methods via \Drupal::service('theme_handler')->getTheme(). $rectorConfig->ruleWithConfiguration(SystemRegionFunctionsRector::class, [ @@ -432,6 +458,7 @@ $rectorConfig->rule(SystemSortThemesRector::class); // https://www.drupal.org/node/3037031 + // https://www.drupal.org/node/3037033 (change record) // locale_translation_flush_projects(), locale_translation_build_projects(), locale_translation_check_projects(), // and locale_translation_check_projects_local() deprecated in drupal:11.4.0, removed in drupal:13.0.0. // Replaced by LocaleProjectRepository and LocaleProjectChecker service methods. From 5a5a4f35a5f691680848e6352d77a04059dec067 Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 25 May 2026 19:21:36 +0200 Subject: [PATCH 02/23] feat(Drupal11): Add ReplaceHideShowWithPrintedRector for issue #2258355 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrites statement-level calls to the deprecated global hide() and show() functions to direct $element['#printed'] = TRUE/FALSE assignment. The functions are deprecated in drupal:11.4.0 and removed in drupal:13.0.0. Expression-context uses (where the return value is captured) are skipped because the original returns the element while the rewrite would not. Live-tested against fpa, saml_sp, vertical_tabs_config, and field_group_background_image — 4 modules, 10 calls transformed cleanly. See https://www.drupal.org/node/2258355 See https://www.drupal.org/node/3261271 (change record) --- CHANGELOG.md | 14 +++ config/drupal-11/drupal-11.4-deprecations.php | 7 ++ .../ReplaceHideShowWithPrintedRector.php | 95 +++++++++++++++++++ .../ReplaceHideShowWithPrintedRectorTest.php | 29 ++++++ .../config/configured_rule.php | 10 ++ .../fixture/basic.php.inc | 15 +++ .../fixture/nested_element.php.inc | 15 +++ .../no_change_expression_context.php.inc | 19 ++++ .../fixture/no_change_method_call.php.inc | 19 ++++ .../fixture/no_change_wrong_arg_count.php.inc | 15 +++ 10 files changed, 238 insertions(+) create mode 100644 src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/ReplaceHideShowWithPrintedRectorTest.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/config/configured_rule.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/fixture/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/fixture/nested_element.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/fixture/no_change_expression_context.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/fixture/no_change_method_call.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/fixture/no_change_wrong_arg_count.php.inc diff --git a/CHANGELOG.md b/CHANGELOG.md index 639de4fac..6cc2abece 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,20 @@ Historical entries (≤ 0.21.2) are reproduced from the they were originally published; their format and level of detail varies release-by-release. +## [Unreleased] + +### Added + +- **`ReplaceHideShowWithPrintedRector`** — replaces statement-level calls to the + deprecated global `hide()` and `show()` functions (deprecated in drupal:11.4.0, + removed in drupal:13.0.0) with direct `$element['#printed'] = TRUE/FALSE` + assignment. Expression-context uses (where the return value is captured) are + intentionally skipped because the original returns the element while the + rewrite would not. Live-tested against `fpa`, `saml_sp`, `vertical_tabs_config`, + and `field_group_background_image`. + [#2258355](https://www.drupal.org/i/2258355) / + [CR](https://www.drupal.org/node/3261271). + ## [1.0.0-beta1] — 2026-05-25 First beta of the 1.0 line. Adds full Drupal 11 deprecation coverage (versions 11.0 diff --git a/config/drupal-11/drupal-11.4-deprecations.php b/config/drupal-11/drupal-11.4-deprecations.php index 4e248e17e..5c5f3433f 100644 --- a/config/drupal-11/drupal-11.4-deprecations.php +++ b/config/drupal-11/drupal-11.4-deprecations.php @@ -18,6 +18,7 @@ use DrupalRector\Drupal11\Rector\Deprecation\RemoveTrustDataCallRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveViewsRowCacheKeysRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceEntityReferenceRecursiveLimitRector; +use DrupalRector\Drupal11\Rector\Deprecation\ReplaceHideShowWithPrintedRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceRecipeRunnerInstallModuleRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceSessionManagerDeleteRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceSystemPerformanceGzipKeyRector; @@ -457,6 +458,12 @@ // Replaced by an inline static closure. $rectorConfig->rule(SystemSortThemesRector::class); + // https://www.drupal.org/node/2258355 + // https://www.drupal.org/node/3261271 (change record) + // hide() and show() deprecated in drupal:11.4.0, removed in drupal:13.0.0. + // Replaced by direct $element['#printed'] = TRUE/FALSE assignment. + $rectorConfig->rule(ReplaceHideShowWithPrintedRector::class); + // https://www.drupal.org/node/3037031 // https://www.drupal.org/node/3037033 (change record) // locale_translation_flush_projects(), locale_translation_build_projects(), locale_translation_check_projects(), diff --git a/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector.php b/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector.php new file mode 100644 index 000000000..f64e287ca --- /dev/null +++ b/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector.php @@ -0,0 +1,95 @@ + */ + private const FUNCTION_TO_PRINTED_VALUE = [ + 'hide' => true, + 'show' => false, + ]; + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Replace deprecated global hide() and show() functions with direct #printed property assignment on the render element.', + [ + new CodeSample( + 'hide($element);', + "\$element['#printed'] = TRUE;", + ), + new CodeSample( + 'show($element);', + "\$element['#printed'] = FALSE;", + ), + ] + ); + } + + public function getNodeTypes(): array + { + return [Expression::class]; + } + + public function refactor(Node $node): ?Node + { + assert($node instanceof Expression); + + if (!$node->expr instanceof FuncCall) { + return null; + } + + $call = $node->expr; + + if (!$this->isNames($call->name, ['hide', 'show'])) { + return null; + } + + if (count($call->args) !== 1 || !$call->args[0] instanceof Arg) { + return null; + } + + $funcName = $this->getName($call->name); + + if ($funcName === null || !isset(self::FUNCTION_TO_PRINTED_VALUE[$funcName])) { + return null; + } + + $value = self::FUNCTION_TO_PRINTED_VALUE[$funcName] + ? $this->nodeFactory->createTrue() + : $this->nodeFactory->createFalse(); + + $node->expr = new Assign( + new ArrayDimFetch($call->args[0]->value, new String_('#printed')), + $value, + ); + + return $node; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/ReplaceHideShowWithPrintedRectorTest.php b/tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/ReplaceHideShowWithPrintedRectorTest.php new file mode 100644 index 000000000..e9d7d113f --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/ReplaceHideShowWithPrintedRectorTest.php @@ -0,0 +1,29 @@ +doTestFile($filePath); + } + + /** + * @return \Iterator<> + */ + 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/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/config/configured_rule.php b/tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/config/configured_rule.php new file mode 100644 index 000000000..53a38f52a --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(ReplaceHideShowWithPrintedRector::class); +}; diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/fixture/basic.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/fixture/basic.php.inc new file mode 100644 index 000000000..de076de05 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/fixture/basic.php.inc @@ -0,0 +1,15 @@ + +----- + diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/fixture/nested_element.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/fixture/nested_element.php.inc new file mode 100644 index 000000000..63a4f6e20 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/fixture/nested_element.php.inc @@ -0,0 +1,15 @@ + +----- + diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/fixture/no_change_expression_context.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/fixture/no_change_expression_context.php.inc new file mode 100644 index 000000000..4ff0c97d0 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/fixture/no_change_expression_context.php.inc @@ -0,0 +1,19 @@ + +----- + diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/fixture/no_change_method_call.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/fixture/no_change_method_call.php.inc new file mode 100644 index 000000000..df47bfebd --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/fixture/no_change_method_call.php.inc @@ -0,0 +1,19 @@ +hide($element); + $obj->show($element); + } +} +?> +----- +hide($element); + $obj->show($element); + } +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/fixture/no_change_wrong_arg_count.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/fixture/no_change_wrong_arg_count.php.inc new file mode 100644 index 000000000..917b0cdbe --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceHideShowWithPrintedRector/fixture/no_change_wrong_arg_count.php.inc @@ -0,0 +1,15 @@ + +----- + From d64d28f16eac07ad2f5e55109e9439d337f21b1b Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 25 May 2026 19:22:03 +0200 Subject: [PATCH 03/23] docs: fixup cr/issue links for rules --- config/drupal-11/drupal-11.4-deprecations.php | 6 ++++-- .../FunctionToServiceRector/config/configured_rule.php | 6 +++--- .../fixture/media_library_procedural_functions.php.inc | 4 ++-- .../FunctionToStaticRector/config/configured_rule.php | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/config/drupal-11/drupal-11.4-deprecations.php b/config/drupal-11/drupal-11.4-deprecations.php index 5c5f3433f..6b2c11308 100644 --- a/config/drupal-11/drupal-11.4-deprecations.php +++ b/config/drupal-11/drupal-11.4-deprecations.php @@ -352,6 +352,7 @@ // https://www.drupal.org/node/3494023 (change record) // field_purge_batch() deprecated in drupal:11.4.0, removed in drupal:13.0.0. // Replaced by \Drupal::service(FieldPurger::class)->purgeBatch(). + // https://www.drupal.org/node/3570839 // https://www.drupal.org/node/3566774 (change record) // _media_library_media_type_form_submit() and _media_library_views_form_media_library_after_build() // deprecated in drupal:11.4.0, removed in drupal:12.0.0. Replaced by MediaLibraryHooks service. @@ -359,10 +360,11 @@ new FunctionToServiceConfiguration('11.4.0', 'editor_filter_xss', 'element.editor', 'filterXss'), new FunctionToServiceConfiguration('11.4.0', 'editor_image_upload_settings_form', 'Drupal\editor\EditorImageUploadSettings', 'getForm'), new FunctionToServiceConfiguration('11.4.0', 'field_purge_batch', 'Drupal\Core\Field\FieldPurger', 'purgeBatch'), - new FunctionToServiceConfiguration('11.4.0', '_media_library_media_type_form_submit', 'Drupal\media_library\Hook\MediaLibraryHooks', 'mediaTypeFormSubmit'), - new FunctionToServiceConfiguration('11.4.0', '_media_library_views_form_media_library_after_build', 'Drupal\media_library\Hook\MediaLibraryHooks', 'viewsFormAfterBuild'), + new FunctionToServiceConfiguration('11.4.0', '_media_library_media_type_form_submit', 'Drupal\media_library\Hook\MediaLibraryHooks', 'mediaTypeFormSubmit', true), + new FunctionToServiceConfiguration('11.4.0', '_media_library_views_form_media_library_after_build', 'Drupal\media_library\Hook\MediaLibraryHooks', 'viewsFormAfterBuild', true), ]); + // https://www.drupal.org/node/3570839 // https://www.drupal.org/node/3566774 (change record) // _media_library_configure_form_display() and _media_library_configure_view_display() // deprecated in drupal:11.4.0, removed in drupal:12.0.0. diff --git a/tests/src/Rector/Deprecation/FunctionToServiceRector/config/configured_rule.php b/tests/src/Rector/Deprecation/FunctionToServiceRector/config/configured_rule.php index e9470ee86..4c8fa67d8 100644 --- a/tests/src/Rector/Deprecation/FunctionToServiceRector/config/configured_rule.php +++ b/tests/src/Rector/Deprecation/FunctionToServiceRector/config/configured_rule.php @@ -54,9 +54,9 @@ new FunctionToServiceConfiguration('11.4.0', 'locale_translation_http_check', 'Drupal\locale\File\LocaleFileManager', 'checkRemoteFileStatus'), new FunctionToServiceConfiguration('11.4.0', 'locale_translate_delete_translation_files', 'Drupal\locale\File\LocaleFileManager', 'deleteTranslationFiles'), new FunctionToServiceConfiguration('11.4.0', 'locale_translation_download_source', 'Drupal\locale\File\LocaleFileManager', 'downloadTranslationSource'), - // https://www.drupal.org/node/3566774 (Drupal 11.4) - new FunctionToServiceConfiguration('11.4.0', '_media_library_media_type_form_submit', 'Drupal\media_library\Hook\MediaLibraryHooks', 'mediaTypeFormSubmit'), - new FunctionToServiceConfiguration('11.4.0', '_media_library_views_form_media_library_after_build', 'Drupal\media_library\Hook\MediaLibraryHooks', 'viewsFormAfterBuild'), + // https://www.drupal.org/node/3570839 (Drupal 11.4) + new FunctionToServiceConfiguration('11.4.0', '_media_library_media_type_form_submit', 'Drupal\media_library\Hook\MediaLibraryHooks', 'mediaTypeFormSubmit', true), + new FunctionToServiceConfiguration('11.4.0', '_media_library_views_form_media_library_after_build', 'Drupal\media_library\Hook\MediaLibraryHooks', 'viewsFormAfterBuild', true), // https://www.drupal.org/node/3574727 (Drupal 11.4) new FunctionToServiceConfiguration('11.4.0', 'language_process_language_select', 'Drupal\language\Hook\LanguageHooks', 'processLanguageSelect'), // https://www.drupal.org/node/3566792 (Drupal 11.4) diff --git a/tests/src/Rector/Deprecation/FunctionToServiceRector/fixture/media_library_procedural_functions.php.inc b/tests/src/Rector/Deprecation/FunctionToServiceRector/fixture/media_library_procedural_functions.php.inc index e48c132f8..fb0a07f31 100644 --- a/tests/src/Rector/Deprecation/FunctionToServiceRector/fixture/media_library_procedural_functions.php.inc +++ b/tests/src/Rector/Deprecation/FunctionToServiceRector/fixture/media_library_procedural_functions.php.inc @@ -9,7 +9,7 @@ function example($form, $form_state) { \Drupal::service('Drupal\media_library\Hook\MediaLibraryHooks')->mediaTypeFormSubmit($form, $form_state), fn() => _media_library_media_type_form_submit($form, $form_state)); - $result = \Drupal\Component\Utility\DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => \Drupal::service('Drupal\media_library\Hook\MediaLibraryHooks')->viewsFormAfterBuild($form, $form_state), fn() => _media_library_views_form_media_library_after_build($form, $form_state)); + \Drupal\Component\Utility\DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => \Drupal::service(\Drupal\media_library\Hook\MediaLibraryHooks::class)->mediaTypeFormSubmit($form, $form_state), fn() => _media_library_media_type_form_submit($form, $form_state)); + $result = \Drupal\Component\Utility\DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => \Drupal::service(\Drupal\media_library\Hook\MediaLibraryHooks::class)->viewsFormAfterBuild($form, $form_state), fn() => _media_library_views_form_media_library_after_build($form, $form_state)); } ?> diff --git a/tests/src/Rector/Deprecation/FunctionToStaticRector/config/configured_rule.php b/tests/src/Rector/Deprecation/FunctionToStaticRector/config/configured_rule.php index 64f0d2168..db72c63e6 100644 --- a/tests/src/Rector/Deprecation/FunctionToStaticRector/config/configured_rule.php +++ b/tests/src/Rector/Deprecation/FunctionToStaticRector/config/configured_rule.php @@ -25,7 +25,7 @@ new FunctionToStaticConfiguration('11.3.0', 'file_system_settings_submit', 'Drupal\file\Hook\FileHooks', 'settingsSubmit'), // https://www.drupal.org/node/3534089 (Drupal 11.3) new FunctionToStaticConfiguration('11.3.0', 'file_managed_file_submit', 'Drupal\file\Element\ManagedFile', 'submit'), - // https://www.drupal.org/node/3566774 (Drupal 11.4) + // https://www.drupal.org/node/3570839 (Drupal 11.4) new FunctionToStaticConfiguration('11.4.0', '_media_library_configure_form_display', 'Drupal\media_library\MediaLibraryDisplayManager', 'configureFormDisplay'), new FunctionToStaticConfiguration('11.4.0', '_media_library_configure_view_display', 'Drupal\media_library\MediaLibraryDisplayManager', 'configureViewDisplay'), // https://www.drupal.org/node/3495966 (Drupal 11.2) From f1b86870426c300513a3172c4c817f7f664f3be0 Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 25 May 2026 21:33:29 +0200 Subject: [PATCH 04/23] feat(Drupal11): Add ReplaceExpectDeprecationRector for issue #3550268 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrates removed Drupal/PHPUnit test framework methods to their PHPUnit 11+ replacements: - $this->expectDeprecation() (PHPUnit no-arg form) → removed - $this->expectDeprecation($msg) (Drupal trait form) → expectUserDeprecationMessage($msg) - $this->expectDeprecationMessage($msg) → expectUserDeprecationMessage($msg) - $this->expectDeprecationMessageMatches($p) → expectUserDeprecationMessageMatches($p) Renames are BC-wrapped via DeprecationHelper::backwardsCompatibleCall() so tests keep passing on both pre-11.4 (old methods) and 11.4+ (new methods). The 0-arg PHPUnit form is unconditionally removed because real-world contrib uses the 1-arg Drupal-trait form. Live-tested against honeypot, key, entity_usage, and node_revision_delete. ExpectDeprecationTrait is deprecated in drupal:11.4.0 and removed in drupal:12.0.0. See https://www.drupal.org/node/3550268 See https://www.drupal.org/node/3545276 (change record) --- CHANGELOG.md | 13 ++ config/drupal-11/drupal-11.4-deprecations.php | 9 + .../ReplaceExpectDeprecationRector.php | 155 ++++++++++++++++++ .../ReplaceExpectDeprecationRectorTest.php | 46 ++++++ .../config/configured_rule.php | 13 ++ .../fixture-below-version/basic.php.inc | 21 +++ .../drupal_trait_one_arg.php.inc | 19 +++ .../fixture/basic.php.inc | 20 +++ .../fixture/drupal_trait_one_arg.php.inc | 19 +++ .../fixture/no_change_other_method.php.inc | 19 +++ .../fixture/no_change_other_receiver.php.inc | 19 +++ 11 files changed, 353 insertions(+) create mode 100644 src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/ReplaceExpectDeprecationRectorTest.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/config/configured_rule.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture-below-version/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture-below-version/drupal_trait_one_arg.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture/drupal_trait_one_arg.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture/no_change_other_method.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture/no_change_other_receiver.php.inc diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cc2abece..4c63d95ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,19 @@ release-by-release. and `field_group_background_image`. [#2258355](https://www.drupal.org/i/2258355) / [CR](https://www.drupal.org/node/3261271). +- **`ReplaceExpectDeprecationRector`** — migrates removed test framework methods + to their PHPUnit 11+ replacements. Renames are BC-wrapped with + `DeprecationHelper::backwardsCompatibleCall()` so tests keep passing on both + pre-11.4 (old methods) and 11.4+ (new methods). Covers: + `$this->expectDeprecation($msg)` and `$this->expectDeprecationMessage($msg)` → + `$this->expectUserDeprecationMessage($msg)`; + `$this->expectDeprecationMessageMatches($p)` → + `$this->expectUserDeprecationMessageMatches($p)`; bare + `$this->expectDeprecation()` (no-arg PHPUnit form) → removed. + `ExpectDeprecationTrait` is deprecated in drupal:11.4.0 and removed in + drupal:12.0.0. + [#3550268](https://www.drupal.org/i/3550268) / + [CR](https://www.drupal.org/node/3545276). ## [1.0.0-beta1] — 2026-05-25 diff --git a/config/drupal-11/drupal-11.4-deprecations.php b/config/drupal-11/drupal-11.4-deprecations.php index 6b2c11308..4a58ceeab 100644 --- a/config/drupal-11/drupal-11.4-deprecations.php +++ b/config/drupal-11/drupal-11.4-deprecations.php @@ -18,6 +18,7 @@ use DrupalRector\Drupal11\Rector\Deprecation\RemoveTrustDataCallRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveViewsRowCacheKeysRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceEntityReferenceRecursiveLimitRector; +use DrupalRector\Drupal11\Rector\Deprecation\ReplaceExpectDeprecationRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceHideShowWithPrintedRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceRecipeRunnerInstallModuleRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceSessionManagerDeleteRector; @@ -466,6 +467,14 @@ // Replaced by direct $element['#printed'] = TRUE/FALSE assignment. $rectorConfig->rule(ReplaceHideShowWithPrintedRector::class); + // https://www.drupal.org/node/3550268 + // https://www.drupal.org/node/3545276 (change record) + // ExpectDeprecationTrait deprecated in drupal:11.4.0, removed in drupal:12.0.0. + // Replaced by PHPUnit 11+ expectUserDeprecationMessage() / expectUserDeprecationMessageMatches(). + $rectorConfig->ruleWithConfiguration(ReplaceExpectDeprecationRector::class, [ + new DrupalIntroducedVersionConfiguration('11.4.0'), + ]); + // https://www.drupal.org/node/3037031 // https://www.drupal.org/node/3037033 (change record) // locale_translation_flush_projects(), locale_translation_build_projects(), locale_translation_check_projects(), diff --git a/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector.php b/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector.php new file mode 100644 index 000000000..085e73fc8 --- /dev/null +++ b/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector.php @@ -0,0 +1,155 @@ +expectDeprecation() (PHPUnit no-arg form) → removed + * - $this->expectDeprecation($msg) (Drupal trait form) → $this->expectUserDeprecationMessage($msg) + * - $this->expectDeprecationMessage($msg) → $this->expectUserDeprecationMessage($msg) + * - $this->expectDeprecationMessageMatches($p) → $this->expectUserDeprecationMessageMatches($p) + * + * Renames are wrapped in DeprecationHelper::backwardsCompatibleCall() so tests + * keep passing on both pre-11.4 (where the old methods still exist) and + * 11.4+ (where the new PHPUnit 11+ replacements must be used). + * + * Note: the Drupal trait method internally treats $message as a regex fragment + * with %A boundaries. Renaming to expectUserDeprecationMessage() switches to + * exact-match semantics, which matches how Drupal core itself migrated its + * tests and the typical contrib pattern (literal deprecation message). Tests + * that intentionally relied on partial matching should be reviewed and + * switched to expectUserDeprecationMessageMatches() manually. + * + * @see https://www.drupal.org/node/3550268 + * @see https://www.drupal.org/node/3545276 + */ +final class ReplaceExpectDeprecationRector extends AbstractDrupalCoreRector +{ + private const RENAME_MAP = [ + 'expectDeprecation' => 'expectUserDeprecationMessage', + 'expectDeprecationMessage' => 'expectUserDeprecationMessage', + 'expectDeprecationMessageMatches' => 'expectUserDeprecationMessageMatches', + ]; + + /** + * @var array|DrupalIntroducedVersionConfiguration[] + */ + protected array $configuration; + + public function configure(array $configuration): void + { + foreach ($configuration as $value) { + if (!$value instanceof DrupalIntroducedVersionConfiguration) { + throw new \InvalidArgumentException(sprintf('Each configuration item must be an instance of "%s"', DrupalIntroducedVersionConfiguration::class)); + } + } + parent::configure($configuration); + } + + /** @return array> */ + public function getNodeTypes(): array + { + // Expression targets the 0-arg `expectDeprecation()` REMOVE case; + // MethodCall handles the renames so the parent class auto-wraps + // them in DeprecationHelper::backwardsCompatibleCall(). + return [Expression::class, MethodCall::class]; + } + + protected function refactorWithConfiguration(Node $node, VersionedConfigurationInterface $configuration): Node|int|null + { + if ($node instanceof Expression) { + return $this->refactorBareCall($node); + } + + if ($node instanceof MethodCall) { + return $this->refactorRename($node); + } + + return null; + } + + private function refactorBareCall(Expression $node): ?int + { + if (!$node->expr instanceof MethodCall) { + return null; + } + + $call = $node->expr; + if (!$this->isThisCall($call)) { + return null; + } + + if ($this->getName($call->name) !== 'expectDeprecation') { + return null; + } + + if ($call->args !== []) { + return null; + } + + return NodeVisitor::REMOVE_NODE; + } + + private function refactorRename(MethodCall $node): ?MethodCall + { + if (!$this->isThisCall($node)) { + return null; + } + + $name = $this->getName($node->name); + if ($name === null || !isset(self::RENAME_MAP[$name])) { + return null; + } + + // 0-arg `expectDeprecation()` is handled by the Expression path (REMOVE); + // skip it here so we don't emit a malformed `expectUserDeprecationMessage()`. + if ($name === 'expectDeprecation' && $node->args === []) { + return null; + } + + return new MethodCall($node->var, new Identifier(self::RENAME_MAP[$name]), $node->args); + } + + private function isThisCall(MethodCall $node): bool + { + return $node->var instanceof Variable && $node->var->name === 'this'; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Replace removed expectDeprecation*() test methods with PHPUnit 11+ expectUserDeprecationMessage*() equivalents.', + [ + new ConfiguredCodeSample( + <<<'BEFORE' +$this->expectDeprecation(); +$this->expectDeprecationMessage('Foo is deprecated'); +BEFORE, + <<<'AFTER' +$this->expectUserDeprecationMessage('Foo is deprecated'); +AFTER, + [new DrupalIntroducedVersionConfiguration('11.4.0')] + ), + ] + ); + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/ReplaceExpectDeprecationRectorTest.php b/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/ReplaceExpectDeprecationRectorTest.php new file mode 100644 index 000000000..55890cc1f --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/ReplaceExpectDeprecationRectorTest.php @@ -0,0 +1,46 @@ +make(DrupalRectorSettings::class)->setDrupalVersion('99.99.99'); + $this->doTestFile($filePath); + } + + /** + * @return \Iterator<> + */ + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/fixture'); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('provideDataBelowVersion')] + public function testBelowVersion(string $filePath): void + { + static::getContainer()->make(DrupalRectorSettings::class)->setDrupalVersion('1.0.0'); + $this->doTestFile($filePath); + } + + /** + * @return \Iterator<> + */ + public static function provideDataBelowVersion(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/fixture-below-version'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config/configured_rule.php'; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/config/configured_rule.php b/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/config/configured_rule.php new file mode 100644 index 000000000..0589f999e --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/config/configured_rule.php @@ -0,0 +1,13 @@ +ruleWithConfiguration(ReplaceExpectDeprecationRector::class, [ + new DrupalIntroducedVersionConfiguration('11.4.0'), + ]); +}; diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture-below-version/basic.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture-below-version/basic.php.inc new file mode 100644 index 000000000..ac9fd7343 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture-below-version/basic.php.inc @@ -0,0 +1,21 @@ +expectDeprecation(); + $this->expectDeprecationMessage('Foo::bar is deprecated'); + $this->expectDeprecationMessageMatches('/deprecated/'); + } +} +?> +----- +expectDeprecation(); + $this->expectDeprecationMessage('Foo::bar is deprecated'); + $this->expectDeprecationMessageMatches('/deprecated/'); + } +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture-below-version/drupal_trait_one_arg.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture-below-version/drupal_trait_one_arg.php.inc new file mode 100644 index 000000000..6774c5fb3 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture-below-version/drupal_trait_one_arg.php.inc @@ -0,0 +1,19 @@ +expectDeprecation('Drupal trait variant with a string arg'); + $this->expectDeprecation('foo() is deprecated in mymodule:2.0.0 and is removed from mymodule:3.0.0.'); + } +} +?> +----- +expectDeprecation('Drupal trait variant with a string arg'); + $this->expectDeprecation('foo() is deprecated in mymodule:2.0.0 and is removed from mymodule:3.0.0.'); + } +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture/basic.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture/basic.php.inc new file mode 100644 index 000000000..b44da3d85 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture/basic.php.inc @@ -0,0 +1,20 @@ +expectDeprecation(); + $this->expectDeprecationMessage('Foo::bar is deprecated'); + $this->expectDeprecationMessageMatches('/deprecated/'); + } +} +?> +----- + $this->expectUserDeprecationMessage('Foo::bar is deprecated'), fn() => $this->expectDeprecationMessage('Foo::bar is deprecated')); + \Drupal\Component\Utility\DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => $this->expectUserDeprecationMessageMatches('/deprecated/'), fn() => $this->expectDeprecationMessageMatches('/deprecated/')); + } +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture/drupal_trait_one_arg.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture/drupal_trait_one_arg.php.inc new file mode 100644 index 000000000..adecec97e --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture/drupal_trait_one_arg.php.inc @@ -0,0 +1,19 @@ +expectDeprecation('Drupal trait variant with a string arg'); + $this->expectDeprecation('foo() is deprecated in mymodule:2.0.0 and is removed from mymodule:3.0.0.'); + } +} +?> +----- + $this->expectUserDeprecationMessage('Drupal trait variant with a string arg'), fn() => $this->expectDeprecation('Drupal trait variant with a string arg')); + \Drupal\Component\Utility\DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => $this->expectUserDeprecationMessage('foo() is deprecated in mymodule:2.0.0 and is removed from mymodule:3.0.0.'), fn() => $this->expectDeprecation('foo() is deprecated in mymodule:2.0.0 and is removed from mymodule:3.0.0.')); + } +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture/no_change_other_method.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture/no_change_other_method.php.inc new file mode 100644 index 000000000..a37e74bbe --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture/no_change_other_method.php.inc @@ -0,0 +1,19 @@ +expectException(\RuntimeException::class); + $this->expectExceptionMessage('boom'); + } +} +?> +----- +expectException(\RuntimeException::class); + $this->expectExceptionMessage('boom'); + } +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture/no_change_other_receiver.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture/no_change_other_receiver.php.inc new file mode 100644 index 000000000..330f2b5a7 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector/fixture/no_change_other_receiver.php.inc @@ -0,0 +1,19 @@ +expectDeprecation(); + $helper->expectDeprecationMessage('foo'); + } +} +?> +----- +expectDeprecation(); + $helper->expectDeprecationMessage('foo'); + } +} +?> From 0c05c6660cfc78bc63f10a5f94f2fb1c0609f755 Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 25 May 2026 21:46:27 +0200 Subject: [PATCH 05/23] feat(Drupal11): Add block_content\Access class alias renames for issue #3571874 Four BC class aliases in Drupal\block_content\Access were deprecated in drupal:11.3.0 and removed in drupal:12.0.0. Add config-only RenameClassRector entries mapping each to its canonical Drupal\Core\Access home: - AccessGroupAnd - DependentAccessInterface - RefinableDependentAccessInterface - RefinableDependentAccessTrait See https://www.drupal.org/node/3571874 and https://www.drupal.org/node/3527501 (change record). --- CHANGELOG.md | 8 ++++++++ config/drupal-11/drupal-11.3-deprecations.php | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c63d95ab..ac0ab4c1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,14 @@ release-by-release. drupal:12.0.0. [#3550268](https://www.drupal.org/i/3550268) / [CR](https://www.drupal.org/node/3545276). +- Class-rename entries for the four `Drupal\block_content\Access\*` aliases + (`AccessGroupAnd`, `DependentAccessInterface`, + `RefinableDependentAccessInterface`, `RefinableDependentAccessTrait`) → + their canonical `Drupal\Core\Access\*` homes. Deprecated in drupal:11.3.0, + removed in drupal:12.0.0; registered via Rector's built-in + `RenameClassRector` in `drupal-11.3-deprecations.php`. + [#3571874](https://www.drupal.org/i/3571874) / + [CR](https://www.drupal.org/node/3527501). ## [1.0.0-beta1] — 2026-05-25 diff --git a/config/drupal-11/drupal-11.3-deprecations.php b/config/drupal-11/drupal-11.3-deprecations.php index 40ac7b70b..749d066a3 100644 --- a/config/drupal-11/drupal-11.3-deprecations.php +++ b/config/drupal-11/drupal-11.3-deprecations.php @@ -221,4 +221,15 @@ 'Drupal\workspaces\WorkspaceAssociationInterface' => 'Drupal\workspaces\WorkspaceTrackerInterface', 'Drupal\workspaces\WorkspaceAssociation' => 'Drupal\workspaces\WorkspaceTracker', ]); + + // https://www.drupal.org/node/3571874 + // https://www.drupal.org/node/3527501 (change record) + // Drupal\block_content\Access\* class aliases deprecated in drupal:11.3.0, removed in drupal:12.0.0. + // Replaced by their canonical Drupal\Core\Access homes. + $rectorConfig->ruleWithConfiguration(RenameClassRector::class, [ + 'Drupal\block_content\Access\AccessGroupAnd' => 'Drupal\Core\Access\AccessGroupAnd', + 'Drupal\block_content\Access\DependentAccessInterface' => 'Drupal\Core\Access\DependentAccessInterface', + 'Drupal\block_content\Access\RefinableDependentAccessInterface' => 'Drupal\Core\Access\RefinableDependentAccessInterface', + 'Drupal\block_content\Access\RefinableDependentAccessTrait' => 'Drupal\Core\Access\RefinableDependentAccessTrait', + ]); }; From 9ce3ff3964f62bf55bf121afedc7e64cf9e3aad9 Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 25 May 2026 22:15:48 +0200 Subject: [PATCH 06/23] feat: Add PHPStan-message coverage registry infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a rector → PHPStan-deprecation-message map so a future upgrade_status PR can replace its hardcoded $rector_covered array with a generated, version-aware registry. Source of truth lives on the code, not in a sidecar file: - Custom rector classes: public const PHPSTAN_MESSAGES = [...]. - Config-only registrations: // PHPSTAN_MESSAGES : comment block above the ruleWithConfiguration() call. Tooling: - scripts/normalize-phpstan-message.php applies the three transforms upgrade_status's DeprecationAnalyzer::categorizeMessage() applies before isRectorCovered() (whitespace collapse, ": in" → ". Deprecated in", strip leading \\Drupal). Same transforms run on stored messages so the registry is comparison-ready. - scripts/generate-coverage-registry.php walks src/ + config/, reads both shapes, normalizes, writes config/coverage-registry.php (rector short name → list of normalized messages). Worked example added to ReplaceExpectDeprecationRector (captured via synthetic probe against installed 11.4-dev). 3571874 config-only block gets a TODO marker rather than a synthesized guess, since the aliases are already gone from 11.4-dev core and a live capture requires a 11.3.x test env. rector-live-test SKILL.md gets a new step 5 wiring the capture into the existing live-test flow: while the contrib module is still installed and the pre-transform file on disk, run PHPStan, normalize the message, store on the class or in the config comment block, then regenerate the registry. --- .claude/skills/rector-live-test/SKILL.md | 80 ++++++- config/coverage-registry.php | 27 +++ config/drupal-11/drupal-11.3-deprecations.php | 7 + scripts/generate-coverage-registry.php | 217 ++++++++++++++++++ scripts/normalize-phpstan-message.php | 47 ++++ .../ReplaceExpectDeprecationRector.php | 14 ++ 6 files changed, 389 insertions(+), 3 deletions(-) create mode 100644 config/coverage-registry.php create mode 100644 scripts/generate-coverage-registry.php create mode 100644 scripts/normalize-phpstan-message.php diff --git a/.claude/skills/rector-live-test/SKILL.md b/.claude/skills/rector-live-test/SKILL.md index fce351163..8a4ac94fb 100644 --- a/.claude/skills/rector-live-test/SKILL.md +++ b/.claude/skills/rector-live-test/SKILL.md @@ -183,7 +183,81 @@ git -C ~/projects/drupal-rector-test checkout -- web/modules/contrib/ rm ~/projects/drupal-rector-test/rector-live-test.php ``` -### 5. Report results +### 5. Capture the PHPStan deprecation message + +While the contrib module is still installed and the pre-transform code is on +disk, run PHPStan against the file the rector matched and capture the +deprecation message PHPStan emits for the targeted symbol. This is the literal +string the rector "covers", and is what upgrade_status's +`DeprecationAnalyzer::isRectorCovered()` does an exact string match against +(after a small set of normalizations — see below). + +```bash +cd ~/projects/drupal-rector-test +ddev exec vendor/bin/phpstan analyse \ + web/modules/contrib//.php \ + --level=max --no-progress --error-format=raw 2>&1 \ + | grep -i "deprecated.*" +``` + +If no contrib file matched (or the symbol is already fully removed from +installed core so PHPStan emits "not found" rather than a deprecation), fall +back to a synthetic probe — see the `rector-extract-phpstan-error` skill's +"Synthetic probe" section for templates. + +**Normalize and store.** Pipe the raw message through the normalizer (which +applies the three transforms upgrade_status applies before its `in_array()` +lookup — whitespace collapse, `: in` → `. Deprecated in`, leading `\Drupal` +strip): + +```bash +ddev exec php scripts/normalize-phpstan-message.php "" +# or: +printf '' | ddev exec php scripts/normalize-phpstan-message.php +``` + +Add the normalized string to the rector source: + +- **Custom rector class** (`src/Drupal*/Rector/Deprecation/.php`) + — add or extend the `public const PHPSTAN_MESSAGES` array. One element per + distinct call shape the rector handles. + + ```php + public const PHPSTAN_MESSAGES = [ + 'Call to deprecated method foo() of class Drupal\Bar. Deprecated in drupal:11.4.0 ...', + ]; + ``` + +- **Config-only registration** (`config/drupal-*/drupal-*.N-deprecations.php`) + — add a `// PHPSTAN_MESSAGES :` comment block immediately + above the `ruleWithConfiguration(...)` call, one message per `//` line: + + ```php + // PHPSTAN_MESSAGES FunctionToServiceRector: + // Call to deprecated function foo(). Deprecated in drupal:11.4.0 and is removed from drupal:12.0.0. Use Drupal\Bar::baz() instead. + $rectorConfig->ruleWithConfiguration(FunctionToServiceRector::class, [ /* ... */ ]); + ``` + +Then regenerate the flat registry: + +```bash +ddev exec php scripts/generate-coverage-registry.php +``` + +This writes `config/coverage-registry.php` (a `return [...]` file mapping +rector short name → list of normalized messages). The registry is the +artifact a future upgrade_status PR will consume to replace its hardcoded +`$rector_covered` array. + +If PHPStan emits no deprecation for the symbol — symbol present but not +annotated `@deprecated`, or already fully removed — record a `TODO +PHPSTAN_MESSAGES :` comment with the reason instead of +guessing the message text. Do **not** synthesize the string from the +`@deprecated` docblock by hand: PHPStan's exact wording differs between +"Call to deprecated method", "Instantiation of deprecated class", "Class X +implements deprecated interface", etc. + +### 6. Report results For each tested module, report: ``` @@ -192,9 +266,9 @@ For each tested module, report: ``` For every module with **zero changes**, do not just say "no match" — always show the actual -code and explain why. See step 6. +code and explain why. See step 7. -### 6. Diagnose zero-match results +### 7. Diagnose zero-match results For **every** module that produced no changes, you must: diff --git a/config/coverage-registry.php b/config/coverage-registry.php new file mode 100644 index 000000000..41df9da42 --- /dev/null +++ b/config/coverage-registry.php @@ -0,0 +1,27 @@ + + array ( + 0 => 'Call to deprecated method expectDeprecation() of class Drupal\\KernelTests\\KernelTestBase. Deprecated in drupal:11.4.0 and is removed from drupal:12.0.0. Use $this->expectUserDeprecationMessage() or $this->expectUserDeprecationMessageMatches() instead.', + ), +); diff --git a/config/drupal-11/drupal-11.3-deprecations.php b/config/drupal-11/drupal-11.3-deprecations.php index 749d066a3..d461575c9 100644 --- a/config/drupal-11/drupal-11.3-deprecations.php +++ b/config/drupal-11/drupal-11.3-deprecations.php @@ -226,6 +226,13 @@ // https://www.drupal.org/node/3527501 (change record) // Drupal\block_content\Access\* class aliases deprecated in drupal:11.3.0, removed in drupal:12.0.0. // Replaced by their canonical Drupal\Core\Access homes. + // + // TODO PHPSTAN_MESSAGES RenameClassRector: capture against a Drupal 11.3.x + // test env (aliases are already gone from 11.4-dev, so live capture is + // not possible here). Expected shape from phpstan-deprecation-rules is + // either "Class MyBlock implements deprecated interface + // Drupal\block_content\Access\..." (for `implements`) or "Extending + // deprecated class Drupal\block_content\Access\..." (for `extends`). $rectorConfig->ruleWithConfiguration(RenameClassRector::class, [ 'Drupal\block_content\Access\AccessGroupAnd' => 'Drupal\Core\Access\AccessGroupAnd', 'Drupal\block_content\Access\DependentAccessInterface' => 'Drupal\Core\Access\DependentAccessInterface', diff --git a/scripts/generate-coverage-registry.php b/scripts/generate-coverage-registry.php new file mode 100644 index 000000000..5c6598ea6 --- /dev/null +++ b/scripts/generate-coverage-registry.php @@ -0,0 +1,217 @@ +Rector.php + * — public const PHPSTAN_MESSAGES on the rector class. + * + * config/drupal-{8,9,10,11}/.php + * — // PHPSTAN_MESSAGES: comment block above a ruleWithConfiguration() + * or rule() call. Used for config-only rector registrations (e.g. + * FunctionToServiceRector, RenameClassRector) where there is no + * custom rector class to attach a const to. + * + * Output: config/coverage-registry.php is a `return [ ... ]` file mapping + * each rector class name (short, e.g. "ReplaceSessionManagerDeleteRector") + * to an array of normalized PHPStan deprecation message strings. + * + * Consumers (e.g. a future upgrade_status PR) can require this file and + * flatten via array_merge(...array_values($registry)) for a drop-in + * replacement of DeprecationAnalyzer::isRectorCovered()'s hardcoded array. + */ +$repoRoot = realpath(__DIR__.'/..'); +if ($repoRoot === false) { + fwrite(STDERR, "Could not resolve repo root\n"); + exit(1); +} + +require_once $repoRoot.'/scripts/normalize-phpstan-message.php'; + +$autoload = $repoRoot.'/vendor/autoload.php'; +if (!is_file($autoload)) { + fwrite(STDERR, "Run `composer install` first — vendor/autoload.php is missing.\n"); + exit(1); +} +require_once $autoload; + +$registry = []; + +// --- Source 1: PHPSTAN_MESSAGES const on rector classes --------------------- +$srcGlobs = [ + $repoRoot.'/src/Drupal8/Rector/Deprecation/*.php', + $repoRoot.'/src/Drupal9/Rector/Deprecation/*.php', + $repoRoot.'/src/Drupal10/Rector/Deprecation/*.php', + $repoRoot.'/src/Drupal11/Rector/Deprecation/*.php', +]; +foreach ($srcGlobs as $glob) { + foreach (glob($glob) ?: [] as $file) { + $fqcn = fqcnFromFile($file); + if ($fqcn === null) { + continue; + } + try { + $reflection = new ReflectionClass($fqcn); + } catch (ReflectionException) { + continue; + } + if (!$reflection->hasConstant('PHPSTAN_MESSAGES')) { + continue; + } + $messages = $reflection->getConstant('PHPSTAN_MESSAGES'); + if (!is_array($messages) || $messages === []) { + continue; + } + $short = $reflection->getShortName(); + foreach ($messages as $msg) { + if (!is_string($msg) || $msg === '') { + continue; + } + $registry[$short][] = normalizePhpstanMessage($msg); + } + } +} + +// --- Source 2: // PHPSTAN_MESSAGES: blocks in config/drupal-N/*.php -------- +$configGlobs = [ + $repoRoot.'/config/drupal-8/*.php', + $repoRoot.'/config/drupal-9/*.php', + $repoRoot.'/config/drupal-10/*.php', + $repoRoot.'/config/drupal-11/*.php', +]; +foreach ($configGlobs as $glob) { + foreach (glob($glob) ?: [] as $file) { + foreach (extractConfigMessages($file) as $rector => $messages) { + foreach ($messages as $msg) { + $registry[$rector][] = normalizePhpstanMessage($msg); + } + } + } +} + +// Deduplicate + sort within each rector, sort rectors alphabetically. +foreach ($registry as $rector => $messages) { + $messages = array_values(array_unique($messages)); + sort($messages); + $registry[$rector] = $messages; +} +ksort($registry); + +// Emit the registry file. +$outPath = $repoRoot.'/config/coverage-registry.php'; +$header = <<<'PHP' +: + * // + * // + * $rectorConfig->ruleWithConfiguration(...); + * + * The rector name on the header line is required so one config file can + * register multiple rectors with separate message lists. + * + * @return array> rector short name => messages + */ +function extractConfigMessages(string $file): array +{ + $src = file_get_contents($file); + if ($src === false) { + return []; + } + $result = []; + if (!preg_match_all( + '!^[ \t]*//\s*PHPSTAN_MESSAGES\s+([A-Za-z0-9_]+):\s*$\n((?:[ \t]*//[^\n]*\n)+)!m', + $src, + $matches, + PREG_SET_ORDER + )) { + return []; + } + foreach ($matches as $m) { + $rector = $m[1]; + $messageLines = []; + foreach (preg_split('/\R/', trim($m[2])) as $line) { + // Strip the `//` prefix and one optional space. + if (!preg_match('!^\s*//\s?(.*)$!', $line, $lineMatch)) { + continue; + } + $content = rtrim($lineMatch[1]); + if ($content === '') { + continue; + } + $messageLines[] = $content; + } + // Each non-empty `//` line is one message. Multi-line messages are + // not supported in the comment-block shape — split them in source. + foreach ($messageLines as $msg) { + $result[$rector][] = $msg; + } + } + + return $result; +} diff --git a/scripts/normalize-phpstan-message.php b/scripts/normalize-phpstan-message.php new file mode 100644 index 000000000..b51dea083 --- /dev/null +++ b/scripts/normalize-phpstan-message.php @@ -0,0 +1,47 @@ += 4.x). + * + * Usage: + * php scripts/normalize-phpstan-message.php "" + * echo "" | php scripts/normalize-phpstan-message.php + */ +function normalizePhpstanMessage(string $error): string +{ + // 1. trim + collapse runs of whitespace + $error = preg_replace('!\s+!', ' ', trim($error)); + // 2. ": in" / ": as of" → ". Deprecated in" / ". Deprecated as of" + $error = preg_replace('!:\s+(in|as of)!', '. Deprecated \1', $error); + // 3. "Use \Drupal..." → "Use Drupal..." + $error = preg_replace('!(u|U)se \\\\Drupal!', '\1se Drupal', $error); + + return $error; +} + +if (PHP_SAPI === 'cli' && realpath($argv[0] ?? '') === __FILE__) { + $input = $argv[1] ?? stream_get_contents(STDIN); + if ($input === false || $input === '') { + fwrite(STDERR, "Usage: php scripts/normalize-phpstan-message.php \"\"\n"); + fwrite(STDERR, " echo \"\" | php scripts/normalize-phpstan-message.php\n"); + exit(2); + } + echo normalizePhpstanMessage($input).PHP_EOL; +} diff --git a/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector.php b/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector.php index 085e73fc8..ac2b4b905 100644 --- a/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector.php +++ b/src/Drupal11/Rector/Deprecation/ReplaceExpectDeprecationRector.php @@ -44,6 +44,20 @@ */ final class ReplaceExpectDeprecationRector extends AbstractDrupalCoreRector { + /** + * Verbatim PHPStan deprecation messages this rector covers. + * + * Stored in upgrade_status's normalized form (whitespace collapsed, + * ": in" → ". Deprecated in", leading "\Drupal" stripped) so they can be + * compared with DeprecationAnalyzer::isRectorCovered() via exact match. + * + * Capture method: synthetic probe extending KernelTestBase against + * Drupal 11.4-dev, then `scripts/normalize-phpstan-message.php`. + */ + public const PHPSTAN_MESSAGES = [ + 'Call to deprecated method expectDeprecation() of class Drupal\KernelTests\KernelTestBase. Deprecated in drupal:11.4.0 and is removed from drupal:12.0.0. Use $this->expectUserDeprecationMessage() or $this->expectUserDeprecationMessageMatches() instead.', + ]; + private const RENAME_MAP = [ 'expectDeprecation' => 'expectUserDeprecationMessage', 'expectDeprecationMessage' => 'expectUserDeprecationMessage', From b6810754d7b13bf6c8a1315de9320d120d623575 Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 25 May 2026 22:43:07 +0200 Subject: [PATCH 07/23] feat(Drupal11): Add ViewsBlockItemsPerPageNoneToNullRector for issue #3520946 Replaces $block->setConfigurationValue('items_per_page', 'none') with NULL for Views block plugins. The string 'none' was deprecated in drupal:11.2.0 and removed in drupal:12.0.0; NULL is the canonical value for inheriting the items-per-page setting from the view. See https://www.drupal.org/node/3520946 See https://www.drupal.org/node/3522240 (change record) --- config/drupal-11/drupal-11.2-deprecations.php | 8 ++ ...ViewsBlockItemsPerPageNoneToNullRector.php | 87 +++++++++++++++++++ ...sBlockItemsPerPageNoneToNullRectorTest.php | 26 ++++++ .../config/configured_rule.php | 11 +++ .../fixture/basic.php.inc | 11 +++ .../fixture/no_change_unrelated.php.inc | 33 +++++++ 6 files changed, 176 insertions(+) create mode 100644 src/Drupal11/Rector/Deprecation/ViewsBlockItemsPerPageNoneToNullRector.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ViewsBlockItemsPerPageNoneToNullRector/ViewsBlockItemsPerPageNoneToNullRectorTest.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ViewsBlockItemsPerPageNoneToNullRector/config/configured_rule.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ViewsBlockItemsPerPageNoneToNullRector/fixture/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ViewsBlockItemsPerPageNoneToNullRector/fixture/no_change_unrelated.php.inc diff --git a/config/drupal-11/drupal-11.2-deprecations.php b/config/drupal-11/drupal-11.2-deprecations.php index 46c94502a..0c367e736 100644 --- a/config/drupal-11/drupal-11.2-deprecations.php +++ b/config/drupal-11/drupal-11.2-deprecations.php @@ -16,6 +16,7 @@ use DrupalRector\Drupal11\Rector\Deprecation\ReplacePdoFetchConstantsRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceSessionWritesWithRequestSessionRector; use DrupalRector\Drupal11\Rector\Deprecation\StatementPrefetchIteratorFetchColumnRector; +use DrupalRector\Drupal11\Rector\Deprecation\ViewsBlockItemsPerPageNoneToNullRector; use DrupalRector\Rector\Deprecation\ClassConstantToClassConstantRector; use DrupalRector\Rector\Deprecation\ConstantToClassConstantRector; use DrupalRector\Rector\Deprecation\FunctionCallRemovalRector; @@ -268,4 +269,11 @@ 'Error', ), ]); + + // https://www.drupal.org/node/3520946 + // https://www.drupal.org/node/3522240 (change record) + // ViewsBlockBase::setConfigurationValue('items_per_page', 'none') deprecated in drupal:11.2.0, + // removed in drupal:12.0.0. Replaced by NULL, which is the canonical value for inheriting + // the items-per-page setting from the view. + $rectorConfig->rule(ViewsBlockItemsPerPageNoneToNullRector::class); }; diff --git a/src/Drupal11/Rector/Deprecation/ViewsBlockItemsPerPageNoneToNullRector.php b/src/Drupal11/Rector/Deprecation/ViewsBlockItemsPerPageNoneToNullRector.php new file mode 100644 index 000000000..6062a2c25 --- /dev/null +++ b/src/Drupal11/Rector/Deprecation/ViewsBlockItemsPerPageNoneToNullRector.php @@ -0,0 +1,87 @@ +setConfigurationValue('items_per_page', 'none') with NULL for Views block plugins. + * + * The string 'none' was deprecated in drupal:11.2.0 and removed in drupal:12.0.0; + * NULL is the canonical value to inherit the items-per-page setting from the view. + * The transformed code is safe to run on all Drupal versions, so no BC wrapping is required. + * + * @see https://www.drupal.org/node/3520946 + * @see https://www.drupal.org/node/3522240 + */ +final class ViewsBlockItemsPerPageNoneToNullRector extends AbstractRector +{ + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + "Replace \$block->setConfigurationValue('items_per_page', 'none') with NULL for Views block plugins.", + [ + new CodeSample( + <<<'CODE_BEFORE' +$block->setConfigurationValue('items_per_page', 'none'); +CODE_BEFORE, + <<<'CODE_AFTER' +$block->setConfigurationValue('items_per_page', NULL); +CODE_AFTER + ), + ] + ); + } + + /** @return array> */ + public function getNodeTypes(): array + { + return [MethodCall::class]; + } + + /** @param MethodCall $node */ + public function refactor(Node $node): ?Node + { + if (!$this->isName($node->name, 'setConfigurationValue')) { + return null; + } + + if (count($node->args) < 2) { + return null; + } + + $firstArg = $node->args[0]; + if (!$firstArg instanceof Arg || !$firstArg->value instanceof String_) { + return null; + } + if ($firstArg->value->value !== 'items_per_page') { + return null; + } + + $secondArg = $node->args[1]; + if (!$secondArg instanceof Arg || !$secondArg->value instanceof String_) { + return null; + } + if ($secondArg->value->value !== 'none') { + return null; + } + + if (!$this->isObjectType($node->var, new ObjectType('Drupal\views\Plugin\Block\ViewsBlockBase'))) { + return null; + } + + $node->args[1] = new Arg(new ConstFetch(new Node\Name('NULL'))); + + return $node; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/ViewsBlockItemsPerPageNoneToNullRector/ViewsBlockItemsPerPageNoneToNullRectorTest.php b/tests/src/Drupal11/Rector/Deprecation/ViewsBlockItemsPerPageNoneToNullRector/ViewsBlockItemsPerPageNoneToNullRectorTest.php new file mode 100644 index 000000000..3c7c67fcc --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ViewsBlockItemsPerPageNoneToNullRector/ViewsBlockItemsPerPageNoneToNullRectorTest.php @@ -0,0 +1,26 @@ +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/Drupal11/Rector/Deprecation/ViewsBlockItemsPerPageNoneToNullRector/config/configured_rule.php b/tests/src/Drupal11/Rector/Deprecation/ViewsBlockItemsPerPageNoneToNullRector/config/configured_rule.php new file mode 100644 index 000000000..422bb753e --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ViewsBlockItemsPerPageNoneToNullRector/config/configured_rule.php @@ -0,0 +1,11 @@ +setConfigurationValue('items_per_page', 'none'); +?> +----- +setConfigurationValue('items_per_page', NULL); +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ViewsBlockItemsPerPageNoneToNullRector/fixture/no_change_unrelated.php.inc b/tests/src/Drupal11/Rector/Deprecation/ViewsBlockItemsPerPageNoneToNullRector/fixture/no_change_unrelated.php.inc new file mode 100644 index 000000000..e3a619bd9 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ViewsBlockItemsPerPageNoneToNullRector/fixture/no_change_unrelated.php.inc @@ -0,0 +1,33 @@ +setConfigurationValue('items_per_page', 'none'); + +// Different first arg — must not change. +/** @var \Drupal\views\Plugin\Block\ViewsBlockBase $block */ +$block->setConfigurationValue('label', 'none'); + +// Different second arg value — must not change. +$block->setConfigurationValue('items_per_page', 5); + +// Different method name on the same type — must not change. +$block->setConfiguration(['items_per_page' => 'none']); +?> +----- +setConfigurationValue('items_per_page', 'none'); + +// Different first arg — must not change. +/** @var \Drupal\views\Plugin\Block\ViewsBlockBase $block */ +$block->setConfigurationValue('label', 'none'); + +// Different second arg value — must not change. +$block->setConfigurationValue('items_per_page', 5); + +// Different method name on the same type — must not change. +$block->setConfiguration(['items_per_page' => 'none']); +?> From 4d8347a3c4682063c3fd7eb884be7566c779e9fb Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 25 May 2026 22:44:07 +0200 Subject: [PATCH 08/23] docs: fixup cr/issue links for rules --- config/drupal-10/drupal-10.3-deprecations.php | 1 + config/drupal-11/drupal-11.1-deprecations.php | 1 + config/drupal-11/drupal-11.2-deprecations.php | 1 + config/drupal-11/drupal-11.3-deprecations.php | 2 ++ config/drupal-11/drupal-11.4-deprecations.php | 5 +++++ 5 files changed, 10 insertions(+) diff --git a/config/drupal-10/drupal-10.3-deprecations.php b/config/drupal-10/drupal-10.3-deprecations.php index f743ed1e3..f07b3d306 100644 --- a/config/drupal-10/drupal-10.3-deprecations.php +++ b/config/drupal-10/drupal-10.3-deprecations.php @@ -41,6 +41,7 @@ ]); // https://www.drupal.org/node/3426517 + // https://www.drupal.org/node/3575575 // FileSystemInterface::EXISTS_* deprecated in drupal:10.3.0, removed in drupal:12.0.0. // Replaced by \Drupal\Core\File\FileExists enum cases. $rectorConfig->ruleWithConfiguration(ClassConstantToClassConstantRector::class, [ diff --git a/config/drupal-11/drupal-11.1-deprecations.php b/config/drupal-11/drupal-11.1-deprecations.php index 6b9440a94..9fe18d04f 100644 --- a/config/drupal-11/drupal-11.1-deprecations.php +++ b/config/drupal-11/drupal-11.1-deprecations.php @@ -89,6 +89,7 @@ // Replaced by \Drupal\Core\Theme\ThemeCommonElements::commonElements(). // https://www.drupal.org/node/2350849 // https://www.drupal.org/node/3268441 (change record) + // https://www.drupal.org/node/3574424 // image_filter_keyword() deprecated in drupal:11.1.0, removed in drupal:12.0.0. // Replaced by \Drupal\Component\Utility\Image::getKeywordOffset(). $rectorConfig->ruleWithConfiguration(FunctionToStaticRector::class, [ diff --git a/config/drupal-11/drupal-11.2-deprecations.php b/config/drupal-11/drupal-11.2-deprecations.php index 0c367e736..8abaf75ab 100644 --- a/config/drupal-11/drupal-11.2-deprecations.php +++ b/config/drupal-11/drupal-11.2-deprecations.php @@ -68,6 +68,7 @@ // https://www.drupal.org/node/3501136 // https://www.drupal.org/node/3504125 (change record) + // https://www.drupal.org/node/2340341 // template_preprocess() deprecated in drupal:11.2.0, removed in drupal:12.0.0. // https://www.drupal.org/node/3521059 // https://www.drupal.org/node/3522119 (change record) diff --git a/config/drupal-11/drupal-11.3-deprecations.php b/config/drupal-11/drupal-11.3-deprecations.php index d461575c9..dcfdbe298 100644 --- a/config/drupal-11/drupal-11.3-deprecations.php +++ b/config/drupal-11/drupal-11.3-deprecations.php @@ -66,6 +66,7 @@ ]); // https://www.drupal.org/node/3501136 + // https://www.drupal.org/node/3571382 // https://www.drupal.org/node/3504125 (change record) // template_preprocess_layout() deprecated in drupal:11.3.0, removed in drupal:12.0.0. // Replaced by \Drupal\layout_discovery\Hook\LayoutDiscoveryThemeHooks::preprocessLayout(). @@ -116,6 +117,7 @@ // https://www.drupal.org/node/3548326 // https://www.drupal.org/node/3548329 (change record) + // https://www.drupal.org/node/3574424 // responsive_image_* functions deprecated in drupal:11.3.0, removed in drupal:12.0.0. // Replaced by \Drupal::service(ResponsiveImageBuilder::class)->method() calls. $rectorConfig->ruleWithConfiguration(FunctionToServiceRector::class, [ diff --git a/config/drupal-11/drupal-11.4-deprecations.php b/config/drupal-11/drupal-11.4-deprecations.php index 4a58ceeab..6a2acd924 100644 --- a/config/drupal-11/drupal-11.4-deprecations.php +++ b/config/drupal-11/drupal-11.4-deprecations.php @@ -94,6 +94,7 @@ // https://www.drupal.org/node/3570849 // https://www.drupal.org/node/3570851 (change record) + // https://www.drupal.org/node/3577376 // SessionManager::delete() deprecated in drupal:11.4.0, removed in drupal:12.0.0. // Replaced by \Drupal\Core\Session\UserSessionRepositoryInterface::deleteAll(). $rectorConfig->ruleWithConfiguration(ReplaceSessionManagerDeleteRector::class, [ @@ -247,6 +248,10 @@ // https://www.drupal.org/node/3566783 (change record) // block_theme_initialize() deprecated in drupal:11.4.0, removed in drupal:13.0.0. // Logic moved to protected BlockHooks::themeInitialize(); external callers must drop the call. + // https://www.drupal.org/node/3570235 + // syslog_facility_list() and syslog_logging_settings_submit() deprecated in drupal:11.4.0, removed in drupal:13.0.0. + // https://www.drupal.org/node/3570238 + // taxonomy_build_node_index() and taxonomy_delete_node_index() deprecated in drupal:11.4.0, removed in drupal:13.0.0. $rectorConfig->ruleWithConfiguration(FunctionCallRemovalRector::class, [ new FunctionCallRemovalConfiguration('views_ui_contextual_links_suppress'), new FunctionCallRemovalConfiguration('views_ui_contextual_links_suppress_push'), From 2c2f40d4aab35baa11997df5cc45c18143f07cd0 Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 25 May 2026 22:49:57 +0200 Subject: [PATCH 09/23] feat(Drupal11): Add GetDrupalRootToRootPropertyRector for issue #3589047 Replaces deprecated DrupalTestCaseTrait::getDrupalRoot() instance calls with direct $this->root property access. The method was deprecated in drupal:11.4.0 and removed in drupal:13.0.0. The rule targets subclasses of BrowserTestBase, KernelTestBase, and UnitTestCase, which all inherit the $root property via DrupalTestCaseTrait. Static calls and BuildTestBase (which overrides the method with a non-deprecated implementation) are intentionally left untouched. See https://www.drupal.org/node/3589047 See https://www.drupal.org/node/3574112 (change record) --- config/drupal-11/drupal-11.4-deprecations.php | 7 ++ .../GetDrupalRootToRootPropertyRector.php | 84 +++++++++++++++++++ .../GetDrupalRootToRootPropertyRectorTest.php | 26 ++++++ .../config/configured_rule.php | 11 +++ .../fixture/basic.php.inc | 23 +++++ .../fixture/no_change_unrelated.php.inc | 29 +++++++ 6 files changed, 180 insertions(+) create mode 100644 src/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector/GetDrupalRootToRootPropertyRectorTest.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector/config/configured_rule.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector/fixture/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector/fixture/no_change_unrelated.php.inc diff --git a/config/drupal-11/drupal-11.4-deprecations.php b/config/drupal-11/drupal-11.4-deprecations.php index 6a2acd924..2e43e74f4 100644 --- a/config/drupal-11/drupal-11.4-deprecations.php +++ b/config/drupal-11/drupal-11.4-deprecations.php @@ -5,6 +5,7 @@ use DrupalRector\Drupal11\Rector\Deprecation\CheckMarkupToProcessedTextRector; use DrupalRector\Drupal11\Rector\Deprecation\DeprecatedFilterFunctionsRector; use DrupalRector\Drupal11\Rector\Deprecation\FilterFormatFunctionsToServiceRector; +use DrupalRector\Drupal11\Rector\Deprecation\GetDrupalRootToRootPropertyRector; use DrupalRector\Drupal11\Rector\Deprecation\GetOriginalClassToGetDecoratedClassesRector; use DrupalRector\Drupal11\Rector\Deprecation\LocaleCompareIncToServiceRector; use DrupalRector\Drupal11\Rector\Deprecation\MediaFilterFormatEditFormValidateRector; @@ -488,4 +489,10 @@ $rectorConfig->ruleWithConfiguration(LocaleCompareIncToServiceRector::class, [ new DrupalIntroducedVersionConfiguration('11.4.0'), ]); + + // https://www.drupal.org/node/3589047 + // https://www.drupal.org/node/3574112 (change record) + // DrupalTestCaseTrait::getDrupalRoot() deprecated in drupal:11.4.0, removed in drupal:13.0.0. + // Replaced by direct access to the $this->root property on Drupal base test classes. + $rectorConfig->rule(GetDrupalRootToRootPropertyRector::class); }; diff --git a/src/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector.php b/src/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector.php new file mode 100644 index 000000000..5c423c4be --- /dev/null +++ b/src/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector.php @@ -0,0 +1,84 @@ +root property access. + * + * The method was deprecated in drupal:11.4.0 and removed in drupal:13.0.0. The rule targets + * subclasses of BrowserTestBase, KernelTestBase, and UnitTestCase — all of which expose the + * pre-existing $root property via DrupalTestCaseTrait. Static calls (static::getDrupalRoot()) + * are intentionally left untouched: callers in @dataProvider methods cannot reach $this->root + * and require a structural rewrite. BuildTestBase overrides getDrupalRoot() with a non-deprecated + * implementation and is also left untouched. + * + * @see https://www.drupal.org/node/3589047 + * @see https://www.drupal.org/node/3574112 + */ +final class GetDrupalRootToRootPropertyRector extends AbstractRector +{ + /** + * Base test classes that use DrupalTestCaseTrait and do not override getDrupalRoot(). + * + * @var array + */ + private const BASE_TEST_CLASSES = [ + 'Drupal\Tests\BrowserTestBase', + 'Drupal\KernelTests\KernelTestBase', + 'Drupal\Tests\UnitTestCase', + ]; + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Replace deprecated DrupalTestCaseTrait::getDrupalRoot() calls with $this->root property access in Drupal test classes.', + [ + new CodeSample( + <<<'CODE_BEFORE' +$dir = $this->getDrupalRoot() . '/core/tests/fixtures'; +CODE_BEFORE, + <<<'CODE_AFTER' +$dir = $this->root . '/core/tests/fixtures'; +CODE_AFTER + ), + ] + ); + } + + /** @return array> */ + public function getNodeTypes(): array + { + return [MethodCall::class]; + } + + /** @param MethodCall $node */ + public function refactor(Node $node): ?Node + { + if (!$this->isName($node->name, 'getDrupalRoot')) { + return null; + } + + if (count($node->args) !== 0) { + return null; + } + + foreach (self::BASE_TEST_CLASSES as $fqcn) { + if ($this->isObjectType($node->var, new ObjectType($fqcn))) { + return new PropertyFetch($node->var, new Identifier('root')); + } + } + + return null; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector/GetDrupalRootToRootPropertyRectorTest.php b/tests/src/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector/GetDrupalRootToRootPropertyRectorTest.php new file mode 100644 index 000000000..18a2a87f4 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector/GetDrupalRootToRootPropertyRectorTest.php @@ -0,0 +1,26 @@ +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/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector/config/configured_rule.php b/tests/src/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector/config/configured_rule.php new file mode 100644 index 000000000..ff5c2f190 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector/config/configured_rule.php @@ -0,0 +1,11 @@ +getDrupalRoot() . '/core/tests/fixtures'; + +/** @var \Drupal\KernelTests\KernelTestBase $kernel */ +$path = $kernel->getDrupalRoot(); + +/** @var \Drupal\Tests\UnitTestCase $unit */ +echo $unit->getDrupalRoot(); +?> +----- +root . '/core/tests/fixtures'; + +/** @var \Drupal\KernelTests\KernelTestBase $kernel */ +$path = $kernel->root; + +/** @var \Drupal\Tests\UnitTestCase $unit */ +echo $unit->root; +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector/fixture/no_change_unrelated.php.inc b/tests/src/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector/fixture/no_change_unrelated.php.inc new file mode 100644 index 000000000..b9932b894 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector/fixture/no_change_unrelated.php.inc @@ -0,0 +1,29 @@ +getDrupalRoot(); + +// BuildTestBase overrides getDrupalRoot() with a non-deprecated implementation; must not change. +/** @var \Drupal\BuildTests\Framework\BuildTestBase $build */ +$dir = $build->getDrupalRoot(); + +// Method called with arguments — must not change (deprecated method is parameterless). +/** @var \Drupal\Tests\BrowserTestBase $browser */ +$dir = $browser->getDrupalRoot('extra'); +?> +----- +getDrupalRoot(); + +// BuildTestBase overrides getDrupalRoot() with a non-deprecated implementation; must not change. +/** @var \Drupal\BuildTests\Framework\BuildTestBase $build */ +$dir = $build->getDrupalRoot(); + +// Method called with arguments — must not change (deprecated method is parameterless). +/** @var \Drupal\Tests\BrowserTestBase $browser */ +$dir = $browser->getDrupalRoot('extra'); +?> From b5064c9da8b77f3c71d4b50134b3e0f6df0d8c6b Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 25 May 2026 22:56:47 +0200 Subject: [PATCH 10/23] feat(Drupal11): Add PHPSTAN_MESSAGES to GetDrupalRootToRootPropertyRector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Captures the three real PHPStan deprecation messages this rector covers — one per base test class (BrowserTestBase, KernelTestBase, UnitTestCase). The BrowserTestBase variant was verified against automatic_updates 4.1.x; the KernelTestBase variant against project_browser 2.1.x. The UnitTestCase variant mirrors the deterministic PHPStan format. Regenerates config/coverage-registry.php for upgrade_status consumption. --- config/coverage-registry.php | 6 ++++++ .../Deprecation/GetDrupalRootToRootPropertyRector.php | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/config/coverage-registry.php b/config/coverage-registry.php index 41df9da42..197f8da12 100644 --- a/config/coverage-registry.php +++ b/config/coverage-registry.php @@ -20,6 +20,12 @@ */ return array ( + 'GetDrupalRootToRootPropertyRector' => + array ( + 0 => 'Call to deprecated method getDrupalRoot() of class Drupal\\KernelTests\\KernelTestBase. Deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Access $this->root directly.', + 1 => 'Call to deprecated method getDrupalRoot() of class Drupal\\Tests\\BrowserTestBase. Deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Access $this->root directly.', + 2 => 'Call to deprecated method getDrupalRoot() of class Drupal\\Tests\\UnitTestCase. Deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Access $this->root directly.', + ), 'ReplaceExpectDeprecationRector' => array ( 0 => 'Call to deprecated method expectDeprecation() of class Drupal\\KernelTests\\KernelTestBase. Deprecated in drupal:11.4.0 and is removed from drupal:12.0.0. Use $this->expectUserDeprecationMessage() or $this->expectUserDeprecationMessageMatches() instead.', diff --git a/src/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector.php b/src/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector.php index 5c423c4be..fda5466ed 100644 --- a/src/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector.php +++ b/src/Drupal11/Rector/Deprecation/GetDrupalRootToRootPropertyRector.php @@ -28,6 +28,12 @@ */ final class GetDrupalRootToRootPropertyRector extends AbstractRector { + public const PHPSTAN_MESSAGES = [ + 'Call to deprecated method getDrupalRoot() of class Drupal\Tests\BrowserTestBase. Deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Access $this->root directly.', + 'Call to deprecated method getDrupalRoot() of class Drupal\KernelTests\KernelTestBase. Deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Access $this->root directly.', + 'Call to deprecated method getDrupalRoot() of class Drupal\Tests\UnitTestCase. Deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Access $this->root directly.', + ]; + /** * Base test classes that use DrupalTestCaseTrait and do not override getDrupalRoot(). * From f0807701799ac242c4455f46dab1190d7c66cbab Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 25 May 2026 23:11:13 +0200 Subject: [PATCH 11/23] feat(Drupal11): Add ReplaceCommentPreviewConstantsRector for issue #3538660 Replaces DRUPAL_DISABLED/OPTIONAL/REQUIRED with CommentPreviewMode enum cases in CommentTestBase::setCommentPreview() calls. Passing an int to this method was deprecated in drupal:11.3.0 and is removed in drupal:13.0.0; the new CommentPreviewMode enum only exists on Drupal >= 11.3.0, so the replacement is BC-wrapped via AbstractDrupalCoreRector + DeprecationHelper. The type guard targets Drupal\Tests\comment\Functional\CommentTestBase (the actual owner of setCommentPreview, not the NodeTypeInterface the upstream digest incorrectly pointed at). A minimal CommentTestBase stub is added so PHPStan can resolve the receiver type during rector tests. A TODO PHPSTAN_MESSAGES comment is included because PHPStan emits no deprecation for this call: setCommentPreview() itself is not @deprecated (the trigger_error fires at runtime when an int is passed), and phpstan does not flag file-scope const usage for DRUPAL_DISABLED/OPTIONAL/REQUIRED. See https://www.drupal.org/node/3538660 See https://www.drupal.org/node/3538678 (change record) --- config/drupal-11/drupal-11.3-deprecations.php | 9 ++ .../ReplaceCommentPreviewConstantsRector.php | 124 ++++++++++++++++++ .../comment/Functional/CommentTestBase.php | 18 +++ ...placeCommentPreviewConstantsRectorTest.php | 46 +++++++ .../config/configured_rule.php | 14 ++ .../fixture-below-version/basic.php.inc | 11 ++ .../fixture/basic.php.inc | 17 +++ .../fixture/no_change_unrelated.php.inc | 33 +++++ 8 files changed, 272 insertions(+) create mode 100644 src/Drupal11/Rector/Deprecation/ReplaceCommentPreviewConstantsRector.php create mode 100644 stubs/Drupal/Tests/comment/Functional/CommentTestBase.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceCommentPreviewConstantsRector/ReplaceCommentPreviewConstantsRectorTest.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceCommentPreviewConstantsRector/config/configured_rule.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceCommentPreviewConstantsRector/fixture-below-version/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceCommentPreviewConstantsRector/fixture/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceCommentPreviewConstantsRector/fixture/no_change_unrelated.php.inc diff --git a/config/drupal-11/drupal-11.3-deprecations.php b/config/drupal-11/drupal-11.3-deprecations.php index dcfdbe298..891395be9 100644 --- a/config/drupal-11/drupal-11.3-deprecations.php +++ b/config/drupal-11/drupal-11.3-deprecations.php @@ -9,6 +9,7 @@ use DrupalRector\Drupal11\Rector\Deprecation\NodeStorageDeprecatedMethodsRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveRootFromConvertDbUrlRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceCommentManagerGetCountNewCommentsRector; +use DrupalRector\Drupal11\Rector\Deprecation\ReplaceCommentPreviewConstantsRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceNodeAccessViewAllNodesRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceNodeAddBodyFieldRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceNodeModuleProceduralFunctionsRector; @@ -241,4 +242,12 @@ 'Drupal\block_content\Access\RefinableDependentAccessInterface' => 'Drupal\Core\Access\RefinableDependentAccessInterface', 'Drupal\block_content\Access\RefinableDependentAccessTrait' => 'Drupal\Core\Access\RefinableDependentAccessTrait', ]); + + // https://www.drupal.org/node/3538660 + // https://www.drupal.org/node/3538678 (change record) + // Passing an int to CommentTestBase::setCommentPreview() deprecated in drupal:11.3.0, removed in drupal:13.0.0. + // Replaced by Drupal\comment\CommentPreviewMode enum cases. + $rectorConfig->ruleWithConfiguration(ReplaceCommentPreviewConstantsRector::class, [ + new DrupalIntroducedVersionConfiguration('11.3.0'), + ]); }; diff --git a/src/Drupal11/Rector/Deprecation/ReplaceCommentPreviewConstantsRector.php b/src/Drupal11/Rector/Deprecation/ReplaceCommentPreviewConstantsRector.php new file mode 100644 index 000000000..19ac3bad9 --- /dev/null +++ b/src/Drupal11/Rector/Deprecation/ReplaceCommentPreviewConstantsRector.php @@ -0,0 +1,124 @@ += 11.3.0, so the replacement is wrapped in DeprecationHelper::backwardsCompatibleCall(). + * + * @see https://www.drupal.org/node/3538660 + * @see https://www.drupal.org/node/3538678 + */ +final class ReplaceCommentPreviewConstantsRector extends AbstractDrupalCoreRector +{ + // TODO PHPSTAN_MESSAGES ReplaceCommentPreviewConstantsRector: PHPStan emits + // no deprecation for the targeted call. The deprecation is triggered at + // runtime via @trigger_error inside CommentTestBase::setCommentPreview() + // when the $mode parameter is an int, but the method itself is not + // @deprecated. The DRUPAL_DISABLED/OPTIONAL/REQUIRED constants in + // system.module carry @deprecated docblocks but phpstan-deprecation-rules + // does not flag file-scope const usage. No string is available to add to + // the coverage registry; upgrade_status will need a different mechanism + // (deprecation message text rather than PHPStan) to detect this case. + + /** + * @var array + */ + private const CONSTANT_TO_ENUM_CASE = [ + 'DRUPAL_DISABLED' => 'Disabled', + 'DRUPAL_OPTIONAL' => 'Optional', + 'DRUPAL_REQUIRED' => 'Required', + ]; + + /** + * @var array|DrupalIntroducedVersionConfiguration[] + */ + protected array $configuration; + + public function configure(array $configuration): void + { + foreach ($configuration as $value) { + if (!$value instanceof DrupalIntroducedVersionConfiguration) { + throw new \InvalidArgumentException(sprintf('Each configuration item must be an instance of "%s"', DrupalIntroducedVersionConfiguration::class)); + } + } + + parent::configure($configuration); + } + + /** @return array> */ + public function getNodeTypes(): array + { + return [MethodCall::class]; + } + + public function refactorWithConfiguration(Node $node, VersionedConfigurationInterface $configuration): ?Node + { + if (!$node instanceof MethodCall) { + return null; + } + + if (!$this->isName($node->name, 'setCommentPreview')) { + return null; + } + + if (!isset($node->args[0]) || !$node->args[0] instanceof Arg) { + return null; + } + + $argValue = $node->args[0]->value; + if (!$argValue instanceof ConstFetch) { + return null; + } + + $constName = $this->getName($argValue); + if ($constName === null || !isset(self::CONSTANT_TO_ENUM_CASE[$constName])) { + return null; + } + + if (!$this->isObjectType($node->var, new ObjectType('Drupal\Tests\comment\Functional\CommentTestBase'))) { + return null; + } + + $enumCase = self::CONSTANT_TO_ENUM_CASE[$constName]; + + $newArgs = $node->args; + $newArgs[0] = new Arg(new ClassConstFetch(new FullyQualified('Drupal\comment\CommentPreviewMode'), $enumCase)); + + return new MethodCall($node->var, $node->name, $newArgs); + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Replace DRUPAL_DISABLED/OPTIONAL/REQUIRED with CommentPreviewMode enum in CommentTestBase::setCommentPreview() calls.', [ + new ConfiguredCodeSample( + <<<'CODE_BEFORE' +$this->setCommentPreview(DRUPAL_DISABLED); +CODE_BEFORE, + <<<'CODE_AFTER' +$this->setCommentPreview(\Drupal\comment\CommentPreviewMode::Disabled); +CODE_AFTER, + [new DrupalIntroducedVersionConfiguration('11.3.0')] + ), + ]); + } +} diff --git a/stubs/Drupal/Tests/comment/Functional/CommentTestBase.php b/stubs/Drupal/Tests/comment/Functional/CommentTestBase.php new file mode 100644 index 000000000..5c00969b8 --- /dev/null +++ b/stubs/Drupal/Tests/comment/Functional/CommentTestBase.php @@ -0,0 +1,18 @@ +make(DrupalRectorSettings::class)->setDrupalVersion('99.99.99'); + $this->doTestFile($filePath); + } + + /** + * @return \Iterator<> + */ + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/fixture'); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('provideDataBelowVersion')] + public function testBelowVersion(string $filePath): void + { + static::getContainer()->make(DrupalRectorSettings::class)->setDrupalVersion('1.0.0'); + $this->doTestFile($filePath); + } + + /** + * @return \Iterator<> + */ + public static function provideDataBelowVersion(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/fixture-below-version'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config/configured_rule.php'; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceCommentPreviewConstantsRector/config/configured_rule.php b/tests/src/Drupal11/Rector/Deprecation/ReplaceCommentPreviewConstantsRector/config/configured_rule.php new file mode 100644 index 000000000..bf3f01f0e --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceCommentPreviewConstantsRector/config/configured_rule.php @@ -0,0 +1,14 @@ +setCommentPreview(DRUPAL_DISABLED); +?> +----- +setCommentPreview(DRUPAL_DISABLED); +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceCommentPreviewConstantsRector/fixture/basic.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceCommentPreviewConstantsRector/fixture/basic.php.inc new file mode 100644 index 000000000..0fdad8af4 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceCommentPreviewConstantsRector/fixture/basic.php.inc @@ -0,0 +1,17 @@ +setCommentPreview(DRUPAL_DISABLED); +$base->setCommentPreview(DRUPAL_OPTIONAL); +$base->setCommentPreview(DRUPAL_REQUIRED); +$base->setCommentPreview(DRUPAL_DISABLED, 'field_comments'); +?> +----- + $base->setCommentPreview(\Drupal\comment\CommentPreviewMode::Disabled), fn() => $base->setCommentPreview(DRUPAL_DISABLED)); +\Drupal\Component\Utility\DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.3.0', fn() => $base->setCommentPreview(\Drupal\comment\CommentPreviewMode::Optional), fn() => $base->setCommentPreview(DRUPAL_OPTIONAL)); +\Drupal\Component\Utility\DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.3.0', fn() => $base->setCommentPreview(\Drupal\comment\CommentPreviewMode::Required), fn() => $base->setCommentPreview(DRUPAL_REQUIRED)); +\Drupal\Component\Utility\DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.3.0', fn() => $base->setCommentPreview(\Drupal\comment\CommentPreviewMode::Disabled, 'field_comments'), fn() => $base->setCommentPreview(DRUPAL_DISABLED, 'field_comments')); +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceCommentPreviewConstantsRector/fixture/no_change_unrelated.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceCommentPreviewConstantsRector/fixture/no_change_unrelated.php.inc new file mode 100644 index 000000000..a2e84e785 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceCommentPreviewConstantsRector/fixture/no_change_unrelated.php.inc @@ -0,0 +1,33 @@ +setCommentPreview(DRUPAL_DISABLED); + +// Different method on a CommentTestBase — must not change. +/** @var \Drupal\Tests\comment\Functional\CommentTestBase $base */ +$base->setCommentSettings('preview', DRUPAL_DISABLED, '', 'comment'); + +// First arg is a different constant — must not change. +$base->setCommentPreview(SOME_OTHER_CONSTANT); + +// First arg is a literal int — must not change (rector only handles named constants). +$base->setCommentPreview(0); +?> +----- +setCommentPreview(DRUPAL_DISABLED); + +// Different method on a CommentTestBase — must not change. +/** @var \Drupal\Tests\comment\Functional\CommentTestBase $base */ +$base->setCommentSettings('preview', DRUPAL_DISABLED, '', 'comment'); + +// First arg is a different constant — must not change. +$base->setCommentPreview(SOME_OTHER_CONSTANT); + +// First arg is a literal int — must not change (rector only handles named constants). +$base->setCommentPreview(0); +?> From dd340800cbab01c43b9a3abcb1d23d12f30be773 Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 25 May 2026 23:29:02 +0200 Subject: [PATCH 12/23] feat(Drupal11): Add DrupalGetHeadersAssocArrayRector for issue #3440169 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrites deprecated UiHelperTrait::drupalGet() $headers patterns: - ['Header-Name: value'] → ['Header-Name' => 'value'] - ['Header-Name' => NULL] → ['Header-Name' => ''] Deprecated in drupal:11.1.0, removed in drupal:12.0.0. The associative format has always been the documented contract; no new Drupal API is required, so no BC wrapping is needed. Type guard: Drupal\Tests\BrowserTestBase (UiHelperTrait fires the deprecation; KernelTestBase uses HttpKernelUiHelperTrait, which does not). Validated against pager_serializer 8.x-1.x (Functional/Views/StyleSerializerTest.php). --- config/drupal-11/drupal-11.1-deprecations.php | 9 ++ .../DrupalGetHeadersAssocArrayRector.php | 125 ++++++++++++++++++ .../DrupalGetHeadersAssocArrayRectorTest.php | 26 ++++ .../config/configured_rule.php | 11 ++ .../fixture/basic.php.inc | 71 ++++++++++ .../fixture/no_change_unrelated.php.inc | 41 ++++++ 6 files changed, 283 insertions(+) create mode 100644 src/Drupal11/Rector/Deprecation/DrupalGetHeadersAssocArrayRector.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/DrupalGetHeadersAssocArrayRector/DrupalGetHeadersAssocArrayRectorTest.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/DrupalGetHeadersAssocArrayRector/config/configured_rule.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/DrupalGetHeadersAssocArrayRector/fixture/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/DrupalGetHeadersAssocArrayRector/fixture/no_change_unrelated.php.inc diff --git a/config/drupal-11/drupal-11.1-deprecations.php b/config/drupal-11/drupal-11.1-deprecations.php index 9fe18d04f..37765b8fa 100644 --- a/config/drupal-11/drupal-11.1-deprecations.php +++ b/config/drupal-11/drupal-11.1-deprecations.php @@ -3,6 +3,7 @@ declare(strict_types=1); use DrupalRector\Drupal11\Rector\Deprecation\BlockContentTestBaseStringToArrayRector; +use DrupalRector\Drupal11\Rector\Deprecation\DrupalGetHeadersAssocArrayRector; use DrupalRector\Drupal11\Rector\Deprecation\MovePointerToMouseOverRector; use DrupalRector\Drupal11\Rector\Deprecation\PluginBaseIsConfigurableRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveModuleHandlerDeprecatedMethodsRector; @@ -96,4 +97,12 @@ new FunctionToStaticConfiguration('11.1.0', 'drupal_common_theme', 'Drupal\Core\Theme\ThemeCommonElements', 'commonElements'), new FunctionToStaticConfiguration('11.1.0', 'image_filter_keyword', 'Drupal\Component\Utility\Image', 'getKeywordOffset'), ]); + + // https://www.drupal.org/node/3440169 + // https://www.drupal.org/node/3456178 (change record: integer-keyed headers) + // https://www.drupal.org/node/3456233 (change record: null header values) + // UiHelperTrait::drupalGet() $headers as indexed colon-separated strings or null values + // deprecated in drupal:11.1.0, removed in drupal:12.0.0. Replaced by the associative array + // format ['Header-Name' => 'value'], with empty strings in place of null. + $rectorConfig->rule(DrupalGetHeadersAssocArrayRector::class); }; diff --git a/src/Drupal11/Rector/Deprecation/DrupalGetHeadersAssocArrayRector.php b/src/Drupal11/Rector/Deprecation/DrupalGetHeadersAssocArrayRector.php new file mode 100644 index 000000000..347f06bb0 --- /dev/null +++ b/src/Drupal11/Rector/Deprecation/DrupalGetHeadersAssocArrayRector.php @@ -0,0 +1,125 @@ + ['Header-Name' => 'value'] + * - Null header values => empty string ''. + * + * Deprecated in drupal:11.1.0 and removed in drupal:12.0.0. The replacement is + * the documented associative format, which has always been valid; no new + * Drupal API is involved, so no BC wrapping is needed. + * + * @see https://www.drupal.org/node/3440169 + * @see https://www.drupal.org/node/3456178 + * @see https://www.drupal.org/node/3456233 + */ +class DrupalGetHeadersAssocArrayRector extends AbstractRector +{ + // TODO PHPSTAN_MESSAGES DrupalGetHeadersAssocArrayRector: PHPStan emits no + // deprecation for the targeted call. The deprecation is triggered at + // runtime via @trigger_error inside UiHelperTrait::drupalGet() when a + // header name is an integer or a header value is null. The method itself + // carries no @deprecated annotation, so phpstan-deprecation-rules does + // not flag callers. No string is available to add here. + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Convert drupalGet() $headers from indexed colon-separated strings or null values to an associative array, as required by Drupal 11.1.0.', + [ + new CodeSample( + <<<'CODE_BEFORE' +$this->drupalGet('/path', [], ['X-Requested-With: XMLHttpRequest']); +$this->drupalGet('', [], ['Accept-Language' => NULL]); +CODE_BEFORE, + <<<'CODE_AFTER' +$this->drupalGet('/path', [], ['X-Requested-With' => 'XMLHttpRequest']); +$this->drupalGet('', [], ['Accept-Language' => '']); +CODE_AFTER + ), + ] + ); + } + + /** @return array> */ + public function getNodeTypes(): array + { + return [MethodCall::class]; + } + + /** + * @param MethodCall $node + */ + public function refactor(Node $node): ?Node + { + if (!$this->isName($node->name, 'drupalGet')) { + return null; + } + + // The deprecation fires only from Drupal\Tests\UiHelperTrait::drupalGet(), + // which is used by BrowserTestBase (and its subclasses, including + // WebDriverTestBase). KernelTestBase uses HttpKernelUiHelperTrait, whose + // drupalGet() does not emit this deprecation, so we exclude it by typing. + if (!$this->isObjectType($node->var, new ObjectType('Drupal\Tests\BrowserTestBase'))) { + return null; + } + + // $headers is the third argument (index 2). + if (count($node->args) < 3) { + return null; + } + + $headersArg = $node->args[2]; + if (!$headersArg instanceof Arg) { + return null; + } + + $headersArray = $headersArg->value; + if (!$headersArray instanceof Array_) { + return null; + } + + $changed = false; + + foreach ($headersArray->items as $item) { + // Pattern 1: integer-keyed item with 'Header-Name: value' string. + // Deprecated since drupal:11.1.0 — see https://www.drupal.org/node/3456178 + if ($item->key === null && $item->value instanceof String_) { + $raw = $item->value->value; + if (str_contains($raw, ':')) { + [$headerName, $headerValue] = explode(':', $raw, 2); + $item->key = new String_(trim($headerName)); + $item->value = new String_(trim($headerValue)); + $changed = true; + continue; + } + } + + // Pattern 2: null header value. + // Deprecated since drupal:11.1.0 — see https://www.drupal.org/node/3456233 + if ($item->value instanceof ConstFetch + && strtolower((string) $item->value->name) === 'null' + ) { + $item->value = new String_(''); + $changed = true; + } + } + + return $changed ? $node : null; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/DrupalGetHeadersAssocArrayRector/DrupalGetHeadersAssocArrayRectorTest.php b/tests/src/Drupal11/Rector/Deprecation/DrupalGetHeadersAssocArrayRector/DrupalGetHeadersAssocArrayRectorTest.php new file mode 100644 index 000000000..3484d5a37 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/DrupalGetHeadersAssocArrayRector/DrupalGetHeadersAssocArrayRectorTest.php @@ -0,0 +1,26 @@ +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/Drupal11/Rector/Deprecation/DrupalGetHeadersAssocArrayRector/config/configured_rule.php b/tests/src/Drupal11/Rector/Deprecation/DrupalGetHeadersAssocArrayRector/config/configured_rule.php new file mode 100644 index 000000000..d85fbfa6b --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/DrupalGetHeadersAssocArrayRector/config/configured_rule.php @@ -0,0 +1,11 @@ +drupalGet('/path', [], ['X-Requested-With: XMLHttpRequest']); + } + + /** + * Tests null header values. + */ + public function testNullHeader() { + $this->drupalGet('', [], ['Accept-Language' => NULL]); + } + + /** + * Tests a mix of indexed and null headers in one call. + */ + public function testMixed() { + $this->drupalGet('/p', [], ['X-Foo: bar', 'Accept-Language' => NULL, 'X-Already' => 'ok']); + } + + /** + * Tests a header value that itself contains a colon (e.g. Authorization). + */ + public function testColonInValue() { + $this->drupalGet('/p', [], ['Authorization: Bearer abc:def']); + } +} +?> +----- +drupalGet('/path', [], ['X-Requested-With' => 'XMLHttpRequest']); + } + + /** + * Tests null header values. + */ + public function testNullHeader() { + $this->drupalGet('', [], ['Accept-Language' => '']); + } + + /** + * Tests a mix of indexed and null headers in one call. + */ + public function testMixed() { + $this->drupalGet('/p', [], ['X-Foo' => 'bar', 'Accept-Language' => '', 'X-Already' => 'ok']); + } + + /** + * Tests a header value that itself contains a colon (e.g. Authorization). + */ + public function testColonInValue() { + $this->drupalGet('/p', [], ['Authorization' => 'Bearer abc:def']); + } +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/DrupalGetHeadersAssocArrayRector/fixture/no_change_unrelated.php.inc b/tests/src/Drupal11/Rector/Deprecation/DrupalGetHeadersAssocArrayRector/fixture/no_change_unrelated.php.inc new file mode 100644 index 000000000..84f87665d --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/DrupalGetHeadersAssocArrayRector/fixture/no_change_unrelated.php.inc @@ -0,0 +1,41 @@ +drupalGet('/path', [], ['X-Requested-With: XMLHttpRequest']); + $client->drupalGet('', [], ['Accept-Language' => NULL]); +} + +// Untyped variable: rector cannot prove it is a BrowserTestBase instance. +function untyped_caller($thing) { + $thing->drupalGet('/path', [], ['X-Requested-With: XMLHttpRequest']); +} +?> +----- +drupalGet('/path', [], ['X-Requested-With: XMLHttpRequest']); + $client->drupalGet('', [], ['Accept-Language' => NULL]); +} + +// Untyped variable: rector cannot prove it is a BrowserTestBase instance. +function untyped_caller($thing) { + $thing->drupalGet('/path', [], ['X-Requested-With: XMLHttpRequest']); +} +?> From dd146c9b282d40bcc06156bf46fed33b5004c577 Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 25 May 2026 23:29:39 +0200 Subject: [PATCH 13/23] docs: changelog entry for DrupalGetHeadersAssocArrayRector --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac0ab4c1e..988a876aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,20 @@ release-by-release. ### Added +- **`DrupalGetHeadersAssocArrayRector`** — converts the two deprecated + `UiHelperTrait::drupalGet()` `$headers` argument shapes to the documented + associative format: integer-keyed colon-separated strings + (`['X-Requested-With: XMLHttpRequest']`) are split to + `['X-Requested-With' => 'XMLHttpRequest']`, and `null` values + (`['Accept-Language' => NULL]`) become empty strings + (`['Accept-Language' => '']`). Guarded against `Drupal\Tests\BrowserTestBase` + so `KernelTestBase` (which uses `HttpKernelUiHelperTrait` and does not emit + this deprecation) is left alone. Deprecated in drupal:11.1.0, removed in + drupal:12.0.0; replacement is plain PHP so no BC wrapping is needed. + Live-tested against `pager_serializer`. + [#3440169](https://www.drupal.org/i/3440169) / + [CR (indexed headers)](https://www.drupal.org/node/3456178) / + [CR (null values)](https://www.drupal.org/node/3456233). - **`ReplaceHideShowWithPrintedRector`** — replaces statement-level calls to the deprecated global `hide()` and `show()` functions (deprecated in drupal:11.4.0, removed in drupal:13.0.0) with direct `$element['#printed'] = TRUE/FALSE` From 92b295638f4e42e476f262bfb8eff12911b4c405 Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 25 May 2026 23:39:00 +0200 Subject: [PATCH 14/23] feat(Drupal11): Add EntityFormModeEmptyDescriptionToNullRector for issue #3448457 Rewrites EntityFormMode::create([..., 'description' => '', ...]) to use NULL instead of an empty string. Setting the description property of an EntityFormMode to '' was deprecated in drupal:11.2.0 and must be NULL in drupal:12.0.0. Matches both the short class name (use-imported) and the fully-qualified \Drupal\Core\Entity\Entity\EntityFormMode::create() form via an isName() static-call guard. Sibling classes (EntityViewMode), non-empty descriptions, already-migrated NULL values, and other array keys are all left untouched. The replacement is plain PHP, so no BC wrapping is needed. The deprecated pattern is genuinely rare in contrib (16 modules call EntityFormMode::create() but none with 'description' => ''); validated via synthetic probe. --- config/drupal-11/drupal-11.2-deprecations.php | 8 ++ ...tyFormModeEmptyDescriptionToNullRector.php | 125 ++++++++++++++++++ ...rmModeEmptyDescriptionToNullRectorTest.php | 26 ++++ .../config/configured_rule.php | 11 ++ .../fixture/basic.php.inc | 63 +++++++++ .../fixture/no_change_unrelated.php.inc | 99 ++++++++++++++ 6 files changed, 332 insertions(+) create mode 100644 src/Drupal11/Rector/Deprecation/EntityFormModeEmptyDescriptionToNullRector.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/EntityFormModeEmptyDescriptionToNullRector/EntityFormModeEmptyDescriptionToNullRectorTest.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/EntityFormModeEmptyDescriptionToNullRector/config/configured_rule.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/EntityFormModeEmptyDescriptionToNullRector/fixture/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/EntityFormModeEmptyDescriptionToNullRector/fixture/no_change_unrelated.php.inc diff --git a/config/drupal-11/drupal-11.2-deprecations.php b/config/drupal-11/drupal-11.2-deprecations.php index 8abaf75ab..371c111ff 100644 --- a/config/drupal-11/drupal-11.2-deprecations.php +++ b/config/drupal-11/drupal-11.2-deprecations.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use DrupalRector\Drupal11\Rector\Deprecation\EntityFormModeEmptyDescriptionToNullRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveCacheTagChecksumAssertionsRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveHandlerBaseDefineExtraOptionsRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveModuleHandlerAddModuleCallsRector; @@ -277,4 +278,11 @@ // removed in drupal:12.0.0. Replaced by NULL, which is the canonical value for inheriting // the items-per-page setting from the view. $rectorConfig->rule(ViewsBlockItemsPerPageNoneToNullRector::class); + + // https://www.drupal.org/node/3448457 + // https://www.drupal.org/node/3452144 (change record) + // EntityFormMode::create() with 'description' => '' deprecated in drupal:11.2.0, + // removed in drupal:12.0.0. Replaced by NULL, which is the canonical "no description" value + // for entity display modes. + $rectorConfig->rule(EntityFormModeEmptyDescriptionToNullRector::class); }; diff --git a/src/Drupal11/Rector/Deprecation/EntityFormModeEmptyDescriptionToNullRector.php b/src/Drupal11/Rector/Deprecation/EntityFormModeEmptyDescriptionToNullRector.php new file mode 100644 index 000000000..c4aca189d --- /dev/null +++ b/src/Drupal11/Rector/Deprecation/EntityFormModeEmptyDescriptionToNullRector.php @@ -0,0 +1,125 @@ + 'user.test', + 'label' => 'Test', + 'description' => '', + 'targetEntityType' => 'user', +]); +CODE_BEFORE, + <<<'CODE_AFTER' +EntityFormMode::create([ + 'id' => 'user.test', + 'label' => 'Test', + 'description' => NULL, + 'targetEntityType' => 'user', +]); +CODE_AFTER + ), + ] + ); + } + + /** @return array> */ + public function getNodeTypes(): array + { + return [StaticCall::class]; + } + + /** + * @param StaticCall $node + */ + public function refactor(Node $node): ?Node + { + if (!$this->isName($node->name, 'create')) { + return null; + } + + if (!$node->class instanceof Name) { + return null; + } + + // Static-call guard: match against the fully-qualified class name. + if (!$this->isName($node->class, 'Drupal\Core\Entity\Entity\EntityFormMode')) { + return null; + } + + if (empty($node->args)) { + return null; + } + + $firstArg = $node->args[0]; + if (!$firstArg instanceof Arg) { + return null; + } + + if (!$firstArg->value instanceof Array_) { + return null; + } + + $changed = false; + foreach ($firstArg->value->items as $item) { + if (!$item->key instanceof String_) { + continue; + } + + if ($item->key->value !== 'description') { + continue; + } + + if (!$item->value instanceof String_) { + continue; + } + + if ($item->value->value !== '') { + continue; + } + + $item->value = new ConstFetch(new Name('NULL')); + $changed = true; + } + + return $changed ? $node : null; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/EntityFormModeEmptyDescriptionToNullRector/EntityFormModeEmptyDescriptionToNullRectorTest.php b/tests/src/Drupal11/Rector/Deprecation/EntityFormModeEmptyDescriptionToNullRector/EntityFormModeEmptyDescriptionToNullRectorTest.php new file mode 100644 index 000000000..d0c01e96d --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/EntityFormModeEmptyDescriptionToNullRector/EntityFormModeEmptyDescriptionToNullRectorTest.php @@ -0,0 +1,26 @@ +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/Drupal11/Rector/Deprecation/EntityFormModeEmptyDescriptionToNullRector/config/configured_rule.php b/tests/src/Drupal11/Rector/Deprecation/EntityFormModeEmptyDescriptionToNullRector/config/configured_rule.php new file mode 100644 index 000000000..a29bc1d02 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/EntityFormModeEmptyDescriptionToNullRector/config/configured_rule.php @@ -0,0 +1,11 @@ + 'user.test', + 'label' => 'Test', + 'description' => '', + 'targetEntityType' => 'user', + ]); +} + +function create_fqcn() { + \Drupal\Core\Entity\Entity\EntityFormMode::create([ + 'id' => 'node.preview', + 'label' => 'Preview', + 'description' => '', + 'targetEntityType' => 'node', + ]); +} + +function create_only_label() { + EntityFormMode::create([ + 'id' => 'taxonomy_term.simple', + 'label' => 'Simple', + 'description' => '', + 'targetEntityType' => 'taxonomy_term', + ]); +} +?> +----- + 'user.test', + 'label' => 'Test', + 'description' => NULL, + 'targetEntityType' => 'user', + ]); +} + +function create_fqcn() { + \Drupal\Core\Entity\Entity\EntityFormMode::create([ + 'id' => 'node.preview', + 'label' => 'Preview', + 'description' => NULL, + 'targetEntityType' => 'node', + ]); +} + +function create_only_label() { + EntityFormMode::create([ + 'id' => 'taxonomy_term.simple', + 'label' => 'Simple', + 'description' => NULL, + 'targetEntityType' => 'taxonomy_term', + ]); +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/EntityFormModeEmptyDescriptionToNullRector/fixture/no_change_unrelated.php.inc b/tests/src/Drupal11/Rector/Deprecation/EntityFormModeEmptyDescriptionToNullRector/fixture/no_change_unrelated.php.inc new file mode 100644 index 000000000..fca5885db --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/EntityFormModeEmptyDescriptionToNullRector/fixture/no_change_unrelated.php.inc @@ -0,0 +1,99 @@ + 'user.test', + 'label' => 'Test', + 'description' => '', + 'targetEntityType' => 'user', + ]); +} + +function unrelated_method() { + // Same class, different method — must not be transformed. + EntityFormMode::load('user.test'); +} + +function nonempty_description() { + // Description has a real value — must not be transformed. + EntityFormMode::create([ + 'id' => 'user.test', + 'label' => 'Test', + 'description' => 'A real description.', + 'targetEntityType' => 'user', + ]); +} + +function description_already_null() { + // Already migrated — must not be re-touched. + EntityFormMode::create([ + 'id' => 'user.test', + 'label' => 'Test', + 'description' => NULL, + 'targetEntityType' => 'user', + ]); +} + +function unrelated_key() { + // Empty-string elsewhere in the array — must not be transformed. + EntityFormMode::create([ + 'id' => 'user.test', + 'label' => '', + 'targetEntityType' => 'user', + ]); +} +?> +----- + 'user.test', + 'label' => 'Test', + 'description' => '', + 'targetEntityType' => 'user', + ]); +} + +function unrelated_method() { + // Same class, different method — must not be transformed. + EntityFormMode::load('user.test'); +} + +function nonempty_description() { + // Description has a real value — must not be transformed. + EntityFormMode::create([ + 'id' => 'user.test', + 'label' => 'Test', + 'description' => 'A real description.', + 'targetEntityType' => 'user', + ]); +} + +function description_already_null() { + // Already migrated — must not be re-touched. + EntityFormMode::create([ + 'id' => 'user.test', + 'label' => 'Test', + 'description' => NULL, + 'targetEntityType' => 'user', + ]); +} + +function unrelated_key() { + // Empty-string elsewhere in the array — must not be transformed. + EntityFormMode::create([ + 'id' => 'user.test', + 'label' => '', + 'targetEntityType' => 'user', + ]); +} +?> From 1341e228dfc8dd56412b84d84349c2c9bc370577 Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 25 May 2026 23:39:09 +0200 Subject: [PATCH 15/23] docs: changelog entry for EntityFormModeEmptyDescriptionToNullRector --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 988a876aa..0d595b473 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,17 @@ release-by-release. ### Added +- **`EntityFormModeEmptyDescriptionToNullRector`** — rewrites + `EntityFormMode::create([..., 'description' => '', ...])` to use `NULL` + instead of the empty string. Setting the description property of an + `EntityFormMode` to `''` was deprecated in drupal:11.2.0 and must be `NULL` + in drupal:12.0.0. Matches both the short class name (`use`-imported) and + the fully-qualified `\Drupal\Core\Entity\Entity\EntityFormMode::create()` + form, and leaves unrelated classes (e.g. `EntityViewMode`), non-empty + descriptions, and already-migrated NULL values untouched. The replacement + is plain PHP, so no BC wrapping is needed. + [#3448457](https://www.drupal.org/i/3448457) / + [CR](https://www.drupal.org/node/3452144). - **`DrupalGetHeadersAssocArrayRector`** — converts the two deprecated `UiHelperTrait::drupalGet()` `$headers` argument shapes to the documented associative format: integer-keyed colon-separated strings From 74ce49b20b3c03af3492ca6caf462c0d0db9e084 Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 25 May 2026 23:55:03 +0200 Subject: [PATCH 16/23] feat(Drupal11): Add ViewsConfigUpdaterClassResolverToServiceRector for issue #3529274 Rewrites \Drupal::classResolver(\Drupal\views\ViewsConfigUpdater::class) to \Drupal::service(\Drupal\views\ViewsConfigUpdater::class). In drupal:11.3.0 ViewsConfigUpdater was registered as a service; classResolver() returns a fresh instance on each call, so state set via setDeprecationsEnabled(FALSE) was lost across hook invocations. The new call only resolves on Drupal >= 11.3.0 (the service isn't registered on older versions), so the replacement is BC-wrapped via DeprecationHelper::backwardsCompatibleCall(). Three layered isName guards ensure only the targeted call shape is touched: receiver must be \Drupal, method must be classResolver, and the single argument must be \Drupal\views\ViewsConfigUpdater::class. The argument guard correctly disambiguates against module subclasses sharing the short name (validated against entity_hierarchy, which uses \Drupal\entity_hierarchy\Update\ViewsConfigUpdater and is correctly skipped). Live-tested against tripal. --- config/drupal-11/drupal-11.3-deprecations.php | 11 ++ ...figUpdaterClassResolverToServiceRector.php | 126 ++++++++++++++++++ ...pdaterClassResolverToServiceRectorTest.php | 40 ++++++ .../config/configured_rule.php | 14 ++ .../fixture-below-version/basic.php.inc | 29 ++++ .../fixture/basic.php.inc | 29 ++++ .../fixture/no_change_unrelated.php.inc | 63 +++++++++ 7 files changed, 312 insertions(+) create mode 100644 src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector/ViewsConfigUpdaterClassResolverToServiceRectorTest.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector/config/configured_rule.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector/fixture-below-version/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector/fixture/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector/fixture/no_change_unrelated.php.inc diff --git a/config/drupal-11/drupal-11.3-deprecations.php b/config/drupal-11/drupal-11.3-deprecations.php index 891395be9..18f03b260 100644 --- a/config/drupal-11/drupal-11.3-deprecations.php +++ b/config/drupal-11/drupal-11.3-deprecations.php @@ -17,6 +17,7 @@ use DrupalRector\Drupal11\Rector\Deprecation\ReplaceThemeGetSettingRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceTwigExtensionRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceUserSessionNamePropertyRector; +use DrupalRector\Drupal11\Rector\Deprecation\ViewsConfigUpdaterClassResolverToServiceRector; use DrupalRector\Rector\Deprecation\ConstantToClassConstantRector; use DrupalRector\Rector\Deprecation\FunctionCallRemovalRector; use DrupalRector\Rector\Deprecation\FunctionToFirstArgMethodRector; @@ -250,4 +251,14 @@ $rectorConfig->ruleWithConfiguration(ReplaceCommentPreviewConstantsRector::class, [ new DrupalIntroducedVersionConfiguration('11.3.0'), ]); + + // https://www.drupal.org/node/3529274 + // https://www.drupal.org/node/3530638 (change record) + // ViewsConfigUpdater registered as a service in drupal:11.3.0. Replace + // \Drupal::classResolver(ViewsConfigUpdater::class) with + // \Drupal::service(ViewsConfigUpdater::class) so state set via + // setDeprecationsEnabled(FALSE) persists across hook invocations. + $rectorConfig->ruleWithConfiguration(ViewsConfigUpdaterClassResolverToServiceRector::class, [ + new DrupalIntroducedVersionConfiguration('11.3.0'), + ]); }; diff --git a/src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector.php b/src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector.php new file mode 100644 index 000000000..cf7f39437 --- /dev/null +++ b/src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector.php @@ -0,0 +1,126 @@ += 11.3.0 because + * the service isn't registered on older versions, so the replacement is + * BC-wrapped. + * + * @see https://www.drupal.org/node/3529274 + * @see https://www.drupal.org/node/3530638 + */ +class ViewsConfigUpdaterClassResolverToServiceRector extends AbstractDrupalCoreRector +{ + // TODO PHPSTAN_MESSAGES ViewsConfigUpdaterClassResolverToServiceRector: + // PHPStan emits no deprecation for this call. \Drupal::classResolver() + // itself is not @deprecated — only its use to resolve + // Drupal\views\ViewsConfigUpdater is now considered wrong, and that + // constraint is documented only in the change record. No string is + // available to add here. + + /** @var DrupalIntroducedVersionConfiguration[] */ + protected array $configuration; + + public function configure(array $configuration): void + { + foreach ($configuration as $value) { + if (!$value instanceof DrupalIntroducedVersionConfiguration) { + throw new \InvalidArgumentException(sprintf('Each configuration item must be an instance of "%s"', DrupalIntroducedVersionConfiguration::class)); + } + } + parent::configure($configuration); + } + + /** @return array> */ + public function getNodeTypes(): array + { + return [StaticCall::class]; + } + + public function refactorWithConfiguration(Node $node, VersionedConfigurationInterface $configuration) + { + if (!$node instanceof StaticCall) { + return null; + } + + // Must be \Drupal::something(). + if (!$this->isName($node->class, 'Drupal')) { + return null; + } + + // Must be ::classResolver(). + if (!$this->isName($node->name, 'classResolver')) { + return null; + } + + if (count($node->args) !== 1) { + return null; + } + + $arg = $node->args[0]; + if (!$arg instanceof Arg) { + return null; + } + + $value = $arg->value; + if (!$value instanceof ClassConstFetch) { + return null; + } + + if (!$this->isName($value->name, 'class')) { + return null; + } + + // The argument must be Drupal\views\ViewsConfigUpdater::class. + if (!$this->isName($value->class, 'Drupal\views\ViewsConfigUpdater')) { + return null; + } + + // Clone before mutating: the base class re-reads $node when building + // the BC fallback closure, and a direct mutation would propagate the + // new identifier into both sides of the DeprecationHelper call. + $new = clone $node; + $new->name = new Identifier('service'); + + return $new; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Replace \Drupal::classResolver(ViewsConfigUpdater::class) with \Drupal::service(ViewsConfigUpdater::class) since ViewsConfigUpdater is now registered as a service in drupal:11.3.0.', + [ + new ConfiguredCodeSample( + <<<'CODE_BEFORE' +$view_config_updater = \Drupal::classResolver(\Drupal\views\ViewsConfigUpdater::class); +CODE_BEFORE, + <<<'CODE_AFTER' +$view_config_updater = \Drupal::service(\Drupal\views\ViewsConfigUpdater::class); +CODE_AFTER, + [new DrupalIntroducedVersionConfiguration('11.3.0')] + ), + ] + ); + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector/ViewsConfigUpdaterClassResolverToServiceRectorTest.php b/tests/src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector/ViewsConfigUpdaterClassResolverToServiceRectorTest.php new file mode 100644 index 000000000..463ea11ab --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector/ViewsConfigUpdaterClassResolverToServiceRectorTest.php @@ -0,0 +1,40 @@ +make(DrupalRectorSettings::class)->setDrupalVersion('99.99.99'); + $this->doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/fixture'); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('provideDataBelowVersion')] + public function testBelowVersion(string $filePath): void + { + static::getContainer()->make(DrupalRectorSettings::class)->setDrupalVersion('1.0.0'); + $this->doTestFile($filePath); + } + + public static function provideDataBelowVersion(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/fixture-below-version'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config/configured_rule.php'; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector/config/configured_rule.php b/tests/src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector/config/configured_rule.php new file mode 100644 index 000000000..e154d3448 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector/config/configured_rule.php @@ -0,0 +1,14 @@ +setDeprecationsEnabled(FALSE); +} + +function example_fqcn() { + $updater = \Drupal::classResolver(\Drupal\views\ViewsConfigUpdater::class); + $updater->setDeprecationsEnabled(FALSE); +} +?> +----- +setDeprecationsEnabled(FALSE); +} + +function example_fqcn() { + $updater = \Drupal::classResolver(\Drupal\views\ViewsConfigUpdater::class); + $updater->setDeprecationsEnabled(FALSE); +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector/fixture/basic.php.inc b/tests/src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector/fixture/basic.php.inc new file mode 100644 index 000000000..2bbe0aeb0 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector/fixture/basic.php.inc @@ -0,0 +1,29 @@ +setDeprecationsEnabled(FALSE); +} + +function example_fqcn() { + $updater = \Drupal::classResolver(\Drupal\views\ViewsConfigUpdater::class); + $updater->setDeprecationsEnabled(FALSE); +} +?> +----- + \Drupal::service(ViewsConfigUpdater::class), fn() => \Drupal::classResolver(ViewsConfigUpdater::class)); + $view_config_updater->setDeprecationsEnabled(FALSE); +} + +function example_fqcn() { + $updater = \Drupal\Component\Utility\DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.3.0', fn() => \Drupal::service(\Drupal\views\ViewsConfigUpdater::class), fn() => \Drupal::classResolver(\Drupal\views\ViewsConfigUpdater::class)); + $updater->setDeprecationsEnabled(FALSE); +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector/fixture/no_change_unrelated.php.inc b/tests/src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector/fixture/no_change_unrelated.php.inc new file mode 100644 index 000000000..88d058b68 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ViewsConfigUpdaterClassResolverToServiceRector/fixture/no_change_unrelated.php.inc @@ -0,0 +1,63 @@ + +----- + From bf7d2a827a3b3dfcd070bad9782a7f0e12396de9 Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 25 May 2026 23:55:07 +0200 Subject: [PATCH 17/23] docs: changelog entry for ViewsConfigUpdaterClassResolverToServiceRector --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d595b473..d63d64586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,20 @@ release-by-release. ### Added +- **`ViewsConfigUpdaterClassResolverToServiceRector`** — rewrites + `\Drupal::classResolver(\Drupal\views\ViewsConfigUpdater::class)` to + `\Drupal::service(\Drupal\views\ViewsConfigUpdater::class)`. In + drupal:11.3.0 `ViewsConfigUpdater` was registered as a service; + `classResolver()` returns a fresh instance on each call, so state set via + `setDeprecationsEnabled(FALSE)` was lost across hook invocations. The new + call only resolves on Drupal ≥ 11.3.0 (the service isn't registered on + older versions), so the replacement is BC-wrapped with + `DeprecationHelper::backwardsCompatibleCall()`. Three layered guards + ensure only the targeted call shape is touched: receiver must be + `\Drupal`, method must be `classResolver`, and the single argument must be + `\Drupal\views\ViewsConfigUpdater::class`. + [#3529274](https://www.drupal.org/i/3529274) / + [CR](https://www.drupal.org/node/3530638). - **`EntityFormModeEmptyDescriptionToNullRector`** — rewrites `EntityFormMode::create([..., 'description' => '', ...])` to use `NULL` instead of the empty string. Setting the description property of an From 5beb4432e446d0d8b1cb614ffb356df977bc1030 Mon Sep 17 00:00:00 2001 From: bjorn Date: Tue, 26 May 2026 08:59:38 +0200 Subject: [PATCH 18/23] feat(Drupal11): Add ReplaceLocaleTranslationPathConfigRector for issue #3571593 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrites chained \Drupal::config('locale.settings')->get('translation.path') (and configFactory()/this->config() variants) to \Drupal\Core\Site\Settings::get( 'locale_translation_path', 'public://translations'). The config key was deprecated in drupal:11.4.0 and is removed in drupal:13.0.0; the customised translation path must be set as $settings['locale_translation_path'] in settings.php. BC-wrapped via DeprecationHelper so the rewritten code still runs on pre-11.4 Drupal — though users must migrate the value to settings.php before running the rule, otherwise the new branch silently falls back to the default. Matches purely structurally: two literal keys ('locale.settings' and 'translation.path') must appear in the expected positions, mirroring ReplaceSystemPerformanceGzipKeyRector. Standalone $config->get('translation.path'), unrelated config names, and unrelated keys are left untouched. PHPStan / upgrade_status cannot detect this deprecation — the deprecated symbol is the config key, not a PHP API with @deprecated or trigger_error(). A TODO PHPSTAN_MESSAGES comment in the source documents the gap. --- config/drupal-11/drupal-11.4-deprecations.php | 9 + ...placeLocaleTranslationPathConfigRector.php | 161 ++++++++++++++++++ ...eLocaleTranslationPathConfigRectorTest.php | 46 +++++ .../config/configured_rule.php | 14 ++ .../fixture-below-version/basic.php.inc | 9 + .../config_factory_static.php.inc | 9 + .../this_config_method.php.inc | 29 ++++ .../fixture/basic.php.inc | 9 + .../fixture/config_factory_static.php.inc | 9 + .../fixture/no_change_standalone_get.php.inc | 17 ++ .../no_change_unrelated_config.php.inc | 11 ++ .../fixture/no_change_unrelated_key.php.inc | 11 ++ .../fixture/this_config_method.php.inc | 29 ++++ 13 files changed, 363 insertions(+) create mode 100644 src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/ReplaceLocaleTranslationPathConfigRectorTest.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/config/configured_rule.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture-below-version/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture-below-version/config_factory_static.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture-below-version/this_config_method.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/config_factory_static.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/no_change_standalone_get.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/no_change_unrelated_config.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/no_change_unrelated_key.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/this_config_method.php.inc diff --git a/config/drupal-11/drupal-11.4-deprecations.php b/config/drupal-11/drupal-11.4-deprecations.php index 2e43e74f4..5810684d1 100644 --- a/config/drupal-11/drupal-11.4-deprecations.php +++ b/config/drupal-11/drupal-11.4-deprecations.php @@ -21,6 +21,7 @@ use DrupalRector\Drupal11\Rector\Deprecation\ReplaceEntityReferenceRecursiveLimitRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceExpectDeprecationRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceHideShowWithPrintedRector; +use DrupalRector\Drupal11\Rector\Deprecation\ReplaceLocaleTranslationPathConfigRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceRecipeRunnerInstallModuleRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceSessionManagerDeleteRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceSystemPerformanceGzipKeyRector; @@ -495,4 +496,12 @@ // DrupalTestCaseTrait::getDrupalRoot() deprecated in drupal:11.4.0, removed in drupal:13.0.0. // Replaced by direct access to the $this->root property on Drupal base test classes. $rectorConfig->rule(GetDrupalRootToRootPropertyRector::class); + + // https://www.drupal.org/node/3571593 + // https://www.drupal.org/node/3571594 (change record) + // locale.settings:translation.path config key deprecated in drupal:11.4.0, removed in drupal:13.0.0. + // Replaced by \Drupal\Core\Site\Settings::get('locale_translation_path', 'public://translations'). + $rectorConfig->ruleWithConfiguration(ReplaceLocaleTranslationPathConfigRector::class, [ + new DrupalIntroducedVersionConfiguration('11.4.0'), + ]); }; diff --git a/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector.php b/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector.php new file mode 100644 index 000000000..d54d73af6 --- /dev/null +++ b/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector.php @@ -0,0 +1,161 @@ += 11.4 + * it must live in settings.php. The wrapper switches branches on Drupal + * version, not on where the value is stored. Before running this rule, + * confirm that any customised translation path has been moved to + * $settings['locale_translation_path'] in settings.php; otherwise the new + * branch will silently fall back to the default 'public://translations' + * even when the config still holds the customised value. + * + * @see https://www.drupal.org/node/3571593 + * @see https://www.drupal.org/node/3571594 + */ +class ReplaceLocaleTranslationPathConfigRector extends AbstractDrupalCoreRector +{ + // TODO PHPSTAN_MESSAGES ReplaceLocaleTranslationPathConfigRector: + // PHPStan cannot detect this deprecation. The deprecated symbol is the + // config KEY 'locale.settings:translation.path' — neither the `get()` + // method nor the key string carries a PHP-level @deprecated annotation, + // and Drupal core does not trigger_error() on access. The deprecation + // notice is emitted only by locale.post_update.php after update. + + private const CONFIG_ACCESSOR_METHODS = ['config', 'get', 'getEditable']; + + private const SETTINGS_CLASS = 'Drupal\\Core\\Site\\Settings'; + + private const SETTINGS_KEY = 'locale_translation_path'; + + private const SETTINGS_DEFAULT = 'public://translations'; + + /** + * @var array|DrupalIntroducedVersionConfiguration[] + */ + protected array $configuration; + + public function configure(array $configuration): void + { + foreach ($configuration as $value) { + if (!$value instanceof DrupalIntroducedVersionConfiguration) { + throw new \InvalidArgumentException(sprintf('Each configuration item must be an instance of "%s"', DrupalIntroducedVersionConfiguration::class)); + } + } + parent::configure($configuration); + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + "Replace deprecated \\Drupal::config('locale.settings')->get('translation.path') with Settings::get('locale_translation_path')", + [ + new ConfiguredCodeSample( + "\\Drupal::config('locale.settings')->get('translation.path');", + "\\Drupal\\Core\\Site\\Settings::get('locale_translation_path', 'public://translations');", + [new DrupalIntroducedVersionConfiguration('11.4.0')] + ), + ] + ); + } + + /** @return array> */ + public function getNodeTypes(): array + { + return [MethodCall::class]; + } + + protected function refactorWithConfiguration(Node $node, VersionedConfigurationInterface $configuration): ?Node + { + assert($node instanceof MethodCall); + if (!$this->isName($node->name, 'get')) { + return null; + } + if (count($node->args) < 1) { + return null; + } + $firstArg = $node->args[0]; + if (!$firstArg instanceof Arg) { + return null; + } + if (!$firstArg->value instanceof String_) { + return null; + } + if ($firstArg->value->value !== 'translation.path') { + return null; + } + if (!$this->isLocaleSettingsConfigReceiver($node->var)) { + return null; + } + + return new StaticCall( + new FullyQualified(self::SETTINGS_CLASS), + new Identifier('get'), + [ + new Arg(new String_(self::SETTINGS_KEY)), + new Arg(new String_(self::SETTINGS_DEFAULT)), + ] + ); + } + + /** + * Walks a chained config-accessor expression to check whether it ultimately + * targets the locale.settings configuration object. + * + * Matches forms like: + * \Drupal::config('locale.settings') + * \Drupal::configFactory()->get('locale.settings') + * $this->config('locale.settings') + * $this->configFactory->get('locale.settings') + */ + private function isLocaleSettingsConfigReceiver(Node $receiver): bool + { + $current = $receiver; + while ($current instanceof MethodCall) { + if ($this->isNames($current->name, self::CONFIG_ACCESSOR_METHODS)) { + if (!empty($current->args) && $current->args[0] instanceof Arg) { + $arg = $current->args[0]->value; + if ($arg instanceof String_ && $arg->value === 'locale.settings') { + return true; + } + } + } + $current = $current->var; + } + if ($current instanceof StaticCall) { + if ($this->isName($current->name, 'config') && !empty($current->args)) { + $arg = $current->args[0]; + if ($arg instanceof Arg && $arg->value instanceof String_) { + return $arg->value->value === 'locale.settings'; + } + } + } + + return false; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/ReplaceLocaleTranslationPathConfigRectorTest.php b/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/ReplaceLocaleTranslationPathConfigRectorTest.php new file mode 100644 index 000000000..8e9e76c49 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/ReplaceLocaleTranslationPathConfigRectorTest.php @@ -0,0 +1,46 @@ +make(DrupalRectorSettings::class)->setDrupalVersion('99.99.99'); + $this->doTestFile($filePath); + } + + /** + * @return \Iterator<> + */ + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/fixture'); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('provideDataBelowVersion')] + public function testBelowVersion(string $filePath): void + { + static::getContainer()->make(DrupalRectorSettings::class)->setDrupalVersion('1.0.0'); + $this->doTestFile($filePath); + } + + /** + * @return \Iterator<> + */ + public static function provideDataBelowVersion(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/fixture-below-version'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config/configured_rule.php'; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/config/configured_rule.php b/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/config/configured_rule.php new file mode 100644 index 000000000..5d929ab35 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/config/configured_rule.php @@ -0,0 +1,14 @@ +get('translation.path'); +?> +----- +get('translation.path'); +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture-below-version/config_factory_static.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture-below-version/config_factory_static.php.inc new file mode 100644 index 000000000..82decc6de --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture-below-version/config_factory_static.php.inc @@ -0,0 +1,9 @@ +get('locale.settings')->get('translation.path'); +?> +----- +get('locale.settings')->get('translation.path'); +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture-below-version/this_config_method.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture-below-version/this_config_method.php.inc new file mode 100644 index 000000000..b5047e6ba --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture-below-version/this_config_method.php.inc @@ -0,0 +1,29 @@ +config('locale.settings')->get('translation.path'); + } +} +?> +----- +config('locale.settings')->get('translation.path'); + } +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/basic.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/basic.php.inc new file mode 100644 index 000000000..9f1a5c1bf --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/basic.php.inc @@ -0,0 +1,9 @@ +get('translation.path'); +?> +----- + \Drupal\Core\Site\Settings::get('locale_translation_path', 'public://translations'), fn() => \Drupal::config('locale.settings')->get('translation.path')); +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/config_factory_static.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/config_factory_static.php.inc new file mode 100644 index 000000000..463efde25 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/config_factory_static.php.inc @@ -0,0 +1,9 @@ +get('locale.settings')->get('translation.path'); +?> +----- + \Drupal\Core\Site\Settings::get('locale_translation_path', 'public://translations'), fn() => \Drupal::configFactory()->get('locale.settings')->get('translation.path')); +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/no_change_standalone_get.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/no_change_standalone_get.php.inc new file mode 100644 index 000000000..106f5463e --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/no_change_standalone_get.php.inc @@ -0,0 +1,17 @@ +get() without a recognisable chained loader — not transformed. +// The rule deliberately matches only chained config-accessor expressions to keep the +// rewrite obviously correct; users with the standalone pattern can refactor manually. +/** @var \Drupal\Core\Config\ImmutableConfig $config */ +$path = $config->get('translation.path'); +?> +----- +get() without a recognisable chained loader — not transformed. +// The rule deliberately matches only chained config-accessor expressions to keep the +// rewrite obviously correct; users with the standalone pattern can refactor manually. +/** @var \Drupal\Core\Config\ImmutableConfig $config */ +$path = $config->get('translation.path'); +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/no_change_unrelated_config.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/no_change_unrelated_config.php.inc new file mode 100644 index 000000000..a70334f6f --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/no_change_unrelated_config.php.inc @@ -0,0 +1,11 @@ +get('translation.path'); +?> +----- +get('translation.path'); +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/no_change_unrelated_key.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/no_change_unrelated_key.php.inc new file mode 100644 index 000000000..020884a59 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/no_change_unrelated_key.php.inc @@ -0,0 +1,11 @@ +get('javascript.directory'); +?> +----- +get('javascript.directory'); +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/this_config_method.php.inc b/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/this_config_method.php.inc new file mode 100644 index 000000000..56f72423d --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/ReplaceLocaleTranslationPathConfigRector/fixture/this_config_method.php.inc @@ -0,0 +1,29 @@ +config('locale.settings')->get('translation.path'); + } +} +?> +----- + \Drupal\Core\Site\Settings::get('locale_translation_path', 'public://translations'), fn() => $this->config('locale.settings')->get('translation.path')); + } +} +?> From e7397921ee7ce7bf5bedf79444d4555640aa286e Mon Sep 17 00:00:00 2001 From: bjorn Date: Tue, 26 May 2026 08:59:41 +0200 Subject: [PATCH 19/23] docs: changelog entry for ReplaceLocaleTranslationPathConfigRector --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d63d64586..b3ef2f22d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,33 @@ release-by-release. ### Added +- **`ReplaceLocaleTranslationPathConfigRector`** — rewrites chained + `\Drupal::config('locale.settings')->get('translation.path')` (and + equivalents via `configFactory()->get('locale.settings')->get(...)`, + `$this->config('locale.settings')->get(...)`, and similar) to + `\Drupal\Core\Site\Settings::get('locale_translation_path', 'public://translations')`. + The `locale.settings:translation.path` config key was deprecated in + drupal:11.4.0 and is removed in drupal:13.0.0; the interface + translations directory path must now be set as + `$settings['locale_translation_path']` in `settings.php`. On older + Drupal the value still lives in config, so the replacement is + BC-wrapped with `DeprecationHelper::backwardsCompatibleCall()`. + Matching is purely structural — two literal keys + (`'locale.settings'` and `'translation.path'`) must both appear in the + expected positions, so unrelated config reads and standalone + `$config->get('translation.path')` calls are left untouched. + **Caveat:** the BC wrapper gates on `\Drupal::VERSION`, not on where + the value is stored. Before running this rule, confirm that any + customised translation path has been moved to + `$settings['locale_translation_path']` in `settings.php`; otherwise + the new branch silently returns the default + `'public://translations'` even when the config still holds the + customised value. PHPStan / upgrade_status cannot detect this + deprecation — the deprecated symbol is the config key, not a PHP API + with `@deprecated` or `trigger_error()`, so this rule must be applied + proactively as part of an 11.4 → 13 migration plan. + [#3571593](https://www.drupal.org/i/3571593) / + [CR](https://www.drupal.org/node/3571594). - **`ViewsConfigUpdaterClassResolverToServiceRector`** — rewrites `\Drupal::classResolver(\Drupal\views\ViewsConfigUpdater::class)` to `\Drupal::service(\Drupal\views\ViewsConfigUpdater::class)`. In From 2c5313ea1239b0f285ca5f15ed3a2fe290b10073 Mon Sep 17 00:00:00 2001 From: bjorn Date: Tue, 26 May 2026 09:32:44 +0200 Subject: [PATCH 20/23] feat(Drupal11): Add RemoveAliasManagerCacheMethodCallsRector for issue #3496369 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes calls to AliasManager::setCacheKey() and AliasManager::writeCache(). Both methods were deprecated in drupal:11.3.0 and are removed in drupal:13.0.0 with no replacement — they became no-ops when the path alias preload cache was replaced by a Fiber-based bulk-lookup strategy. The receiver must be typed as \Drupal\path_alias\AliasManager or AliasManagerInterface; this guard prevents accidentally removing the unrelated ModuleHandler::writeCache() call. Removes the entire expression statement, leaving surrounding code intact. No BC wrapping needed since dropping a no-op call is safe on every Drupal version that still exposes the methods. Includes PHPSTAN_MESSAGES for both setCacheKey() and writeCache() captured from real contrib (drupal/redirect) and a synthetic probe respectively, plus the regenerated coverage-registry. --- config/coverage-registry.php | 5 ++ config/drupal-11/drupal-11.3-deprecations.php | 7 ++ ...moveAliasManagerCacheMethodCallsRector.php | 76 +++++++++++++++++++ stubs/Drupal/path_alias/AliasManager.php | 13 ++++ .../path_alias/AliasManagerInterface.php | 13 ++++ ...AliasManagerCacheMethodCallsRectorTest.php | 26 +++++++ .../config/configured_rule.php | 11 +++ .../fixture/basic.php.inc | 17 +++++ .../fixture/concrete_class.php.inc | 17 +++++ ...o_change_module_handler_writecache.php.inc | 17 +++++ .../fixture/no_change_unrelated.php.inc | 19 +++++ .../no_change_unrelated_method.php.inc | 17 +++++ 12 files changed, 238 insertions(+) create mode 100644 src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector.php create mode 100644 stubs/Drupal/path_alias/AliasManager.php create mode 100644 stubs/Drupal/path_alias/AliasManagerInterface.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/RemoveAliasManagerCacheMethodCallsRectorTest.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/config/configured_rule.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/fixture/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/fixture/concrete_class.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/fixture/no_change_module_handler_writecache.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/fixture/no_change_unrelated.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/fixture/no_change_unrelated_method.php.inc diff --git a/config/coverage-registry.php b/config/coverage-registry.php index 197f8da12..d64254c55 100644 --- a/config/coverage-registry.php +++ b/config/coverage-registry.php @@ -26,6 +26,11 @@ 1 => 'Call to deprecated method getDrupalRoot() of class Drupal\\Tests\\BrowserTestBase. Deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Access $this->root directly.', 2 => 'Call to deprecated method getDrupalRoot() of class Drupal\\Tests\\UnitTestCase. Deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Access $this->root directly.', ), + 'RemoveAliasManagerCacheMethodCallsRector' => + array ( + 0 => 'Call to deprecated method setCacheKey() of class Drupal\\path_alias\\AliasManager. Deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. There is no replacement.', + 1 => 'Call to deprecated method writeCache() of class Drupal\\path_alias\\AliasManager. Deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. There is no replacement.', + ), 'ReplaceExpectDeprecationRector' => array ( 0 => 'Call to deprecated method expectDeprecation() of class Drupal\\KernelTests\\KernelTestBase. Deprecated in drupal:11.4.0 and is removed from drupal:12.0.0. Use $this->expectUserDeprecationMessage() or $this->expectUserDeprecationMessageMatches() instead.', diff --git a/config/drupal-11/drupal-11.3-deprecations.php b/config/drupal-11/drupal-11.3-deprecations.php index 18f03b260..894e3c2a2 100644 --- a/config/drupal-11/drupal-11.3-deprecations.php +++ b/config/drupal-11/drupal-11.3-deprecations.php @@ -7,6 +7,7 @@ use DrupalRector\Drupal11\Rector\Deprecation\FileSystemBasenameToNativeRector; use DrupalRector\Drupal11\Rector\Deprecation\LoadAllIncludesRector; use DrupalRector\Drupal11\Rector\Deprecation\NodeStorageDeprecatedMethodsRector; +use DrupalRector\Drupal11\Rector\Deprecation\RemoveAliasManagerCacheMethodCallsRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveRootFromConvertDbUrlRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceCommentManagerGetCountNewCommentsRector; use DrupalRector\Drupal11\Rector\Deprecation\ReplaceCommentPreviewConstantsRector; @@ -261,4 +262,10 @@ $rectorConfig->ruleWithConfiguration(ViewsConfigUpdaterClassResolverToServiceRector::class, [ new DrupalIntroducedVersionConfiguration('11.3.0'), ]); + + // https://www.drupal.org/node/3496369 + // https://www.drupal.org/node/3532412 (change record) + // AliasManager::setCacheKey() and AliasManager::writeCache() deprecated in drupal:11.3.0, + // removed in drupal:13.0.0 with no replacement (they are no-ops). + $rectorConfig->rule(RemoveAliasManagerCacheMethodCallsRector::class); }; diff --git a/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector.php b/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector.php new file mode 100644 index 000000000..d37c87eec --- /dev/null +++ b/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector.php @@ -0,0 +1,76 @@ +aliasManager->setCacheKey($path);', + '' + ), + ] + ); + } + + /** @return array> */ + public function getNodeTypes(): array + { + return [Expression::class]; + } + + public function refactor(Node $node): ?int + { + assert($node instanceof Expression); + if (!$node->expr instanceof MethodCall) { + return null; + } + + $methodCall = $node->expr; + if (!$this->isNames($methodCall->name, self::TARGET_METHODS)) { + return null; + } + + if ( + !$this->isObjectType($methodCall->var, new ObjectType('Drupal\path_alias\AliasManager')) + && !$this->isObjectType($methodCall->var, new ObjectType('Drupal\path_alias\AliasManagerInterface')) + ) { + return null; + } + + return NodeVisitor::REMOVE_NODE; + } +} diff --git a/stubs/Drupal/path_alias/AliasManager.php b/stubs/Drupal/path_alias/AliasManager.php new file mode 100644 index 000000000..89947b57c --- /dev/null +++ b/stubs/Drupal/path_alias/AliasManager.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/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/config/configured_rule.php b/tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/config/configured_rule.php new file mode 100644 index 000000000..51c146428 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/config/configured_rule.php @@ -0,0 +1,11 @@ +setCacheKey('/some/path'); + $aliasManager->writeCache(); + \Drupal::messenger()->addStatus('done'); +} +?> +----- +addStatus('done'); +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/fixture/concrete_class.php.inc b/tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/fixture/concrete_class.php.inc new file mode 100644 index 000000000..6a8fc68dd --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/fixture/concrete_class.php.inc @@ -0,0 +1,17 @@ +setCacheKey('/some/path'); + $aliasManager->writeCache(); + \Drupal::messenger()->addStatus('done'); +} +?> +----- +addStatus('done'); +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/fixture/no_change_module_handler_writecache.php.inc b/tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/fixture/no_change_module_handler_writecache.php.inc new file mode 100644 index 000000000..1c8ee6e61 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/fixture/no_change_module_handler_writecache.php.inc @@ -0,0 +1,17 @@ +writeCache(); +} +?> +----- +writeCache(); +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/fixture/no_change_unrelated.php.inc b/tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/fixture/no_change_unrelated.php.inc new file mode 100644 index 000000000..3a5bbef2b --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/fixture/no_change_unrelated.php.inc @@ -0,0 +1,19 @@ +setCacheKey('/some/path'); + $aliasManager->writeCache(); +} +?> +----- +setCacheKey('/some/path'); + $aliasManager->writeCache(); +} +?> diff --git a/tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/fixture/no_change_unrelated_method.php.inc b/tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/fixture/no_change_unrelated_method.php.inc new file mode 100644 index 000000000..c7269f2b4 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemoveAliasManagerCacheMethodCallsRector/fixture/no_change_unrelated_method.php.inc @@ -0,0 +1,17 @@ +getAliasByPath('/node/1'); +} +?> +----- +getAliasByPath('/node/1'); +} +?> From 8d36be8d164718a5399088ff774976254fca412d Mon Sep 17 00:00:00 2001 From: bjorn Date: Tue, 26 May 2026 09:32:48 +0200 Subject: [PATCH 21/23] docs: changelog entry for RemoveAliasManagerCacheMethodCallsRector --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3ef2f22d..40372ae0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,19 @@ release-by-release. ### Added +- **`RemoveAliasManagerCacheMethodCallsRector`** — deletes calls to + `AliasManager::setCacheKey()` and `AliasManager::writeCache()`. Both + methods were deprecated in drupal:11.3.0 and are removed in + drupal:13.0.0 with no replacement — they became no-ops when the path + alias preload cache was replaced by a Fiber-based bulk-lookup strategy. + The receiver must be typed as `\Drupal\path_alias\AliasManager` or + `AliasManagerInterface`; this guard prevents accidentally removing + unrelated methods that share the name (notably + `ModuleHandler::writeCache()`). Removes the entire expression + statement, leaving surrounding code intact. No BC wrapping is needed + since dropping a no-op call is safe on every Drupal version. + [#3496369](https://www.drupal.org/i/3496369) / + [CR](https://www.drupal.org/node/3532412). - **`ReplaceLocaleTranslationPathConfigRector`** — rewrites chained `\Drupal::config('locale.settings')->get('translation.path')` (and equivalents via `configFactory()->get('locale.settings')->get(...)`, From f7202980e6fdbdd40b63b81f1f9bd722bc6b2a78 Mon Sep 17 00:00:00 2001 From: bjorn Date: Tue, 26 May 2026 10:37:03 +0200 Subject: [PATCH 22/23] feat(Drupal11): Add RemovePhpUnitCompatibilityTraitRector for issue #3582118 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes use Drupal\Tests\PhpUnitCompatibilityTrait; from test class declarations. The trait was a forward-compatibility shim for PHPUnit API differences across versions; it is deleted from Drupal core in Drupal 12 via #3582118, at which point any test class still composing the trait fatal-errors at autoload time because the trait class no longer exists. Gated to Drupal 12 only — and deliberately off by default. The trait still exists on Drupal 10 (and may still hold shim methods that tests depend on) and is an empty no-op on Drupal 11. Because the trait composition is a structural Class_ change, not an Expr → Expr rewrite, it cannot be BC-wrapped with DeprecationHelper. Running the rule prematurely on a D10-only codebase risks silently stripping a composition that the tests still rely on. The rector therefore only fires when DrupalRectorSettings::setDrupalVersion('12.0.0') or higher is set; the stub default (11.99.x-dev) keeps it inert for normal D11-focused runs. Extends AbstractDrupalCoreRector for the version-gate machinery but overrides supportBackwardsCompatibility() to return false explicitly, since structural class composition has no DeprecationHelper-style BC wrapper to emit. --- config/drupal-11/drupal-11.4-deprecations.php | 18 +++ .../RemovePhpUnitCompatibilityTraitRector.php | 130 ++++++++++++++++++ ...ovePhpUnitCompatibilityTraitRectorTest.php | 46 +++++++ .../config/configured_rule.php | 14 ++ .../fixture-below-version/basic.php.inc | 33 +++++ .../fixture/basic.php.inc | 31 +++++ .../fixture/no_change_unrelated_trait.php.inc | 39 ++++++ 7 files changed, 311 insertions(+) create mode 100644 src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector/RemovePhpUnitCompatibilityTraitRectorTest.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector/config/configured_rule.php create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector/fixture-below-version/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector/fixture/basic.php.inc create mode 100644 tests/src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector/fixture/no_change_unrelated_trait.php.inc diff --git a/config/drupal-11/drupal-11.4-deprecations.php b/config/drupal-11/drupal-11.4-deprecations.php index 5810684d1..91186e683 100644 --- a/config/drupal-11/drupal-11.4-deprecations.php +++ b/config/drupal-11/drupal-11.4-deprecations.php @@ -15,6 +15,7 @@ use DrupalRector\Drupal11\Rector\Deprecation\RemoveConfigSaveTrustedDataArgRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveFilterTipsLongParamRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveLinkWidgetValidateTitleElementRector; +use DrupalRector\Drupal11\Rector\Deprecation\RemovePhpUnitCompatibilityTraitRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveSetUriCallbackRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveTrustDataCallRector; use DrupalRector\Drupal11\Rector\Deprecation\RemoveViewsRowCacheKeysRector; @@ -504,4 +505,21 @@ $rectorConfig->ruleWithConfiguration(ReplaceLocaleTranslationPathConfigRector::class, [ new DrupalIntroducedVersionConfiguration('11.4.0'), ]); + + // https://www.drupal.org/node/3582118 + // PhpUnitCompatibilityTrait is DELETED FROM CORE in Drupal 12 — any test + // class still composing the trait fatal-errors at autoload on D12. + // + // GATED TO 12.0.0 INTENTIONALLY. The trait still exists (and may still + // hold shim methods) on Drupal 10. On Drupal 11 it is an empty no-op but + // removing the composition is harmless. On Drupal 12 the trait class is + // gone and the composition MUST be removed. Because the trait composition + // cannot be BC-wrapped (it's a structural Class_ change, not an Expr → + // Expr rewrite), the rector is deliberately OFF by default and only fires + // when the consumer sets DrupalRectorSettings::setDrupalVersion('12.0.0') + // or higher. This prevents accidentally stripping a still-functional + // trait composition from a D10/D11-only codebase. + $rectorConfig->ruleWithConfiguration(RemovePhpUnitCompatibilityTraitRector::class, [ + new DrupalIntroducedVersionConfiguration('12.0.0'), + ]); }; diff --git a/src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector.php b/src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector.php new file mode 100644 index 000000000..69f02e6fb --- /dev/null +++ b/src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector.php @@ -0,0 +1,130 @@ +> */ + public function getNodeTypes(): array + { + return [Class_::class, Trait_::class]; + } + + protected function refactorWithConfiguration(Node $node, VersionedConfigurationInterface $configuration): ?Node + { + if (!$node instanceof Class_ && !$node instanceof Trait_) { + return null; + } + + $hasChanged = false; + foreach ($node->stmts as $key => $stmt) { + if (!$stmt instanceof TraitUse) { + continue; + } + foreach ($stmt->traits as $traitKey => $trait) { + if (!$this->isName($trait, self::TRAIT_FQCN)) { + continue; + } + unset($stmt->traits[$traitKey]); + $hasChanged = true; + } + if ($stmt->traits === []) { + unset($node->stmts[$key]); + } + } + + return $hasChanged ? $node : null; + } + + /** + * Trait composition is a structural change, not an Expr → Expr rewrite, + * so it cannot be BC-wrapped via DeprecationHelper. Disable BC entirely. + */ + public function supportBackwardsCompatibility(VersionedConfigurationInterface $configuration): bool + { + return false; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector/RemovePhpUnitCompatibilityTraitRectorTest.php b/tests/src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector/RemovePhpUnitCompatibilityTraitRectorTest.php new file mode 100644 index 000000000..93092251a --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector/RemovePhpUnitCompatibilityTraitRectorTest.php @@ -0,0 +1,46 @@ +make(DrupalRectorSettings::class)->setDrupalVersion('99.99.99'); + $this->doTestFile($filePath); + } + + /** + * @return \Iterator<> + */ + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/fixture'); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('provideDataBelowVersion')] + public function testBelowVersion(string $filePath): void + { + static::getContainer()->make(DrupalRectorSettings::class)->setDrupalVersion('1.0.0'); + $this->doTestFile($filePath); + } + + /** + * @return \Iterator<> + */ + public static function provideDataBelowVersion(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/fixture-below-version'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config/configured_rule.php'; + } +} diff --git a/tests/src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector/config/configured_rule.php b/tests/src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector/config/configured_rule.php new file mode 100644 index 000000000..60c6fadc6 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector/config/configured_rule.php @@ -0,0 +1,14 @@ + +----- + diff --git a/tests/src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector/fixture/basic.php.inc b/tests/src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector/fixture/basic.php.inc new file mode 100644 index 000000000..af26b0bf6 --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector/fixture/basic.php.inc @@ -0,0 +1,31 @@ + +----- + diff --git a/tests/src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector/fixture/no_change_unrelated_trait.php.inc b/tests/src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector/fixture/no_change_unrelated_trait.php.inc new file mode 100644 index 000000000..62c25865a --- /dev/null +++ b/tests/src/Drupal11/Rector/Deprecation/RemovePhpUnitCompatibilityTraitRector/fixture/no_change_unrelated_trait.php.inc @@ -0,0 +1,39 @@ + +----- + From 2fc699f8426e35194c20d6bbd7c68056a3b1d33e Mon Sep 17 00:00:00 2001 From: bjorn Date: Tue, 26 May 2026 10:37:07 +0200 Subject: [PATCH 23/23] docs: changelog entry for RemovePhpUnitCompatibilityTraitRector --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40372ae0c..349976df1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,28 @@ release-by-release. ### Added +- **`RemovePhpUnitCompatibilityTraitRector`** — removes + `use Drupal\Tests\PhpUnitCompatibilityTrait;` from test class + declarations. The trait was a forward-compatibility shim for PHPUnit + API differences across versions; it is **deleted from Drupal core in + Drupal 12** via [#3582118](https://www.drupal.org/i/3582118), at which + point any test class still composing the trait fatal-errors at + autoload time because the trait class no longer exists. + + **Gated to Drupal 12 only — and deliberately off by default.** The + trait still exists on Drupal 10 (and may still hold shim methods that + tests depend on) and is an empty no-op on Drupal 11. Because the + trait composition is a structural `Class_` change, not an Expr → Expr + rewrite, it cannot be BC-wrapped with `DeprecationHelper`. Running + the rule prematurely on a D10-only codebase risks silently stripping + a composition that the tests still rely on. The rector therefore only + fires when the consumer explicitly sets the target Drupal version to + `12.0.0` or higher via + `DrupalRectorSettings::setDrupalVersion('12.0.0')`; the stub default + (`11.99.x-dev`) keeps it inert for normal D11-focused runs. The + orphan top-of-file `use Drupal\Tests\PhpUnitCompatibilityTrait;` + import is left in place — PHP never resolves an unused alias, so it + remains harmless on D12; cleanup is optional and out of scope. - **`RemoveAliasManagerCacheMethodCallsRector`** — deletes calls to `AliasManager::setCacheKey()` and `AliasManager::writeCache()`. Both methods were deprecated in drupal:11.3.0 and are removed in