diff --git a/config/nextcloud-29/nextcloud-29-deprecations.php b/config/nextcloud-29/nextcloud-29-deprecations.php new file mode 100644 index 0000000..1983bea --- /dev/null +++ b/config/nextcloud-29/nextcloud-29-deprecations.php @@ -0,0 +1,12 @@ +sets([NextcloudSets::NEXTCLOUD_27]); + $rectorConfig->rule(ReplaceIConfigWithIAppConfigRector::class); +}; diff --git a/config/nextcloud-33/nextcloud-33-deprecations.php b/config/nextcloud-33/nextcloud-33-deprecations.php index 82ddfc3..1a7039b 100644 --- a/config/nextcloud-33/nextcloud-33-deprecations.php +++ b/config/nextcloud-33/nextcloud-33-deprecations.php @@ -9,7 +9,7 @@ use Rector\Php80\ValueObject\AnnotationToAttribute; return static function (RectorConfig $rectorConfig): void { - $rectorConfig->sets([NextcloudSets::NEXTCLOUD_27]); + $rectorConfig->sets([NextcloudSets::NEXTCLOUD_29]); $rectorConfig->rule(ReplaceFetchAllMethodCallRector::class); $rectorConfig->ruleWithConfiguration( AnnotationToAttributeRector::class, diff --git a/src/Rector/ReplaceIConfigWithIAppConfigRector.php b/src/Rector/ReplaceIConfigWithIAppConfigRector.php new file mode 100644 index 0000000..8a1e099 --- /dev/null +++ b/src/Rector/ReplaceIConfigWithIAppConfigRector.php @@ -0,0 +1,257 @@ + '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> + */ + 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 + */ + 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 $propertyNames + * + * @return list + */ + 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; + } + + private function buildPromotedAppConfigParam(string $name): Param + { + return new Param( + new Variable($name), + null, + new FullyQualified('OCP\IAppConfig'), + false, + false, + [], + Modifiers::PRIVATE, + ); + } +} diff --git a/src/Set/NextcloudSets.php b/src/Set/NextcloudSets.php index 35468c6..e6f37ea 100644 --- a/src/Set/NextcloudSets.php +++ b/src/Set/NextcloudSets.php @@ -10,9 +10,9 @@ final class NextcloudSets public const NEXTCLOUD_26 = __DIR__ . '/../../config/nextcloud-26/nextcloud-26-deprecations.php'; public const NEXTCLOUD_27 = __DIR__ . '/../../config/nextcloud-27/nextcloud-27-deprecations.php'; public const NEXTCLOUD_28 = self::NEXTCLOUD_27; - public const NEXTCLOUD_29 = self::NEXTCLOUD_27; - public const NEXTCLOUD_30 = self::NEXTCLOUD_27; - public const NEXTCLOUD_31 = self::NEXTCLOUD_27; - public const NEXTCLOUD_32 = self::NEXTCLOUD_27; + public const NEXTCLOUD_29 = __DIR__ . '/../../config/nextcloud-29/nextcloud-29-deprecations.php'; + public const NEXTCLOUD_30 = self::NEXTCLOUD_29; + public const NEXTCLOUD_31 = self::NEXTCLOUD_30; + public const NEXTCLOUD_32 = self::NEXTCLOUD_31; public const NEXTCLOUD_33 = __DIR__ . '/../../config/nextcloud-33/nextcloud-33-deprecations.php'; } diff --git a/tests/Rector/ReplaceIConfigWithIAppConfigRector/Fixture/test_fixture.php.inc b/tests/Rector/ReplaceIConfigWithIAppConfigRector/Fixture/test_fixture.php.inc new file mode 100644 index 0000000..20a175a --- /dev/null +++ b/tests/Rector/ReplaceIConfigWithIAppConfigRector/Fixture/test_fixture.php.inc @@ -0,0 +1,60 @@ +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'); + } +} + +?> +----- +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'); + } +} + +?> diff --git a/tests/Rector/ReplaceIConfigWithIAppConfigRector/ReplaceIConfigWithIAppConfigRectorTest.php b/tests/Rector/ReplaceIConfigWithIAppConfigRector/ReplaceIConfigWithIAppConfigRectorTest.php new file mode 100644 index 0000000..6569443 --- /dev/null +++ b/tests/Rector/ReplaceIConfigWithIAppConfigRector/ReplaceIConfigWithIAppConfigRectorTest.php @@ -0,0 +1,33 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/config.php'; + } +} diff --git a/tests/Rector/ReplaceIConfigWithIAppConfigRector/config/config.php b/tests/Rector/ReplaceIConfigWithIAppConfigRector/config/config.php new file mode 100644 index 0000000..741eb58 --- /dev/null +++ b/tests/Rector/ReplaceIConfigWithIAppConfigRector/config/config.php @@ -0,0 +1,17 @@ +withImportNames(removeUnusedImports: false) + ->withRules([ + ReplaceIConfigWithIAppConfigRector::class, + ]);