Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
90.50% |
305 / 337 |
|
65.00% |
13 / 20 |
CRAP | |
0.00% |
0 / 1 |
DelegatingConstraintChecker | |
90.50% |
305 / 337 |
|
65.00% |
13 / 20 |
98.09 | |
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 | |
50.00% |
5 / 10 |
|
0.00% |
0 / 1 |
2.50 | |||
getValidEntityTypes | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
2.02 | |||
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% |
23 / 23 |
|
100.00% |
1 / 1 |
6 | |||
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 fn ( $status ) => $status !== CheckResult::STATUS_NOT_IN_SCOPE |
207 | ) ); |
208 | } |
209 | |
210 | private function getValidEntityTypes( Constraint $constraint ): array { |
211 | if ( !array_key_exists( $constraint->getConstraintTypeItemId(), $this->checkerMap ) ) { |
212 | return array_keys( ConstraintChecker::ALL_ENTITY_TYPES_SUPPORTED ); |
213 | } |
214 | |
215 | return array_keys( array_filter( |
216 | $this->checkerMap[$constraint->getConstraintTypeItemId()]->getSupportedEntityTypes(), |
217 | static fn ( $status ) => $status !== CheckResult::STATUS_NOT_IN_SCOPE |
218 | ) ); |
219 | } |
220 | |
221 | /** |
222 | * Like ConstraintChecker::checkConstraintParameters, |
223 | * but for meta-parameters common to all checkers. |
224 | * |
225 | * @param Constraint $constraint |
226 | * |
227 | * @return ConstraintParameterException[] |
228 | */ |
229 | private function checkCommonConstraintParameters( Constraint $constraint ): array { |
230 | $constraintParameters = $constraint->getConstraintParameters(); |
231 | try { |
232 | $this->constraintParameterParser->checkError( $constraintParameters ); |
233 | } catch ( ConstraintParameterException $e ) { |
234 | return [ $e ]; |
235 | } |
236 | |
237 | $problems = []; |
238 | try { |
239 | $this->constraintParameterParser->parseExceptionParameter( $constraintParameters ); |
240 | } catch ( ConstraintParameterException $e ) { |
241 | $problems[] = $e; |
242 | } |
243 | try { |
244 | $this->constraintParameterParser->parseConstraintClarificationParameter( $constraintParameters ); |
245 | } catch ( ConstraintParameterException $e ) { |
246 | $problems[] = $e; |
247 | } |
248 | try { |
249 | $this->constraintParameterParser->parseConstraintStatusParameter( $constraintParameters ); |
250 | } catch ( ConstraintParameterException $e ) { |
251 | $problems[] = $e; |
252 | } |
253 | try { |
254 | $this->constraintParameterParser->parseConstraintScopeParameters( |
255 | $constraintParameters, |
256 | $constraint->getConstraintTypeItemId(), |
257 | $this->getValidContextTypes( $constraint ), |
258 | $this->getValidEntityTypes( $constraint ) |
259 | ); |
260 | } catch ( ConstraintParameterException $e ) { |
261 | $problems[] = $e; |
262 | } |
263 | return $problems; |
264 | } |
265 | |
266 | /** |
267 | * Check the constraint parameters of all constraints for the given property ID. |
268 | * |
269 | * @param NumericPropertyId $propertyId |
270 | * @return ConstraintParameterException[][] first level indexed by constraint ID, |
271 | * second level like checkConstraintParametersOnConstraintId (but without possibility of null) |
272 | */ |
273 | public function checkConstraintParametersOnPropertyId( NumericPropertyId $propertyId ): array { |
274 | $constraints = $this->constraintLookup->queryConstraintsForProperty( $propertyId ); |
275 | $result = []; |
276 | |
277 | foreach ( $constraints as $constraint ) { |
278 | $problems = $this->checkCommonConstraintParameters( $constraint ); |
279 | |
280 | if ( array_key_exists( $constraint->getConstraintTypeItemId(), $this->checkerMap ) ) { |
281 | $checker = $this->checkerMap[$constraint->getConstraintTypeItemId()]; |
282 | $problems = array_merge( $problems, $checker->checkConstraintParameters( $constraint ) ); |
283 | } |
284 | |
285 | $result[$constraint->getConstraintId()] = $problems; |
286 | } |
287 | |
288 | return $result; |
289 | } |
290 | |
291 | /** |
292 | * Check the constraint parameters of the constraint with the given ID. |
293 | * |
294 | * @param string $constraintId |
295 | * |
296 | * @return ConstraintParameterException[]|null list of constraint parameter exceptions |
297 | * (empty means all parameters okay), or null if constraint is not found |
298 | */ |
299 | public function checkConstraintParametersOnConstraintId( string $constraintId ): ?array { |
300 | $propertyId = $this->statementGuidParser->parse( $constraintId )->getEntityId(); |
301 | '@phan-var NumericPropertyId $propertyId'; |
302 | $constraints = $this->constraintLookup->queryConstraintsForProperty( $propertyId ); |
303 | |
304 | foreach ( $constraints as $constraint ) { |
305 | if ( $constraint->getConstraintId() === $constraintId ) { |
306 | $problems = $this->checkCommonConstraintParameters( $constraint ); |
307 | |
308 | if ( array_key_exists( $constraint->getConstraintTypeItemId(), $this->checkerMap ) ) { |
309 | $checker = $this->checkerMap[$constraint->getConstraintTypeItemId()]; |
310 | $problems = array_merge( $problems, $checker->checkConstraintParameters( $constraint ) ); |
311 | } |
312 | |
313 | return $problems; |
314 | } |
315 | } |
316 | |
317 | return null; |
318 | } |
319 | |
320 | /** |
321 | * @param StatementListProvidingEntity $entity |
322 | * @param string[]|null $constraintIds list of constraints to check (if null: all constraints) |
323 | * @param callable|null $defaultResultsPerContext optional function to pre-populate the check results |
324 | * |
325 | * @return CheckResult[] |
326 | */ |
327 | private function checkEveryStatement( |
328 | StatementListProvidingEntity $entity, |
329 | ?array $constraintIds, |
330 | ?callable $defaultResultsPerContext |
331 | ): array { |
332 | $result = []; |
333 | |
334 | /** @var Statement $statement */ |
335 | foreach ( $entity->getStatements() as $statement ) { |
336 | $result = array_merge( $result, |
337 | $this->checkStatement( |
338 | $entity, |
339 | $statement, |
340 | $constraintIds, |
341 | $defaultResultsPerContext |
342 | ) ); |
343 | } |
344 | |
345 | return $result; |
346 | } |
347 | |
348 | /** |
349 | * @param StatementListProvidingEntity $entity |
350 | * @param Statement $statement |
351 | * @param string[]|null $constraintIds list of constraints to check (if null: all constraints) |
352 | * @param callable|null $defaultResultsPerContext optional function to pre-populate the check results |
353 | * |
354 | * @return CheckResult[] |
355 | */ |
356 | private function checkStatement( |
357 | StatementListProvidingEntity $entity, |
358 | Statement $statement, |
359 | ?array $constraintIds, |
360 | ?callable $defaultResultsPerContext |
361 | ): array { |
362 | $result = []; |
363 | |
364 | $result = array_merge( $result, |
365 | $this->checkConstraintsForMainSnak( |
366 | $entity, |
367 | $statement, |
368 | $constraintIds, |
369 | $defaultResultsPerContext |
370 | ) ); |
371 | |
372 | if ( $this->checkQualifiers ) { |
373 | $result = array_merge( $result, |
374 | $this->checkConstraintsForQualifiers( |
375 | $entity, |
376 | $statement, |
377 | $constraintIds, |
378 | $defaultResultsPerContext |
379 | ) ); |
380 | } |
381 | |
382 | if ( $this->checkReferences ) { |
383 | $result = array_merge( $result, |
384 | $this->checkConstraintsForReferences( |
385 | $entity, |
386 | $statement, |
387 | $constraintIds, |
388 | $defaultResultsPerContext |
389 | ) ); |
390 | } |
391 | |
392 | return $result; |
393 | } |
394 | |
395 | /** |
396 | * Get the constraints to actually check for a given property ID. |
397 | * If $constraintIds is not null, only check constraints with those constraint IDs, |
398 | * otherwise check all constraints for that property. |
399 | * |
400 | * @param PropertyId $propertyId |
401 | * @param string[]|null $constraintIds |
402 | * @return Constraint[] |
403 | */ |
404 | private function getConstraintsToUse( PropertyId $propertyId, ?array $constraintIds ): array { |
405 | if ( !( $propertyId instanceof NumericPropertyId ) ) { |
406 | throw new InvalidArgumentException( |
407 | 'Non-numeric property ID not supported:' . $propertyId->getSerialization() |
408 | ); |
409 | } |
410 | $constraints = $this->constraintLookup->queryConstraintsForProperty( $propertyId ); |
411 | if ( $constraintIds !== null ) { |
412 | $constraintsToUse = []; |
413 | foreach ( $constraints as $constraint ) { |
414 | if ( in_array( $constraint->getConstraintId(), $constraintIds ) ) { |
415 | $constraintsToUse[] = $constraint; |
416 | } |
417 | } |
418 | return $constraintsToUse; |
419 | } else { |
420 | return $constraints; |
421 | } |
422 | } |
423 | |
424 | /** |
425 | * @param StatementListProvidingEntity $entity |
426 | * @param Statement $statement |
427 | * @param string[]|null $constraintIds list of constraints to check (if null: all constraints) |
428 | * @param callable|null $defaultResults optional function to pre-populate the check results |
429 | * |
430 | * @return CheckResult[] |
431 | */ |
432 | private function checkConstraintsForMainSnak( |
433 | StatementListProvidingEntity $entity, |
434 | Statement $statement, |
435 | ?array $constraintIds, |
436 | ?callable $defaultResults |
437 | ): array { |
438 | $context = new MainSnakContext( $entity, $statement ); |
439 | $constraints = $this->getConstraintsToUse( |
440 | $statement->getPropertyId(), |
441 | $constraintIds |
442 | ); |
443 | $result = $defaultResults !== null ? $defaultResults( $context ) : []; |
444 | |
445 | foreach ( $constraints as $constraint ) { |
446 | $parameters = $constraint->getConstraintParameters(); |
447 | try { |
448 | $exceptions = $this->constraintParameterParser->parseExceptionParameter( $parameters ); |
449 | } catch ( ConstraintParameterException $e ) { |
450 | $result[] = new CheckResult( |
451 | $context, |
452 | $constraint, |
453 | CheckResult::STATUS_BAD_PARAMETERS, |
454 | $e->getViolationMessage() |
455 | ); |
456 | continue; |
457 | } |
458 | |
459 | if ( in_array( $entity->getId(), $exceptions ) ) { |
460 | $message = new ViolationMessage( 'wbqc-violation-message-exception' ); |
461 | $result[] = new CheckResult( $context, $constraint, CheckResult::STATUS_EXCEPTION, $message ); |
462 | continue; |
463 | } |
464 | |
465 | $result[] = $this->getCheckResultFor( $context, $constraint ); |
466 | } |
467 | |
468 | return $result; |
469 | } |
470 | |
471 | /** |
472 | * @param StatementListProvidingEntity $entity |
473 | * @param Statement $statement |
474 | * @param string[]|null $constraintIds list of constraints to check (if null: all constraints) |
475 | * @param callable|null $defaultResultsPerContext optional function to pre-populate the check results |
476 | * |
477 | * @return CheckResult[] |
478 | */ |
479 | private function checkConstraintsForQualifiers( |
480 | StatementListProvidingEntity $entity, |
481 | Statement $statement, |
482 | ?array $constraintIds, |
483 | ?callable $defaultResultsPerContext |
484 | ): array { |
485 | $result = []; |
486 | |
487 | if ( in_array( |
488 | $statement->getPropertyId()->getSerialization(), |
489 | $this->propertiesWithViolatingQualifiers |
490 | ) ) { |
491 | return $result; |
492 | } |
493 | |
494 | foreach ( $statement->getQualifiers() as $qualifier ) { |
495 | $qualifierContext = new QualifierContext( $entity, $statement, $qualifier ); |
496 | if ( $defaultResultsPerContext !== null ) { |
497 | $result = array_merge( $result, $defaultResultsPerContext( $qualifierContext ) ); |
498 | } |
499 | $qualifierConstraints = $this->getConstraintsToUse( |
500 | $qualifierContext->getSnak()->getPropertyId(), |
501 | $constraintIds |
502 | ); |
503 | foreach ( $qualifierConstraints as $qualifierConstraint ) { |
504 | $result[] = $this->getCheckResultFor( $qualifierContext, $qualifierConstraint ); |
505 | } |
506 | } |
507 | |
508 | return $result; |
509 | } |
510 | |
511 | /** |
512 | * @param StatementListProvidingEntity $entity |
513 | * @param Statement $statement |
514 | * @param string[]|null $constraintIds list of constraints to check (if null: all constraints) |
515 | * @param callable|null $defaultResultsPerContext optional function to pre-populate the check results |
516 | * |
517 | * @return CheckResult[] |
518 | */ |
519 | private function checkConstraintsForReferences( |
520 | StatementListProvidingEntity $entity, |
521 | Statement $statement, |
522 | ?array $constraintIds, |
523 | ?callable $defaultResultsPerContext |
524 | ): array { |
525 | $result = []; |
526 | |
527 | /** @var Reference $reference */ |
528 | foreach ( $statement->getReferences() as $reference ) { |
529 | foreach ( $reference->getSnaks() as $snak ) { |
530 | $referenceContext = new ReferenceContext( |
531 | $entity, $statement, $reference, $snak |
532 | ); |
533 | if ( $defaultResultsPerContext !== null ) { |
534 | $result = array_merge( $result, $defaultResultsPerContext( $referenceContext ) ); |
535 | } |
536 | $referenceConstraints = $this->getConstraintsToUse( |
537 | $referenceContext->getSnak()->getPropertyId(), |
538 | $constraintIds |
539 | ); |
540 | foreach ( $referenceConstraints as $referenceConstraint ) { |
541 | $result[] = $this->getCheckResultFor( |
542 | $referenceContext, |
543 | $referenceConstraint |
544 | ); |
545 | } |
546 | } |
547 | } |
548 | |
549 | return $result; |
550 | } |
551 | |
552 | private function getCheckResultFor( Context $context, Constraint $constraint ): CheckResult { |
553 | if ( array_key_exists( $constraint->getConstraintTypeItemId(), $this->checkerMap ) ) { |
554 | $checker = $this->checkerMap[$constraint->getConstraintTypeItemId()]; |
555 | $result = $this->handleScope( $checker, $context, $constraint ); |
556 | |
557 | if ( $result !== null ) { |
558 | $this->addMetadata( $context, $result ); |
559 | return $result; |
560 | } |
561 | |
562 | $startTime = microtime( true ); |
563 | try { |
564 | $result = $checker->checkConstraint( $context, $constraint ); |
565 | } catch ( ConstraintParameterException $e ) { |
566 | $result = new CheckResult( |
567 | $context, |
568 | $constraint, |
569 | CheckResult::STATUS_BAD_PARAMETERS, |
570 | $e->getViolationMessage() |
571 | ); |
572 | } catch ( SparqlHelperException $e ) { |
573 | $message = new ViolationMessage( 'wbqc-violation-message-sparql-error' ); |
574 | $result = new CheckResult( $context, $constraint, CheckResult::STATUS_TODO, $message ); |
575 | } |
576 | $endTime = microtime( true ); |
577 | |
578 | $this->addMetadata( $context, $result ); |
579 | |
580 | $this->addConstraintClarification( $result ); |
581 | |
582 | $this->downgradeResultStatus( $result ); |
583 | |
584 | $this->loggingHelper->logConstraintCheck( |
585 | $context, |
586 | $constraint, |
587 | $result, |
588 | get_class( $checker ), |
589 | $endTime - $startTime, |
590 | __METHOD__ |
591 | ); |
592 | |
593 | return $result; |
594 | } else { |
595 | return new CheckResult( $context, $constraint, CheckResult::STATUS_TODO, null ); |
596 | } |
597 | } |
598 | |
599 | private function handleScope( |
600 | ConstraintChecker $checker, |
601 | Context $context, |
602 | Constraint $constraint |
603 | ): ?CheckResult { |
604 | $validContextTypes = $this->getValidContextTypes( $constraint ); |
605 | $validEntityTypes = $this->getValidEntityTypes( $constraint ); |
606 | try { |
607 | [ $checkedContextTypes, $checkedEntityTypes ] = $this->constraintParameterParser->parseConstraintScopeParameters( |
608 | $constraint->getConstraintParameters(), |
609 | $constraint->getConstraintTypeItemId(), |
610 | $validContextTypes, |
611 | $validEntityTypes |
612 | ); |
613 | } catch ( ConstraintParameterException $e ) { |
614 | return new CheckResult( $context, $constraint, CheckResult::STATUS_BAD_PARAMETERS, $e->getViolationMessage() ); |
615 | } |
616 | |
617 | $checkedContextTypes ??= $checker->getDefaultContextTypes(); |
618 | $contextType = $context->getType(); |
619 | if ( !in_array( $contextType, $checkedContextTypes ) ) { |
620 | return new CheckResult( $context, $constraint, CheckResult::STATUS_NOT_IN_SCOPE, null ); |
621 | } |
622 | if ( $checker->getSupportedContextTypes()[$contextType] === CheckResult::STATUS_TODO ) { |
623 | return new CheckResult( $context, $constraint, CheckResult::STATUS_TODO, null ); |
624 | } |
625 | |
626 | $checkedEntityTypes ??= $validEntityTypes; |
627 | $entityType = $context->getEntity()->getType(); |
628 | if ( !in_array( $entityType, $checkedEntityTypes ) ) { |
629 | return new CheckResult( $context, $constraint, CheckResult::STATUS_NOT_IN_SCOPE, null ); |
630 | } |
631 | if ( $checker->getSupportedEntityTypes()[$entityType] === CheckResult::STATUS_TODO ) { |
632 | return new CheckResult( $context, $constraint, CheckResult::STATUS_TODO, null ); |
633 | } |
634 | |
635 | return null; |
636 | } |
637 | |
638 | private function addMetadata( Context $context, CheckResult $result ): void { |
639 | $result->withMetadata( Metadata::merge( [ |
640 | $result->getMetadata(), |
641 | Metadata::ofDependencyMetadata( DependencyMetadata::merge( [ |
642 | DependencyMetadata::ofEntityId( $context->getEntity()->getId() ), |
643 | DependencyMetadata::ofEntityId( $result->getConstraint()->getPropertyId() ), |
644 | ] ) ), |
645 | ] ) ); |
646 | } |
647 | |
648 | private function addConstraintClarification( CheckResult $result ): void { |
649 | $constraint = $result->getConstraint(); |
650 | try { |
651 | $constraintClarification = $this->constraintParameterParser |
652 | ->parseConstraintClarificationParameter( $constraint->getConstraintParameters() ); |
653 | $result->setConstraintClarification( $constraintClarification ); |
654 | } catch ( ConstraintParameterException $e ) { |
655 | $result->setStatus( CheckResult::STATUS_BAD_PARAMETERS ); |
656 | $result->setMessage( $e->getViolationMessage() ); |
657 | } |
658 | } |
659 | |
660 | private function downgradeResultStatus( CheckResult $result ): void { |
661 | $constraint = $result->getConstraint(); |
662 | try { |
663 | $constraintStatus = $this->constraintParameterParser |
664 | ->parseConstraintStatusParameter( $constraint->getConstraintParameters() ); |
665 | } catch ( ConstraintParameterException $e ) { |
666 | $result->setStatus( CheckResult::STATUS_BAD_PARAMETERS ); |
667 | $result->setMessage( $e->getViolationMessage() ); |
668 | return; |
669 | } |
670 | if ( $constraintStatus === null ) { |
671 | // downgrade violation to warning |
672 | if ( $result->getStatus() === CheckResult::STATUS_VIOLATION ) { |
673 | $result->setStatus( CheckResult::STATUS_WARNING ); |
674 | } |
675 | } elseif ( $constraintStatus === 'suggestion' ) { |
676 | // downgrade violation to suggestion |
677 | if ( $result->getStatus() === CheckResult::STATUS_VIOLATION ) { |
678 | $result->setStatus( CheckResult::STATUS_SUGGESTION ); |
679 | } |
680 | } else { |
681 | if ( $constraintStatus !== 'mandatory' ) { |
682 | // @codeCoverageIgnoreStart |
683 | throw new LogicException( |
684 | "Unknown constraint status '$constraintStatus', " . |
685 | "only known statuses are 'mandatory' and 'suggestion'" |
686 | ); |
687 | // @codeCoverageIgnoreEnd |
688 | } |
689 | } |
690 | } |
691 | |
692 | /** |
693 | * @param CheckResult[] $result |
694 | * |
695 | * @return CheckResult[] |
696 | */ |
697 | private function sortResult( array $result ): array { |
698 | if ( count( $result ) < 2 ) { |
699 | return $result; |
700 | } |
701 | |
702 | $sortFunction = static function ( CheckResult $a, CheckResult $b ) { |
703 | $orderNum = 0; |
704 | $order = [ |
705 | CheckResult::STATUS_BAD_PARAMETERS => $orderNum++, |
706 | CheckResult::STATUS_VIOLATION => $orderNum++, |
707 | CheckResult::STATUS_WARNING => $orderNum++, |
708 | CheckResult::STATUS_SUGGESTION => $orderNum++, |
709 | CheckResult::STATUS_EXCEPTION => $orderNum++, |
710 | CheckResult::STATUS_COMPLIANCE => $orderNum++, |
711 | CheckResult::STATUS_DEPRECATED => $orderNum++, |
712 | CheckResult::STATUS_NOT_IN_SCOPE => $orderNum++, |
713 | 'other' => $orderNum++, |
714 | ]; |
715 | |
716 | $statusA = $a->getStatus(); |
717 | $statusB = $b->getStatus(); |
718 | |
719 | $orderA = array_key_exists( $statusA, $order ) ? $order[ $statusA ] : $order[ 'other' ]; |
720 | $orderB = array_key_exists( $statusB, $order ) ? $order[ $statusB ] : $order[ 'other' ]; |
721 | |
722 | if ( $orderA === $orderB ) { |
723 | $cursorA = $a->getContextCursor(); |
724 | $cursorB = $b->getContextCursor(); |
725 | |
726 | if ( $cursorA instanceof EntityContextCursor ) { |
727 | return $cursorB instanceof EntityContextCursor ? 0 : -1; |
728 | } |
729 | if ( $cursorB instanceof EntityContextCursor ) { |
730 | return $cursorA instanceof EntityContextCursor ? 0 : 1; |
731 | } |
732 | |
733 | $pidA = $cursorA->getSnakPropertyId(); |
734 | $pidB = $cursorB->getSnakPropertyId(); |
735 | |
736 | if ( $pidA === $pidB ) { |
737 | $hashA = $cursorA->getSnakHash(); |
738 | $hashB = $cursorB->getSnakHash(); |
739 | |
740 | if ( $hashA === $hashB ) { |
741 | if ( $a instanceof NullResult ) { |
742 | return $b instanceof NullResult ? 0 : -1; |
743 | } |
744 | if ( $b instanceof NullResult ) { |
745 | return $a instanceof NullResult ? 0 : 1; |
746 | } |
747 | |
748 | $typeA = $a->getConstraint()->getConstraintTypeItemId(); |
749 | $typeB = $b->getConstraint()->getConstraintTypeItemId(); |
750 | |
751 | if ( $typeA == $typeB ) { |
752 | return 0; |
753 | } else { |
754 | return ( $typeA > $typeB ) ? 1 : -1; |
755 | } |
756 | } else { |
757 | return ( $hashA > $hashB ) ? 1 : -1; |
758 | } |
759 | } else { |
760 | return ( $pidA > $pidB ) ? 1 : -1; |
761 | } |
762 | } else { |
763 | return ( $orderA > $orderB ) ? 1 : -1; |
764 | } |
765 | }; |
766 | |
767 | uasort( $result, $sortFunction ); |
768 | |
769 | return $result; |
770 | } |
771 | |
772 | } |