Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.76% covered (warning)
87.76%
86 / 98
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
TypeCheckerHelper
87.76% covered (warning)
87.76%
86 / 98
57.14% covered (warning)
57.14%
4 / 7
21.81
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 isSubclassOf
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
7
 isSubclassOfWithSparqlFallback
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
1 / 1
3
 hasClassInRelation
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
5
 hasCorrectType
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getBestStatementsByPropertyIds
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 getViolationMessage
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace WikibaseQuality\ConstraintReport\ConstraintCheck\Helper;
4
5use MediaWiki\Config\Config;
6use OverflowException;
7use Wikibase\DataModel\Entity\EntityId;
8use Wikibase\DataModel\Entity\EntityIdValue;
9use Wikibase\DataModel\Entity\ItemId;
10use Wikibase\DataModel\Entity\NumericPropertyId;
11use Wikibase\DataModel\Entity\PropertyId;
12use Wikibase\DataModel\Services\Lookup\EntityLookup;
13use Wikibase\DataModel\Snak\PropertyValueSnak;
14use Wikibase\DataModel\Snak\Snak;
15use Wikibase\DataModel\Statement\Statement;
16use Wikibase\DataModel\Statement\StatementList;
17use Wikibase\DataModel\Statement\StatementListProvider;
18use WikibaseQuality\ConstraintReport\ConstraintCheck\Cache\CachedBool;
19use WikibaseQuality\ConstraintReport\ConstraintCheck\Cache\Metadata;
20use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\ViolationMessage;
21use WikibaseQuality\ConstraintReport\Role;
22use 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 */
30class 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}