Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.67% covered (success)
90.67%
311 / 343
65.00% covered (warning)
65.00%
13 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
DelegatingConstraintChecker
90.67% covered (success)
90.67%
311 / 343
65.00% covered (warning)
65.00%
13 / 20
100.02
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
58.33% covered (warning)
58.33%
7 / 12
0.00% covered (danger)
0.00%
0 / 1
2.29
 getValidEntityTypes
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
2.01
 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%
25 / 25
100.00% covered (success)
100.00%
1 / 1
8
 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 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}