-
Notifications
You must be signed in to change notification settings - Fork 1
feat(IAppConfig): Add rector to migrate IConfig calls to IAppConfig #78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| use Nextcloud\Rector\Rector\ReplaceIConfigWithIAppConfigRector; | ||
| use Nextcloud\Rector\Set\NextcloudSets; | ||
| use Rector\Config\RectorConfig; | ||
|
|
||
| return static function (RectorConfig $rectorConfig): void { | ||
| $rectorConfig->sets([NextcloudSets::NEXTCLOUD_27]); | ||
| $rectorConfig->rule(ReplaceIConfigWithIAppConfigRector::class); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,257 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /* | ||
| * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||
| * SPDX-License-Identifier: AGPL-3.0-or-later | ||
| */ | ||
|
|
||
| namespace Nextcloud\Rector\Rector; | ||
|
|
||
| use PHPStan\Type\ObjectType; | ||
| use PhpParser\Modifiers; | ||
| use PhpParser\Node; | ||
| use PhpParser\Node\Expr\MethodCall; | ||
| use PhpParser\Node\Expr\PropertyFetch; | ||
| use PhpParser\Node\Expr\Variable; | ||
| use PhpParser\Node\Identifier; | ||
| use PhpParser\Node\Name\FullyQualified; | ||
| use PhpParser\Node\Param; | ||
| use PhpParser\Node\Stmt\ClassMethod; | ||
| use PhpParser\Node\Stmt\Class_; | ||
| use Rector\Rector\AbstractRector; | ||
| use Rector\ValueObject\MethodName; | ||
| use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; | ||
| use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; | ||
|
|
||
| use function array_keys; | ||
| use function is_string; | ||
|
|
||
| final class ReplaceIConfigWithIAppConfigRector extends AbstractRector | ||
| { | ||
| private const APP_CONFIG_CLASS = 'OCP\IAppConfig'; | ||
|
|
||
| /** | ||
| * Map of deprecated \OCP\IConfig methods to their \OCP\IAppConfig replacements. | ||
| * The argument lists are forwarded as-is; the new methods accept additional | ||
| * optional parameters that default to a behaviour matching the old methods. | ||
| */ | ||
| private const METHOD_MAP = [ | ||
| 'getAppValue' => 'getValue', | ||
| 'getAppKeys' => 'getKeys', | ||
| 'setAppValue' => 'setValue', | ||
| 'deleteAppValue' => 'deleteKey', | ||
| 'deleteAppValues' => 'deleteApp', | ||
| ]; | ||
|
|
||
| public function getRuleDefinition(): RuleDefinition | ||
| { | ||
| return new RuleDefinition( | ||
| 'Replace deprecated \OCP\IConfig app-config methods with their \OCP\IAppConfig counterparts,' | ||
| . ' injecting IAppConfig alongside the existing IConfig.', | ||
| [ | ||
| new CodeSample( | ||
| <<<'CODE_SAMPLE' | ||
| use OCP\IConfig; | ||
|
|
||
| class SomeClass | ||
| { | ||
| public function __construct(private IConfig $config) {} | ||
|
|
||
| public function run(): string | ||
| { | ||
| return $this->config->getAppValue('myapp', 'mykey', 'default'); | ||
| } | ||
| } | ||
| CODE_SAMPLE, | ||
| <<<'CODE_SAMPLE' | ||
| use OCP\IAppConfig; | ||
| use OCP\IConfig; | ||
|
|
||
| class SomeClass | ||
| { | ||
| public function __construct(private IConfig $config, private IAppConfig $appConfig) {} | ||
|
|
||
| public function run(): string | ||
| { | ||
| return $this->appConfig->getValueString('myapp', 'mykey', 'default'); | ||
| } | ||
| } | ||
| CODE_SAMPLE, | ||
| ), | ||
| ], | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @return array<class-string<Node>> | ||
| */ | ||
| public function getNodeTypes(): array | ||
| { | ||
| return [Class_::class]; | ||
| } | ||
|
|
||
| public function refactor(Node $node): ?Node | ||
| { | ||
| if (!($node instanceof Class_)) { | ||
| return null; | ||
| } | ||
|
|
||
| $constructor = $node->getMethod(MethodName::CONSTRUCT); | ||
| if (!$constructor instanceof ClassMethod) { | ||
| return null; | ||
| } | ||
|
|
||
| $iConfigPropertyNames = $this->collectIConfigPromotedPropertyNames($constructor); | ||
| if ($iConfigPropertyNames === []) { | ||
| return null; | ||
| } | ||
|
|
||
| $callsToRewrite = $this->collectDeprecatedAppConfigCalls($node, $iConfigPropertyNames); | ||
| if ($callsToRewrite === []) { | ||
| return null; | ||
| } | ||
|
|
||
| $appConfigName = $this->findExistingAppConfigPropertyName($constructor); | ||
| if ($appConfigName === null) { | ||
| $appConfigName = $this->makeUniquePropertyName($constructor, 'appConfig'); | ||
| $constructor->params[] = $this->buildPromotedAppConfigParam($appConfigName); | ||
| } | ||
|
|
||
| foreach ($callsToRewrite as $call) { | ||
| $oldMethodName = $this->getName($call->name); | ||
| if ($oldMethodName === null || !isset(self::METHOD_MAP[$oldMethodName])) { | ||
| continue; | ||
| } | ||
| /** @var PropertyFetch $propertyFetch */ | ||
| $propertyFetch = $call->var; | ||
| $propertyFetch->name = new Identifier($appConfigName); | ||
| $call->name = new Identifier(self::METHOD_MAP[$oldMethodName]); | ||
| } | ||
|
|
||
| return $node; | ||
| } | ||
|
|
||
| /** | ||
| * @return list<string> | ||
| */ | ||
| private function collectIConfigPromotedPropertyNames(ClassMethod $constructor): array | ||
| { | ||
| $names = []; | ||
| foreach ($constructor->getParams() as $param) { | ||
| if ($param->flags === 0) { | ||
| continue; | ||
| } | ||
| if (!$this->isObjectType($param, new ObjectType('OCP\IConfig'))) { | ||
| continue; | ||
| } | ||
| $name = $this->getName($param->var); | ||
| if (is_string($name)) { | ||
| $names[] = $name; | ||
| } | ||
| } | ||
|
|
||
| return $names; | ||
| } | ||
|
|
||
| /** | ||
| * @param list<string> $propertyNames | ||
| * | ||
| * @return list<MethodCall> | ||
| */ | ||
| private function collectDeprecatedAppConfigCalls(Class_ $class, array $propertyNames): array | ||
| { | ||
| $deprecatedMethods = array_keys(self::METHOD_MAP); | ||
| $calls = []; | ||
| foreach ($class->getMethods() as $classMethod) { | ||
| $stmts = $classMethod->getStmts(); | ||
| if ($stmts === null) { | ||
| continue; | ||
| } | ||
| $this->traverseNodesWithCallable( | ||
| $stmts, | ||
| function (Node $subNode) use ($propertyNames, $deprecatedMethods, &$calls): ?Node { | ||
| if (!$subNode instanceof MethodCall) { | ||
| return null; | ||
| } | ||
| if (!$this->isNames($subNode->name, $deprecatedMethods)) { | ||
| return null; | ||
| } | ||
| if (!$subNode->var instanceof PropertyFetch) { | ||
| return null; | ||
| } | ||
| $propertyFetch = $subNode->var; | ||
| if (!$propertyFetch->var instanceof Variable) { | ||
| return null; | ||
| } | ||
| if (!$this->isName($propertyFetch->var, 'this')) { | ||
| return null; | ||
| } | ||
| foreach ($propertyNames as $propertyName) { | ||
| if ($this->isName($propertyFetch->name, $propertyName)) { | ||
| $calls[] = $subNode; | ||
|
|
||
| break; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| return $calls; | ||
| } | ||
|
|
||
| private function findExistingAppConfigPropertyName(ClassMethod $constructor): ?string | ||
| { | ||
| foreach ($constructor->getParams() as $param) { | ||
| if ($param->flags === 0) { | ||
| continue; | ||
| } | ||
| if (!$this->isObjectType($param, new ObjectType(self::APP_CONFIG_CLASS))) { | ||
| continue; | ||
| } | ||
| $name = $this->getName($param->var); | ||
| if (is_string($name)) { | ||
| return $name; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| private function makeUniquePropertyName(ClassMethod $constructor, string $desired): string | ||
| { | ||
| $taken = []; | ||
| foreach ($constructor->getParams() as $param) { | ||
| $name = $this->getName($param->var); | ||
| if (is_string($name)) { | ||
| $taken[$name] = true; | ||
| } | ||
| } | ||
| if (!isset($taken[$desired])) { | ||
| return $desired; | ||
| } | ||
| $i = 2; | ||
| while (isset($taken[$desired . $i])) { | ||
| $i++; | ||
| } | ||
|
|
||
| return $desired . $i; | ||
| } | ||
|
Comment on lines
+225
to
+243
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like it will inject the same class a second time rather than re-using the same property.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. well only when referencing another class, in case that was not clear to you. |
||
|
|
||
| private function buildPromotedAppConfigParam(string $name): Param | ||
| { | ||
| return new Param( | ||
| new Variable($name), | ||
| null, | ||
| new FullyQualified('OCP\IAppConfig'), | ||
| false, | ||
| false, | ||
| [], | ||
| Modifiers::PRIVATE, | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| <?php | ||
|
|
||
| /* | ||
| * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||
| * SPDX-License-Identifier: AGPL-3.0-or-later | ||
| */ | ||
|
|
||
| namespace Nextcloud\Rector\Test\Rector\ReplaceIConfigWithIAppConfigRector\Fixture; | ||
|
|
||
| use OCP\IConfig; | ||
|
|
||
| class SomeClass | ||
| { | ||
| public function __construct(private IConfig $config) | ||
| { | ||
| } | ||
|
|
||
| public function run(): void | ||
| { | ||
| $this->config->getAppValue('myapp', 'mykey'); | ||
| $this->config->getAppValue('myapp', 'mykey', 'default'); | ||
| $this->config->getAppKeys('myapp'); | ||
| $this->config->setAppValue('myapp', 'mykey', 'value'); | ||
| $this->config->deleteAppValue('myapp', 'mykey'); | ||
| $this->config->deleteAppValues('myapp'); | ||
| } | ||
| } | ||
|
|
||
| ?> | ||
| ----- | ||
| <?php | ||
|
|
||
| /* | ||
| * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||
| * SPDX-License-Identifier: AGPL-3.0-or-later | ||
| */ | ||
|
|
||
| namespace Nextcloud\Rector\Test\Rector\ReplaceIConfigWithIAppConfigRector\Fixture; | ||
|
|
||
| use OCP\IAppConfig; | ||
| use OCP\IConfig; | ||
|
|
||
| class SomeClass | ||
| { | ||
| public function __construct(private IConfig $config, private IAppConfig $appConfig) | ||
| { | ||
| } | ||
|
|
||
| public function run(): void | ||
| { | ||
| $this->appConfig->getValue('myapp', 'mykey'); | ||
| $this->appConfig->getValue('myapp', 'mykey', 'default'); | ||
| $this->appConfig->getKeys('myapp'); | ||
| $this->appConfig->setValue('myapp', 'mykey', 'value'); | ||
| $this->appConfig->deleteKey('myapp', 'mykey'); | ||
| $this->appConfig->deleteApp('myapp'); | ||
| } | ||
| } | ||
|
|
||
| ?> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /* | ||
| * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||
| * SPDX-License-Identifier: AGPL-3.0-or-later | ||
| */ | ||
|
|
||
| namespace Nextcloud\Rector\Test\Rector\ReplaceIConfigWithIAppConfigRector; | ||
|
|
||
| use Iterator; | ||
| use PHPUnit\Framework\Attributes\DataProvider; | ||
| use Rector\Testing\PHPUnit\AbstractRectorTestCase; | ||
|
|
||
| final class ReplaceIConfigWithIAppConfigRectorTest extends AbstractRectorTestCase | ||
| { | ||
| #[DataProvider('provideData')] | ||
| public function test(string $filePath): void | ||
| { | ||
| $this->doTestFile($filePath); | ||
| } | ||
|
|
||
| public static function provideData(): Iterator | ||
| { | ||
| return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); | ||
| } | ||
|
|
||
| public function provideConfigFilePath(): string | ||
| { | ||
| return __DIR__ . '/config/config.php'; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isNames should be used instead, like above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, not sure how I missed that