Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
90.67% |
311 / 343 |
|
65.00% |
13 / 20 |
CRAP | |
0.00% |
0 / 1 |
DelegatingConstraintChecker | |
90.67% |
311 / 343 |
|
65.00% |
13 / 20 |
100.02 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
checkAgainstConstraintsOnEntityId | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
4 | |||
checkAgainstConstraintsOnClaimId | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
3 | |||
getValidContextTypes | |
58.33% |
7 / 12 |
|
0.00% |
0 / 1 |
2.29 | |||
getValidEntityTypes | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
2.01 | |||
checkCommonConstraintParameters | |
91.30% |
21 / 23 |
|
0.00% |
0 / 1 |
6.02 | |||
checkConstraintParametersOnPropertyId | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
checkConstraintParametersOnConstraintId | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
checkEveryStatement | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
checkStatement | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
3 | |||
getConstraintsToUse | |
75.00% |
9 / 12 |
|
0.00% |
0 / 1 |
5.39 | |||
checkConstraintsForMainSnak | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
5 | |||
checkConstraintsForQualifiers | |
94.12% |
16 / 17 |
|
0.00% |
0 / 1 |
5.01 | |||
checkConstraintsForReferences | |
16.67% |
3 / 18 |
|
0.00% |
0 / 1 |
19.47 | |||
getCheckResultFor | |
100.00% |
32 / 32 |
|
100.00% |
1 / 1 |
5 | |||
handleScope | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
8 | |||
addMetadata | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
addConstraintClarification | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
downgradeResultStatus | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
7 | |||
sortResult | |
89.36% |
42 / 47 |
|
0.00% |
0 / 1 |
20.48 |
1 | <?php |
2 | |
3 | declare( strict_types = 1 ); |
4 | |
5 | namespace WikibaseQuality\ConstraintReport\ConstraintCheck; |
6 | |
7 | use InvalidArgumentException; |
8 | use LogicException; |
9 | use Wikibase\DataModel\Entity\EntityId; |
10 | use Wikibase\DataModel\Entity\NumericPropertyId; |
11 | use Wikibase\DataModel\Entity\PropertyId; |
12 | use Wikibase\DataModel\Entity\StatementListProvidingEntity; |
13 | use Wikibase\DataModel\Reference; |
14 | use Wikibase\DataModel\Services\Lookup\EntityLookup; |
15 | use Wikibase\DataModel\Services\Statement\StatementGuidParser; |
16 | use Wikibase\DataModel\Statement\Statement; |
17 | use WikibaseQuality\ConstraintReport\Constraint; |
18 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Cache\DependencyMetadata; |
19 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Cache\Metadata; |
20 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\Context; |
21 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\EntityContextCursor; |
22 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\MainSnakContext; |
23 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\QualifierContext; |
24 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\ReferenceContext; |
25 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterException; |
26 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterParser; |
27 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\LoggingHelper; |
28 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\SparqlHelperException; |
29 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\ViolationMessage; |
30 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult; |
31 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\NullResult; |
32 | use WikibaseQuality\ConstraintReport\ConstraintLookup; |
33 | |
34 | /** |
35 | * Used to start the constraint-check process and to delegate |
36 | * the statements that has to be checked to the corresponding checkers |
37 | * |
38 | * @author BP2014N1 |
39 | * @license GPL-2.0-or-later |
40 | */ |
41 | class DelegatingConstraintChecker { |
42 | |
43 | private EntityLookup $entityLookup; |
44 | |
45 | /** |
46 | * @var ConstraintChecker[] |
47 | */ |
48 | private array $checkerMap; |
49 | |
50 | private ConstraintLookup $constraintLookup; |
51 | |
52 | private ConstraintParameterParser $constraintParameterParser; |
53 | |
54 | private StatementGuidParser $statementGuidParser; |
55 | |
56 | private LoggingHelper $loggingHelper; |
57 | |
58 | private bool $checkQualifiers; |
59 | |
60 | private bool $checkReferences; |
61 | |
62 | /** |
63 | * @var string[] |
64 | */ |
65 | private array $propertiesWithViolatingQualifiers; |
66 | |
67 | /** |
68 | * @param EntityLookup $lookup |
69 | * @param ConstraintChecker[] $checkerMap |
70 | * @param ConstraintLookup $constraintRepository |
71 | * @param ConstraintParameterParser $constraintParameterParser |
72 | * @param StatementGuidParser $statementGuidParser |
73 | * @param LoggingHelper $loggingHelper |
74 | * @param bool $checkQualifiers whether to check qualifiers |
75 | * @param bool $checkReferences whether to check references |
76 | * @param string[] $propertiesWithViolatingQualifiers on statements of these properties, |
77 | * qualifiers will not be checked |
78 | */ |
79 | public function __construct( |
80 | EntityLookup $lookup, |
81 | array $checkerMap, |
82 | ConstraintLookup $constraintRepository, |
83 | ConstraintParameterParser $constraintParameterParser, |
84 | StatementGuidParser $statementGuidParser, |
85 | LoggingHelper $loggingHelper, |
86 | bool $checkQualifiers, |
87 | bool $checkReferences, |
88 | array $propertiesWithViolatingQualifiers |
89 | ) { |
90 | $this->entityLookup = $lookup; |
91 | $this->checkerMap = $checkerMap; |
92 | $this->constraintLookup = $constraintRepository; |
93 | $this->constraintParameterParser = $constraintParameterParser; |
94 | $this->statementGuidParser = $statementGuidParser; |
95 | $this->loggingHelper = $loggingHelper; |
96 | $this->checkQualifiers = $checkQualifiers; |
97 | $this->checkReferences = $checkReferences; |
98 | $this->propertiesWithViolatingQualifiers = $propertiesWithViolatingQualifiers; |
99 | } |
100 | |
101 | /** |
102 | * Starts the whole constraint-check process for entity or constraint ID on entity. |
103 | * Statements of the entity will be checked against every constraint that is defined on the property. |
104 | * |
105 | * @param EntityId $entityId |
106 | * @param string[]|null $constraintIds |
107 | * @param callable|null $defaultResultsPerContext |
108 | * Optional function to pre-populate the check results per context. |
109 | * For each {@link Context} where constraints will be checked, |
110 | * this function (if not null) is first called with that context as argument, |
111 | * and may return an array of check results to which the regular results are appended. |
112 | * @param callable|null $defaultResultsPerEntity |
113 | * Optional function to pre-populate the check results per entity. |
114 | * This function (if not null) is called once with $entityId as argument, |
115 | * and may return an array of check results to which the regular results are appended. |
116 | * |
117 | * @return CheckResult[] |
118 | */ |
119 | public function checkAgainstConstraintsOnEntityId( |
120 | EntityId $entityId, |
121 | array $constraintIds = null, |
122 | callable $defaultResultsPerContext = null, |
123 | callable $defaultResultsPerEntity = null |
124 | ): array { |
125 | $checkResults = []; |
126 | $entity = $this->entityLookup->getEntity( $entityId ); |
127 | |
128 | if ( $entity instanceof StatementListProvidingEntity ) { |
129 | $startTime = microtime( true ); |
130 | |
131 | $checkResults = $this->checkEveryStatement( |
132 | $entity, |
133 | $constraintIds, |
134 | $defaultResultsPerContext |
135 | ); |
136 | |
137 | $endTime = microtime( true ); |
138 | |
139 | if ( $constraintIds === null ) { // only log full constraint checks |
140 | $this->loggingHelper->logConstraintCheckOnEntity( |
141 | $entityId, |
142 | $checkResults, |
143 | $endTime - $startTime, |
144 | __METHOD__ |
145 | ); |
146 | } |
147 | } |
148 | |
149 | if ( $defaultResultsPerEntity !== null ) { |
150 | $checkResults = array_merge( $defaultResultsPerEntity( $entityId ), $checkResults ); |
151 | } |
152 | |
153 | return $this->sortResult( $checkResults ); |
154 | } |
155 | |
156 | /** |
157 | * Starts the whole constraint-check process. |
158 | * Statements of the entity will be checked against every constraint that is defined on the claim. |
159 | * |
160 | * @param string $guid |
161 | * @param string[]|null $constraintIds |
162 | * @param callable|null $defaultResults Optional function to pre-populate the check results. |
163 | * For each {@link Context} where constraints will be checked, |
164 | * this function (if not null) is first called with that context as argument, |
165 | * and may return an array of check results to which the regular results are appended. |
166 | * |
167 | * @return CheckResult[] |
168 | */ |
169 | public function checkAgainstConstraintsOnClaimId( |
170 | string $guid, |
171 | array $constraintIds = null, |
172 | callable $defaultResults = null |
173 | ): array { |
174 | |
175 | $parsedGuid = $this->statementGuidParser->parse( $guid ); |
176 | $entityId = $parsedGuid->getEntityId(); |
177 | $entity = $this->entityLookup->getEntity( $entityId ); |
178 | if ( $entity instanceof StatementListProvidingEntity ) { |
179 | $statement = $entity->getStatements()->getFirstStatementWithGuid( $guid ); |
180 | if ( $statement ) { |
181 | $result = $this->checkStatement( |
182 | $entity, |
183 | $statement, |
184 | $constraintIds, |
185 | $defaultResults |
186 | ); |
187 | $output = $this->sortResult( $result ); |
188 | return $output; |
189 | } |
190 | } |
191 | |
192 | return []; |
193 | } |
194 | |
195 | private function getValidContextTypes( Constraint $constraint ): array { |
196 | if ( !array_key_exists( $constraint->getConstraintTypeItemId(), $this->checkerMap ) ) { |
197 | return [ |
198 | Context::TYPE_STATEMENT, |
199 | Context::TYPE_QUALIFIER, |
200 | Context::TYPE_REFERENCE, |
201 | ]; |
202 | } |
203 | |
204 | return array_keys( array_filter( |
205 | $this->checkerMap[$constraint->getConstraintTypeItemId()]->getSupportedContextTypes(), |
206 | static function ( $resultStatus ) { |
207 | return $resultStatus !== CheckResult::STATUS_NOT_IN_SCOPE; |
208 | } |
209 | ) ); |
210 | } |
211 | |
212 | private function getValidEntityTypes( Constraint $constraint ): array { |
213 | if ( !array_key_exists( $constraint->getConstraintTypeItemId(), $this->checkerMap ) ) { |
214 | return array_keys( ConstraintChecker::ALL_ENTITY_TYPES_SUPPORTED ); |
215 | } |
216 | |
217 | return array_keys( array_filter( |
218 | $this->checkerMap[$constraint->getConstraintTypeItemId()]->getSupportedEntityTypes(), |
219 | static function ( $resultStatus ) { |
220 | return $resultStatus !== CheckResult::STATUS_NOT_IN_SCOPE; |
221 | } |
222 | ) ); |
223 | } |
224 | |
225 | /** |
226 | * Like ConstraintChecker::checkConstraintParameters, |
227 | * but for meta-parameters common to all checkers. |
228 | * |
229 | * @param Constraint $constraint |
230 | * |
231 | * @return ConstraintParameterException[] |
232 | */ |
233 | private function checkCommonConstraintParameters( Constraint $constraint ): array { |
234 | $constraintParameters = $constraint->getConstraintParameters(); |
235 | try { |
236 | $this->constraintParameterParser->checkError( $constraintParameters ); |
237 | } catch ( ConstraintParameterException $e ) { |
238 | return [ $e ]; |
239 | } |
240 | |
241 | $problems = []; |
242 | try { |
243 | $this->constraintParameterParser->parseExceptionParameter( $constraintParameters ); |
244 | } catch ( ConstraintParameterException $e ) { |
245 | $problems[] = $e; |
246 | } |
247 | try { |
248 | $this->constraintParameterParser->parseConstraintClarificationParameter( $constraintParameters ); |
249 | } catch ( ConstraintParameterException $e ) { |
250 | $problems[] = $e; |
251 | } |
252 | try { |
253 | $this->constraintParameterParser->parseConstraintStatusParameter( $constraintParameters ); |
254 | } catch ( ConstraintParameterException $e ) { |
255 | $problems[] = $e; |
256 | } |
257 | try { |
258 | $this->constraintParameterParser->parseConstraintScopeParameters( |
259 | $constraintParameters, |
260 | $constraint->getConstraintTypeItemId(), |
261 | $this->getValidContextTypes( $constraint ), |
262 | $this->getValidEntityTypes( $constraint ) |
263 | ); |
264 | } catch ( ConstraintParameterException $e ) { |
265 | $problems[] = $e; |
266 | } |
267 | return $problems; |
268 | } |
269 | |
270 | /** |
271 | * Check the constraint parameters of all constraints for the given property ID. |
272 | * |
273 | * @param NumericPropertyId $propertyId |
274 | * @return ConstraintParameterException[][] first level indexed by constraint ID, |
275 | * second level like checkConstraintParametersOnConstraintId (but without possibility of null) |
276 | */ |
277 | public function checkConstraintParametersOnPropertyId( NumericPropertyId $propertyId ): array { |
278 | $constraints = $this->constraintLookup->queryConstraintsForProperty( $propertyId ); |
279 | $result = []; |
280 | |
281 | foreach ( $constraints as $constraint ) { |
282 | $problems = $this->checkCommonConstraintParameters( $constraint ); |
283 | |
284 | if ( array_key_exists( $constraint->getConstraintTypeItemId(), $this->checkerMap ) ) { |
285 | $checker = $this->checkerMap[$constraint->getConstraintTypeItemId()]; |
286 | $problems = array_merge( $problems, $checker->checkConstraintParameters( $constraint ) ); |
287 | } |
288 | |
289 | $result[$constraint->getConstraintId()] = $problems; |
290 | } |
291 | |
292 | return $result; |
293 | } |
294 | |
295 | /** |
296 | * Check the constraint parameters of the constraint with the given ID. |
297 | * |
298 | * @param string $constraintId |
299 | * |
300 | * @return ConstraintParameterException[]|null list of constraint parameter exceptions |
301 | * (empty means all parameters okay), or null if constraint is not found |
302 | */ |
303 | public function checkConstraintParametersOnConstraintId( string $constraintId ): ?array { |
304 | $propertyId = $this->statementGuidParser->parse( $constraintId )->getEntityId(); |
305 | '@phan-var NumericPropertyId $propertyId'; |
306 | $constraints = $this->constraintLookup->queryConstraintsForProperty( $propertyId ); |
307 | |
308 | foreach ( $constraints as $constraint ) { |
309 | if ( $constraint->getConstraintId() === $constraintId ) { |
310 | $problems = $this->checkCommonConstraintParameters( $constraint ); |
311 | |
312 | if ( array_key_exists( $constraint->getConstraintTypeItemId(), $this->checkerMap ) ) { |
313 | $checker = $this->checkerMap[$constraint->getConstraintTypeItemId()]; |
314 | $problems = array_merge( $problems, $checker->checkConstraintParameters( $constraint ) ); |
315 | } |
316 | |
317 | return $problems; |
318 | } |
319 | } |
320 | |
321 | return null; |
322 | } |
323 | |
324 | /** |
325 | * @param StatementListProvidingEntity $entity |
326 | * @param string[]|null $constraintIds list of constraints to check (if null: all constraints) |
327 | * @param callable|null $defaultResultsPerContext optional function to pre-populate the check results |
328 | * |
329 | * @return CheckResult[] |
330 | */ |
331 | private function checkEveryStatement( |
332 | StatementListProvidingEntity $entity, |
333 | ?array $constraintIds, |
334 | ?callable $defaultResultsPerContext |
335 | ): array { |
336 | $result = []; |
337 | |
338 | /** @var Statement $statement */ |
339 | foreach ( $entity->getStatements() as $statement ) { |
340 | $result = array_merge( $result, |
341 | $this->checkStatement( |
342 | $entity, |
343 | $statement, |
344 | $constraintIds, |
345 | $defaultResultsPerContext |
346 | ) ); |
347 | } |
348 | |
349 | return $result; |
350 | } |
351 | |
352 | /** |
353 | * @param StatementListProvidingEntity $entity |
354 | * @param Statement $statement |
355 | * @param string[]|null $constraintIds list of constraints to check (if null: all constraints) |
356 | * @param callable|null $defaultResultsPerContext optional function to pre-populate the check results |
357 | * |
358 | * @return CheckResult[] |
359 | */ |
360 | private function checkStatement( |
361 | StatementListProvidingEntity $entity, |
362 | Statement $statement, |
363 | ?array $constraintIds, |
364 | ?callable $defaultResultsPerContext |
365 | ): array { |
366 | $result = []; |
367 | |
368 | $result = array_merge( $result, |
369 | $this->checkConstraintsForMainSnak( |
370 | $entity, |
371 | $statement, |
372 | $constraintIds, |
373 | $defaultResultsPerContext |
374 | ) ); |
375 | |
376 | if ( $this->checkQualifiers ) { |
377 | $result = array_merge( $result, |
378 | $this->checkConstraintsForQualifiers( |
379 | $entity, |
380 | $statement, |
381 | $constraintIds, |
382 | $defaultResultsPerContext |
383 | ) ); |
384 | } |
385 | |
386 | if ( $this->checkReferences ) { |
387 | $result = array_merge( $result, |
388 | $this->checkConstraintsForReferences( |
389 | $entity, |
390 | $statement, |
391 | $constraintIds, |
392 | $defaultResultsPerContext |
393 | ) ); |
394 | } |
395 | |
396 | return $result; |
397 | } |
398 | |
399 | /** |
400 | * Get the constraints to actually check for a given property ID. |
401 | * If $constraintIds is not null, only check constraints with those constraint IDs, |
402 | * otherwise check all constraints for that property. |
403 | * |
404 | * @param PropertyId $propertyId |
405 | * @param string[]|null $constraintIds |
406 | * @return Constraint[] |
407 | */ |
408 | private function getConstraintsToUse( PropertyId $propertyId, ?array $constraintIds ): array { |
409 | if ( !( $propertyId instanceof NumericPropertyId ) ) { |
410 | throw new InvalidArgumentException( |
411 | 'Non-numeric property ID not supported:' . $propertyId->getSerialization() |
412 | ); |
413 | } |
414 | $constraints = $this->constraintLookup->queryConstraintsForProperty( $propertyId ); |
415 | if ( $constraintIds !== null ) { |
416 | $constraintsToUse = []; |
417 | foreach ( $constraints as $constraint ) { |
418 | if ( in_array( $constraint->getConstraintId(), $constraintIds ) ) { |
419 | $constraintsToUse[] = $constraint; |
420 | } |
421 | } |
422 | return $constraintsToUse; |
423 | } else { |
424 | return $constraints; |
425 | } |
426 | } |
427 | |
428 | /** |
429 | * @param StatementListProvidingEntity $entity |
430 | * @param Statement $statement |
431 | * @param string[]|null $constraintIds list of constraints to check (if null: all constraints) |
432 | * @param callable|null $defaultResults optional function to pre-populate the check results |
433 | * |
434 | * @return CheckResult[] |
435 | */ |
436 | private function checkConstraintsForMainSnak( |
437 | StatementListProvidingEntity $entity, |
438 | Statement $statement, |
439 | ?array $constraintIds, |
440 | ?callable $defaultResults |
441 | ): array { |
442 | $context = new MainSnakContext( $entity, $statement ); |
443 | $constraints = $this->getConstraintsToUse( |
444 | $statement->getPropertyId(), |
445 | $constraintIds |
446 | ); |
447 | $result = $defaultResults !== null ? $defaultResults( $context ) : []; |
448 | |
449 | foreach ( $constraints as $constraint ) { |
450 | $parameters = $constraint->getConstraintParameters(); |
451 | try { |
452 | $exceptions = $this->constraintParameterParser->parseExceptionParameter( $parameters ); |
453 | } catch ( ConstraintParameterException $e ) { |
454 | $result[] = new CheckResult( |
455 | $context, |
456 | $constraint, |
457 | CheckResult::STATUS_BAD_PARAMETERS, |
458 | $e->getViolationMessage() |
459 | ); |
460 | continue; |
461 | } |
462 | |
463 | if ( in_array( $entity->getId(), $exceptions ) ) { |
464 | $message = new ViolationMessage( 'wbqc-violation-message-exception' ); |
465 | $result[] = new CheckResult( $context, $constraint, CheckResult::STATUS_EXCEPTION, $message ); |
466 | continue; |
467 | } |
468 | |
469 | $result[] = $this->getCheckResultFor( $context, $constraint ); |
470 | } |
471 | |
472 | return $result; |
473 | } |
474 | |
475 | /** |
476 | * @param StatementListProvidingEntity $entity |
477 | * @param Statement $statement |
478 | * @param string[]|null $constraintIds list of constraints to check (if null: all constraints) |
479 | * @param callable|null $defaultResultsPerContext optional function to pre-populate the check results |
480 | * |
481 | * @return CheckResult[] |
482 | */ |
483 | private function checkConstraintsForQualifiers( |
484 | StatementListProvidingEntity $entity, |
485 | Statement $statement, |
486 | ?array $constraintIds, |
487 | ?callable $defaultResultsPerContext |
488 | ): array { |
489 | $result = []; |
490 | |
491 | if ( in_array( |
492 | $statement->getPropertyId()->getSerialization(), |
493 | $this->propertiesWithViolatingQualifiers |
494 | ) ) { |
495 | return $result; |
496 | } |
497 | |
498 | foreach ( $statement->getQualifiers() as $qualifier ) { |
499 | $qualifierContext = new QualifierContext( $entity, $statement, $qualifier ); |
500 | if ( $defaultResultsPerContext !== null ) { |
501 | $result = array_merge( $result, $defaultResultsPerContext( $qualifierContext ) ); |
502 | } |
503 | $qualifierConstraints = $this->getConstraintsToUse( |
504 | $qualifierContext->getSnak()->getPropertyId(), |
505 | $constraintIds |
506 | ); |
507 | foreach ( $qualifierConstraints as $qualifierConstraint ) { |
508 | $result[] = $this->getCheckResultFor( $qualifierContext, $qualifierConstraint ); |
509 | } |
510 | } |
511 | |
512 | return $result; |
513 | } |
514 | |
515 | /** |
516 | * @param StatementListProvidingEntity $entity |
517 | * @param Statement $statement |
518 | * @param string[]|null $constraintIds list of constraints to check (if null: all constraints) |
519 | * @param callable|null $defaultResultsPerContext optional function to pre-populate the check results |
520 | * |
521 | * @return CheckResult[] |
522 | */ |
523 | private function checkConstraintsForReferences( |
524 | StatementListProvidingEntity $entity, |
525 | Statement $statement, |
526 | ?array $constraintIds, |
527 | ?callable $defaultResultsPerContext |
528 | ): array { |
529 | $result = []; |
530 | |
531 | /** @var Reference $reference */ |
532 | foreach ( $statement->getReferences() as $reference ) { |
533 | foreach ( $reference->getSnaks() as $snak ) { |
534 | $referenceContext = new ReferenceContext( |
535 | $entity, $statement, $reference, $snak |
536 | ); |
537 | if ( $defaultResultsPerContext !== null ) { |
538 | $result = array_merge( $result, $defaultResultsPerContext( $referenceContext ) ); |
539 | } |
540 | $referenceConstraints = $this->getConstraintsToUse( |
541 | $referenceContext->getSnak()->getPropertyId(), |
542 | $constraintIds |
543 | ); |
544 | foreach ( $referenceConstraints as $referenceConstraint ) { |
545 | $result[] = $this->getCheckResultFor( |
546 | $referenceContext, |
547 | $referenceConstraint |
548 | ); |
549 | } |
550 | } |
551 | } |
552 | |
553 | return $result; |
554 | } |
555 | |
556 | private function getCheckResultFor( Context $context, Constraint $constraint ): CheckResult { |
557 | if ( array_key_exists( $constraint->getConstraintTypeItemId(), $this->checkerMap ) ) { |
558 | $checker = $this->checkerMap[$constraint->getConstraintTypeItemId()]; |
559 | $result = $this->handleScope( $checker, $context, $constraint ); |
560 | |
561 | if ( $result !== null ) { |
562 | $this->addMetadata( $context, $result ); |
563 | return $result; |
564 | } |
565 | |
566 | $startTime = microtime( true ); |
567 | try { |
568 | $result = $checker->checkConstraint( $context, $constraint ); |
569 | } catch ( ConstraintParameterException $e ) { |
570 | $result = new CheckResult( |
571 | $context, |
572 | $constraint, |
573 | CheckResult::STATUS_BAD_PARAMETERS, |
574 | $e->getViolationMessage() |
575 | ); |
576 | } catch ( SparqlHelperException $e ) { |
577 | $message = new ViolationMessage( 'wbqc-violation-message-sparql-error' ); |
578 | $result = new CheckResult( $context, $constraint, CheckResult::STATUS_TODO, $message ); |
579 | } |
580 | $endTime = microtime( true ); |
581 | |
582 | $this->addMetadata( $context, $result ); |
583 | |
584 | $this->addConstraintClarification( $result ); |
585 | |
586 | $this->downgradeResultStatus( $result ); |
587 | |
588 | $this->loggingHelper->logConstraintCheck( |
589 | $context, |
590 | $constraint, |
591 | $result, |
592 | get_class( $checker ), |
593 | $endTime - $startTime, |
594 | __METHOD__ |
595 | ); |
596 | |
597 | return $result; |
598 | } else { |
599 | return new CheckResult( $context, $constraint, CheckResult::STATUS_TODO, null ); |
600 | } |
601 | } |
602 | |
603 | private function handleScope( |
604 | ConstraintChecker $checker, |
605 | Context $context, |
606 | Constraint $constraint |
607 | ): ?CheckResult { |
608 | $validContextTypes = $this->getValidContextTypes( $constraint ); |
609 | $validEntityTypes = $this->getValidEntityTypes( $constraint ); |
610 | try { |
611 | [ $checkedContextTypes, $checkedEntityTypes ] = $this->constraintParameterParser->parseConstraintScopeParameters( |
612 | $constraint->getConstraintParameters(), |
613 | $constraint->getConstraintTypeItemId(), |
614 | $validContextTypes, |
615 | $validEntityTypes |
616 | ); |
617 | } catch ( ConstraintParameterException $e ) { |
618 | return new CheckResult( $context, $constraint, CheckResult::STATUS_BAD_PARAMETERS, $e->getViolationMessage() ); |
619 | } |
620 | |
621 | if ( $checkedContextTypes === null ) { |
622 | $checkedContextTypes = $checker->getDefaultContextTypes(); |
623 | } |
624 | $contextType = $context->getType(); |
625 | if ( !in_array( $contextType, $checkedContextTypes ) ) { |
626 | return new CheckResult( $context, $constraint, CheckResult::STATUS_NOT_IN_SCOPE, null ); |
627 | } |
628 | if ( $checker->getSupportedContextTypes()[$contextType] === CheckResult::STATUS_TODO ) { |
629 | return new CheckResult( $context, $constraint, CheckResult::STATUS_TODO, null ); |
630 | } |
631 | |
632 | if ( $checkedEntityTypes === null ) { |
633 | $checkedEntityTypes = $validEntityTypes; |
634 | } |
635 | $entityType = $context->getEntity()->getType(); |
636 | if ( !in_array( $entityType, $checkedEntityTypes ) ) { |
637 | return new CheckResult( $context, $constraint, CheckResult::STATUS_NOT_IN_SCOPE, null ); |
638 | } |
639 | if ( $checker->getSupportedEntityTypes()[$entityType] === CheckResult::STATUS_TODO ) { |
640 | return new CheckResult( $context, $constraint, CheckResult::STATUS_TODO, null ); |
641 | } |
642 | |
643 | return null; |
644 | } |
645 | |
646 | private function addMetadata( Context $context, CheckResult $result ): void { |
647 | $result->withMetadata( Metadata::merge( [ |
648 | $result->getMetadata(), |
649 | Metadata::ofDependencyMetadata( DependencyMetadata::merge( [ |
650 | DependencyMetadata::ofEntityId( $context->getEntity()->getId() ), |
651 | DependencyMetadata::ofEntityId( $result->getConstraint()->getPropertyId() ), |
652 | ] ) ), |
653 | ] ) ); |
654 | } |
655 | |
656 | private function addConstraintClarification( CheckResult $result ): void { |
657 | $constraint = $result->getConstraint(); |
658 | try { |
659 | $constraintClarification = $this->constraintParameterParser |
660 | ->parseConstraintClarificationParameter( $constraint->getConstraintParameters() ); |
661 | $result->setConstraintClarification( $constraintClarification ); |
662 | } catch ( ConstraintParameterException $e ) { |
663 | $result->setStatus( CheckResult::STATUS_BAD_PARAMETERS ); |
664 | $result->setMessage( $e->getViolationMessage() ); |
665 | } |
666 | } |
667 | |
668 | private function downgradeResultStatus( CheckResult $result ): void { |
669 | $constraint = $result->getConstraint(); |
670 | try { |
671 | $constraintStatus = $this->constraintParameterParser |
672 | ->parseConstraintStatusParameter( $constraint->getConstraintParameters() ); |
673 | } catch ( ConstraintParameterException $e ) { |
674 | $result->setStatus( CheckResult::STATUS_BAD_PARAMETERS ); |
675 | $result->setMessage( $e->getViolationMessage() ); |
676 | return; |
677 | } |
678 | if ( $constraintStatus === null ) { |
679 | // downgrade violation to warning |
680 | if ( $result->getStatus() === CheckResult::STATUS_VIOLATION ) { |
681 | $result->setStatus( CheckResult::STATUS_WARNING ); |
682 | } |
683 | } elseif ( $constraintStatus === 'suggestion' ) { |
684 | // downgrade violation to suggestion |
685 | if ( $result->getStatus() === CheckResult::STATUS_VIOLATION ) { |
686 | $result->setStatus( CheckResult::STATUS_SUGGESTION ); |
687 | } |
688 | } else { |
689 | if ( $constraintStatus !== 'mandatory' ) { |
690 | // @codeCoverageIgnoreStart |
691 | throw new LogicException( |
692 | "Unknown constraint status '$constraintStatus', " . |
693 | "only known statuses are 'mandatory' and 'suggestion'" |
694 | ); |
695 | // @codeCoverageIgnoreEnd |
696 | } |
697 | } |
698 | } |
699 | |
700 | /** |
701 | * @param CheckResult[] $result |
702 | * |
703 | * @return CheckResult[] |
704 | */ |
705 | private function sortResult( array $result ): array { |
706 | if ( count( $result ) < 2 ) { |
707 | return $result; |
708 | } |
709 | |
710 | $sortFunction = static function ( CheckResult $a, CheckResult $b ) { |
711 | $orderNum = 0; |
712 | $order = [ |
713 | CheckResult::STATUS_BAD_PARAMETERS => $orderNum++, |
714 | CheckResult::STATUS_VIOLATION => $orderNum++, |
715 | CheckResult::STATUS_WARNING => $orderNum++, |
716 | CheckResult::STATUS_SUGGESTION => $orderNum++, |
717 | CheckResult::STATUS_EXCEPTION => $orderNum++, |
718 | CheckResult::STATUS_COMPLIANCE => $orderNum++, |
719 | CheckResult::STATUS_DEPRECATED => $orderNum++, |
720 | CheckResult::STATUS_NOT_IN_SCOPE => $orderNum++, |
721 | 'other' => $orderNum++, |
722 | ]; |
723 | |
724 | $statusA = $a->getStatus(); |
725 | $statusB = $b->getStatus(); |
726 | |
727 | $orderA = array_key_exists( $statusA, $order ) ? $order[ $statusA ] : $order[ 'other' ]; |
728 | $orderB = array_key_exists( $statusB, $order ) ? $order[ $statusB ] : $order[ 'other' ]; |
729 | |
730 | if ( $orderA === $orderB ) { |
731 | $cursorA = $a->getContextCursor(); |
732 | $cursorB = $b->getContextCursor(); |
733 | |
734 | if ( $cursorA instanceof EntityContextCursor ) { |
735 | return $cursorB instanceof EntityContextCursor ? 0 : -1; |
736 | } |
737 | if ( $cursorB instanceof EntityContextCursor ) { |
738 | return $cursorA instanceof EntityContextCursor ? 0 : 1; |
739 | } |
740 | |
741 | $pidA = $cursorA->getSnakPropertyId(); |
742 | $pidB = $cursorB->getSnakPropertyId(); |
743 | |
744 | if ( $pidA === $pidB ) { |
745 | $hashA = $cursorA->getSnakHash(); |
746 | $hashB = $cursorB->getSnakHash(); |
747 | |
748 | if ( $hashA === $hashB ) { |
749 | if ( $a instanceof NullResult ) { |
750 | return $b instanceof NullResult ? 0 : -1; |
751 | } |
752 | if ( $b instanceof NullResult ) { |
753 | return $a instanceof NullResult ? 0 : 1; |
754 | } |
755 | |
756 | $typeA = $a->getConstraint()->getConstraintTypeItemId(); |
757 | $typeB = $b->getConstraint()->getConstraintTypeItemId(); |
758 | |
759 | if ( $typeA == $typeB ) { |
760 | return 0; |
761 | } else { |
762 | return ( $typeA > $typeB ) ? 1 : -1; |
763 | } |
764 | } else { |
765 | return ( $hashA > $hashB ) ? 1 : -1; |
766 | } |
767 | } else { |
768 | return ( $pidA > $pidB ) ? 1 : -1; |
769 | } |
770 | } else { |
771 | return ( $orderA > $orderB ) ? 1 : -1; |
772 | } |
773 | }; |
774 | |
775 | uasort( $result, $sortFunction ); |
776 | |
777 | return $result; |
778 | } |
779 | |
780 | } |