1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Wikibase\Repo\Tests\Api; |
4
|
|
|
|
5
|
|
|
use ApiTestCase; |
6
|
|
|
use ApiUsageException; |
7
|
|
|
use ChangeTags; |
8
|
|
|
use MediaWiki\MediaWikiServices; |
9
|
|
|
use OutOfBoundsException; |
10
|
|
|
use PHPUnit\Framework\Constraint\Constraint; |
11
|
|
|
use TestSites; |
12
|
|
|
use TestUser; |
13
|
|
|
use Title; |
14
|
|
|
use User; |
15
|
|
|
use Wikibase\Repo\WikibaseRepo; |
16
|
|
|
use Wikimedia\TestingAccessWrapper; |
17
|
|
|
use WikiPage; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* @license GPL-2.0-or-later |
21
|
|
|
* @author John Erling Blad < [email protected] > |
22
|
|
|
* @author Daniel Kinzler |
23
|
|
|
* @author Addshore |
24
|
|
|
*/ |
25
|
|
|
abstract class WikibaseApiTestCase extends ApiTestCase { |
26
|
|
|
|
27
|
|
|
/** @var User */ |
28
|
|
|
protected $user; |
29
|
|
|
|
30
|
|
|
protected function setUp(): void { |
31
|
|
|
|
32
|
|
|
parent::setUp(); |
33
|
|
|
|
34
|
|
|
$this->setupUser(); |
35
|
|
|
|
36
|
|
|
$this->setupSiteLinkGroups(); |
37
|
|
|
|
38
|
|
|
$siteStore = new \HashSiteStore( TestSites::getSites() ); |
39
|
|
|
$this->setService( 'SiteStore', $siteStore ); |
40
|
|
|
$this->setService( 'SiteLookup', $siteStore ); |
41
|
|
|
} |
42
|
|
|
|
43
|
|
|
protected function createTestUser() { |
44
|
|
|
return new TestUser( |
45
|
|
|
'Apitesteditor', |
46
|
|
|
'Api Test Editor', |
47
|
|
|
'[email protected]', |
48
|
|
|
[ 'wbeditor' ] |
49
|
|
|
); |
50
|
|
|
} |
51
|
|
|
|
52
|
|
|
private function setupUser() { |
53
|
|
|
self::$users['wbeditor'] = $this->createTestUser(); |
54
|
|
|
|
55
|
|
|
$this->user = self::$users['wbeditor']->getUser(); |
56
|
|
|
$this->setMwGlobals( 'wgGroupPermissions', [ '*' => [ |
57
|
|
|
'property-create' => true, |
58
|
|
|
'createpage' => true, |
59
|
|
|
'bot' => true, |
60
|
|
|
'item-term' => true, |
61
|
|
|
'item-merge' => true, |
62
|
|
|
'item-redirect' => true, |
63
|
|
|
'property-term' => true, |
64
|
|
|
'read' => true, |
65
|
|
|
'edit' => true, |
66
|
|
|
'writeapi' => true |
67
|
|
|
] ] ); |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
private function setupSiteLinkGroups() { |
71
|
|
|
global $wgWBRepoSettings; |
72
|
|
|
|
73
|
|
|
$customRepoSettings = $wgWBRepoSettings; |
74
|
|
|
$customRepoSettings['siteLinkGroups'] = [ 'wikipedia' ]; |
75
|
|
|
$this->setMwGlobals( 'wgWBRepoSettings', $customRepoSettings ); |
76
|
|
|
MediaWikiServices::getInstance()->resetServiceForTesting( 'SiteLookup' ); |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Appends an edit token to a request. |
81
|
|
|
* |
82
|
|
|
* @param array $params |
83
|
|
|
* @param array|null $session |
84
|
|
|
* @param User|null $user |
85
|
|
|
* @param string $tokenType |
86
|
|
|
* |
87
|
|
|
* @throws ApiUsageException |
88
|
|
|
* @return array( array|null $resultData, WebRequest $request, array $sessionArray ) |
|
|
|
|
89
|
|
|
*/ |
90
|
|
|
protected function doApiRequestWithToken( |
91
|
|
|
array $params, |
92
|
|
|
array $session = null, |
93
|
|
|
User $user = null, |
94
|
|
|
$tokenType = 'csrf' |
95
|
|
|
) { |
96
|
|
|
if ( !$user ) { |
97
|
|
|
$user = \RequestContext::getMain()->getUser(); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
if ( !array_key_exists( 'token', $params ) ) { |
101
|
|
|
$params['token'] = $user->getEditToken(); |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
return $this->doApiRequest( $params, $session, false, $user, $tokenType ); |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* @param string[] $handles |
109
|
|
|
* @param string[] $idMap |
110
|
|
|
*/ |
111
|
|
|
protected function initTestEntities( array $handles, array $idMap = [] ) { |
112
|
|
|
$activeHandles = EntityTestHelper::getActiveHandles(); |
113
|
|
|
$user = $this->getTestSysop()->getUser(); |
114
|
|
|
|
115
|
|
|
foreach ( $activeHandles as $handle => $id ) { |
116
|
|
|
$title = $this->getTestEntityTitle( $handle ); |
117
|
|
|
|
118
|
|
|
$page = WikiPage::factory( $title ); |
119
|
|
|
$page->doDeleteArticleReal( 'Test reset', $user ); |
120
|
|
|
EntityTestHelper::unRegisterEntity( $handle ); |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
foreach ( $handles as $handle ) { |
124
|
|
|
$params = EntityTestHelper::getEntity( $handle ); |
125
|
|
|
$params['action'] = 'wbeditentity'; |
126
|
|
|
|
127
|
|
|
EntityTestHelper::injectIds( $params, $idMap ); |
128
|
|
|
EntityTestHelper::injectIds( $params, EntityTestHelper::$defaultPlaceholderValues ); |
129
|
|
|
|
130
|
|
|
list( $res, , ) = $this->doApiRequestWithToken( $params ); |
131
|
|
|
EntityTestHelper::registerEntity( $handle, $res['entity']['id'], $res['entity'] ); |
132
|
|
|
|
133
|
|
|
$idMap["%$handle%"] = $res['entity']['id']; |
134
|
|
|
} |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* @param string $handle |
139
|
|
|
* |
140
|
|
|
* @return null|Title |
141
|
|
|
*/ |
142
|
|
|
protected function getTestEntityTitle( $handle ) { |
143
|
|
|
try { |
144
|
|
|
$wikibaseRepo = WikibaseRepo::getDefaultInstance(); |
145
|
|
|
$idString = EntityTestHelper::getId( $handle ); |
146
|
|
|
$id = $wikibaseRepo->getEntityIdParser()->parse( $idString ); |
147
|
|
|
$title = $wikibaseRepo->getEntityTitleLookup()->getTitleForId( $id ); |
148
|
|
|
} catch ( OutOfBoundsException $ex ) { |
149
|
|
|
$title = null; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
return $title; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Loads an entity from the database (via an API call). |
157
|
|
|
* |
158
|
|
|
* @param string $id |
159
|
|
|
* |
160
|
|
|
* @return array |
161
|
|
|
*/ |
162
|
|
|
protected function loadEntity( $id ) { |
163
|
|
|
list( $res, , ) = $this->doApiRequest( |
164
|
|
|
[ |
165
|
|
|
'action' => 'wbgetentities', |
166
|
|
|
'format' => 'json', // make sure IDs are used as keys. |
167
|
|
|
'ids' => $id ] |
168
|
|
|
); |
169
|
|
|
|
170
|
|
|
return $res['entities'][$id]; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* Do the test for exceptions from Api queries. |
175
|
|
|
* |
176
|
|
|
* @param array $params Array of params for the API query. |
177
|
|
|
* @param array $exception Details of the exception to expect (type, code, message, message-key). |
178
|
|
|
* @param User|null $user |
179
|
|
|
* @param bool $token Whether to include a CSRF token |
180
|
|
|
*/ |
181
|
|
|
protected function doTestQueryExceptions( |
182
|
|
|
array $params, |
183
|
|
|
array $exception, |
184
|
|
|
User $user = null, |
185
|
|
|
$token = true |
186
|
|
|
) { |
187
|
|
|
try { |
188
|
|
|
if ( $token ) { |
189
|
|
|
$this->doApiRequestWithToken( $params, null, $user ); |
190
|
|
|
} else { |
191
|
|
|
$this->doApiRequest( $params, null, false, $user ); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
$this->fail( 'Failed to throw ApiUsageException' ); |
195
|
|
|
} catch ( ApiUsageException $e ) { |
|
|
|
|
196
|
|
|
if ( array_key_exists( 'type', $exception ) ) { |
197
|
|
|
$this->assertInstanceOf( $exception['type'], $e ); |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
if ( array_key_exists( 'code', $exception ) ) { |
201
|
|
|
$msg = TestingAccessWrapper::newFromObject( $e )->getApiMessage(); |
202
|
|
|
$this->assertThat( |
203
|
|
|
$msg->getApiCode(), |
204
|
|
|
$exception['code'] instanceof Constraint |
|
|
|
|
205
|
|
|
? $exception['code'] |
206
|
|
|
: $this->equalTo( $exception['code'] ) |
207
|
|
|
); |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
if ( array_key_exists( 'message', $exception ) ) { |
211
|
|
|
$this->assertStringContainsString( $exception['message'], $e->getMessage() ); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
if ( array_key_exists( 'message-key', $exception ) ) { |
215
|
|
|
$status = $e->getStatusValue(); |
216
|
|
|
$this->assertTrue( |
217
|
|
|
$status->hasMessage( $exception['message-key'] ), |
218
|
|
|
'Status message key' |
219
|
|
|
); |
220
|
|
|
} |
221
|
|
|
} |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* Utility function for converting an array from "deep" (indexed) to "flat" (keyed) structure. |
226
|
|
|
* Arrays that already use a flat structure are left unchanged. |
227
|
|
|
* |
228
|
|
|
* Arrays with a deep structure are expected to be list of entries that are associative arrays, |
229
|
|
|
* where which entry has at least the fields given by $keyField and $valueField. |
230
|
|
|
* |
231
|
|
|
* Arrays with a flat structure are associative and assign values to meaningful keys. |
232
|
|
|
* |
233
|
|
|
* @param array $data the input array. |
234
|
|
|
* @param string $keyField The name of the field in each entry that shall be used as the key in |
235
|
|
|
* the flat structure. |
236
|
|
|
* @param string $valueField The name of the field in each entry that shall be used as the value |
237
|
|
|
* in the flat structure. |
238
|
|
|
* @param bool $multiValue Whether the value in the flat structure shall be an indexed array of |
239
|
|
|
* values instead of a single value. |
240
|
|
|
* @param array &$into optional aggregator. |
241
|
|
|
* |
242
|
|
|
* @return array The flat version of $data. |
243
|
|
|
*/ |
244
|
|
|
protected function flattenArray( array $data, $keyField, $valueField, $multiValue = false, array &$into = [] ) { |
245
|
|
|
foreach ( $data as $index => $value ) { |
246
|
|
|
if ( is_array( $value ) ) { |
247
|
|
|
if ( isset( $value[$keyField] ) && isset( $value[$valueField] ) ) { |
248
|
|
|
// found "deep" entry in the array |
249
|
|
|
$k = $value[ $keyField ]; |
250
|
|
|
$v = $value[ $valueField ]; |
251
|
|
|
} elseif ( isset( $value[0] ) && !is_array( $value[0] ) && $multiValue ) { |
252
|
|
|
// found "flat" multi-value entry in the array |
253
|
|
|
$k = $index; |
254
|
|
|
$v = $value; |
255
|
|
|
} else { |
256
|
|
|
// found list, recurse |
257
|
|
|
$this->flattenArray( $value, $keyField, $valueField, $multiValue, $into ); |
258
|
|
|
continue; |
259
|
|
|
} |
260
|
|
|
} else { |
261
|
|
|
// found "flat" entry in the array |
262
|
|
|
$k = $index; |
263
|
|
|
$v = $value; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
if ( $multiValue ) { |
267
|
|
|
if ( is_array( $v ) ) { |
268
|
|
|
$into[$k] = empty( $into[$k] ) ? $v : array_merge( $into[$k], $v ); |
269
|
|
|
} else { |
270
|
|
|
$into[$k][] = $v; |
271
|
|
|
} |
272
|
|
|
} else { |
273
|
|
|
$into[$k] = $v; |
274
|
|
|
} |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
return $into; |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
/** |
281
|
|
|
* Compares two entity structures and asserts that they are equal. Only fields present in $expected are considered. |
282
|
|
|
* $expected and $actual can both be either in "flat" or in "deep" form, they are converted as needed before comparison. |
283
|
|
|
* |
284
|
|
|
* @param array $expected |
285
|
|
|
* @param array $actual |
286
|
|
|
* @param bool $expectEmptyArrays Should we expect empty arrays or just ignore them? |
287
|
|
|
*/ |
288
|
|
|
protected function assertEntityEquals( array $expected, array $actual, $expectEmptyArrays = true ) { |
289
|
|
|
if ( isset( $expected['id'] ) && !empty( $expected['id'] ) ) { |
290
|
|
|
$this->assertEquals( $expected['id'], $actual['id'], 'id' ); |
291
|
|
|
} |
292
|
|
|
if ( isset( $expected['lastrevid'] ) ) { |
293
|
|
|
$this->assertEquals( $expected['lastrevid'], $actual['lastrevid'], 'lastrevid' ); |
294
|
|
|
} |
295
|
|
|
if ( isset( $expected['type'] ) ) { |
296
|
|
|
$this->assertEquals( $expected['type'], $actual['type'], 'type' ); |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
if ( isset( $expected['labels'] ) ) { |
300
|
|
|
if ( !( $expectEmptyArrays === false && $expected['labels'] === [] ) ) { |
301
|
|
|
$data = $this->flattenArray( $actual['labels'], 'language', 'value' ); |
302
|
|
|
$exp = $this->flattenArray( $expected['labels'], 'language', 'value' ); |
303
|
|
|
|
304
|
|
|
// keys are significant in flat form |
305
|
|
|
$this->assertArrayEquals( $exp, $data, false, true ); |
306
|
|
|
} |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
if ( isset( $expected['descriptions'] ) ) { |
310
|
|
|
if ( !( $expectEmptyArrays === false && $expected['descriptions'] === [] ) ) { |
311
|
|
|
$data = $this->flattenArray( $actual['descriptions'], 'language', 'value' ); |
312
|
|
|
$exp = $this->flattenArray( $expected['descriptions'], 'language', 'value' ); |
313
|
|
|
|
314
|
|
|
// keys are significant in flat form |
315
|
|
|
$this->assertArrayEquals( $exp, $data, false, true ); |
316
|
|
|
} |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
if ( isset( $expected['sitelinks'] ) ) { |
320
|
|
|
if ( !( $expectEmptyArrays === false && $expected['sitelinks'] === [] ) ) { |
321
|
|
|
$data = $this->flattenArray( $actual['sitelinks'] ?? [], 'site', 'title' ); |
322
|
|
|
$exp = $this->flattenArray( $expected['sitelinks'], 'site', 'title' ); |
323
|
|
|
|
324
|
|
|
// keys are significant in flat form |
325
|
|
|
$this->assertArrayEquals( $exp, $data, false, true ); |
326
|
|
|
} |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
if ( isset( $expected['aliases'] ) ) { |
330
|
|
|
if ( !( $expectEmptyArrays === false && $expected['aliases'] === [] ) ) { |
331
|
|
|
$data = $this->flattenArray( $actual['aliases'], 'language', 'value', true ); |
332
|
|
|
$exp = $this->flattenArray( $expected['aliases'], 'language', 'value', true ); |
333
|
|
|
|
334
|
|
|
// keys are significant in flat form |
335
|
|
|
$this->assertArrayEquals( $exp, $data, false, true ); |
336
|
|
|
} |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
if ( isset( $expected['claims'] ) ) { |
340
|
|
|
if ( !( $expectEmptyArrays === false && $expected['claims'] === [] ) ) { |
341
|
|
|
$data = $this->flattenArray( $actual['claims'], 'mainsnak', 'value', true ); |
342
|
|
|
$exp = $this->flattenArray( $expected['claims'], 'language', 'value', true ); |
343
|
|
|
$count = count( $expected['claims'] ); |
344
|
|
|
|
345
|
|
|
for ( $i = 0; $i < $count; $i++ ) { |
346
|
|
|
$this->assertArrayHasKey( $i, $data['id'] ); |
347
|
|
|
$this->assertGreaterThanOrEqual( 39, strlen( $data['id'][$i] ) ); |
348
|
|
|
} |
349
|
|
|
//unset stuff we dont actually want to compare |
350
|
|
|
if ( isset( $exp['id'] ) ) { |
351
|
|
|
$this->assertArrayHasKey( 'id', $data ); |
352
|
|
|
} |
353
|
|
|
unset( $exp['id'] ); |
354
|
|
|
unset( $exp['datatype'] ); |
355
|
|
|
unset( $exp['hash'] ); |
356
|
|
|
unset( $exp['qualifiers-order'] ); |
357
|
|
|
unset( $data['datatype'] ); |
358
|
|
|
unset( $data['id'] ); |
359
|
|
|
unset( $data['hash'] ); |
360
|
|
|
unset( $data['qualifiers-order'] ); |
361
|
|
|
$this->assertArrayEquals( $exp, $data, false, true ); |
362
|
|
|
} |
363
|
|
|
} |
364
|
|
|
} |
365
|
|
|
|
366
|
|
|
/** |
367
|
|
|
* Asserts that the given API response represents a successful call. |
368
|
|
|
* |
369
|
|
|
* @param array $response |
370
|
|
|
*/ |
371
|
|
|
protected function assertResultSuccess( array $response ) { |
372
|
|
|
$this->assertArrayHasKey( 'success', $response, "Missing 'success' marker in response." ); |
373
|
|
|
$this->assertResultHasEntityType( $response ); |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
/** |
377
|
|
|
* Asserts that the given API response has a valid entity type if the result contains an entity |
378
|
|
|
* |
379
|
|
|
* @param array $response |
380
|
|
|
*/ |
381
|
|
|
protected function assertResultHasEntityType( array $response ) { |
382
|
|
|
$wikibaseRepo = WikibaseRepo::getDefaultInstance(); |
383
|
|
|
|
384
|
|
|
if ( isset( $response['entity'] ) ) { |
385
|
|
|
if ( isset( $response['entity']['type'] ) ) { |
386
|
|
|
$this->assertContains( |
387
|
|
|
$response['entity']['type'], |
388
|
|
|
$wikibaseRepo->getEnabledEntityTypes(), |
389
|
|
|
"Missing valid 'type' in response." |
390
|
|
|
); |
391
|
|
|
} |
392
|
|
|
} elseif ( isset( $response['entities'] ) ) { |
393
|
|
|
foreach ( $response['entities'] as $entity ) { |
394
|
|
|
if ( isset( $entity['type'] ) ) { |
395
|
|
|
$this->assertContains( |
396
|
|
|
$entity['type'], |
397
|
|
|
$wikibaseRepo->getEnabledEntityTypes(), |
398
|
|
|
"Missing valid 'type' in response." |
399
|
|
|
); |
400
|
|
|
} |
401
|
|
|
} |
402
|
|
|
} |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* Asserts that the revision with the given ID has a summary matching $regex |
407
|
|
|
* |
408
|
|
|
* @param string|string[] $regex The regex to match, or an array to build a regex from. |
409
|
|
|
* @param int $revid |
410
|
|
|
*/ |
411
|
|
|
protected function assertRevisionSummary( $regex, $revid ) { |
412
|
|
|
if ( is_array( $regex ) ) { |
413
|
|
|
$r = ''; |
414
|
|
|
|
415
|
|
|
foreach ( $regex as $s ) { |
416
|
|
|
if ( $r !== '' ) { |
417
|
|
|
$r .= '.*'; |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
$r .= preg_quote( $s, '!' ); |
421
|
|
|
} |
422
|
|
|
|
423
|
|
|
$regex = "!$r!"; |
424
|
|
|
} |
425
|
|
|
|
426
|
|
|
$revRecord = MediaWikiServices::getInstance() |
427
|
|
|
->getRevisionLookup() |
428
|
|
|
->getRevisionById( $revid ); |
429
|
|
|
$this->assertNotNull( $revRecord, "revision not found: $revid" ); |
430
|
|
|
|
431
|
|
|
$comment = $revRecord->getComment(); |
432
|
|
|
$this->assertInstanceOf( 'CommentStoreComment', $comment ); |
433
|
|
|
$this->assertRegExp( $regex, $comment->text ); |
434
|
|
|
} |
435
|
|
|
|
436
|
|
|
protected function assertCanTagSuccessfulRequest( |
437
|
|
|
array $params, |
438
|
|
|
array $session = null, |
439
|
|
|
User $user = null, |
440
|
|
|
$tokenType = 'csrf' |
441
|
|
|
) { |
442
|
|
|
$dummyTag = __METHOD__ . '-dummy-tag'; |
443
|
|
|
ChangeTags::defineTag( $dummyTag ); |
444
|
|
|
|
445
|
|
|
$params[ 'tags' ] = $dummyTag; |
446
|
|
|
|
447
|
|
|
list( $result, , ) = $this->doApiRequestWithToken( $params, $session, $user, $tokenType ); |
448
|
|
|
|
449
|
|
|
$this->assertArrayNotHasKey( 'warnings', $result, json_encode( $result ) ); |
450
|
|
|
$this->assertArrayHasKey( 'success', $result ); |
451
|
|
|
$lastRevid = $this->getLastRevIdFromResult( $result ); |
452
|
|
|
if ( $lastRevid === null ) { |
453
|
|
|
$this->fail( |
454
|
|
|
'API result does not have lastrevid. Actual result: ' |
455
|
|
|
. json_encode( $result, JSON_PRETTY_PRINT ) |
456
|
|
|
); |
457
|
|
|
} |
458
|
|
|
|
459
|
|
|
$this->assertTrue( in_array( |
460
|
|
|
$dummyTag, |
461
|
|
|
ChangeTags::getTags( wfGetDB( DB_MASTER ), null, $lastRevid ) |
462
|
|
|
) ); |
463
|
|
|
} |
464
|
|
|
|
465
|
|
|
private function getLastRevIdFromResult( array $result ) { |
466
|
|
|
if ( isset( $result['entity']['lastrevid'] ) ) { |
467
|
|
|
return $result['entity']['lastrevid']; |
468
|
|
|
} |
469
|
|
|
if ( isset( $result['pageinfo']['lastrevid'] ) ) { |
470
|
|
|
return $result['pageinfo']['lastrevid']; |
471
|
|
|
} |
472
|
|
|
|
473
|
|
|
return null; |
474
|
|
|
} |
475
|
|
|
|
476
|
|
|
} |
477
|
|
|
|
This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.