@flarum/cli 3.0.11 → 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.
Files changed (76) hide show
  1. package/README.md +33 -1
  2. package/bin/libgit2.1.5.dylib +0 -0
  3. package/bin/splitsh-lite-darwin-arm64 +0 -0
  4. package/boilerplate/skeleton/extension/composer.json +4 -0
  5. package/boilerplate/skeleton/extension/js/package.json +5 -5
  6. package/boilerplate/skeleton/extension/js/tsconfig.json +3 -0
  7. package/boilerplate/skeleton/extension/js/tsconfig.test.json +1 -0
  8. package/boilerplate/skeleton/extension/phpstan.neon +0 -1
  9. package/boilerplate/skeleton/extension/tests/phpunit.integration.xml +1 -0
  10. package/boilerplate/skeleton/extension/tests/phpunit.unit.xml +2 -1
  11. package/boilerplate/stubs/backend/advanced-api-resource.php +3 -3
  12. package/boilerplate/stubs/backend/model-factory.php +17 -0
  13. package/boilerplate/stubs/backend/model.php +3 -0
  14. package/lib/base-command.d.ts +21 -3
  15. package/lib/base-command.js +52 -5
  16. package/lib/commands/make/backend/model.js +10 -0
  17. package/lib/commands/monorepo/split.js +1 -1
  18. package/lib/commands/upgrade/2.0.d.ts +43 -0
  19. package/lib/commands/upgrade/2.0.js +128 -2
  20. package/lib/steps/gen-ext-scaffolder.d.ts +5 -0
  21. package/lib/steps/gen-ext-scaffolder.js +17 -2
  22. package/lib/steps/monorepo/split.d.ts +2 -1
  23. package/lib/steps/monorepo/split.js +20 -5
  24. package/lib/steps/stubs/backend/advanced-api-resource.js +1 -1
  25. package/lib/steps/stubs/backend/model-factory.d.ts +24 -0
  26. package/lib/steps/stubs/backend/model-factory.js +33 -0
  27. package/lib/steps/upgrade/twopointoh/backend/email-views.js +22 -30
  28. package/lib/steps/upgrade/twopointoh/backend/extension-models.d.ts +30 -0
  29. package/lib/steps/upgrade/twopointoh/backend/extension-models.js +34 -0
  30. package/lib/steps/upgrade/twopointoh/backend/json-api.d.ts +6 -0
  31. package/lib/steps/upgrade/twopointoh/backend/json-api.js +14 -0
  32. package/lib/steps/upgrade/twopointoh/backend/misc.d.ts +6 -0
  33. package/lib/steps/upgrade/twopointoh/backend/misc.js +19 -7
  34. package/lib/steps/upgrade/twopointoh/backend/model-factories.d.ts +18 -0
  35. package/lib/steps/upgrade/twopointoh/backend/model-factories.js +73 -0
  36. package/lib/steps/upgrade/twopointoh/backend/phpunit-xml.d.ts +8 -0
  37. package/lib/steps/upgrade/twopointoh/backend/phpunit-xml.js +33 -0
  38. package/lib/steps/upgrade/twopointoh/backend/phpunit.d.ts +2 -0
  39. package/lib/steps/upgrade/twopointoh/backend/phpunit.js +34 -21
  40. package/lib/steps/upgrade/twopointoh/backend/search.d.ts +9 -0
  41. package/lib/steps/upgrade/twopointoh/backend/search.js +28 -10
  42. package/lib/steps/upgrade/twopointoh/base.d.ts +38 -0
  43. package/lib/steps/upgrade/twopointoh/base.js +122 -24
  44. package/lib/steps/upgrade/twopointoh/dependencies.d.ts +58 -1
  45. package/lib/steps/upgrade/twopointoh/dependencies.js +343 -15
  46. package/lib/steps/upgrade/twopointoh/frontend/admin-extender.d.ts +86 -0
  47. package/lib/steps/upgrade/twopointoh/frontend/admin-extender.js +842 -0
  48. package/lib/steps/upgrade/twopointoh/frontend/code-splitting/usage-of-lazy-modules.js +110 -18
  49. package/lib/steps/upgrade/twopointoh/frontend/export-registry/import-ext.js +13 -16
  50. package/lib/steps/upgrade/twopointoh/frontend/misc.d.ts +23 -0
  51. package/lib/steps/upgrade/twopointoh/frontend/misc.js +147 -8
  52. package/lib/steps/upgrade/twopointoh/infra.d.ts +16 -1
  53. package/lib/steps/upgrade/twopointoh/infra.js +87 -5
  54. package/lib/steps/upgrade/twopointoh/step-failures.d.ts +32 -0
  55. package/lib/steps/upgrade/twopointoh/step-failures.js +54 -0
  56. package/lib/steps/upgrade/twopointoh/todos.d.ts +22 -0
  57. package/lib/steps/upgrade/twopointoh/todos.js +57 -0
  58. package/lib/steps/upgrade/twopointoh/typescript-4-9.d.ts +12 -0
  59. package/lib/steps/upgrade/twopointoh/typescript-4-9.js +200 -0
  60. package/lib/utils/ast.js +5 -10
  61. package/lib/utils/babel-ts.d.ts +9 -0
  62. package/lib/utils/babel-ts.js +18 -0
  63. package/lib/utils/composer.d.ts +5 -0
  64. package/lib/utils/composer.js +10 -1
  65. package/lib/utils/terminal.d.ts +13 -0
  66. package/lib/utils/terminal.js +26 -0
  67. package/oclif.manifest.json +1 -1
  68. package/package.json +1 -1
  69. package/php-subsystem/index.php +12 -0
  70. package/php-subsystem/src/NodeUtil.php +19 -9
  71. package/php-subsystem/src/NodeVisitors/AddExtender.php +49 -1
  72. package/php-subsystem/src/Upgrade/TwoPointOh/JsonApi.php +2 -20
  73. package/php-subsystem/src/Upgrade/TwoPointOh/Misc.php +13 -17
  74. package/php-subsystem/src/Upgrade/TwoPointOh/ModelFactories.php +943 -0
  75. package/php-subsystem/src/Upgrade/TwoPointOh/PhpUnit.php +476 -71
  76. 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
