diff --git a/.github/workflows/functional_test__single_rectors.yml b/.github/workflows/functional_test__single_rectors.yml new file mode 100644 index 00000000..85082e9d --- /dev/null +++ b/.github/workflows/functional_test__single_rectors.yml @@ -0,0 +1,75 @@ + +name: functional_test_single + +# This test will run on every pull request, and on every commit on any branch +on: + push: + branches: + - main + pull_request: + schedule: + # Run tests every week (to check for rector changes) + - cron: '0 0 * * 0' + +jobs: + run_functional_test_single: + name: Functional Test | single rectors + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php-version }}" + coverage: none + tools: composer:v2 + extensions: dom, curl, libxml, mbstring, zip, pdo, mysql, pdo_mysql, gd + - name: Setup Drupal + uses: bluehorndigital/setup-drupal@v1.0.4 + with: + version: '^11.0' + path: ~/drupal + - name: Install Drupal Rector + run: | + cd ~/drupal + composer require palantirnet/drupal-rector:@dev --no-progress + - name: Prepare rector_examples folder in the drupal modules directory + run: | + cd ~/drupal + mkdir -p web/modules/custom + cp -R vendor/palantirnet/drupal-rector/tests/functional/hookconvertrector/fixture/hookconvertrector web/modules/custom/hookconvertrector + # dry-run is expected to return exit code 2 if there are changes, which we are expecting to happen, here. + # an error code of 1 represents other errors. + # @see \Rector\Core\Console\ExitCode::CHANGED_CODE + - name: Run rector against Drupal (dry-run) + run: | + cd ~/drupal + for d in web/modules/custom/*; do + if [ -d "$d" ]; then + echo "Processing $d" + cp vendor/palantirnet/drupal-rector/tests/functional/$(basename ${d})/rector.php . + vendor/bin/rector process $d -vvv --dry-run --debug || if (($? == 2)); then true; else exit 1; fi + fi + done + - name: Run rector against Drupal + run: | + cd ~/drupal + for d in web/modules/custom/*; do + if [ -d "$d" ]; then + echo "Processing $d" + cp vendor/palantirnet/drupal-rector/tests/functional/$(basename ${d})/rector.php . + vendor/bin/rector process $d --debug + fi + done + # diff options: + # -r: recursive + # -u: show the joined context, like git diff + # -b: ignore whitespace + # -B: ignore lines that are only whitespace + - name: Check that the updated examples match expectations + run: | + cd ~/drupal + for d in web/modules/custom/*; do + if [ -d "$d" ]; then + diff -rubB "$d" "vendor/palantirnet/drupal-rector/tests/functional/$(basename ${d})/fixture/$(basename ${d})_updated" + fi + done diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index e045696e..64f7498a 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -6,6 +6,9 @@ __DIR__.'/tests', __DIR__.'/config/drupal-*', ]) + ->exclude([ + 'functional/hookconvertrector/fixture', + ]) ; return (new PhpCsFixer\Config()) diff --git a/src/Drupal8/Rector/Deprecation/DBRector.php b/src/Drupal8/Rector/Deprecation/DBRector.php index 4963b5f7..d4c6df70 100644 --- a/src/Drupal8/Rector/Deprecation/DBRector.php +++ b/src/Drupal8/Rector/Deprecation/DBRector.php @@ -56,7 +56,7 @@ class DBRector extends AbstractRector implements ConfigurableRectorInterface protected $optionsArgumentPosition; /** - * @var \DrupalRector\Drupal8\Rector\ValueObject\DBConfiguration[] + * @var DBConfiguration[] */ private array $configuration; diff --git a/src/Drupal8/Rector/Deprecation/DrupalServiceRenameRector.php b/src/Drupal8/Rector/Deprecation/DrupalServiceRenameRector.php index 43726353..61b17330 100644 --- a/src/Drupal8/Rector/Deprecation/DrupalServiceRenameRector.php +++ b/src/Drupal8/Rector/Deprecation/DrupalServiceRenameRector.php @@ -14,7 +14,7 @@ class DrupalServiceRenameRector extends AbstractRector implements ConfigurableRectorInterface { /** - * @var \DrupalRector\Drupal8\Rector\ValueObject\DrupalServiceRenameConfiguration[] + * @var DrupalServiceRenameConfiguration[] */ protected array $staticArgumentRenameConfigs = []; diff --git a/src/Drupal8/Rector/Deprecation/EntityLoadRector.php b/src/Drupal8/Rector/Deprecation/EntityLoadRector.php index 7c3bb7fa..66b5e5b5 100644 --- a/src/Drupal8/Rector/Deprecation/EntityLoadRector.php +++ b/src/Drupal8/Rector/Deprecation/EntityLoadRector.php @@ -27,7 +27,7 @@ final class EntityLoadRector extends AbstractRector implements ConfigurableRectorInterface { /** - * @var \DrupalRector\Drupal8\Rector\ValueObject\EntityLoadConfiguration[] + * @var EntityLoadConfiguration[] */ protected array $entityTypes; diff --git a/src/Drupal9/Rector/Deprecation/ExtensionPathRector.php b/src/Drupal9/Rector/Deprecation/ExtensionPathRector.php index e0d9ca33..a8043d94 100644 --- a/src/Drupal9/Rector/Deprecation/ExtensionPathRector.php +++ b/src/Drupal9/Rector/Deprecation/ExtensionPathRector.php @@ -15,7 +15,7 @@ class ExtensionPathRector extends AbstractRector implements ConfigurableRectorInterface { /** - * @var \DrupalRector\Drupal9\Rector\ValueObject\ExtensionPathConfiguration[] + * @var ExtensionPathConfiguration[] */ private array $configuration; diff --git a/src/Drupal9/Rector/Deprecation/UiHelperTraitDrupalPostFormRector.php b/src/Drupal9/Rector/Deprecation/UiHelperTraitDrupalPostFormRector.php index 208f4ebf..9a3462ee 100644 --- a/src/Drupal9/Rector/Deprecation/UiHelperTraitDrupalPostFormRector.php +++ b/src/Drupal9/Rector/Deprecation/UiHelperTraitDrupalPostFormRector.php @@ -48,7 +48,7 @@ public function getNodeTypes(): array * * @throws ShouldNotHappenException * - * @return array + * @return array */ private function safeArgDestructure(Node\Expr\MethodCall $node): array { diff --git a/src/Rector/AbstractDrupalCoreRector.php b/src/Rector/AbstractDrupalCoreRector.php index 89ac7c5b..8edfb174 100644 --- a/src/Rector/AbstractDrupalCoreRector.php +++ b/src/Rector/AbstractDrupalCoreRector.php @@ -16,7 +16,7 @@ abstract class AbstractDrupalCoreRector extends AbstractRector implements ConfigurableRectorInterface { /** - * @var array|\DrupalRector\Contract\VersionedConfigurationInterface[] + * @var array|VersionedConfigurationInterface[] */ protected array $configuration = []; diff --git a/src/Rector/Convert/HookConvertRector.php b/src/Rector/Convert/HookConvertRector.php new file mode 100644 index 00000000..3c04ca09 --- /dev/null +++ b/src/Rector/Convert/HookConvertRector.php @@ -0,0 +1,365 @@ +isDryRun = in_array('--'.Option::DRY_RUN, $_SERVER['argv'] ?? []) || in_array('-'.Option::DRY_RUN_SHORT, $_SERVER['argv'] ?? []); + $this->printer = $printer; + + try { + if (class_exists(InstalledVersions::class) && ($corePath = InstalledVersions::getInstallPath('drupal/core'))) { + $this->drupalCorePath = realpath($corePath); + } + } catch (\OutOfBoundsException $e) { + } + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Hook conversion script', [ + new CodeSample( + <<<'CODE_SAMPLE' +Drupal Hook Implementation +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +https://www.drupal.org/node/3442349 +CODE_SAMPLE + ), + ]); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Function_::class, Use_::class]; + } + + public function refactor(Node $node): Node|int|null + { + $filePath = $this->file->getFilePath(); + $ext = pathinfo($filePath, \PATHINFO_EXTENSION); + if (!in_array($ext, ['inc', 'module'])) { + return null; + } + if ($filePath !== $this->inputFilename) { + $this->initializeHookClass(); + } + if ($node instanceof Use_) { + // For some unknown reason some Use_ statements are passed twice + // to this method. + $newNode = new Use_($node->uses, $node->type, ['comments' => []] + $node->getAttributes()); + $this->useStmts[$this->printer->prettyPrint([$newNode])] = $newNode; + } + + if ($node instanceof Function_) { + if ($node->name->toString() === 'system_theme') { + return null; + } + /* + @todo Something like this should fix the issue with the legacy hooks + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attribute) { + if (str_ends_with($this->nodeNameResolver->getName($attribute->name), 'LegacyHook')) { + return null; + } + } + } + */ + + if ($this->module && ($method = $this->createMethodFromFunction($node))) { + $this->hookClass->stmts[] = $method; + if ($node->name->toString() === 'system_page_attachments') { + $method->stmts = [new Node\Stmt\Expression(new Node\Expr\FuncCall(new Node\Name('_system_page_attachments'), self::convertParamsToArgs($node)))]; + $node->name = new Node\Identifier('_system_page_attachments'); + + return $node; + } + + return str_starts_with($filePath, $this->drupalCorePath) ? NodeVisitor::REMOVE_NODE : $this->getLegacyHookFunction($node); + } + } + + return null; + } + + protected function initializeHookClass(): void + { + $this->__destruct(); + $this->moduleDir = $this->file->getFilePath(); + $this->inputFilename = $this->moduleDir; + // Find the relevant info.yml: it's either in the current directory or + // one of the parents. + while (($this->moduleDir = dirname($this->moduleDir)) && !($info = glob("$this->moduleDir/*.info.yml"))) { + } + if (!empty($info)) { + $infoFile = reset($info); + $this->module = basename($infoFile, '.info.yml'); + $filename = pathinfo($this->file->getFilePath(), \PATHINFO_FILENAME); + $hookClassName = ucfirst(CaseStringHelper::camelCase(str_replace('.', '_', $filename).'_hooks')); + $counter = ''; + do { + $candidate = "$hookClassName$counter"; + $hookClassFilename = "$this->moduleDir/src/Hook/$candidate.php"; + $counter = $counter ? $counter + 1 : 1; + } while (file_exists($hookClassFilename)); + $namespace = implode('\\', ['Drupal', $this->module, 'Hook']); + $this->hookClass = new Class_(new Node\Identifier($candidate)); + // Using $this->nodeFactory->createStaticCall() results in + // use \Drupal; on top which is not desirable. + $classConst = new Node\Expr\ClassConstFetch(new FullyQualified("$namespace\\$candidate"), 'class'); + $this->drupalServiceCall = new Node\Expr\StaticCall(new FullyQualified('Drupal'), 'service', [new Node\Arg($classConst)]); + $this->useStmts = []; + } + } + + public function __destruct() + { + if ($this->module && $this->hookClass->stmts) { + $className = $this->hookClass->name->toString(); + // Put the file together. + $namespace = "Drupal\\$this->module\\Hook"; + $hookClassStmts = [ + new Node\Stmt\Namespace_(new Node\Name($namespace)), + ...$this->useStmts, + new Use_([new Node\Stmt\UseUse(new Node\Name('Drupal\Core\Hook\Attribute\Hook'))]), + $this->hookClass, + ]; + $this->hookClass->setDocComment(new \PhpParser\Comment\Doc("/**\n * Hook implementations for $this->module.\n */")); + // Write it out if not a dry run + if ($this->isDryRun === false) { + @mkdir("$this->moduleDir/src"); + @mkdir("$this->moduleDir/src/Hook"); + + file_put_contents("$this->moduleDir/src/Hook/$className.php", $this->printer->prettyPrintFile($hookClassStmts)); + if (!str_starts_with($this->moduleDir, $this->drupalCorePath)) { + static::writeServicesYml("$this->moduleDir/$this->module.services.yml", "$namespace\\$className"); + } + } + } + $this->module = ''; + } + + protected function createMethodFromFunction(Function_ $node): ?ClassMethod + { + if ($info = $this->getHookAndModuleName($node)) { + ['hook' => $hook, 'module' => $implementsModule] = $info; + $procOnly = [ + 'install', + 'requirements', + 'schema', + 'uninstall', + 'update_last_removed', + 'module_implements_alter', + 'hook_info', + 'install_tasks', + 'install_tasks_alter', + ]; + if (in_array($hook, $procOnly) || str_starts_with($hook, 'preprocess') || str_starts_with($hook, 'process')) { + return null; + } + // Resolve __FUNCTION__ and unqualify things so TRUE doesn't + // become \TRUE. + $visitor = new class(new String_($node->name->toString())) extends NodeVisitorAbstract { + public function __construct(protected String_ $functionName) + { + } + + public function leaveNode(Node $node) + { + if (isset($node->name) && $node->name instanceof FullyQualified) { + $name = new Node\Name($node->name); + if ($name->isUnqualified()) { + $node->name = $name; + + return $node; + } + } + + if ($node instanceof Node\Expr\Array_) { + $node->setAttribute(AttributeKey::NEWLINED_ARRAY_PRINT, true); + } + + return $node instanceof Node\Scalar\MagicConst\Function_ ? $this->functionName : parent::leaveNode($node); + } + }; + $traverser = new NodeTraverser(); + $traverser->addVisitor($visitor); + $traverser->traverse([$node]); + // Convert the function to a method. + $method = new ClassMethod($this->getMethodName($node), get_object_vars($node), $node->getAttributes()); + $method->flags = Modifiers::PUBLIC; + // Assemble the arguments for the #[Hook] attribute. + $arguments = [new Node\Arg(new String_($hook))]; + if ($implementsModule !== $this->module) { + $arguments[] = new Node\Arg(new String_($implementsModule), name: new Node\Identifier('module')); + } + $method->attrGroups[] = new Node\AttributeGroup([new Node\Attribute(new Node\Name('Hook'), $arguments)]); + + return $method; + } + + return null; + } + + /** + * Get the hook and module name from a function name and doxygen. + * + * If the doxygen has Implements hook_foo() in it then this method attempts + * to find a matching module name and hook. Function names like + * user_access_test_user_access() are ambiguous: it could be the user module + * implementing the hook_ENTITY_TYPE_access hook for the access_test_user + * entity type or it could be the user_access_test module implementing it for + * the user entity type. The current module name is preferred by the method + * then the shortest possible module name producing a match is returned. + * + * @param Function_ $node + * A function node + * + * @return array + * If a match was found then an associative array with keys hook and module + * with corresponding values. Otherwise, the array is empty. + */ + protected function getHookAndModuleName(Function_ $node): array + { + // If the doxygen contains "Implements hook_foo()" then parse the hook + // name. A difficulty here is "Implements hook_form_FORM_ID_alter". + // Find these by looking for an optional part starting with an + // uppercase letter. + if (preg_match('/^ \* Implements hook_([a-zA-Z0-9_]+)/m', (string) $node->getDocComment()?->getReformattedText(), $matches)) { + $parts = explode('_', $matches[1]); + $isUppercase = false; + foreach ($parts as &$part) { + if (!$part) { + continue; + } + if ($part === strtoupper($part)) { + if (!$isUppercase) { + $isUppercase = true; + $part = '[a-z0-9_]+'; + } + } else { + $isUpperCase = false; + } + } + $hookRegex = implode('_', $parts); + $hookRegex = "_(?$hookRegex)"; + $functionName = $node->name->toString(); + // And now find the module and the hook. + foreach ([$this->module, '.+?'] as $module) { + if (preg_match("/^(?$module)$hookRegex$/", $functionName, $matches)) { + return $matches; + } + } + } + + return []; + } + + /** + * @param Function_ $node + * A function declaration for example the entire user_user_role_insert() + * function + * + * @return string + * The function name converted to camelCase for e.g. userRoleInsert. The + * current module name is removed from the beginning. + */ + protected function getMethodName(Function_ $node): string + { + $name = preg_replace("/^{$this->module}_/", '', $node->name->toString()); + + return CaseStringHelper::camelCase($name); + } + + public function getLegacyHookFunction(Function_ $node): Function_ + { + $methodCall = new Node\Expr\MethodCall($this->drupalServiceCall, $this->getMethodName($node), self::convertParamsToArgs($node)); + $hasReturn = (new NodeFinder())->findFirstInstanceOf([$node], Node\Stmt\Return_::class); + $node->stmts = [$hasReturn ? new Node\Stmt\Return_($methodCall) : new Node\Stmt\Expression($methodCall)]; + // Mark this function as a legacy hook. + $node->attrGroups[] = new Node\AttributeGroup([new Node\Attribute(new FullyQualified('Drupal\Core\Hook\Attribute\LegacyHook'))]); + + return $node; + } + + protected static function writeServicesYml(string $fileName, string $fullyClassifiedClassName): void + { + $services = is_file($fileName) ? file_get_contents($fileName) : ''; + $id = "\n $fullyClassifiedClassName:\n"; + if (!str_contains($services, $id)) { + if (!str_contains($services, 'services:')) { + $services .= "\nservices:"; + } + $services .= "$id class: $fullyClassifiedClassName\n autowire: true\n"; + file_put_contents($fileName, $services); + } + } + + /** + * @param Function_ $node + * + * @return Node\Arg[] + */ + protected static function convertParamsToArgs(Function_ $node): array + { + return array_map(fn (Node\Param $param) => new Node\Arg($param->var), $node->getParams()); + } +} diff --git a/tests/functional/hookconvertrector/fixture/hookconvertrector/hookconvertrector.info.yml b/tests/functional/hookconvertrector/fixture/hookconvertrector/hookconvertrector.info.yml new file mode 100644 index 00000000..e69de29b diff --git a/tests/functional/hookconvertrector/fixture/hookconvertrector/hookconvertrector.module b/tests/functional/hookconvertrector/fixture/hookconvertrector/hookconvertrector.module new file mode 100644 index 00000000..cc0e1f98 --- /dev/null +++ b/tests/functional/hookconvertrector/fixture/hookconvertrector/hookconvertrector.module @@ -0,0 +1,77 @@ + 'red', + 'green' => 'green', + 'blue' => 'blue', + ]; +} + + +/** + * Implements hook_user_add(). + */ +#[LegacyHook] +function hookconvertrector_user_add($edit, UserInterface $account, $method) { + $red = 'red'; + $method = ['red', 'green', 'blue']; + $edit = [ + 'red' => 'red', + 'green' => 'green', + 'blue' => 'blue', + ]; +} + +/** + * Implements hook_page_attachments(). + */ +function hookconvertrector_page_attachments(array &$page) { + // Routes that don't use BigPipe also don't need no-JS detection. + if (\Drupal::routeMatch()->getRouteObject()->getOption('_no_big_pipe')) { + return; + } + + $request = \Drupal::request(); + // BigPipe is only used when there is an actual session, so only add the no-JS + // detection when there actually is a session. + // @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy. + $session_exists = \Drupal::service('session_configuration')->hasSession($request); + $page['#cache']['contexts'][] = 'session.exists'; + // Only do the no-JS detection while we don't know if there's no JS support: + // avoid endless redirect loops. + $has_big_pipe_nojs_cookie = $request->cookies->has(BigPipeStrategy::NOJS_COOKIE); + $page['#cache']['contexts'][] = 'cookies:' . BigPipeStrategy::NOJS_COOKIE; + if ($session_exists) { + if (!$has_big_pipe_nojs_cookie) { + // Let server set the BigPipe no-JS cookie. + $page['#attached']['html_head'][] = [ + [ + // Redirect through a 'Refresh' meta tag if JavaScript is disabled. + '#tag' => 'meta', + '#noscript' => TRUE, + '#attributes' => [ + 'http-equiv' => 'Refresh', + 'content' => '0; URL=' . Url::fromRoute('big_pipe.nojs', [], ['query' => \Drupal::service('redirect.destination')->getAsArray()])->toString(), + ], + ], + 'big_pipe_detect_nojs', + ]; + } + else { + // Let client delete the BigPipe no-JS cookie. + $page['#attached']['html_head'][] = [ + [ + '#tag' => 'script', + '#value' => 'document.cookie = "' . BigPipeStrategy::NOJS_COOKIE . '=1; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"', + ], + 'big_pipe_detect_js', + ]; + } + } +} diff --git a/tests/functional/hookconvertrector/fixture/hookconvertrector_updated/hookconvertrector.info.yml b/tests/functional/hookconvertrector/fixture/hookconvertrector_updated/hookconvertrector.info.yml new file mode 100644 index 00000000..e69de29b diff --git a/tests/functional/hookconvertrector/fixture/hookconvertrector_updated/hookconvertrector.module b/tests/functional/hookconvertrector/fixture/hookconvertrector_updated/hookconvertrector.module new file mode 100644 index 00000000..5d58c340 --- /dev/null +++ b/tests/functional/hookconvertrector/fixture/hookconvertrector_updated/hookconvertrector.module @@ -0,0 +1,37 @@ +userCancel($edit, $account, $method); +} + +/** + * Implements hook_user_add(). + */ +#[LegacyHook] +function hookconvertrector_user_add($edit, UserInterface $account, $method) +{ + $red = 'red'; + $method = ['red', 'green', 'blue']; + $edit = [ + 'red' => 'red', + 'green' => 'green', + 'blue' => 'blue', + ]; +} + +/** + * Implements hook_page_attachments(). + */ +#[LegacyHook] +function hookconvertrector_page_attachments(array &$page) +{ + return \Drupal::service(HookconvertrectorHooks::class)->pageAttachments($page); +} diff --git a/tests/functional/hookconvertrector/fixture/hookconvertrector_updated/hookconvertrector.services.yml b/tests/functional/hookconvertrector/fixture/hookconvertrector_updated/hookconvertrector.services.yml new file mode 100644 index 00000000..3be2c759 --- /dev/null +++ b/tests/functional/hookconvertrector/fixture/hookconvertrector_updated/hookconvertrector.services.yml @@ -0,0 +1,5 @@ + +services: + Drupal\hookconvertrector\Hook\HookconvertrectorHooks: + class: Drupal\hookconvertrector\Hook\HookconvertrectorHooks + autowire: true diff --git a/tests/functional/hookconvertrector/fixture/hookconvertrector_updated/src/Hook/HookconvertrectorHooks.php b/tests/functional/hookconvertrector/fixture/hookconvertrector_updated/src/Hook/HookconvertrectorHooks.php new file mode 100644 index 00000000..28b36722 --- /dev/null +++ b/tests/functional/hookconvertrector/fixture/hookconvertrector_updated/src/Hook/HookconvertrectorHooks.php @@ -0,0 +1,80 @@ + 'red', + 'green' => 'green', + 'blue' => 'blue', + ]; + } + + /** + * Implements hook_page_attachments(). + */ + #[Hook('page_attachments')] + public function pageAttachments(array &$page) + { + // Routes that don't use BigPipe also don't need no-JS detection. + if (\Drupal::routeMatch()->getRouteObject()->getOption('_no_big_pipe')) { + return; + } + $request = \Drupal::request(); + // BigPipe is only used when there is an actual session, so only add the no-JS + // detection when there actually is a session. + // @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy. + $session_exists = \Drupal::service('session_configuration')->hasSession($request); + $page['#cache']['contexts'][] = 'session.exists'; + // Only do the no-JS detection while we don't know if there's no JS support: + // avoid endless redirect loops. + $has_big_pipe_nojs_cookie = $request->cookies->has(\BigPipeStrategy::NOJS_COOKIE); + $page['#cache']['contexts'][] = 'cookies:' . \BigPipeStrategy::NOJS_COOKIE; + if ($session_exists) { + if (!$has_big_pipe_nojs_cookie) { + // Let server set the BigPipe no-JS cookie. + $page['#attached']['html_head'][] = [ + [ + // Redirect through a 'Refresh' meta tag if JavaScript is disabled. + '#tag' => 'meta', + '#noscript' => TRUE, + '#attributes' => [ + 'http-equiv' => 'Refresh', + 'content' => '0; URL=' . \Url::fromRoute('big_pipe.nojs', [ + ], [ + 'query' => \Drupal::service('redirect.destination')->getAsArray(), + ])->toString(), + ], + ], + 'big_pipe_detect_nojs', + ]; + } else { + // Let client delete the BigPipe no-JS cookie. + $page['#attached']['html_head'][] = [ + [ + '#tag' => 'script', + '#value' => 'document.cookie = "' . \BigPipeStrategy::NOJS_COOKIE . '=1; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"', + ], + 'big_pipe_detect_js', + ]; + } + } + } +} diff --git a/tests/functional/hookconvertrector/rector.php b/tests/functional/hookconvertrector/rector.php new file mode 100644 index 00000000..2b882d5b --- /dev/null +++ b/tests/functional/hookconvertrector/rector.php @@ -0,0 +1,25 @@ +rule(DrupalRector\Rector\Convert\HookConvertRector::class); + + $drupalFinder = new DrupalFinder(); + $drupalFinder->locateRoot(__DIR__); + $drupalRoot = $drupalFinder->getDrupalRoot(); + $rectorConfig->autoloadPaths([ + $drupalRoot.'/core', + $drupalRoot.'/modules', + $drupalRoot.'/profiles', + $drupalRoot.'/themes', + ]); + + $rectorConfig->skip(['*/upgrade_status/tests/modules/*']); + $rectorConfig->fileExtensions(['php', 'module', 'theme', 'install', 'profile', 'inc', 'engine']); + $rectorConfig->importNames(true, false); + $rectorConfig->importShortClasses(false); +};