Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.50% covered (success)
90.50%
305 / 337
65.00% covered (warning)
65.00%
13 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
DelegatingConstraintChecker
90.50% covered (success)
90.50%
305 / 337
65.00% covered (warning)
65.00%
13 / 20
98.09
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 checkAgainstConstraintsOnEntityId
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
4
 checkAgainstConstraintsOnClaimId
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 getValidContextTypes
50.00% covered (danger)
50.00%
5 / 10
0.00% covered (danger)
0.00%
0 / 1
2.50
 getValidEntityTypes
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 checkCommonConstraintParameters
91.30% covered (success)
91.30%
21 / 23
0.00% covered (danger)
0.00%
0 / 1
6.02
 checkConstraintParametersOnPropertyId
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 checkConstraintParametersOnConstraintId
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 checkEveryStatement
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 checkStatement
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
3
 getConstraintsToUse
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
5.39
 checkConstraintsForMainSnak
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
5
 checkConstraintsForQualifiers
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
5.01
 checkConstraintsForReferences
16.67% covered (danger)
16.67%
3 / 18
0.00% covered (danger)
0.00%
0 / 1
19.47
 getCheckResultFor
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
1 / 1
5
 handleScope
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
6
 addMetadata
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 addConstraintClarification
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 downgradeResultStatus
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
 sortResult
89.36% covered (warning)
89.36%
42 / 47
0.00% covered (danger)
0.00%
0 / 1
20.48
1<?php
2
3declare( strict_types = 1 );
4
5namespace WikibaseQuality\ConstraintReport\ConstraintCheck;
6
7use InvalidArgumentException;
8use LogicException;
9use Wikibase\DataModel\Entity\EntityId;
10use Wikibase\DataModel\Entity\NumericPropertyId;
11use Wikibase\DataModel\Entity\PropertyId;
12use Wikibase\DataModel\Entity\StatementListProvidingEntity;
13use Wikibase\DataModel\Reference;
14use Wikibase\DataModel\Services\Lookup\EntityLookup;
15use Wikibase\DataModel\Services\Statement\StatementGuidParser;
16use Wikibase\DataModel\Statement\Statement;
17use WikibaseQuality\ConstraintReport\Constraint;
18use WikibaseQuality\ConstraintReport\ConstraintCheck\Cache\DependencyMetadata;
19use WikibaseQuality\ConstraintReport\ConstraintCheck\Cache\Metadata;
20use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\Context;
21use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\EntityContextCursor;
22use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\MainSnakContext;
23use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\QualifierContext;
24use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\ReferenceContext;
25use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterException;
26use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterParser;
27use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\LoggingHelper;
28use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\SparqlHelperException;
29use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\ViolationMessage;
30use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult;
31use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\NullResult;
32use 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 */
41class 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}