Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
87.76% |
86 / 98 |
|
57.14% |
4 / 7 |
CRAP | |
0.00% |
0 / 1 |
TypeCheckerHelper | |
87.76% |
86 / 98 |
|
57.14% |
4 / 7 |
21.81 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
isSubclassOf | |
95.24% |
20 / 21 |
|
0.00% |
0 / 1 |
7 | |||
isSubclassOfWithSparqlFallback | |
100.00% |
33 / 33 |
|
100.00% |
1 / 1 |
3 | |||
hasClassInRelation | |
95.00% |
19 / 20 |
|
0.00% |
0 / 1 |
5 | |||
hasCorrectType | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getBestStatementsByPropertyIds | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
getViolationMessage | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace WikibaseQuality\ConstraintReport\ConstraintCheck\Helper; |
4 | |
5 | use MediaWiki\Config\Config; |
6 | use OverflowException; |
7 | use Wikibase\DataModel\Entity\EntityId; |
8 | use Wikibase\DataModel\Entity\EntityIdValue; |
9 | use Wikibase\DataModel\Entity\ItemId; |
10 | use Wikibase\DataModel\Entity\NumericPropertyId; |
11 | use Wikibase\DataModel\Entity\PropertyId; |
12 | use Wikibase\DataModel\Services\Lookup\EntityLookup; |
13 | use Wikibase\DataModel\Snak\PropertyValueSnak; |
14 | use Wikibase\DataModel\Snak\Snak; |
15 | use Wikibase\DataModel\Statement\Statement; |
16 | use Wikibase\DataModel\Statement\StatementList; |
17 | use Wikibase\DataModel\Statement\StatementListProvider; |
18 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Cache\CachedBool; |
19 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Cache\Metadata; |
20 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\ViolationMessage; |
21 | use WikibaseQuality\ConstraintReport\Role; |
22 | use Wikimedia\Stats\StatsFactory; |
23 | |
24 | /** |
25 | * Class for helper functions for range checkers. |
26 | * |
27 | * @author BP2014N1 |
28 | * @license GPL-2.0-or-later |
29 | */ |
30 | class TypeCheckerHelper { |
31 | |
32 | /** |
33 | * @var EntityLookup |
34 | */ |
35 | private $entityLookup; |
36 | |
37 | /** |
38 | * @var Config |
39 | */ |
40 | private $config; |
41 | |
42 | /** |
43 | * @var SparqlHelper |
44 | */ |
45 | private $sparqlHelper; |
46 | |
47 | /** |
48 | * @var StatsFactory |
49 | */ |
50 | private $statsFactory; |
51 | |
52 | /** |
53 | * @param EntityLookup $lookup |
54 | * @param Config $config |
55 | * @param SparqlHelper $sparqlHelper |
56 | * @param StatsFactory $statsFactory |
57 | */ |
58 | public function __construct( |
59 | EntityLookup $lookup, |
60 | Config $config, |
61 | SparqlHelper $sparqlHelper, |
62 | StatsFactory $statsFactory |
63 | ) { |
64 | $this->entityLookup = $lookup; |
65 | $this->config = $config; |
66 | $this->sparqlHelper = $sparqlHelper; |
67 | $this->statsFactory = $statsFactory; |
68 | } |
69 | |
70 | /** |
71 | * Checks if $comparativeClass is a subclass |
72 | * of one of the item ID serializations in $classesToCheck. |
73 | * If the class hierarchy is not exhausted before |
74 | * the configured limit (WBQualityConstraintsTypeCheckMaxEntities) is reached, |
75 | * an OverflowException is thrown. |
76 | * |
77 | * @param EntityId $comparativeClass |
78 | * @param string[] $classesToCheck |
79 | * @param int &$entitiesChecked |
80 | * |
81 | * @return bool |
82 | * @throws OverflowException if $entitiesChecked exceeds the configured limit |
83 | */ |
84 | private function isSubclassOf( EntityId $comparativeClass, array $classesToCheck, &$entitiesChecked = 0 ) { |
85 | $maxEntities = $this->config->get( 'WBQualityConstraintsTypeCheckMaxEntities' ); |
86 | if ( ++$entitiesChecked > $maxEntities ) { |
87 | throw new OverflowException( 'Too many entities to check' ); |
88 | } |
89 | |
90 | $item = $this->entityLookup->getEntity( $comparativeClass ); |
91 | if ( !( $item instanceof StatementListProvider ) ) { |
92 | return false; // lookup failed, probably because item doesn't exist |
93 | } |
94 | |
95 | $subclassId = $this->config->get( 'WBQualityConstraintsSubclassOfId' ); |
96 | $statements = $item->getStatements() |
97 | ->getByPropertyId( new NumericPropertyId( $subclassId ) ) |
98 | ->getBestStatements(); |
99 | /** @var Statement $statement */ |
100 | foreach ( $statements as $statement ) { |
101 | $mainSnak = $statement->getMainSnak(); |
102 | |
103 | if ( !$this->hasCorrectType( $mainSnak ) ) { |
104 | continue; |
105 | } |
106 | /** @var PropertyValueSnak $mainSnak */ |
107 | /** @var EntityIdValue $dataValue */ |
108 | |
109 | $dataValue = $mainSnak->getDataValue(); |
110 | '@phan-var EntityIdValue $dataValue'; |
111 | $comparativeClass = $dataValue->getEntityId(); |
112 | |
113 | if ( in_array( $comparativeClass->getSerialization(), $classesToCheck ) ) { |
114 | return true; |
115 | } |
116 | |
117 | if ( $this->isSubclassOf( $comparativeClass, $classesToCheck, $entitiesChecked ) ) { |
118 | return true; |
119 | } |
120 | } |
121 | |
122 | return false; |
123 | } |
124 | |
125 | /** |
126 | * Checks if $comparativeClass is a subclass |
127 | * of one of the item ID serializations in $classesToCheck. |
128 | * If isSubclassOf() aborts due to hitting the configured limit, |
129 | * the injected {@link SparqlHelper} is consulted if present, |
130 | * otherwise the check returns false. |
131 | * |
132 | * @param EntityId $comparativeClass |
133 | * @param string[] $classesToCheck |
134 | * |
135 | * @return CachedBool |
136 | * @throws SparqlHelperException if SPARQL is used and the query times out or some other error occurs |
137 | */ |
138 | public function isSubclassOfWithSparqlFallback( EntityId $comparativeClass, array $classesToCheck ) { |
139 | $timing = $this->statsFactory->getTiming( 'isSubclassOf_duration_seconds' ) |
140 | ->setLabel( 'result', 'success' ) |
141 | ->setLabel( 'TypeCheckerImplementation', 'php' ) |
142 | ->copyToStatsdAt( 'wikibase.quality.constraints.type.php.success.timing' ); |
143 | $timing->start(); |
144 | |
145 | try { |
146 | $entitiesChecked = 0; |
147 | $isSubclass = $this->isSubclassOf( $comparativeClass, $classesToCheck, $entitiesChecked ); |
148 | $timing->stop(); |
149 | |
150 | // not really a timing, but works like one (we want percentiles etc.) |
151 | // TODO: probably a good candidate for T348796 |
152 | $this->statsFactory->getTiming( 'isSubclassOf_entities_total' ) |
153 | ->setLabel( 'TypeCheckerImplementation', 'php' ) |
154 | ->setLabel( 'result', 'success' ) |
155 | ->copyToStatsdAt( 'wikibase.quality.constraints.type.php.success.entities' ) |
156 | ->observe( $entitiesChecked ); |
157 | |
158 | return new CachedBool( $isSubclass, Metadata::blank() ); |
159 | } catch ( OverflowException $e ) { |
160 | $timing->setLabel( 'result', 'overflow' ) |
161 | ->copyToStatsdAt( 'wikibase.quality.constraints.type.php.overflow.timing' ) |
162 | ->stop(); |
163 | |
164 | if ( !( $this->sparqlHelper instanceof DummySparqlHelper ) ) { |
165 | $this->statsFactory->getCounter( 'sparql_typeFallback_total' ) |
166 | ->copyToStatsdAt( 'wikibase.quality.constraints.sparql.typeFallback' ) |
167 | ->increment(); |
168 | |
169 | $timing->setLabel( 'TypeCheckerImplementation', 'sparql' ) |
170 | ->setLabel( 'result', 'success' ) |
171 | ->copyToStatsdAt( 'wikibase.quality.constraints.type.sparql.success.timing' ) |
172 | ->start(); |
173 | |
174 | $hasType = $this->sparqlHelper->hasType( |
175 | $comparativeClass->getSerialization(), |
176 | $classesToCheck |
177 | ); |
178 | |
179 | $timing->stop(); |
180 | |
181 | return $hasType; |
182 | } else { |
183 | return new CachedBool( false, Metadata::blank() ); |
184 | } |
185 | } |
186 | } |
187 | |
188 | /** |
189 | * Checks, if one of the itemId serializations in $classesToCheck |
190 | * is contained in the list of $statements |
191 | * via properties $relationIds or if it is a subclass of |
192 | * one of the items referenced in $statements via $relationIds |
193 | * |
194 | * @param StatementList $statements |
195 | * @param string[] $relationIds |
196 | * @param string[] $classesToCheck |
197 | * |
198 | * @return CachedBool |
199 | * @throws SparqlHelperException if SPARQL is used and the query times out or some other error occurs |
200 | */ |
201 | public function hasClassInRelation( StatementList $statements, array $relationIds, array $classesToCheck ) { |
202 | $metadatas = []; |
203 | |
204 | foreach ( $this->getBestStatementsByPropertyIds( $statements, $relationIds ) as $statement ) { |
205 | $mainSnak = $statement->getMainSnak(); |
206 | |
207 | if ( !$this->hasCorrectType( $mainSnak ) ) { |
208 | continue; |
209 | } |
210 | /** @var PropertyValueSnak $mainSnak */ |
211 | /** @var EntityIdValue $dataValue */ |
212 | |
213 | $dataValue = $mainSnak->getDataValue(); |
214 | '@phan-var EntityIdValue $dataValue'; |
215 | $comparativeClass = $dataValue->getEntityId(); |
216 | |
217 | if ( in_array( $comparativeClass->getSerialization(), $classesToCheck ) ) { |
218 | // discard $metadatas, we know this is fresh |
219 | return new CachedBool( true, Metadata::blank() ); |
220 | } |
221 | |
222 | $result = $this->isSubclassOfWithSparqlFallback( $comparativeClass, $classesToCheck ); |
223 | $metadatas[] = $result->getMetadata(); |
224 | if ( $result->getBool() ) { |
225 | return new CachedBool( |
226 | true, |
227 | Metadata::merge( $metadatas ) |
228 | ); |
229 | } |
230 | } |
231 | |
232 | return new CachedBool( |
233 | false, |
234 | Metadata::merge( $metadatas ) |
235 | ); |
236 | } |
237 | |
238 | /** |
239 | * @param Snak $mainSnak |
240 | * @return bool |
241 | * @phan-assert PropertyValueSnak $mainSnak |
242 | */ |
243 | private function hasCorrectType( Snak $mainSnak ) { |
244 | return $mainSnak instanceof PropertyValueSnak |
245 | && $mainSnak->getDataValue()->getType() === 'wikibase-entityid'; |
246 | } |
247 | |
248 | /** |
249 | * @param StatementList $statements |
250 | * @param string[] $propertyIdSerializations |
251 | * |
252 | * @return Statement[] |
253 | */ |
254 | private function getBestStatementsByPropertyIds( |
255 | StatementList $statements, |
256 | array $propertyIdSerializations |
257 | ) { |
258 | $statementArrays = []; |
259 | |
260 | foreach ( $propertyIdSerializations as $propertyIdSerialization ) { |
261 | $propertyId = new NumericPropertyId( $propertyIdSerialization ); |
262 | $statementArrays[] = $statements |
263 | ->getByPropertyId( $propertyId ) |
264 | ->getBestStatements() |
265 | ->toArray(); |
266 | } |
267 | |
268 | return call_user_func_array( 'array_merge', $statementArrays ); |
269 | } |
270 | |
271 | /** |
272 | * @param PropertyId $propertyId ID of the property that introduced the constraint |
273 | * @param EntityId $entityId ID of the entity that does not have the required type |
274 | * @param string[] $classes item ID serializations of the classes that $entityId should have |
275 | * @param string $checker "type" or "valueType" (for message key) |
276 | * @param string $relation "instance", "subclass", or "instanceOrSubclass" (for message key) |
277 | * |
278 | * @return ViolationMessage |
279 | */ |
280 | public function getViolationMessage( |
281 | PropertyId $propertyId, |
282 | EntityId $entityId, |
283 | array $classes, |
284 | $checker, |
285 | $relation |
286 | ) { |
287 | $classes = array_map( |
288 | static function ( $itemIdSerialization ) { |
289 | return new ItemId( $itemIdSerialization ); |
290 | }, |
291 | $classes |
292 | ); |
293 | |
294 | // Possible messages: |
295 | // wbqc-violation-message-type-instance |
296 | // wbqc-violation-message-type-subclass |
297 | // wbqc-violation-message-type-instanceOrSubclass |
298 | // wbqc-violation-message-valueType-instance |
299 | // wbqc-violation-message-valueType-subclass |
300 | // wbqc-violation-message-valueType-instanceOrSubclass |
301 | return ( new ViolationMessage( 'wbqc-violation-message-' . $checker . '-' . $relation ) ) |
302 | ->withEntityId( $propertyId, Role::CONSTRAINT_PROPERTY ) |
303 | ->withEntityId( $entityId, Role::SUBJECT ) |
304 | ->withEntityIdList( $classes, Role::OBJECT ); |
305 | } |
306 | |
307 | } |