Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
93.88% |
92 / 98 |
|
40.00% |
2 / 5 |
CRAP | |
0.00% |
0 / 1 |
ContemporaryChecker | |
93.88% |
92 / 98 |
|
40.00% |
2 / 5 |
30.21 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
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% |
61 / 65 |
|
0.00% |
0 / 1 |
11.03 | |||
getExtremeValue | |
95.00% |
19 / 20 |
|
0.00% |
0 / 1 |
12 | |||
getViolationMessage | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
checkConstraintParameters | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace WikibaseQuality\ConstraintReport\ConstraintCheck\Checker; |
4 | |
5 | use DataValues\DataValue; |
6 | use MediaWiki\Config\Config; |
7 | use MediaWiki\Config\ConfigException; |
8 | use Wikibase\DataModel\Entity\EntityId; |
9 | use Wikibase\DataModel\Entity\EntityIdValue; |
10 | use Wikibase\DataModel\Entity\ItemId; |
11 | use Wikibase\DataModel\Entity\NumericPropertyId; |
12 | use Wikibase\DataModel\Services\Lookup\EntityLookup; |
13 | use Wikibase\DataModel\Snak\PropertyValueSnak; |
14 | use Wikibase\DataModel\Statement\Statement; |
15 | use Wikibase\DataModel\Statement\StatementList; |
16 | use Wikibase\DataModel\Statement\StatementListProvider; |
17 | use WikibaseQuality\ConstraintReport\Constraint; |
18 | use WikibaseQuality\ConstraintReport\ConstraintCheck\ConstraintChecker; |
19 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\Context; |
20 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\RangeCheckerHelper; |
21 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\ViolationMessage; |
22 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult; |
23 | use WikibaseQuality\ConstraintReport\Role; |
24 | |
25 | /** |
26 | * @author David Abián |
27 | * @license GPL-2.0-or-later |
28 | */ |
29 | class 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 | } |