- // In the first traversal, we collect all the data providers and tests.
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 $tests = [];
157
+ public $attributes = [];
37
158
 
38
- public function enterNode(Node $node)
159
+ public function __construct(private PhpUnit $phpUnit)
39
160
  {
40
- if ($node instanceof Node\Stmt\ClassMethod) {
41
- $docComment = $node->getDocComment();
161
+ }
42
162
 
43
- if (preg_match('/\*\s*@test\b/', $docComment)) {
44
- $this->tests[] = $node;
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
- if (preg_match('/\*\s*@dataProvider\s+([a-zA-Z0-9_]+)/', $docComment, $matches)) {
48
- $this->dataProviders[] = $matches[1];
49
- }
169
+ NodeUtil::addUsesToNamespace($node, $uses);
170
+ }
50
171
 
51
- return NodeVisitor::DONT_TRAVERSE_CHILDREN;
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
- public function leaveNode(Node $node)
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
- if (! empty($this->dataProviders)) {
61
- $uses[] = 'PHPUnit\Framework\Attributes\DataProvider';
62
- }
184
+ if (! $groups && ! $unconverted) {
185
+ return null;
186
+ }
63
187
 
64
- if (! empty($this->tests)) {
65
- $uses[] = 'PHPUnit\Framework\Attributes\Test';
66
- }
188
+ if ($groups) {
189
+ $comments = array_values(array_filter($node->getComments(), fn ($comment) => ! $comment instanceof Doc));
67
190
 
68
- if (! empty($uses)) {
69
- NodeUtil::addUsesToNamespace($node, $uses);
191
+ if ($doc !== null) {
192
+ $comments[] = new Doc($doc);
70
193
  }
194
+
195
+ $node->setAttribute('comments', $comments);
71
196
  }
72
197
 
73
- if ($node instanceof Node\Stmt\ClassMethod) {
74
- // replace
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
- $docComment = $node->getDocComment();
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
- if (preg_match('/\*\s*@test\b/', $docComment)) {
81
- $node->attrGroups[] = new Node\AttributeGroup([
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
- if (preg_match('/\*\s*@dataProvider\s+([a-zA-Z0-9_]+)/', $docComment, $matches)) {
87
- $node->attrGroups[] = new Node\AttributeGroup([
88
- new Node\Attribute(new Node\Name('DataProvider'), [
89
- new Node\Arg(new Node\Scalar\String_($matches[1])),
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
- // Can we remove the doc comment?
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
- if (! preg_match('/[A-z]/', $docComment)) {
99
- $comments = $node->getComments();
219
+ return $node;
220
+ }
100
221
 
101
- foreach ($comments as $i => $comment) {
102
- if ($comment instanceof Doc) {
103
- unset($comments[$i]);
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 $node;
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
- $traverser->addVisitor($visitor = new class extends \PhpParser\NodeVisitorAbstract {
181
- protected $models = [
182
- 'users' => 'Flarum\\User\\User',
183
- 'discussions' => 'Flarum\\Discussion\\Discussion',
184
- 'groups' => 'Flarum\\Group\\Group',
185
- 'tags' => 'Flarum\\Tags\\Tag',
186
- 'flags' => 'Flarum\\Flags\\Flag',
187
- 'posts' => 'Flarum\\Post\\Post',
188
- 'access_tokens' => 'Flarum\\Http\\AccessToken',
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
- $exploded = explode('\\', $this->models[$table]);
205
- $className = end($exploded);
206
- $item->key = new Node\Expr\ClassConstFetch(new Node\Name($className), 'class');
207
- $this->usedModels[$this->models[$table]] = true;
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
  }