@flarum/cli 3.0.12 → 3.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -1
- package/boilerplate/skeleton/extension/composer.json +4 -0
- package/boilerplate/skeleton/extension/js/package.json +4 -4
- package/boilerplate/skeleton/extension/js/tsconfig.json +3 -0
- package/boilerplate/skeleton/extension/js/tsconfig.test.json +1 -0
- package/boilerplate/stubs/backend/advanced-api-resource.php +3 -3
- package/boilerplate/stubs/backend/model-factory.php +17 -0
- package/boilerplate/stubs/backend/model.php +3 -0
- package/lib/base-command.d.ts +21 -3
- package/lib/base-command.js +52 -5
- package/lib/commands/make/backend/model.js +10 -0
- package/lib/commands/upgrade/2.0.d.ts +43 -0
- package/lib/commands/upgrade/2.0.js +128 -2
- package/lib/steps/gen-ext-scaffolder.d.ts +5 -0
- package/lib/steps/gen-ext-scaffolder.js +17 -2
- package/lib/steps/stubs/backend/advanced-api-resource.js +1 -1
- package/lib/steps/stubs/backend/model-factory.d.ts +24 -0
- package/lib/steps/stubs/backend/model-factory.js +33 -0
- package/lib/steps/upgrade/twopointoh/backend/email-views.js +22 -30
- package/lib/steps/upgrade/twopointoh/backend/extension-models.d.ts +30 -0
- package/lib/steps/upgrade/twopointoh/backend/extension-models.js +34 -0
- package/lib/steps/upgrade/twopointoh/backend/json-api.d.ts +6 -0
- package/lib/steps/upgrade/twopointoh/backend/json-api.js +14 -0
- package/lib/steps/upgrade/twopointoh/backend/misc.d.ts +6 -0
- package/lib/steps/upgrade/twopointoh/backend/misc.js +19 -7
- package/lib/steps/upgrade/twopointoh/backend/model-factories.d.ts +18 -0
- package/lib/steps/upgrade/twopointoh/backend/model-factories.js +73 -0
- package/lib/steps/upgrade/twopointoh/backend/phpunit.d.ts +2 -0
- package/lib/steps/upgrade/twopointoh/backend/phpunit.js +16 -8
- package/lib/steps/upgrade/twopointoh/backend/search.d.ts +9 -0
- package/lib/steps/upgrade/twopointoh/backend/search.js +28 -10
- package/lib/steps/upgrade/twopointoh/base.d.ts +38 -0
- package/lib/steps/upgrade/twopointoh/base.js +73 -12
- package/lib/steps/upgrade/twopointoh/dependencies.d.ts +58 -1
- package/lib/steps/upgrade/twopointoh/dependencies.js +342 -19
- package/lib/steps/upgrade/twopointoh/frontend/admin-extender.d.ts +86 -0
- package/lib/steps/upgrade/twopointoh/frontend/admin-extender.js +842 -0
- package/lib/steps/upgrade/twopointoh/frontend/code-splitting/usage-of-lazy-modules.js +110 -18
- package/lib/steps/upgrade/twopointoh/frontend/export-registry/import-ext.js +13 -16
- package/lib/steps/upgrade/twopointoh/frontend/misc.d.ts +23 -0
- package/lib/steps/upgrade/twopointoh/frontend/misc.js +147 -8
- package/lib/steps/upgrade/twopointoh/infra.d.ts +16 -1
- package/lib/steps/upgrade/twopointoh/infra.js +87 -5
- package/lib/steps/upgrade/twopointoh/step-failures.d.ts +8 -0
- package/lib/steps/upgrade/twopointoh/step-failures.js +6 -0
- package/lib/steps/upgrade/twopointoh/todos.d.ts +22 -0
- package/lib/steps/upgrade/twopointoh/todos.js +57 -0
- package/lib/steps/upgrade/twopointoh/typescript-4-9.d.ts +12 -0
- package/lib/steps/upgrade/twopointoh/typescript-4-9.js +200 -0
- package/lib/utils/ast.js +3 -2
- package/lib/utils/babel-ts.d.ts +9 -0
- package/lib/utils/babel-ts.js +18 -0
- package/lib/utils/composer.d.ts +5 -0
- package/lib/utils/composer.js +10 -1
- package/lib/utils/terminal.d.ts +13 -0
- package/lib/utils/terminal.js +26 -0
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/php-subsystem/index.php +12 -0
- package/php-subsystem/src/NodeUtil.php +19 -9
- package/php-subsystem/src/NodeVisitors/AddExtender.php +49 -1
- package/php-subsystem/src/Upgrade/TwoPointOh/JsonApi.php +2 -20
- package/php-subsystem/src/Upgrade/TwoPointOh/Misc.php +13 -17
- package/php-subsystem/src/Upgrade/TwoPointOh/ModelFactories.php +943 -0
- package/php-subsystem/src/Upgrade/TwoPointOh/PhpUnit.php +476 -71
- package/php-subsystem/src/Upgrade/TwoPointOh/Search.php +97 -13
|
@@ -6,6 +6,7 @@ use Flarum\CliPhpSubsystem\NodeUtil;
|
|
|
6
6
|
use Flarum\CliPhpSubsystem\NodeVisitors\AddUses;
|
|
7
7
|
use Flarum\CliPhpSubsystem\Upgrade\Replacement;
|
|
8
8
|
use Flarum\CliPhpSubsystem\Upgrade\ReplacementResult;
|
|
9
|
+
use PhpParser\Comment;
|
|
9
10
|
use PhpParser\Comment\Doc;
|
|
10
11
|
use PhpParser\Modifiers;
|
|
11
12
|
use PhpParser\Node;
|
|
@@ -13,13 +14,134 @@ use PhpParser\NodeVisitor;
|
|
|
13
14
|
|
|
14
15
|
class PhpUnit extends Replacement
|
|
15
16
|
{
|
|
17
|
+
/**
|
|
18
|
+
* PHPUnit 12 reads test metadata only from attributes. These annotations
|
|
19
|
+
* have one: [attribute, whether it takes the rest of the line, where PHPUnit
|
|
20
|
+
* allows it].
|
|
21
|
+
*/
|
|
22
|
+
protected const ANNOTATIONS = [
|
|
23
|
+
'test' => ['Test', false, ['method']],
|
|
24
|
+
'group' => ['Group', true, ['class', 'method']],
|
|
25
|
+
'ticket' => ['Ticket', true, ['class', 'method']],
|
|
26
|
+
'testdox' => ['TestDox', true, ['class', 'method']],
|
|
27
|
+
'doesNotPerformAssertions' => ['DoesNotPerformAssertions', false, ['class', 'method']],
|
|
28
|
+
'runInSeparateProcess' => ['RunInSeparateProcess', false, ['method']],
|
|
29
|
+
'runTestsInSeparateProcesses' => ['RunTestsInSeparateProcesses', false, ['class']],
|
|
30
|
+
'before' => ['Before', false, ['method']],
|
|
31
|
+
'after' => ['After', false, ['method']],
|
|
32
|
+
'beforeClass' => ['BeforeClass', false, ['method']],
|
|
33
|
+
'afterClass' => ['AfterClass', false, ['method']],
|
|
34
|
+
'preCondition' => ['PreCondition', false, ['method']],
|
|
35
|
+
'postCondition' => ['PostCondition', false, ['method']],
|
|
36
|
+
// On a method, PHPUnit 12.3 deprecates it.
|
|
37
|
+
'coversNothing' => ['CoversNothing', false, ['class']],
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Annotations PHPUnit read that need an attribute the upgrade can't work
|
|
42
|
+
* out. They stay, flagged.
|
|
43
|
+
*/
|
|
44
|
+
protected const OTHER_ANNOTATIONS = [
|
|
45
|
+
'backupGlobals', 'backupStaticAttributes', 'covers', 'coversDefaultClass', 'large', 'medium',
|
|
46
|
+
'preserveGlobalState', 'requires', 'small', 'testWith', 'uses',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
public const ATTRIBUTES_DOCS = 'https://docs.phpunit.de/en/12.5/attributes.html';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Assertions PHPUnit 10 removed, each for one that does the same.
|
|
53
|
+
*/
|
|
54
|
+
public const RENAMED_ASSERTIONS = [
|
|
55
|
+
'assertObjectHasAttribute' => 'assertObjectHasProperty',
|
|
56
|
+
'assertObjectNotHasAttribute' => 'assertObjectNotHasProperty',
|
|
57
|
+
'assertRegExp' => 'assertMatchesRegularExpression',
|
|
58
|
+
'assertNotRegExp' => 'assertDoesNotMatchRegularExpression',
|
|
59
|
+
'assertFileNotExists' => 'assertFileDoesNotExist',
|
|
60
|
+
'assertFileNotIsReadable' => 'assertFileIsNotReadable',
|
|
61
|
+
'assertFileNotIsWritable' => 'assertFileIsNotWritable',
|
|
62
|
+
'assertDirectoryNotExists' => 'assertDirectoryDoesNotExist',
|
|
63
|
+
'assertDirectoryNotIsReadable' => 'assertDirectoryIsNotReadable',
|
|
64
|
+
'assertDirectoryNotIsWritable' => 'assertDirectoryIsNotWritable',
|
|
65
|
+
'assertNotIsReadable' => 'assertIsNotReadable',
|
|
66
|
+
'assertNotIsWritable' => 'assertIsNotWritable',
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The TestCase stub helpers PHPUnit 12 removed, and the will*() method
|
|
71
|
+
* that replaces `->will($this->helper(...))`.
|
|
72
|
+
*/
|
|
73
|
+
public const STUB_HELPERS = [
|
|
74
|
+
'returnValue' => 'willReturn',
|
|
75
|
+
'returnValueMap' => 'willReturnMap',
|
|
76
|
+
'returnArgument' => 'willReturnArgument',
|
|
77
|
+
'returnCallback' => 'willReturnCallback',
|
|
78
|
+
'returnSelf' => 'willReturnSelf',
|
|
79
|
+
'throwException' => 'willThrowException',
|
|
80
|
+
'onConsecutiveCalls' => 'willReturnOnConsecutiveCalls',
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* What PHPUnit removed without a drop-in replacement, by the version that
|
|
85
|
+
* removed it. These names are PHPUnit's alone, so they are flagged on any
|
|
86
|
+
* object.
|
|
87
|
+
*/
|
|
88
|
+
public const REMOVED = [
|
|
89
|
+
'withConsecutive' => '10',
|
|
90
|
+
'setMethods' => '10',
|
|
91
|
+
'setMethodsExcept' => '10',
|
|
92
|
+
'assertEqualXMLStructure' => '10',
|
|
93
|
+
'getMockForAbstractClass' => '12',
|
|
94
|
+
'getMockForTrait' => '12',
|
|
95
|
+
'addMethods' => '12',
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Likewise, but only TestCase methods: other objects have methods of the
|
|
100
|
+
* same name (a translator's setLocale(), say), so these are flagged only
|
|
101
|
+
* on `$this`, `self` and `static`.
|
|
102
|
+
*/
|
|
103
|
+
public const REMOVED_FROM_TEST_CASE = [
|
|
104
|
+
'at' => '10',
|
|
105
|
+
'getMockClass' => '10',
|
|
106
|
+
'expectDeprecation' => '10',
|
|
107
|
+
'expectDeprecationMessage' => '10',
|
|
108
|
+
'expectDeprecationMessageMatches' => '10',
|
|
109
|
+
'expectError' => '10',
|
|
110
|
+
'expectErrorMessage' => '10',
|
|
111
|
+
'expectErrorMessageMatches' => '10',
|
|
112
|
+
'expectNotice' => '10',
|
|
113
|
+
'expectNoticeMessage' => '10',
|
|
114
|
+
'expectNoticeMessageMatches' => '10',
|
|
115
|
+
'expectWarning' => '10',
|
|
116
|
+
'expectWarningMessage' => '10',
|
|
117
|
+
'expectWarningMessageMatches' => '10',
|
|
118
|
+
'iniSet' => '12',
|
|
119
|
+
'setLocale' => '12',
|
|
120
|
+
'createTestProxy' => '12',
|
|
121
|
+
'getMockFromWsdl' => '12',
|
|
122
|
+
'getObjectForTrait' => '12',
|
|
123
|
+
// Stub helpers used other than through will().
|
|
124
|
+
'returnValue' => '12',
|
|
125
|
+
'returnValueMap' => '12',
|
|
126
|
+
'returnArgument' => '12',
|
|
127
|
+
'returnCallback' => '12',
|
|
128
|
+
'returnSelf' => '12',
|
|
129
|
+
'throwException' => '12',
|
|
130
|
+
'onConsecutiveCalls' => '12',
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
protected const CHANGELOGS = [
|
|
134
|
+
'10' => 'https://github.com/sebastianbergmann/phpunit/blob/10.0.0/ChangeLog-10.0.md',
|
|
135
|
+
'12' => 'https://github.com/sebastianbergmann/phpunit/blob/12.0.0/ChangeLog-12.0.md',
|
|
136
|
+
];
|
|
137
|
+
|
|
16
138
|
protected $dataProviders = [];
|
|
17
139
|
|
|
18
140
|
protected $usedModels = [];
|
|
19
141
|
|
|
20
142
|
protected function operations(): array
|
|
21
143
|
{
|
|
22
|
-
return ['run', 'postRun', 'tablesToModels', 'postTablesToModels'];
|
|
144
|
+
return ['run', 'postRun', 'replaceRemovedApis', 'flagRemovedApis', 'tablesToModels', 'postTablesToModels'];
|
|
23
145
|
}
|
|
24
146
|
|
|
25
147
|
function run(string $file, string $code, array $ast, array $data): ?ReplacementResult
|
|
@@ -30,87 +152,84 @@ class PhpUnit extends Replacement
|
|
|
30
152
|
|
|
31
153
|
$traverser = $this->traverser();
|
|
32
154
|
|
|
33
|
-
|
|
34
|
-
$traverser->addVisitor($visitor = new class extends \PhpParser\NodeVisitorAbstract {
|
|
155
|
+
$traverser->addVisitor($visitor = new class ($this) extends \PhpParser\NodeVisitorAbstract {
|
|
35
156
|
public $dataProviders = [];
|
|
36
|
-
public $
|
|
157
|
+
public $attributes = [];
|
|
37
158
|
|
|
38
|
-
public function
|
|
159
|
+
public function __construct(private PhpUnit $phpUnit)
|
|
39
160
|
{
|
|
40
|
-
|
|
41
|
-
$docComment = $node->getDocComment();
|
|
161
|
+
}
|
|
42
162
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
163
|
+
public function leaveNode(Node $node)
|
|
164
|
+
{
|
|
165
|
+
if ($node instanceof Node\Stmt\Namespace_ && ! empty($this->attributes)) {
|
|
166
|
+
$uses = array_map(fn (string $attribute) => "PHPUnit\\Framework\\Attributes\\$attribute", array_keys($this->attributes));
|
|
167
|
+
sort($uses);
|
|
46
168
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
169
|
+
NodeUtil::addUsesToNamespace($node, $uses);
|
|
170
|
+
}
|
|
50
171
|
|
|
51
|
-
|
|
172
|
+
$target = match (true) {
|
|
173
|
+
$node instanceof Node\Stmt\ClassMethod => 'method',
|
|
174
|
+
$node instanceof Node\Stmt\Class_ => 'class',
|
|
175
|
+
default => null,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
if (! $target || ! $node->getDocComment()) {
|
|
179
|
+
return null;
|
|
52
180
|
}
|
|
53
|
-
}
|
|
54
181
|
|
|
55
|
-
|
|
56
|
-
{
|
|
57
|
-
if ($node instanceof Node\Stmt\Namespace_) {
|
|
58
|
-
$uses = [];
|
|
182
|
+
[$doc, $groups, $providers, $unconverted] = $this->phpUnit->readMetadata($node->getDocComment()->getText(), $target);
|
|
59
183
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
184
|
+
if (! $groups && ! $unconverted) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
63
187
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
188
|
+
if ($groups) {
|
|
189
|
+
$comments = array_values(array_filter($node->getComments(), fn ($comment) => ! $comment instanceof Doc));
|
|
67
190
|
|
|
68
|
-
if (
|
|
69
|
-
|
|
191
|
+
if ($doc !== null) {
|
|
192
|
+
$comments[] = new Doc($doc);
|
|
70
193
|
}
|
|
194
|
+
|
|
195
|
+
$node->setAttribute('comments', $comments);
|
|
71
196
|
}
|
|
72
197
|
|
|
73
|
-
if ($
|
|
74
|
-
|
|
75
|
-
// @test => \PHPUnit\Metadata\Test
|
|
76
|
-
// @dataProvider => \PHPUnit\Metadata\DataProvider
|
|
198
|
+
if ($unconverted) {
|
|
199
|
+
$names = implode(', ', array_map(fn (string $name) => "@$name", array_unique($unconverted)));
|
|
77
200
|
|
|
78
|
-
$
|
|
201
|
+
$node->setAttribute('comments', PhpUnit::withComment(
|
|
202
|
+
$node->getComments(),
|
|
203
|
+
"// @TODO: PHPUnit 12 no longer reads $names here. Use attributes instead: ".PhpUnit::ATTRIBUTES_DOCS
|
|
204
|
+
));
|
|
205
|
+
}
|
|
79
206
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
new Node\Attribute(new Node\Name('Test')),
|
|
83
|
-
]);
|
|
84
|
-
}
|
|
207
|
+
foreach ($groups as $group) {
|
|
208
|
+
$name = $group->attrs[0]->name->toString();
|
|
85
209
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
]),
|
|
91
|
-
]);
|
|
210
|
+
// A test already given the attribute keeps just that.
|
|
211
|
+
if (! $this->hasAttribute($node, $name)) {
|
|
212
|
+
$node->attrGroups[] = $group;
|
|
213
|
+
$this->attributes[$name] = true;
|
|
92
214
|
}
|
|
215
|
+
}
|
|
93
216
|
|
|
94
|
-
|
|
95
|
-
$docComment = preg_replace('/^\s*\/?\**\s*@test\b(\*\/)?.*$/m', '', $docComment);
|
|
96
|
-
$docComment = preg_replace('/^\s*\/?\**\s*@dataProvider\b(\*\/)?.*$/m', '', $docComment);
|
|
217
|
+
array_push($this->dataProviders, ...$providers);
|
|
97
218
|
|
|
98
|
-
|
|
99
|
-
|
|
219
|
+
return $node;
|
|
220
|
+
}
|
|
100
221
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
222
|
+
protected function hasAttribute(Node $node, string $name): bool
|
|
223
|
+
{
|
|
224
|
+
foreach ($node->attrGroups as $group) {
|
|
225
|
+
foreach ($group->attrs as $attribute) {
|
|
226
|
+
if ($attribute->name->getLast() === $name) {
|
|
227
|
+
return true;
|
|
105
228
|
}
|
|
106
|
-
|
|
107
|
-
$node->setAttribute('comments', $comments);
|
|
108
|
-
} else {
|
|
109
|
-
$node->setDocComment(new Doc($docComment));
|
|
110
229
|
}
|
|
111
230
|
}
|
|
112
231
|
|
|
113
|
-
return
|
|
232
|
+
return false;
|
|
114
233
|
}
|
|
115
234
|
});
|
|
116
235
|
|
|
@@ -121,6 +240,139 @@ class PhpUnit extends Replacement
|
|
|
121
240
|
return new ReplacementResult($ast);
|
|
122
241
|
}
|
|
123
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Read the PHPUnit metadata out of a doc comment.
|
|
245
|
+
*
|
|
246
|
+
* @return array{0: ?string, 1: Node\AttributeGroup[], 2: string[], 3: string[]}
|
|
247
|
+
* The doc comment without the annotations that became attributes (null
|
|
248
|
+
* if nothing else was in it), those attributes in the order they were
|
|
249
|
+
* written, the data providers in the same class, and the annotations
|
|
250
|
+
* that could not be converted.
|
|
251
|
+
*/
|
|
252
|
+
public function readMetadata(string $doc, string $target): array
|
|
253
|
+
{
|
|
254
|
+
$lines = explode("\n", $doc);
|
|
255
|
+
$kept = [];
|
|
256
|
+
$groups = [];
|
|
257
|
+
$providers = [];
|
|
258
|
+
$unconverted = [];
|
|
259
|
+
|
|
260
|
+
foreach ($lines as $line) {
|
|
261
|
+
$content = preg_replace('~^\s*(/\*\*|\*(?!/))?\s*~', '', $line);
|
|
262
|
+
$content = preg_replace('~\s*\*/\s*$~', '', $content);
|
|
263
|
+
|
|
264
|
+
if (! preg_match('/^@(\w+)(?:\s+(.*))?$/', $content, $matches)) {
|
|
265
|
+
$kept[] = $line;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
[$name, $argument] = [$matches[1], trim($matches[2] ?? '')];
|
|
270
|
+
|
|
271
|
+
$group = match (true) {
|
|
272
|
+
$target === 'method' && in_array($name, ['dataProvider', 'depends']) => $this->reference($name, $argument, $providers),
|
|
273
|
+
isset(self::ANNOTATIONS[$name]) && in_array($target, self::ANNOTATIONS[$name][2]) => $this->attribute($name, $argument),
|
|
274
|
+
default => null,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
if ($group) {
|
|
278
|
+
$groups[] = $group;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (isset(self::ANNOTATIONS[$name]) || in_array($name, self::OTHER_ANNOTATIONS) || in_array($name, ['dataProvider', 'depends'])) {
|
|
283
|
+
$unconverted[] = $name;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
$kept[] = $line;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (! $groups) {
|
|
290
|
+
return [$doc, [], [], $unconverted];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// #[Test] first, as Flarum's own tests have it; the rest as written.
|
|
294
|
+
$isTest = fn (Node\AttributeGroup $group) => $group->attrs[0]->name->toString() === 'Test';
|
|
295
|
+
usort($groups, fn (Node\AttributeGroup $a, Node\AttributeGroup $b) => $isTest($b) <=> $isTest($a));
|
|
296
|
+
|
|
297
|
+
return [$this->rebuildDoc($kept), $groups, $providers, $unconverted];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* An attribute for a @dataProvider or @depends naming a method in this
|
|
302
|
+
* class or, as `Class::method`, in another.
|
|
303
|
+
*/
|
|
304
|
+
protected function reference(string $name, string $argument, array &$providers): ?Node\AttributeGroup
|
|
305
|
+
{
|
|
306
|
+
$attribute = $name === 'dataProvider' ? 'DataProvider' : 'Depends';
|
|
307
|
+
|
|
308
|
+
if (preg_match('/^\w+$/', $argument)) {
|
|
309
|
+
if ($name === 'dataProvider') {
|
|
310
|
+
$providers[] = $argument;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return $this->group($attribute, [new Node\Scalar\String_($argument)]);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// PHPUnit took the class name as written, so it is fully qualified.
|
|
317
|
+
if (preg_match('/^\\\\?([A-Za-z_][\w\\\\]*)::(\w+)$/', $argument, $matches) && $matches[2] !== 'class') {
|
|
318
|
+
return $this->group($attribute.'External', [
|
|
319
|
+
new Node\Expr\ClassConstFetch(new Node\Name\FullyQualified($matches[1]), 'class'),
|
|
320
|
+
new Node\Scalar\String_($matches[2]),
|
|
321
|
+
]);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
protected function attribute(string $name, string $argument): ?Node\AttributeGroup
|
|
328
|
+
{
|
|
329
|
+
[$attribute, $takesArgument] = self::ANNOTATIONS[$name];
|
|
330
|
+
|
|
331
|
+
if (! $takesArgument) {
|
|
332
|
+
return $this->group($attribute);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return $argument === '' ? null : $this->group($attribute, [new Node\Scalar\String_($argument)]);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
protected function group(string $attribute, array $arguments = []): Node\AttributeGroup
|
|
339
|
+
{
|
|
340
|
+
return new Node\AttributeGroup([
|
|
341
|
+
new Node\Attribute(new Node\Name($attribute), array_map(fn (Node\Expr $argument) => new Node\Arg($argument), $arguments)),
|
|
342
|
+
]);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* The doc comment from the lines left in it, or null if none of them says
|
|
347
|
+
* anything. Blank lines left at either end go.
|
|
348
|
+
*/
|
|
349
|
+
protected function rebuildDoc(array $lines): ?string
|
|
350
|
+
{
|
|
351
|
+
$blank = fn (string $line) => preg_match('~^\s*\*?\s*$~', $line);
|
|
352
|
+
|
|
353
|
+
// A one-line doc comment held only the annotation.
|
|
354
|
+
if (count($lines) < 2) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
$first = array_shift($lines);
|
|
359
|
+
$last = array_pop($lines);
|
|
360
|
+
|
|
361
|
+
while ($lines && $blank(end($lines))) {
|
|
362
|
+
array_pop($lines);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
while ($lines && $blank(reset($lines))) {
|
|
366
|
+
array_shift($lines);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (! $lines && trim($first) === '/**') {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return implode("\n", [$first, ...$lines, $last]);
|
|
374
|
+
}
|
|
375
|
+
|
|
124
376
|
function postRun(string $file, string $code, array $ast, array $data): ?ReplacementResult
|
|
125
377
|
{
|
|
126
378
|
if (strpos($file, '.php') === false) {
|
|
@@ -177,19 +429,27 @@ class PhpUnit extends Replacement
|
|
|
177
429
|
// Discussion::class => [...],
|
|
178
430
|
// ]);
|
|
179
431
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
432
|
+
// The class each short name already stands for in the file.
|
|
433
|
+
$imports = [];
|
|
434
|
+
|
|
435
|
+
foreach ((new \PhpParser\NodeFinder())->findInstanceOf($ast, Node\Stmt\Use_::class) as $use) {
|
|
436
|
+
if ($use->type === Node\Stmt\Use_::TYPE_NORMAL) {
|
|
437
|
+
foreach ($use->uses as $useUse) {
|
|
438
|
+
$imports[$useUse->getAlias()->toString()] = $useUse->name->toString();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Core's models with a factory, and the extension's own, by table.
|
|
444
|
+
$models = array_merge(ModelFactories::CORE_MODELS, $data['models'] ?? []);
|
|
190
445
|
|
|
446
|
+
$traverser->addVisitor($visitor = new class ($models, $imports) extends \PhpParser\NodeVisitorAbstract {
|
|
191
447
|
public $usedModels = [];
|
|
192
448
|
|
|
449
|
+
public function __construct(protected array $models, protected array $names)
|
|
450
|
+
{
|
|
451
|
+
}
|
|
452
|
+
|
|
193
453
|
public function leaveNode(Node $node)
|
|
194
454
|
{
|
|
195
455
|
if ($node instanceof Node\Expr\MethodCall) {
|
|
@@ -201,10 +461,17 @@ class PhpUnit extends Replacement
|
|
|
201
461
|
$table = $item->key->value;
|
|
202
462
|
|
|
203
463
|
if (isset($this->models[$table])) {
|
|
204
|
-
$
|
|
205
|
-
$
|
|
206
|
-
|
|
207
|
-
|
|
464
|
+
$class = $this->models[$table];
|
|
465
|
+
$short = substr($class, strrpos($class, '\\') + 1);
|
|
466
|
+
|
|
467
|
+
// A short name that already stands for another class is written in full.
|
|
468
|
+
if (isset($this->names[$short]) && $this->names[$short] !== $class) {
|
|
469
|
+
$item->key = new Node\Expr\ClassConstFetch(new Node\Name\FullyQualified($class), 'class');
|
|
470
|
+
} else {
|
|
471
|
+
$this->names[$short] = $class;
|
|
472
|
+
$item->key = new Node\Expr\ClassConstFetch(new Node\Name($short), 'class');
|
|
473
|
+
$this->usedModels[$class] = true;
|
|
474
|
+
}
|
|
208
475
|
}
|
|
209
476
|
}
|
|
210
477
|
}
|
|
@@ -238,4 +505,142 @@ class PhpUnit extends Replacement
|
|
|
238
505
|
|
|
239
506
|
return new ReplacementResult($traverser->traverse($ast));
|
|
240
507
|
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Rename the assertions PHPUnit 10 replaced, and configure stubs through
|
|
511
|
+
* the will*() methods instead of the helpers PHPUnit 12 removed.
|
|
512
|
+
*/
|
|
513
|
+
function replaceRemovedApis(string $file, string $code, array $ast, array $data): ?ReplacementResult
|
|
514
|
+
{
|
|
515
|
+
if (strpos($file, '.php') === false) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
$traverser = $this->traverser();
|
|
520
|
+
|
|
521
|
+
$traverser->addVisitor(new class () extends \PhpParser\NodeVisitorAbstract {
|
|
522
|
+
public function leaveNode(Node $node)
|
|
523
|
+
{
|
|
524
|
+
if (! ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\StaticCall) || ! $node->name instanceof Node\Identifier) {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (isset(PhpUnit::RENAMED_ASSERTIONS[$node->name->name])) {
|
|
529
|
+
$node->name = new Node\Identifier(PhpUnit::RENAMED_ASSERTIONS[$node->name->name]);
|
|
530
|
+
|
|
531
|
+
return $node;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ->will($this->returnValue(1)) is ->willReturn(1).
|
|
535
|
+
if ($node instanceof Node\Expr\MethodCall && $node->name->name === 'will' && count($node->args) === 1 && $node->args[0] instanceof Node\Arg) {
|
|
536
|
+
$helper = $node->args[0]->value;
|
|
537
|
+
|
|
538
|
+
if (PhpUnit::onTestCase($helper) && $helper->name instanceof Node\Identifier && isset(PhpUnit::STUB_HELPERS[$helper->name->name])) {
|
|
539
|
+
return new Node\Expr\MethodCall($node->var, PhpUnit::STUB_HELPERS[$helper->name->name], $helper->args, $node->getAttributes());
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
return new ReplacementResult($traverser->traverse($ast));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Flag what PHPUnit removed without a drop-in replacement, above the
|
|
552
|
+
* statement that uses it.
|
|
553
|
+
*/
|
|
554
|
+
function flagRemovedApis(string $file, string $code, array $ast, array $data): ?ReplacementResult
|
|
555
|
+
{
|
|
556
|
+
if (strpos($file, '.php') === false) {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
$traverser = $this->traverser();
|
|
561
|
+
|
|
562
|
+
$traverser->addVisitor(new NodeVisitor\ParentConnectingVisitor());
|
|
563
|
+
|
|
564
|
+
$traverser->addVisitor($visitor = new class () extends \PhpParser\NodeVisitorAbstract {
|
|
565
|
+
/** @var \SplObjectStorage<Node\Stmt, array<string, string>> */
|
|
566
|
+
public \SplObjectStorage $flags;
|
|
567
|
+
|
|
568
|
+
public function __construct()
|
|
569
|
+
{
|
|
570
|
+
$this->flags = new \SplObjectStorage();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
public function leaveNode(Node $node)
|
|
574
|
+
{
|
|
575
|
+
if (! ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\StaticCall) || ! $node->name instanceof Node\Identifier) {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
$name = $node->name->name;
|
|
580
|
+
$version = PhpUnit::REMOVED[$name] ?? (PhpUnit::onTestCase($node) ? PhpUnit::REMOVED_FROM_TEST_CASE[$name] ?? null : null);
|
|
581
|
+
|
|
582
|
+
if (! $version) {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
$statement = $node;
|
|
587
|
+
|
|
588
|
+
while ($statement && ! $statement instanceof Node\Stmt) {
|
|
589
|
+
$statement = $statement->getAttribute('parent');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if ($statement) {
|
|
593
|
+
$flags = isset($this->flags[$statement]) ? $this->flags[$statement] : [];
|
|
594
|
+
$flags[$name] = $version;
|
|
595
|
+
$this->flags[$statement] = $flags;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
$ast = $traverser->traverse($ast);
|
|
603
|
+
|
|
604
|
+
foreach ($visitor->flags as $statement) {
|
|
605
|
+
$comments = $statement->getComments();
|
|
606
|
+
|
|
607
|
+
foreach ($visitor->flags[$statement] as $name => $version) {
|
|
608
|
+
$comments = self::withComment($comments, "// @TODO: PHPUnit 12 has no $name(). ".self::CHANGELOGS[$version]);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
$statement->setAttribute('comments', $comments);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return new ReplacementResult($ast);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* The comments with one more line, unless an earlier run already added it.
|
|
619
|
+
*/
|
|
620
|
+
public static function withComment(array $comments, string $text): array
|
|
621
|
+
{
|
|
622
|
+
foreach ($comments as $comment) {
|
|
623
|
+
if ($comment->getText() === $text) {
|
|
624
|
+
return $comments;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return [...$comments, new Comment($text)];
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Whether a call is to the test case itself: `$this->`, `self::`, `static::`.
|
|
633
|
+
*/
|
|
634
|
+
public static function onTestCase(Node $node): bool
|
|
635
|
+
{
|
|
636
|
+
if ($node instanceof Node\Expr\MethodCall) {
|
|
637
|
+
return $node->var instanceof Node\Expr\Variable && $node->var->name === 'this';
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if ($node instanceof Node\Expr\StaticCall) {
|
|
641
|
+
return $node->class instanceof Node\Name && in_array($node->class->toLowerString(), ['self', 'static', 'parent']);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
241
646
|
}
|