Skip to content

Commit d54e779

Browse files
committed
feat(iuserconfig): Add rector to migrate IConfig user related calls to IUserConfig
Signed-off-by: Joas Schilling <coding@schilljs.com>
1 parent 55a7a7e commit d54e779

8 files changed

Lines changed: 520 additions & 201 deletions

File tree

config/nextcloud-33/nextcloud-33-deprecations.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,22 @@
44

55
use Nextcloud\Rector\Rector\AnnotationToAttributeRector;
66
use Nextcloud\Rector\Rector\ReplaceFetchAllMethodCallRector;
7+
use Nextcloud\Rector\Rector\ReplaceIConfigWithIUserConfigRector;
78
use Nextcloud\Rector\Set\NextcloudSets;
89
use Rector\Config\RectorConfig;
910
use Rector\Php80\ValueObject\AnnotationToAttribute;
1011

1112
return static function (RectorConfig $rectorConfig): void {
1213
$rectorConfig->sets([NextcloudSets::NEXTCLOUD_29]);
1314
$rectorConfig->rule(ReplaceFetchAllMethodCallRector::class);
15+
$rectorConfig->rule(ReplaceIConfigWithIUserConfigRector::class);
1416
$rectorConfig->ruleWithConfiguration(
1517
AnnotationToAttributeRector::class,
1618
[
17-
new AnnotationToAttribute('NoSameSiteCookieRequired', 'OCP\AppFramework\Http\Attribute\NoSameSiteCookieRequired'),
19+
new AnnotationToAttribute(
20+
'NoSameSiteCookieRequired',
21+
'OCP\AppFramework\Http\Attribute\NoSameSiteCookieRequired',
22+
),
1823
new AnnotationToAttribute('NoTwoFactorRequired', 'OCP\AppFramework\Http\Attribute\NoTwoFactorRequired'),
1924
],
2025
);

src/Rector/AReplaceClassRector.php

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace Nextcloud\Rector\Rector;
11+
12+
use Override;
13+
use PHPStan\Type\ObjectType;
14+
use PhpParser\Modifiers;
15+
use PhpParser\Node;
16+
use PhpParser\Node\Expr\MethodCall;
17+
use PhpParser\Node\Expr\PropertyFetch;
18+
use PhpParser\Node\Expr\Variable;
19+
use PhpParser\Node\Identifier;
20+
use PhpParser\Node\Name\FullyQualified;
21+
use PhpParser\Node\Param;
22+
use PhpParser\Node\Stmt\ClassMethod;
23+
use PhpParser\Node\Stmt\Class_;
24+
use Rector\Rector\AbstractRector;
25+
use Rector\ValueObject\MethodName;
26+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
27+
28+
use function array_keys;
29+
use function is_string;
30+
31+
abstract class AReplaceClassRector extends AbstractRector
32+
{
33+
abstract public function getOldClassName(): string;
34+
35+
abstract public function getNewClassName(): string;
36+
37+
abstract public function getDesiredVarName(): string;
38+
39+
/**
40+
* @return array<string, string>
41+
*/
42+
abstract public function getMethodMap(): array;
43+
44+
abstract public function getRuleDefinition(): RuleDefinition;
45+
46+
protected function getNewMethod(?string $oldMethod): ?string
47+
{
48+
if ($oldMethod === null) {
49+
return null;
50+
}
51+
52+
return $this->getMethodMap()[$oldMethod] ?? null;
53+
}
54+
55+
/**
56+
* @return array<class-string<Node>>
57+
*/
58+
#[Override]
59+
public function getNodeTypes(): array
60+
{
61+
return [Class_::class];
62+
}
63+
64+
#[Override]
65+
public function refactor(Node $node): ?Node
66+
{
67+
if (!($node instanceof Class_)) {
68+
return null;
69+
}
70+
71+
$constructor = $node->getMethod(MethodName::CONSTRUCT);
72+
if (!$constructor instanceof ClassMethod) {
73+
return null;
74+
}
75+
76+
$iConfigPropertyNames = $this->collectOldPromotedPropertyNames($constructor);
77+
if ($iConfigPropertyNames === []) {
78+
return null;
79+
}
80+
81+
$callsToRewrite = $this->collectDeprecatedCalls($node, $iConfigPropertyNames);
82+
if ($callsToRewrite === []) {
83+
return null;
84+
}
85+
86+
$appConfigName = $this->findExistingPropertyName($constructor);
87+
if ($appConfigName === null) {
88+
$appConfigName = $this->makeUniquePropertyName($constructor, $this->getDesiredVarName());
89+
$constructor->params[] = $this->buildPromotedParam($appConfigName);
90+
}
91+
92+
foreach ($callsToRewrite as $call) {
93+
$oldMethodName = $this->getName($call->name);
94+
$newMethodName = $this->getNewMethod($oldMethodName);
95+
if ($oldMethodName === null || $newMethodName === null) {
96+
continue;
97+
}
98+
/** @var PropertyFetch $propertyFetch */
99+
$propertyFetch = $call->var;
100+
$propertyFetch->name = new Identifier($appConfigName);
101+
$call->name = new Identifier($newMethodName);
102+
}
103+
104+
return $node;
105+
}
106+
107+
/**
108+
* @return list<string>
109+
*/
110+
private function collectOldPromotedPropertyNames(ClassMethod $constructor): array
111+
{
112+
$names = [];
113+
foreach ($constructor->getParams() as $param) {
114+
if ($param->flags === 0) {
115+
continue;
116+
}
117+
if (!$this->isObjectType($param, new ObjectType($this->getOldClassName()))) {
118+
continue;
119+
}
120+
$name = $this->getName($param->var);
121+
if (is_string($name)) {
122+
$names[] = $name;
123+
}
124+
}
125+
126+
return $names;
127+
}
128+
129+
/**
130+
* @param list<string> $propertyNames
131+
*
132+
* @return list<MethodCall>
133+
*/
134+
private function collectDeprecatedCalls(Class_ $class, array $propertyNames): array
135+
{
136+
$deprecatedMethods = array_keys($this->getMethodMap());
137+
$calls = [];
138+
foreach ($class->getMethods() as $classMethod) {
139+
$stmts = $classMethod->getStmts();
140+
if ($stmts === null) {
141+
continue;
142+
}
143+
$this->traverseNodesWithCallable(
144+
$stmts,
145+
function (Node $subNode) use ($propertyNames, $deprecatedMethods, &$calls): ?Node {
146+
if (!$subNode instanceof MethodCall) {
147+
return null;
148+
}
149+
if (!$this->isNames($subNode->name, $deprecatedMethods)) {
150+
return null;
151+
}
152+
if (!$subNode->var instanceof PropertyFetch) {
153+
return null;
154+
}
155+
$propertyFetch = $subNode->var;
156+
if (!$propertyFetch->var instanceof Variable) {
157+
return null;
158+
}
159+
if (!$this->isName($propertyFetch->var, 'this')) {
160+
return null;
161+
}
162+
if ($this->isNames($propertyFetch->name, $propertyNames)) {
163+
$calls[] = $subNode;
164+
}
165+
166+
return null;
167+
},
168+
);
169+
}
170+
171+
return $calls;
172+
}
173+
174+
private function findExistingPropertyName(ClassMethod $constructor): ?string
175+
{
176+
foreach ($constructor->getParams() as $param) {
177+
if ($param->flags === 0) {
178+
continue;
179+
}
180+
if (!$this->isObjectType($param, new ObjectType($this->getNewClassName()))) {
181+
continue;
182+
}
183+
$name = $this->getName($param->var);
184+
if (is_string($name)) {
185+
return $name;
186+
}
187+
}
188+
189+
return null;
190+
}
191+
192+
private function makeUniquePropertyName(ClassMethod $constructor, string $desired): string
193+
{
194+
$taken = [];
195+
foreach ($constructor->getParams() as $param) {
196+
$name = $this->getName($param->var);
197+
if (is_string($name)) {
198+
$taken[$name] = true;
199+
}
200+
}
201+
if (!isset($taken[$desired])) {
202+
return $desired;
203+
}
204+
$i = 2;
205+
while (isset($taken[$desired . $i])) {
206+
$i++;
207+
}
208+
209+
return $desired . $i;
210+
}
211+
212+
private function buildPromotedParam(string $name): Param
213+
{
214+
return new Param(
215+
new Variable($name),
216+
null,
217+
new FullyQualified($this->getNewClassName()),
218+
false,
219+
false,
220+
[],
221+
Modifiers::PRIVATE,
222+
);
223+
}
224+
}

0 commit comments

Comments
 (0)