From b0894c786515739c2bb19c8dbfff341f6ae63c96 Mon Sep 17 00:00:00 2001 From: Felix Yeung <19355619+imfelixyeung@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:59:42 +0100 Subject: [PATCH 1/6] feat: implement rector rule to remove $CFG->wwwroot from moodle_urls --- .../RemoveCfgWwwrootFromMoodleUrlRector.php | 116 ++++++++++++++++++ composer.json | 3 +- composer.lock | 61 ++++++++- 3 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 classes/local/rector/rules/RemoveCfgWwwrootFromMoodleUrlRector.php diff --git a/classes/local/rector/rules/RemoveCfgWwwrootFromMoodleUrlRector.php b/classes/local/rector/rules/RemoveCfgWwwrootFromMoodleUrlRector.php new file mode 100644 index 00000000..ded68f42 --- /dev/null +++ b/classes/local/rector/rules/RemoveCfgWwwrootFromMoodleUrlRector.php @@ -0,0 +1,116 @@ +. + +namespace local_devkit\local\rector\rules; + +use core\url; +use moodle_url; +use PhpParser\Node; +use PhpParser\Node\Expr\BinaryOp\Concat; +use PhpParser\Node\Expr\New_; +use PhpParser\Node\Expr\PropertyFetch; +use PhpParser\Node\Expr\Variable; +use Rector\Rector\AbstractRector; +use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; +use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; + +/** + * Removes unneeded $CFG->wwwroot from moodle_url usage. + * + * phpcs:disable moodle.NamingConventions.ValidFunctionName + * + * @package local_devkit + * @copyright 2026 Felix + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class RemoveCfgWwwrootFromMoodleUrlRector extends AbstractRector { + /** + * Definition + */ + public function getRuleDefinition(): RuleDefinition { + return new RuleDefinition( + 'Removes $CFG->wwwroot concatenation from moodle_url instantiation and upgrades to \core\url', + [ + new CodeSample( + 'new moodle_url($CFG->wwwroot . \'/mod/assign/view.php\');', + 'new \core\url(\'/mod/assign/view.php\');', + ), + ], + ); + } + + /** + * {@inheritDoc} + * @return array> + */ + #[\Override] + public function getNodeTypes(): array { + return [New_::class]; + } + + /** + * {@inheritDoc} + * @param New_ $node + */ + #[\Override] + public function refactor(Node $node): ?Node { + if (!$this->is_moodle_url($node)) { + return null; + } + + if ($node->args === []) { + return null; + } + + $arg1 = $node->args[0]->value; + if (!$arg1 instanceof Concat) { + return null; + } + + if (!$this->is_cfg_wwwroot_property($arg1->left)) { + return null; + } + + $node->args[0]->value = $arg1->right; + + return $node; + } + + /** + * Checks if the given node's class is a moodle url class. + */ + private function is_moodle_url(New_ $node): bool { + $classes = [ + moodle_url::class, + url::class, + ]; + return $this->isNames($node->class, $classes); + } + + /** + * Checks if an AST node matches `$CFG->wwwroot` + */ + private function is_cfg_wwwroot_property(Node $node): bool { + if (!$node instanceof PropertyFetch) { + return false; + } + + // Check if the variable is named `$CFG` and property is `wwwroot`. + return $node->var instanceof Variable + && $this->isName($node->var, 'CFG') + && $this->isName($node->name, 'wwwroot'); + } +} diff --git a/composer.json b/composer.json index 1a9ab580..8a0b351a 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,8 @@ "phpstan/phpstan-deprecation-rules": "^2.0", "laravel/pint": "^1.29", "phpstan/phpstan-strict-rules": "^2.0", - "rector/rector": "^2.5" + "rector/rector": "^2.5", + "symplify/rule-doc-generator-contracts": "^11.2" }, "authors": [ { diff --git a/composer.lock b/composer.lock index 7c343055..5e4e7a48 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "05aa70c28b53c72c92c8e6924631f81d", + "content-hash": "22c26b347853ea4fb718df9f486a8d3a", "packages": [ { "name": "doctrine/deprecations", @@ -2759,6 +2759,65 @@ } ], "time": "2026-07-13T15:24:18+00:00" + }, + { + "name": "symplify/rule-doc-generator-contracts", + "version": "11.2.0", + "source": { + "type": "git", + "url": "https://github.com/symplify/rule-doc-generator-contracts.git", + "reference": "479cfcfd46047f80624aba931d9789e50475b5c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symplify/rule-doc-generator-contracts/zipball/479cfcfd46047f80624aba931d9789e50475b5c6", + "reference": "479cfcfd46047f80624aba931d9789e50475b5c6", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/extension-installer": "^1.2", + "rector/rector": "^0.15.10", + "symplify/easy-ci": "^11.1", + "symplify/easy-coding-standard": "^11.1", + "symplify/easy-testing": "^11.1", + "symplify/phpstan-extensions": "^11.1", + "symplify/phpstan-rules": "11.2.3.72", + "tomasvotruba/unused-public": "^0.0.34" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symplify\\RuleDocGenerator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Contracts for production code of RuleDocGenerator", + "support": { + "source": "https://github.com/symplify/rule-doc-generator-contracts/tree/11.2.0" + }, + "funding": [ + { + "url": "https://www.paypal.me/rectorphp", + "type": "custom" + }, + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2024-03-18T22:02:54+00:00" } ], "aliases": [], From 8666a8be534ae657c4b9a9d348fc02fd353622e9 Mon Sep 17 00:00:00 2001 From: Felix Yeung <19355619+imfelixyeung@users.noreply.github.com> Date: Fri, 24 Jul 2026 22:56:34 +0100 Subject: [PATCH 2/6] feat: implement rector rule to update old classnames to new namespaced names --- .../RenameMoodleDeprecatedClassesRector.php | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 classes/local/rector/rules/RenameMoodleDeprecatedClassesRector.php diff --git a/classes/local/rector/rules/RenameMoodleDeprecatedClassesRector.php b/classes/local/rector/rules/RenameMoodleDeprecatedClassesRector.php new file mode 100644 index 00000000..befcc204 --- /dev/null +++ b/classes/local/rector/rules/RenameMoodleDeprecatedClassesRector.php @@ -0,0 +1,150 @@ +. + +namespace local_devkit\local\rector\rules; + +use core\context\block; +use core\context\course; +use core\context\coursecat; +use core\context\module; +use core\context\system; +use core\context\user; +use PhpParser\Node; +use Rector\Rector\AbstractRector; +use Rector\Renaming\Rector\Name\RenameClassRector; +use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; +use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; + +use function is_string; +use function strlen; + +/** + * Updates old classnames to new namespaced names. + * + * phpcs:disable moodle.NamingConventions.ValidFunctionName + * + * @package local_devkit + * @copyright 2026 Felix + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class RenameMoodleDeprecatedClassesRector extends AbstractRector { + /** + * Map of old Moodle class aliases to their new namespaced equivalents. + */ + private const array CONTEX_CLASS_MAP = [ + // These are not included in legacyclasses.php. + \context_block::class => block::class, + \context_course::class => course::class, + \context_coursecat::class => coursecat::class, + \context_helper::class => \core\context_helper::class, + \context_module::class => module::class, + \context_system::class => system::class, + \context_user::class => user::class, + \context::class => \core\context::class, + ]; + + /** @var array $classmap */ + private static array $classmap = []; + + /** + * Loads the classmap. + */ + private function construct_classmap(): void { + global $CFG; + if (self::$classmap) { + return; + } + self::$classmap = self::CONTEX_CLASS_MAP; + + if (!isset($CFG)) { + return; + } + $legacyclasses = null; + require("$CFG->libdir/db/legacyclasses.php"); + + /** @var array $legacyclasses */ + $legacyclasses ??= []; + + foreach ($legacyclasses as $oldclassname => $path) { + try { + if (!class_exists($oldclassname)) { + continue; + } + + if (is_string($path)) { + $subsystem = 'core'; + $file = $path; + } else { + [$subsystem, $file] = $path; + } + + $suffix = '.php'; + if (str_ends_with($file, $suffix)) { + $file = substr($file, 0, strlen($file) - strlen($suffix)); + } + $file = str_replace('/', '\\', $file); + $newclassname = "$subsystem\\$file"; + + if (class_exists($newclassname)) { + self::$classmap[$oldclassname] = $newclassname; + } + // phpcs:ignore Generic.CodeAnalysis.EmptyStatement + } catch (\Throwable) { + // Too bad. + } + } + } + + /** + * Constructor. + */ + public function __construct( + /** @var RenameClassRector $renamereactor */ + private readonly RenameClassRector $renamereactor, + ) { + } + + /** + * Rule definition + */ + public function getRuleDefinition(): RuleDefinition { + return new RuleDefinition( + 'Automatically renames legacy Moodle class aliases to their modern namespaced equivalents', + [ + new CodeSample( + '$context = context_system::instance();', + '$context = \core\context\system::instance();', + ), + ], + ); + } + + /** + * {@inheritDoc} + * @return array> + */ + #[\Override] + public function getNodeTypes(): array { + return $this->renamereactor->getNodeTypes(); + } + + #[\Override] + public function refactor(Node $node): ?Node { + $this->construct_classmap(); + $this->renamereactor->configure(self::$classmap); + return $this->renamereactor->refactor($node); + } +} From 8c165b534aadb45f12b8597298e272306f252e49 Mon Sep 17 00:00:00 2001 From: Felix Yeung <19355619+imfelixyeung@users.noreply.github.com> Date: Fri, 24 Jul 2026 23:11:11 +0100 Subject: [PATCH 3/6] fix: resolve phpstan issues --- .../RemoveCfgWwwrootFromMoodleUrlRector.php | 22 ++++++++++++------- .../RenameMoodleDeprecatedClassesRector.php | 9 ++++++-- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/classes/local/rector/rules/RemoveCfgWwwrootFromMoodleUrlRector.php b/classes/local/rector/rules/RemoveCfgWwwrootFromMoodleUrlRector.php index ded68f42..ce736d41 100644 --- a/classes/local/rector/rules/RemoveCfgWwwrootFromMoodleUrlRector.php +++ b/classes/local/rector/rules/RemoveCfgWwwrootFromMoodleUrlRector.php @@ -19,6 +19,7 @@ use core\url; use moodle_url; use PhpParser\Node; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\BinaryOp\Concat; use PhpParser\Node\Expr\New_; use PhpParser\Node\Expr\PropertyFetch; @@ -61,12 +62,12 @@ public function getNodeTypes(): array { return [New_::class]; } - /** - * {@inheritDoc} - * @param New_ $node - */ #[\Override] public function refactor(Node $node): ?Node { + if (!$node instanceof New_) { + return null; + } + if (!$this->is_moodle_url($node)) { return null; } @@ -75,16 +76,21 @@ public function refactor(Node $node): ?Node { return null; } - $arg1 = $node->args[0]->value; - if (!$arg1 instanceof Concat) { + $arg0 = $node->args[0]; + + if (!$arg0 instanceof Arg) { + return null; + } + + if (!$arg0->value instanceof Concat) { return null; } - if (!$this->is_cfg_wwwroot_property($arg1->left)) { + if (!$this->is_cfg_wwwroot_property($arg0->value->left)) { return null; } - $node->args[0]->value = $arg1->right; + $arg0->value = $arg0->value->right; return $node; } diff --git a/classes/local/rector/rules/RenameMoodleDeprecatedClassesRector.php b/classes/local/rector/rules/RenameMoodleDeprecatedClassesRector.php index befcc204..899355fb 100644 --- a/classes/local/rector/rules/RenameMoodleDeprecatedClassesRector.php +++ b/classes/local/rector/rules/RenameMoodleDeprecatedClassesRector.php @@ -43,6 +43,7 @@ final class RenameMoodleDeprecatedClassesRector extends AbstractRector { /** * Map of old Moodle class aliases to their new namespaced equivalents. + * @var array */ private const array CONTEX_CLASS_MAP = [ // These are not included in legacyclasses.php. @@ -56,7 +57,7 @@ final class RenameMoodleDeprecatedClassesRector extends AbstractRector { \context::class => \core\context::class, ]; - /** @var array $classmap */ + /** @var array $classmap */ private static array $classmap = []; /** @@ -64,7 +65,7 @@ final class RenameMoodleDeprecatedClassesRector extends AbstractRector { */ private function construct_classmap(): void { global $CFG; - if (self::$classmap) { + if (self::$classmap !== []) { return; } self::$classmap = self::CONTEX_CLASS_MAP; @@ -72,6 +73,8 @@ private function construct_classmap(): void { if (!isset($CFG)) { return; } + + /** @var mixed $legacyclasses */ $legacyclasses = null; require("$CFG->libdir/db/legacyclasses.php"); @@ -145,6 +148,8 @@ public function getNodeTypes(): array { public function refactor(Node $node): ?Node { $this->construct_classmap(); $this->renamereactor->configure(self::$classmap); + // phpcs:ignore + // @phpstan-ignore argument.type return $this->renamereactor->refactor($node); } } From 5d3a73867363652e11a21316a97032775a866d42 Mon Sep 17 00:00:00 2001 From: Felix Yeung <19355619+imfelixyeung@users.noreply.github.com> Date: Fri, 24 Jul 2026 23:18:33 +0100 Subject: [PATCH 4/6] style: format --- classes/local/api/plugins.php | 3 ++- .../local/rector/rules/RemoveCfgWwwrootFromMoodleUrlRector.php | 2 ++ settings.php | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/classes/local/api/plugins.php b/classes/local/api/plugins.php index 154726e8..c28fe50c 100644 --- a/classes/local/api/plugins.php +++ b/classes/local/api/plugins.php @@ -17,6 +17,7 @@ namespace local_devkit\local\api; use core\plugin_manager; +use core\plugininfo\base; /** * Plugins API. @@ -44,7 +45,7 @@ class plugins { */ public static function list(bool $includestandard = false): array { $manager = plugin_manager::instance(); - /** @var array> $plugininfo */ + /** @var array> $plugininfo */ $plugininfo = $manager->get_plugins(); $results = []; diff --git a/classes/local/rector/rules/RemoveCfgWwwrootFromMoodleUrlRector.php b/classes/local/rector/rules/RemoveCfgWwwrootFromMoodleUrlRector.php index ce736d41..f35f1e70 100644 --- a/classes/local/rector/rules/RemoveCfgWwwrootFromMoodleUrlRector.php +++ b/classes/local/rector/rules/RemoveCfgWwwrootFromMoodleUrlRector.php @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +declare(strict_types=1); + namespace local_devkit\local\rector\rules; use core\url; diff --git a/settings.php b/settings.php index 432e1412..05188e5a 100644 --- a/settings.php +++ b/settings.php @@ -24,6 +24,7 @@ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use core\lang_string; use core\output\html_writer; use local_devkit\local\data\editor; use local_devkit\output\tables\linter_config; From ba706da00614a328c1916e38bcf6c01974322d69 Mon Sep 17 00:00:00 2001 From: Felix Yeung <19355619+imfelixyeung@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:53:30 +0100 Subject: [PATCH 5/6] feat: add rule to simplify require config.php --- .../rules/SimplifyRequireConfigPathRector.php | 206 ++++++++++++++++++ .../rules/SimplifyRequireConfigPathRector.php | 58 +++++ 2 files changed, 264 insertions(+) create mode 100644 classes/local/rector/rules/SimplifyRequireConfigPathRector.php create mode 100644 tests/fixtures/rector/rules/SimplifyRequireConfigPathRector.php diff --git a/classes/local/rector/rules/SimplifyRequireConfigPathRector.php b/classes/local/rector/rules/SimplifyRequireConfigPathRector.php new file mode 100644 index 00000000..26b7e0b5 --- /dev/null +++ b/classes/local/rector/rules/SimplifyRequireConfigPathRector.php @@ -0,0 +1,206 @@ +. + +declare(strict_types=1); + +namespace local_devkit\local\rector\rules; + +use PhpParser\Node; +use PhpParser\Node\Arg; +use PhpParser\Node\Expr\BinaryOp\Concat; +use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Expr\Include_; +use PhpParser\Node\Scalar\Int_; +use PhpParser\Node\Scalar\MagicConst\Dir; +use PhpParser\Node\Scalar\MagicConst\File; +use PhpParser\Node\Scalar\String_; +use Rector\Rector\AbstractRector; +use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; +use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; + +use function count; +use function str_ends_with; + +/** + * Simplifies require/require_once paths to use __DIR__ relative paths. + * + * Converts `require(dirname(dirname(__DIR__)) . '/config.php');` + * into `require(__DIR__ . '/../../config.php');` + * + * Also handles `__FILE__` variants (e.g. `dirname(dirname(__FILE__))`). + * Works at any nesting depth. + * + * phpcs:disable moodle.NamingConventions.ValidFunctionName + * + * @package local_devkit + * @copyright 2026 Felix + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class SimplifyRequireConfigPathRector extends AbstractRector { + /** + * Definition + */ + public function getRuleDefinition(): RuleDefinition { + return new RuleDefinition( + 'Simplifies require/require_once paths to use __DIR__ relative paths for better IDE navigation', + [ + new CodeSample( + "require(dirname(dirname(__DIR__)) . '/config.php');", + "require(__DIR__ . '/../../config.php');", + ), + new CodeSample( + "require_once(dirname(dirname(dirname(__DIR__))) . '/config.php');", + "require_once(__DIR__ . '/../../../config.php');", + ), + new CodeSample( + "require(dirname(__DIR__, 2) . '/config.php');", + "require(__DIR__ . '/../../config.php');", + ), + new CodeSample( + "require_once(dirname(__FILE__, 3) . '/config.php');", + "require_once(__DIR__ . '/../../../config.php');", + ), + new CodeSample( + "require(dirname(dirname(__FILE__)) . '/config.php');", + "require(__DIR__ . '/../../config.php');", + ), + new CodeSample( + "require_once(dirname(__DIR__) . '/config.php');", + "require_once(__DIR__ . '/../config.php');", + ), + ], + ); + } + + /** + * {@inheritDoc} + * @return array> + */ + #[\Override] + public function getNodeTypes(): array { + return [Include_::class]; + } + + #[\Override] + public function refactor(Node $node): ?Node { + if (!$node instanceof Include_) { + return null; + } + + if ($node->type !== Include_::TYPE_REQUIRE_ONCE && $node->type !== Include_::TYPE_REQUIRE) { + return null; + } + + if (!$node->expr instanceof Concat) { + return null; + } + + $concat = $node->expr; + + $depth = $this->resolve_dirname_depth($concat->left); + if ($depth === null) { + return null; + } + + if (!$concat->right instanceof String_) { + return null; + } + + $suffix = $concat->right->value; + if (!str_ends_with($suffix, '/config.php')) { + return null; + } + + $prefix = str_repeat('/..', $depth); + + $concat->left = new Dir(); + $concat->right = new String_("$prefix$suffix"); + + return $node; + } + + /** + * Resolves the dirname depth from a chain of nested dirname() calls + * or from the dirname(__DIR__, N) multi-arg form. + * + * Returns null if the node is not a dirname chain wrapping __DIR__ or __FILE__. + */ + private function resolve_dirname_depth(Node $node): ?int { + if ($node instanceof FuncCall && $this->isName($node, 'dirname')) { + $depth = $this->resolve_multi_arg_dirname($node); + if ($depth !== null) { + return $depth; + } + } + + return $this->resolve_chain_dirname($node); + } + + /** + * Resolves depth from dirname(__DIR__, N) form. + */ + private function resolve_multi_arg_dirname(FuncCall $node): ?int { + if (count($node->args) !== 2) { + return null; + } + + $arg0 = $node->args[0]; + $arg1 = $node->args[1]; + + if (!$arg0 instanceof Arg || !$arg1 instanceof Arg) { + return null; + } + + if (!$arg0->value instanceof Dir && !$arg0->value instanceof File) { + return null; + } + + if (!$arg1->value instanceof Int_) { + return null; + } + + return $arg1->value->value; + } + + /** + * Resolves depth from dirname(dirname(...dirname(__DIR__)...)) form. + */ + private function resolve_chain_dirname(Node $node): ?int { + $depth = 0; + $current = $node; + while ($current instanceof FuncCall && $this->isName($current, 'dirname')) { + if (count($current->args) !== 1) { + return null; + } + + $arg = $current->args[0]; + if (!$arg instanceof Arg) { + return null; + } + + $depth++; + $value = $arg->value; + + if ($value instanceof Dir || $value instanceof File) { + return $depth; + } + + $current = $value; + } + + return null; + } +} diff --git a/tests/fixtures/rector/rules/SimplifyRequireConfigPathRector.php b/tests/fixtures/rector/rules/SimplifyRequireConfigPathRector.php new file mode 100644 index 00000000..23554ec8 --- /dev/null +++ b/tests/fixtures/rector/rules/SimplifyRequireConfigPathRector.php @@ -0,0 +1,58 @@ +. + +/** + * Fixture file for testing SimplifyRequireConfigPathRector. + * + * Each require/require_once below should be transformed to use __DIR__. + * + * @package local_devkit + * @copyright 2026 Felix + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// Chain forms: dirname(dirname(...dirname(__DIR__)...)). +require_once(dirname(dirname(__DIR__)) . '/config.php'); +require(dirname(dirname(__DIR__)) . '/config.php'); +require_once(dirname(dirname(dirname(__DIR__))) . '/config.php'); +require(dirname(dirname(dirname(__DIR__))) . '/config.php'); +require(dirname(dirname(dirname(dirname(__DIR__)))) . '/config.php'); + +// Chain forms with __FILE__ instead of __DIR__. +require_once(dirname(dirname(__FILE__)) . '/config.php'); +require(dirname(dirname(__FILE__)) . '/config.php'); +require(dirname(dirname(dirname(__FILE__))) . '/config.php'); + +// Multi-arg forms: dirname(__DIR__, N). +require_once(dirname(__DIR__, 2) . '/config.php'); +require(dirname(__DIR__, 2) . '/config.php'); +require(dirname(__DIR__, 3) . '/config.php'); +require(dirname(__DIR__, 4) . '/config.php'); + +// Multi-arg forms with __FILE__. +require_once(dirname(__FILE__, 2) . '/config.php'); +require(dirname(__FILE__, 3) . '/config.php'); + +// Should NOT be transformed: different suffix. +require(dirname(dirname(__DIR__)) . '/lib.php'); +require(dirname(__DIR__, 2) . '/setup.php'); + +// Should NOT be transformed: include/include_once. +include(dirname(dirname(__DIR__)) . '/config.php'); +include_once(dirname(dirname(__DIR__)) . '/config.php'); + +// Depth 1: also normalised to __DIR__ form. +require_once(dirname(__DIR__) . '/config.php'); From ec330ea6acbc7d5f7fc83ed49e37377f6464fe9b Mon Sep 17 00:00:00 2001 From: Felix Yeung <19355619+imfelixyeung@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:25:09 +0100 Subject: [PATCH 6/6] feat: add rule get db via di instead of global --- .../rector/rules/NormaliseGlobalDbRector.php | 128 ++++++++++++++++++ .../rector/rules/NormaliseGlobalDbRector.php | 61 +++++++++ 2 files changed, 189 insertions(+) create mode 100644 classes/local/rector/rules/NormaliseGlobalDbRector.php create mode 100644 tests/fixtures/rector/rules/NormaliseGlobalDbRector.php diff --git a/classes/local/rector/rules/NormaliseGlobalDbRector.php b/classes/local/rector/rules/NormaliseGlobalDbRector.php new file mode 100644 index 00000000..b351cd3f --- /dev/null +++ b/classes/local/rector/rules/NormaliseGlobalDbRector.php @@ -0,0 +1,128 @@ +. + +declare(strict_types=1); + +namespace local_devkit\local\rector\rules; + +use PhpParser\Node; +use PhpParser\Node\Arg; +use PhpParser\Node\Expr\Assign; +use PhpParser\Node\Expr\ClassConstFetch; +use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Expr\Variable; +use PhpParser\Node\Name; +use PhpParser\Node\Stmt\Expression; +use PhpParser\Node\Stmt\Global_; +use Rector\Rector\AbstractRector; +use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; +use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; + +use function count; + +/** + * Replaces `global $DB;` with `$DB = \core\di::get(\moodle_database::class);`. + * + * phpcs:disable moodle.NamingConventions.ValidFunctionName + * + * @package local_devkit + * @copyright 2026 Felix + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class NormaliseGlobalDbRector extends AbstractRector { + /** + * Definition + */ + public function getRuleDefinition(): RuleDefinition { + return new RuleDefinition( + 'Replaces global $DB with $DB = \core\di::get(\moodle_database::class)', + [ + new CodeSample( + "global \$DB;\n\$DB->get_record('user', ['id' => 1]);", + "\$DB = \\core\\di::get(\\moodle_database::class);\n\$DB->get_record('user', ['id' => 1]);", + ), + new CodeSample( + "global \$CFG, \$DB, \$USER;\n\$DB->get_records('course');", + "global \$CFG, \$USER;\n\$DB = \\core\\di::get(\\moodle_database::class);\n\$DB->get_records('course');", + ), + ], + ); + } + + /** + * {@inheritDoc} + * @return array> + */ + #[\Override] + public function getNodeTypes(): array { + return [Global_::class]; + } + + #[\Override] + public function refactor(Node $node): Node|array|null { + if (!$node instanceof Global_) { + return null; + } + + $dbvar = null; + foreach ($node->vars as $var) { + if ($var instanceof Variable && $var->name === 'DB') { + $dbvar = $var; + break; + } + } + + if ($dbvar === null) { + return null; + } + + $assignment = new Expression($this->createDbAssignment()); + + if (count($node->vars) === 1) { + return $assignment; + } + + $node->vars = array_values( + array_filter( + $node->vars, + fn($var) => $var !== $dbvar, + ), + ); + + return [$node, $assignment]; + } + + /** + * Creates the assignment expression for $DB = \core\di::get(\moodle_database::class) + */ + private function createDbAssignment(): Assign { + return new Assign( + new Variable('DB'), + new StaticCall( + new Name('\\core\\di'), + 'get', + [ + new Arg( + new ClassConstFetch( + new Name('\\moodle_database'), + 'class', + ), + ), + ], + ), + ); + } +} diff --git a/tests/fixtures/rector/rules/NormaliseGlobalDbRector.php b/tests/fixtures/rector/rules/NormaliseGlobalDbRector.php new file mode 100644 index 00000000..ebeac6a0 --- /dev/null +++ b/tests/fixtures/rector/rules/NormaliseGlobalDbRector.php @@ -0,0 +1,61 @@ +. + +/** + * Fixture file for testing NormaliseGlobalDbRector. + * + * `global $DB;` alone → `$DB = \core\di::get(\moodle_database::class);` + * `global $X, $DB, $Y;` → `global $X, $Y;` + `$DB = \core\di::get(\moodle_database::class);` + * + * @package local_devkit + * @copyright 2026 Felix + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// Full transformation: global $DB alone replaced with DI assignment. +function test_simple(): void { + global $DB; + + $record = $DB->get_record('user', ['id' => 1]); + $DB->insert_record('user', $record); +} + +// Multiple items in one global: $DB removed, assignment added after. +function test_mixed_global(): void { + global $CFG, $DB, $USER; + + $DB->get_records('course'); + $USER->id; +} + +// $DB on its own line → replaced; other globals on separate lines stay. +function test_separate_globals(): void { + global $DB; + global $USER, $CFG; + + $DB->count_records('user'); +} + +// Should NOT be transformed: no global $DB at all. +function test_no_global_db(): void { + global $USER; + $USER->id; +} + +// Should NOT be transformed: $DB used without global (outside scope). +function test_db_without_global(): void { + $DB->get_record('course', ['id' => 1]); +}