diff --git a/config/nextcloud-33/nextcloud-33-deprecations.php b/config/nextcloud-33/nextcloud-33-deprecations.php index 1a7039b..06f4911 100644 --- a/config/nextcloud-33/nextcloud-33-deprecations.php +++ b/config/nextcloud-33/nextcloud-33-deprecations.php @@ -4,6 +4,7 @@ use Nextcloud\Rector\Rector\AnnotationToAttributeRector; use Nextcloud\Rector\Rector\ReplaceFetchAllMethodCallRector; +use Nextcloud\Rector\Rector\ReplaceIConfigWithIUserConfigRector; use Nextcloud\Rector\Set\NextcloudSets; use Rector\Config\RectorConfig; use Rector\Php80\ValueObject\AnnotationToAttribute; @@ -11,10 +12,14 @@ return static function (RectorConfig $rectorConfig): void { $rectorConfig->sets([NextcloudSets::NEXTCLOUD_29]); $rectorConfig->rule(ReplaceFetchAllMethodCallRector::class); + $rectorConfig->rule(ReplaceIConfigWithIUserConfigRector::class); $rectorConfig->ruleWithConfiguration( AnnotationToAttributeRector::class, [ - new AnnotationToAttribute('NoSameSiteCookieRequired', 'OCP\AppFramework\Http\Attribute\NoSameSiteCookieRequired'), + new AnnotationToAttribute( + 'NoSameSiteCookieRequired', + 'OCP\AppFramework\Http\Attribute\NoSameSiteCookieRequired', + ), new AnnotationToAttribute('NoTwoFactorRequired', 'OCP\AppFramework\Http\Attribute\NoTwoFactorRequired'), ], ); diff --git a/src/Rector/AReplaceClassRector.php b/src/Rector/AReplaceClassRector.php new file mode 100644 index 0000000..4a4a373 --- /dev/null +++ b/src/Rector/AReplaceClassRector.php @@ -0,0 +1,224 @@ + + */ + abstract public function getMethodMap(): array; + + abstract public function getRuleDefinition(): RuleDefinition; + + protected function getNewMethod(?string $oldMethod): ?string + { + if ($oldMethod === null) { + return null; + } + + return $this->getMethodMap()[$oldMethod] ?? null; + } + + /** + * @return array> + */ + #[Override] + public function getNodeTypes(): array + { + return [Class_::class]; + } + + #[Override] + 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->collectOldPromotedPropertyNames($constructor); + if ($iConfigPropertyNames === []) { + return null; + } + + $callsToRewrite = $this->collectDeprecatedCalls($node, $iConfigPropertyNames); + if ($callsToRewrite === []) { + return null; + } + + $appConfigName = $this->findExistingPropertyName($constructor); + if ($appConfigName === null) { + $appConfigName = $this->makeUniquePropertyName($constructor, $this->getDesiredVarName()); + $constructor->params[] = $this->buildPromotedParam($appConfigName); + } + + foreach ($callsToRewrite as $call) { + $oldMethodName = $this->getName($call->name); + $newMethodName = $this->getNewMethod($oldMethodName); + if ($oldMethodName === null || $newMethodName === null) { + continue; + } + /** @var PropertyFetch $propertyFetch */ + $propertyFetch = $call->var; + $propertyFetch->name = new Identifier($appConfigName); + $call->name = new Identifier($newMethodName); + } + + return $node; + } + + /** + * @return list + */ + private function collectOldPromotedPropertyNames(ClassMethod $constructor): array + { + $names = []; + foreach ($constructor->getParams() as $param) { + if ($param->flags === 0) { + continue; + } + if (!$this->isObjectType($param, new ObjectType($this->getOldClassName()))) { + continue; + } + $name = $this->getName($param->var); + if (is_string($name)) { + $names[] = $name; + } + } + + return $names; + } + + /** + * @param list $propertyNames + * + * @return list + */ + private function collectDeprecatedCalls(Class_ $class, array $propertyNames): array + { + $deprecatedMethods = array_keys($this->getMethodMap()); + $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; + } + if ($this->isNames($propertyFetch->name, $propertyNames)) { + $calls[] = $subNode; + } + + return null; + }, + ); + } + + return $calls; + } + + private function findExistingPropertyName(ClassMethod $constructor): ?string + { + foreach ($constructor->getParams() as $param) { + if ($param->flags === 0) { + continue; + } + if (!$this->isObjectType($param, new ObjectType($this->getNewClassName()))) { + 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 buildPromotedParam(string $name): Param + { + return new Param( + new Variable($name), + null, + new FullyQualified($this->getNewClassName()), + false, + false, + [], + Modifiers::PRIVATE, + ); + } +} diff --git a/src/Rector/ReplaceIConfigWithIAppConfigRector.php b/src/Rector/ReplaceIConfigWithIAppConfigRector.php index b206075..faa78b0 100644 --- a/src/Rector/ReplaceIConfigWithIAppConfigRector.php +++ b/src/Rector/ReplaceIConfigWithIAppConfigRector.php @@ -10,42 +10,42 @@ namespace Nextcloud\Rector\Rector; use Override; -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 +final class ReplaceIConfigWithIAppConfigRector extends AReplaceClassRector { - private const APP_CONFIG_CLASS = 'OCP\IAppConfig'; + #[Override] + public function getOldClassName(): string + { + return 'OCP\IConfig'; + } - /** - * 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', - ]; + #[Override] + public function getNewClassName(): string + { + return 'OCP\IAppConfig'; + } + + #[Override] + public function getDesiredVarName(): string + { + return 'appConfig'; + } + + #[Override] + public function getMethodMap(): array + { + return [ + 'getAppValue' => 'getValue', + 'getAppKeys' => 'getKeys', + 'setAppValue' => 'setValue', + 'deleteAppValue' => 'deleteKey', + 'deleteAppValues' => 'deleteApp', + ]; + } + #[Override] public function getRuleDefinition(): RuleDefinition { return new RuleDefinition( @@ -84,174 +84,4 @@ public function run(): string ], ); } - - /** - * @return array> - */ - #[Override] - public function getNodeTypes(): array - { - return [Class_::class]; - } - - #[Override] - 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; - } - - if ($this->isNames($propertyFetch->name, $propertyNames)) { - $calls[] = $subNode; - } - - 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/Rector/ReplaceIConfigWithIUserConfigRector.php b/src/Rector/ReplaceIConfigWithIUserConfigRector.php new file mode 100644 index 0000000..b92cf7f --- /dev/null +++ b/src/Rector/ReplaceIConfigWithIUserConfigRector.php @@ -0,0 +1,91 @@ + 'getAllValues', + 'getUserKeys' => 'getKeys', + 'getUserValue' => 'getValueString', + 'getUserValueForUsers' => 'getValuesByUsers', + 'getUsersForUserValue' => 'searchUsersByValueString', + 'setUserValue' => 'setValueString', + 'deleteUserValue' => 'deleteUserConfig', + 'deleteAllUserValues' => 'deleteAllUserConfig', + 'deleteAppFromAllUsers' => 'deleteApp', + ]; + } + + #[Override] + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Replace deprecated \OCP\IConfig user-config methods with their \OCP\IUserConfig counterparts,' + . ' injecting IUserConfig 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->getUserValue('user', 'myapp', 'mykey', 'default'); + } +} +CODE_SAMPLE, + <<<'CODE_SAMPLE' +use OCP\IConfig; +use OCP\IUserConfig; + +class SomeClass +{ + public function __construct(private IConfig $config, private IUserConfig $userConfig) {} + + public function run(): string + { + return $this->userConfig->getValueString('user', 'myapp', 'mykey', 'default'); + } +} +CODE_SAMPLE, + ), + ], + ); + } +} diff --git a/tests/Rector/ReplaceIConfigWithIUserConfigRector/Fixture/test_fixture.php.inc b/tests/Rector/ReplaceIConfigWithIUserConfigRector/Fixture/test_fixture.php.inc new file mode 100644 index 0000000..00f491d --- /dev/null +++ b/tests/Rector/ReplaceIConfigWithIUserConfigRector/Fixture/test_fixture.php.inc @@ -0,0 +1,59 @@ +config->getUserValue('user', 'myapp', 'mykey'); + $this->config->getUserValue('user', 'myapp', 'mykey', 'default'); + $this->config->getUserKeys('user', 'myapp'); + $this->config->setUserValue('user', 'myapp', 'mykey', 'value'); + $this->config->deleteUserValue('user', 'myapp', 'mykey'); + $this->config->deleteAppFromAllUsers('myapp'); + } +} + +?> +----- +userConfig->getValueString('user', 'myapp', 'mykey'); + $this->userConfig->getValueString('user', 'myapp', 'mykey', 'default'); + $this->userConfig->getKeys('user', 'myapp'); + $this->userConfig->setValueString('user', 'myapp', 'mykey', 'value'); + $this->userConfig->deleteUserConfig('user', 'myapp', 'mykey'); + $this->userConfig->deleteApp('myapp'); + } +} + +?> diff --git a/tests/Rector/ReplaceIConfigWithIUserConfigRector/Fixture/test_fixture_double.php.inc b/tests/Rector/ReplaceIConfigWithIUserConfigRector/Fixture/test_fixture_double.php.inc new file mode 100644 index 0000000..1c04146 --- /dev/null +++ b/tests/Rector/ReplaceIConfigWithIUserConfigRector/Fixture/test_fixture_double.php.inc @@ -0,0 +1,61 @@ +config->getUserValue('user', 'myapp', 'mykey'); + $this->config->getUserValue('user', 'myapp', 'mykey', 'default'); + $this->config->getUserKeys('user', 'myapp'); + $this->config->setUserValue('user', 'myapp', 'mykey', 'value'); + $this->config->deleteUserValue('user', 'myapp', 'mykey'); + $this->config->deleteAppFromAllUsers('myapp'); + } +} + +?> +----- +userConfig->getValueString('user', 'myapp', 'mykey'); + $this->userConfig->getValueString('user', 'myapp', 'mykey', 'default'); + $this->userConfig->getKeys('user', 'myapp'); + $this->userConfig->setValueString('user', 'myapp', 'mykey', 'value'); + $this->userConfig->deleteUserConfig('user', 'myapp', 'mykey'); + $this->userConfig->deleteApp('myapp'); + } +} + +?> diff --git a/tests/Rector/ReplaceIConfigWithIUserConfigRector/ReplaceIConfigWithIUserConfigRectorTest.php b/tests/Rector/ReplaceIConfigWithIUserConfigRector/ReplaceIConfigWithIUserConfigRectorTest.php new file mode 100644 index 0000000..514614e --- /dev/null +++ b/tests/Rector/ReplaceIConfigWithIUserConfigRector/ReplaceIConfigWithIUserConfigRectorTest.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/ReplaceIConfigWithIUserConfigRector/config/config.php b/tests/Rector/ReplaceIConfigWithIUserConfigRector/config/config.php new file mode 100644 index 0000000..0c8acd1 --- /dev/null +++ b/tests/Rector/ReplaceIConfigWithIUserConfigRector/config/config.php @@ -0,0 +1,16 @@ +withRules([ + ReplaceIConfigWithIUserConfigRector::class, + ]);