Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.88% covered (success)
93.88%
92 / 98
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
ContemporaryChecker
93.88% covered (success)
93.88%
92 / 98
40.00% covered (danger)
40.00%
2 / 5
30.21
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getSupportedContextTypes
n/a
0 / 0
n/a
0 / 0
1
 getDefaultContextTypes
n/a
0 / 0
n/a
0 / 0
1
 getSupportedEntityTypes
n/a
0 / 0
n/a
0 / 0
1
 checkConstraint
93.85% covered (success)
93.85%
61 / 65
0.00% covered (danger)
0.00%
0 / 1
11.03
 getExtremeValue
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
12
 getViolationMessage
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 checkConstraintParameters
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace WikibaseQuality\ConstraintReport\ConstraintCheck\Checker;
4
5use DataValues\DataValue;
6use MediaWiki\Config\Config;
7use MediaWiki\Config\ConfigException;
8use Wikibase\DataModel\Entity\EntityId;
9use Wikibase\DataModel\Entity\EntityIdValue;
10use Wikibase\DataModel\Entity\ItemId;
11use Wikibase\DataModel\Entity\NumericPropertyId;
12use Wikibase\DataModel\Services\Lookup\EntityLookup;
13use Wikibase\DataModel\Snak\PropertyValueSnak;
14use Wikibase\DataModel\Statement\Statement;
15use Wikibase\DataModel\Statement\StatementList;
16use Wikibase\DataModel\Statement\StatementListProvider;
17use WikibaseQuality\ConstraintReport\Constraint;
18use WikibaseQuality\ConstraintReport\ConstraintCheck\ConstraintChecker;
19use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\Context;
20use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\RangeCheckerHelper;
21use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\ViolationMessage;
22use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult;
23use WikibaseQuality\ConstraintReport\Role;
24
25/**
26 * @author David Abián
27 * @license GPL-2.0-or-later
28 */
29class ContemporaryChecker implements ConstraintChecker {
30
31    /**
32     * @var RangeCheckerHelper
33     */
34    private $rangeCheckerHelper;
35
36    /**
37     * @var Config
38     */
39    private $config;
40
41    /**
42     * @var EntityLookup
43     */
44    private $entityLookup;
45
46    /**
47     * Name of the configuration variable for the array of IDs of the properties that
48     * state the start time of the entities.
49     */
50    public const CONFIG_VARIABLE_START_PROPERTY_IDS = 'WBQualityConstraintsStartTimePropertyIds';
51
52    /**
53     * Name of the configuration variable for the array of IDs of the properties that
54     * state the end time of the entities.
55     */
56    public const CONFIG_VARIABLE_END_PROPERTY_IDS = 'WBQualityConstraintsEndTimePropertyIds';
57
58    public function __construct(
59        EntityLookup $entityLookup,
60        RangeCheckerHelper $rangeCheckerHelper,
61        Config $config
62    ) {
63        $this->entityLookup = $entityLookup;
64        $this->rangeCheckerHelper = $rangeCheckerHelper;
65        $this->config = $config;
66    }
67
68    /**
69     * @codeCoverageIgnore This method is purely declarative.
70     */
71    public function getSupportedContextTypes() {
72        return [
73            Context::TYPE_STATEMENT => CheckResult::STATUS_COMPLIANCE,
74            Context::TYPE_QUALIFIER => CheckResult::STATUS_NOT_IN_SCOPE,
75            Context::TYPE_REFERENCE => CheckResult::STATUS_NOT_IN_SCOPE,
76        ];
77    }
78
79    /**
80     * @codeCoverageIgnore This method is purely declarative.
81     */
82    public function getDefaultContextTypes() {
83        return [ Context::TYPE_STATEMENT ];
84    }
85
86    /** @codeCoverageIgnore This method is purely declarative. */
87    public function getSupportedEntityTypes() {
88        return self::ALL_ENTITY_TYPES_SUPPORTED;
89    }
90
91    /**
92     * Checks 'Contemporary' constraint.
93     *
94     * @param Context $context
95     * @param Constraint $constraint
96     *
97     * @return CheckResult
98     * @throws ConfigException
99     */
100    public function checkConstraint( Context $context, Constraint $constraint ) {
101        if ( $context->getSnakRank() === Statement::RANK_DEPRECATED ) {
102            return new CheckResult( $context, $constraint, CheckResult::STATUS_DEPRECATED );
103        }
104        $snak = $context->getSnak();
105        if ( !$snak instanceof PropertyValueSnak ) {
106            // nothing to check
107            return new CheckResult( $context, $constraint, CheckResult::STATUS_COMPLIANCE );
108        }
109
110        $dataValue = $snak->getDataValue();
111        if ( !$dataValue instanceof EntityIdValue ) {
112            // wrong data type
113            $message = ( new ViolationMessage( 'wbqc-violation-message-value-needed-of-type' ) )
114                ->withEntityId( new ItemId( $constraint->getConstraintTypeItemId() ), Role::CONSTRAINT_TYPE_ITEM )
115                ->withDataValueType( 'wikibase-entityid' );
116            return new CheckResult( $context, $constraint, CheckResult::STATUS_VIOLATION, $message );
117        }
118
119        $objectId = $dataValue->getEntityId();
120        $objectItem = $this->entityLookup->getEntity( $objectId );
121        if ( !( $objectItem instanceof StatementListProvider ) ) {
122            // object was deleted/doesn't exist
123            $message = new ViolationMessage( 'wbqc-violation-message-value-entity-must-exist' );
124            return new CheckResult( $context, $constraint, CheckResult::STATUS_VIOLATION, $message );
125        }
126        /** @var Statement[] $objectStatements */
127        $objectStatements = $objectItem->getStatements()->toArray();
128
129        $subjectId = $context->getEntity()->getId();
130        $subjectStatements = $context->getEntity()->getStatements()->toArray();
131        /** @var String[] $startPropertyIds */
132        $startPropertyIds = $this->config->get( self::CONFIG_VARIABLE_START_PROPERTY_IDS );
133        /** @var String[] $endPropertyIds */
134        $endPropertyIds = $this->config->get( self::CONFIG_VARIABLE_END_PROPERTY_IDS );
135        $subjectStartValue = $this->getExtremeValue(
136            $startPropertyIds,
137            $subjectStatements,
138            'start'
139        );
140        $objectStartValue = $this->getExtremeValue(
141            $startPropertyIds,
142            $objectStatements,
143            'start'
144        );
145        $subjectEndValue = $this->getExtremeValue(
146            $endPropertyIds,
147            $subjectStatements,
148            'end'
149        );
150        $objectEndValue = $this->getExtremeValue(
151            $endPropertyIds,
152            $objectStatements,
153            'end'
154        );
155        if (
156            $this->rangeCheckerHelper->getComparison( $subjectStartValue, $subjectEndValue ) <= 0 &&
157            $this->rangeCheckerHelper->getComparison( $objectStartValue, $objectEndValue ) <= 0 && (
158                $this->rangeCheckerHelper->getComparison( $subjectEndValue, $objectStartValue ) < 0 ||
159                $this->rangeCheckerHelper->getComparison( $objectEndValue, $subjectStartValue ) < 0
160            )
161        ) {
162            if (
163                $subjectEndValue == null ||
164                $this->rangeCheckerHelper->getComparison( $objectEndValue, $subjectEndValue ) < 0
165            ) {
166                $earlierEntityId = $objectId;
167                $minEndValue = $objectEndValue;
168                $maxStartValue = $subjectStartValue;
169            } else {
170                $earlierEntityId = $subjectId;
171                $minEndValue = $subjectEndValue;
172                $maxStartValue = $objectStartValue;
173            }
174            $message = $this->getViolationMessage(
175                // @phan-suppress-next-line PhanTypeMismatchArgumentNullable
176                $earlierEntityId,
177                $subjectId,
178                $context->getSnak()->getPropertyId(),
179                $objectId,
180                // @phan-suppress-next-line PhanTypeMismatchArgumentNullable
181                $minEndValue,
182                $maxStartValue
183            );
184            $status = CheckResult::STATUS_VIOLATION;
185        } else {
186            $message = null;
187            $status = CheckResult::STATUS_COMPLIANCE;
188        }
189        return new CheckResult( $context, $constraint, $status, $message );
190    }
191
192    /**
193     * @param string[] $extremePropertyIds
194     * @param Statement[] $statements
195     * @param string $startOrEnd 'start' or 'end'
196     *
197     * @return DataValue|null
198     */
199    private function getExtremeValue( $extremePropertyIds, $statements, $startOrEnd ) {
200        if ( $startOrEnd !== 'start' && $startOrEnd !== 'end' ) {
201            throw new \InvalidArgumentException( '$startOrEnd must be \'start\' or \'end\'.' );
202        }
203        $extremeValue = null;
204        foreach ( $extremePropertyIds as $extremePropertyId ) {
205            $statementList = new StatementList( ...$statements );
206            $extremeStatements = $statementList->getByPropertyId( new NumericPropertyId( $extremePropertyId ) );
207            /** @var Statement $extremeStatement */
208            foreach ( $extremeStatements as $extremeStatement ) {
209                if ( $extremeStatement->getRank() !== Statement::RANK_DEPRECATED ) {
210                    $snak = $extremeStatement->getMainSnak();
211                    if ( !$snak instanceof PropertyValueSnak ) {
212                        return null;
213                    } else {
214                        $comparison = $this->rangeCheckerHelper->getComparison(
215                            $snak->getDataValue(),
216                            $extremeValue
217                        );
218                        if (
219                            $extremeValue === null ||
220                            ( $startOrEnd === 'start' && $comparison < 0 ) ||
221                            ( $startOrEnd === 'end' && $comparison > 0 )
222                        ) {
223                            $extremeValue = $snak->getDataValue();
224                        }
225                    }
226                }
227            }
228        }
229        return $extremeValue;
230    }
231
232    /**
233     * @param EntityId $earlierEntityId
234     * @param EntityId $subjectId
235     * @param EntityId $propertyId
236     * @param EntityId $objectId
237     * @param DataValue $minEndValue
238     * @param DataValue $maxStartValue
239     *
240     * @return ViolationMessage
241     */
242    private function getViolationMessage(
243        EntityId $earlierEntityId,
244        EntityId $subjectId,
245        EntityId $propertyId,
246        EntityId $objectId,
247        DataValue $minEndValue,
248        DataValue $maxStartValue
249    ) {
250        $messageKey = $earlierEntityId === $subjectId ?
251            'wbqc-violation-message-contemporary-subject-earlier' :
252            'wbqc-violation-message-contemporary-value-earlier';
253        return ( new ViolationMessage( $messageKey ) )
254            ->withEntityId( $subjectId, Role::SUBJECT )
255            ->withEntityId( $propertyId, Role::PREDICATE )
256            ->withEntityId( $objectId, Role::OBJECT )
257            ->withDataValue( $minEndValue, Role::OBJECT )
258            ->withDataValue( $maxStartValue, Role::OBJECT );
259    }
260
261    public function checkConstraintParameters( Constraint $constraint ) {
262        // no parameters
263        return [];
264    }
265
266}