Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions config/nextcloud-29/nextcloud-29-deprecations.php
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);
};
2 changes: 1 addition & 1 deletion config/nextcloud-33/nextcloud-33-deprecations.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
257 changes: 257 additions & 0 deletions src/Rector/ReplaceIConfigWithIAppConfigRector.php
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)) {
Comment on lines +191 to +192
Copy link
Copy Markdown
Collaborator

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?

Copy link
Copy Markdown
Contributor Author

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

$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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.
When its already there it's used. I added new tests in #81 to show that


private function buildPromotedAppConfigParam(string $name): Param
{
return new Param(
new Variable($name),
null,
new FullyQualified('OCP\IAppConfig'),
false,
false,
[],
Modifiers::PRIVATE,
);
}
}
8 changes: 4 additions & 4 deletions src/Set/NextcloudSets.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
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';
}
}
Loading
Loading