Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.75% covered (success)
93.75%
435 / 464
87.80% covered (warning)
87.80%
36 / 41
CRAP
0.00% covered (danger)
0.00%
0 / 1
ConstraintParameterParser
93.75% covered (success)
93.75%
435 / 464
87.80% covered (warning)
87.80%
36 / 41
121.40
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
 checkError
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 requireSingleParameter
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 requireValueParameter
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 parseEntityIdParameter
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 parseClassParameter
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 parseRelationParameter
76.19% covered (warning)
76.19%
16 / 21
0.00% covered (danger)
0.00%
0 / 1
3.12
 parsePropertyIdParameter
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 parsePropertyParameter
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 parseItemIdParameter
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 parseItemsParameter
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
8
 parseItemIdsParameter
69.23% covered (warning)
69.23%
9 / 13
0.00% covered (danger)
0.00%
0 / 1
2.12
 mapItemId
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 parsePropertiesParameter
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
5
 parseValueOrNoValueParameter
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 parseValueOrNoValueOrNowParameter
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 exactlyOneQuantityWithUnit
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 parseRangeParameter
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
10
 parseQuantityRangeParameter
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 parseTimeRangeParameter
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 parseLanguageParameter
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 parseStringParameter
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 parseNamespaceParameter
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 parseFormatParameter
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 parseExceptionParameter
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 parseConstraintStatusParameter
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
5
 requireMonolingualTextParameter
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 parseMultilingualTextParameter
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 parseSyntaxClarificationParameter
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 parseConstraintClarificationParameter
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 parseConstraintScopeParameters
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
1 / 1
6
 checkValidScope
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 parseUnitParameter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseUnitItem
66.67% covered (warning)
66.67%
10 / 15
0.00% covered (danger)
0.00%
0 / 1
4.59
 parseUnitsParameter
78.57% covered (warning)
78.57%
11 / 14
0.00% covered (danger)
0.00%
0 / 1
5.25
 getEntityTypeMapping
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 parseEntityTypesParameter
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
 parseSeparatorsParameter
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getConstraintScopeContextTypeMapping
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getPropertyScopeContextTypeMapping
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 parsePropertyScopeParameter
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare( strict_types = 1 );
4
5namespace WikibaseQuality\ConstraintReport\ConstraintCheck\Helper;
6
7use DataValues\DataValue;
8use DataValues\MonolingualTextValue;
9use DataValues\MultilingualTextValue;
10use DataValues\StringValue;
11use DataValues\UnboundedQuantityValue;
12use LogicException;
13use MediaWiki\Config\Config;
14use Wikibase\DataModel\Deserializers\DeserializerFactory;
15use Wikibase\DataModel\Deserializers\SnakDeserializer;
16use Wikibase\DataModel\Entity\EntityId;
17use Wikibase\DataModel\Entity\EntityIdValue;
18use Wikibase\DataModel\Entity\ItemId;
19use Wikibase\DataModel\Entity\NumericPropertyId;
20use Wikibase\DataModel\Entity\PropertyId;
21use Wikibase\DataModel\Snak\PropertyNoValueSnak;
22use Wikibase\DataModel\Snak\PropertySomeValueSnak;
23use Wikibase\DataModel\Snak\PropertyValueSnak;
24use Wikibase\DataModel\Snak\Snak;
25use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\Context;
26use WikibaseQuality\ConstraintReport\ConstraintCheck\ItemIdSnakValue;
27use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\ViolationMessage;
28use WikibaseQuality\ConstraintReport\Role;
29
30/**
31 * Helper for parsing constraint parameters
32 * that were imported from constraint statements.
33 *
34 * All public methods of this class expect constraint parameters
35 * (see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()})
36 * and return parameter objects or throw {@link ConstraintParameterException}s.
37 * The results are used by the checkers,
38 * which may include rendering them into violation messages.
39 *
40 * @author Lucas Werkmeister
41 * @license GPL-2.0-or-later
42 */
43class ConstraintParameterParser {
44
45    private Config $config;
46    private SnakDeserializer $snakDeserializer;
47    private string $unitItemConceptBaseUri;
48
49    /**
50     * @param Config $config
51     *   contains entity IDs used in constraint parameters (constraint statement qualifiers)
52     * @param DeserializerFactory $factory
53     *   used to parse constraint statement qualifiers into constraint parameters
54     * @param string $unitItemConceptBaseUri
55     *   concept base URI of items used for units
56     */
57    public function __construct(
58        Config $config,
59        DeserializerFactory $factory,
60        string $unitItemConceptBaseUri
61    ) {
62        $this->config = $config;
63        $this->snakDeserializer = $factory->newSnakDeserializer();
64        $this->unitItemConceptBaseUri = $unitItemConceptBaseUri;
65    }
66
67    /**
68     * Check if any errors are recorded in the constraint parameters.
69     * @throws ConstraintParameterException
70     */
71    public function checkError( array $parameters ): void {
72        if ( array_key_exists( '@error', $parameters ) ) {
73            $error = $parameters['@error'];
74            if ( array_key_exists( 'toolong', $error ) && $error['toolong'] ) {
75                $msg = 'wbqc-violation-message-parameters-error-toolong';
76            } else {
77                $msg = 'wbqc-violation-message-parameters-error-unknown';
78            }
79            throw new ConstraintParameterException( new ViolationMessage( $msg ) );
80        }
81    }
82
83    /**
84     * Require that $parameters contains exactly one $parameterId parameter.
85     * @throws ConstraintParameterException
86     */
87    private function requireSingleParameter( array $parameters, string $parameterId ): void {
88        if ( count( $parameters[$parameterId] ) !== 1 ) {
89            throw new ConstraintParameterException(
90                ( new ViolationMessage( 'wbqc-violation-message-parameter-single' ) )
91                    ->withEntityId( new NumericPropertyId( $parameterId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
92            );
93        }
94    }
95
96    /**
97     * Require that $snak is a {@link PropertyValueSnak}.
98     * @throws ConstraintParameterException
99     */
100    private function requireValueParameter( Snak $snak, string $parameterId ): void {
101        if ( !( $snak instanceof PropertyValueSnak ) ) {
102            throw new ConstraintParameterException(
103                ( new ViolationMessage( 'wbqc-violation-message-parameter-value' ) )
104                    ->withEntityId( new NumericPropertyId( $parameterId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
105            );
106        }
107    }
108
109    /**
110     * Parse a single entity ID parameter.
111     * @throws ConstraintParameterException
112     */
113    private function parseEntityIdParameter( array $snakSerialization, string $parameterId ): EntityId {
114        $snak = $this->snakDeserializer->deserialize( $snakSerialization );
115        $this->requireValueParameter( $snak, $parameterId );
116        $value = $snak->getDataValue();
117        if ( $value instanceof EntityIdValue ) {
118            return $value->getEntityId();
119        } else {
120            throw new ConstraintParameterException(
121                ( new ViolationMessage( 'wbqc-violation-message-parameter-entity' ) )
122                    ->withEntityId( new NumericPropertyId( $parameterId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
123                    ->withDataValue( $value, Role::CONSTRAINT_PARAMETER_VALUE )
124            );
125        }
126    }
127
128    /**
129     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
130     * @param string $constraintTypeItemId used in error messages
131     * @throws ConstraintParameterException if the parameter is invalid or missing
132     * @return string[] class entity ID serializations
133     */
134    public function parseClassParameter( array $constraintParameters, string $constraintTypeItemId ): array {
135        $this->checkError( $constraintParameters );
136        $classId = $this->config->get( 'WBQualityConstraintsClassId' );
137        if ( !array_key_exists( $classId, $constraintParameters ) ) {
138            throw new ConstraintParameterException(
139                ( new ViolationMessage( 'wbqc-violation-message-parameter-needed' ) )
140                    ->withEntityId( new ItemId( $constraintTypeItemId ), Role::CONSTRAINT_TYPE_ITEM )
141                    ->withEntityId( new NumericPropertyId( $classId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
142            );
143        }
144
145        $classes = [];
146        foreach ( $constraintParameters[$classId] as $class ) {
147            $classes[] = $this->parseEntityIdParameter( $class, $classId )->getSerialization();
148        }
149        return $classes;
150    }
151
152    /**
153     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
154     * @param string $constraintTypeItemId used in error messages
155     * @throws ConstraintParameterException if the parameter is invalid or missing
156     * @return string 'instance', 'subclass', or 'instanceOrSubclass'
157     */
158    public function parseRelationParameter( array $constraintParameters, string $constraintTypeItemId ): string {
159        $this->checkError( $constraintParameters );
160        $relationId = $this->config->get( 'WBQualityConstraintsRelationId' );
161        if ( !array_key_exists( $relationId, $constraintParameters ) ) {
162            throw new ConstraintParameterException(
163                ( new ViolationMessage( 'wbqc-violation-message-parameter-needed' ) )
164                    ->withEntityId( new ItemId( $constraintTypeItemId ), Role::CONSTRAINT_TYPE_ITEM )
165                    ->withEntityId( new NumericPropertyId( $relationId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
166            );
167        }
168
169        $this->requireSingleParameter( $constraintParameters, $relationId );
170        $relationEntityId = $this->parseEntityIdParameter( $constraintParameters[$relationId][0], $relationId );
171        if ( !( $relationEntityId instanceof ItemId ) ) {
172            throw new ConstraintParameterException(
173                ( new ViolationMessage( 'wbqc-violation-message-parameter-item' ) )
174                    ->withEntityId( new NumericPropertyId( $relationId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
175                    ->withDataValue( new EntityIdValue( $relationEntityId ), Role::CONSTRAINT_PARAMETER_VALUE )
176            );
177        }
178        return $this->mapItemId( $relationEntityId, [
179            $this->config->get( 'WBQualityConstraintsInstanceOfRelationId' ) => 'instance',
180            $this->config->get( 'WBQualityConstraintsSubclassOfRelationId' ) => 'subclass',
181            $this->config->get( 'WBQualityConstraintsInstanceOrSubclassOfRelationId' ) => 'instanceOrSubclass',
182        ], $relationId );
183    }
184
185    /**
186     * Parse a single property ID parameter.
187     * @param array $snakSerialization
188     * @param string $parameterId used in error messages
189     * @throws ConstraintParameterException
190     * @return PropertyId
191     */
192    private function parsePropertyIdParameter( array $snakSerialization, string $parameterId ): PropertyId {
193        $snak = $this->snakDeserializer->deserialize( $snakSerialization );
194        $this->requireValueParameter( $snak, $parameterId );
195        $value = $snak->getDataValue();
196        if ( $value instanceof EntityIdValue ) {
197            $id = $value->getEntityId();
198            if ( $id instanceof PropertyId ) {
199                return $id;
200            }
201        }
202        throw new ConstraintParameterException(
203            ( new ViolationMessage( 'wbqc-violation-message-parameter-property' ) )
204                ->withEntityId( new NumericPropertyId( $parameterId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
205                ->withDataValue( $value, Role::CONSTRAINT_PARAMETER_VALUE )
206        );
207    }
208
209    /**
210     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
211     * @param string $constraintTypeItemId used in error messages
212     *
213     * @throws ConstraintParameterException if the parameter is invalid or missing
214     * @return PropertyId
215     */
216    public function parsePropertyParameter( array $constraintParameters, string $constraintTypeItemId ): PropertyId {
217        $this->checkError( $constraintParameters );
218        $propertyId = $this->config->get( 'WBQualityConstraintsPropertyId' );
219        if ( !array_key_exists( $propertyId, $constraintParameters ) ) {
220            throw new ConstraintParameterException(
221                ( new ViolationMessage( 'wbqc-violation-message-parameter-needed' ) )
222                    ->withEntityId( new ItemId( $constraintTypeItemId ), Role::CONSTRAINT_TYPE_ITEM )
223                    ->withEntityId( new NumericPropertyId( $propertyId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
224            );
225        }
226
227        $this->requireSingleParameter( $constraintParameters, $propertyId );
228        return $this->parsePropertyIdParameter( $constraintParameters[$propertyId][0], $propertyId );
229    }
230
231    private function parseItemIdParameter( PropertyValueSnak $snak, string $parameterId ): ItemIdSnakValue {
232        $dataValue = $snak->getDataValue();
233        if ( $dataValue instanceof EntityIdValue ) {
234            $entityId = $dataValue->getEntityId();
235            if ( $entityId instanceof ItemId ) {
236                return ItemIdSnakValue::fromItemId( $entityId );
237            }
238        }
239        throw new ConstraintParameterException(
240            ( new ViolationMessage( 'wbqc-violation-message-parameter-item' ) )
241                ->withEntityId( new NumericPropertyId( $parameterId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
242                ->withDataValue( $dataValue, Role::CONSTRAINT_PARAMETER_VALUE )
243        );
244    }
245
246    /**
247     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
248     * @param string $constraintTypeItemId used in error messages
249     * @param bool $required whether the parameter is required (error if absent) or not ([] if absent)
250     * @param string|null $parameterId the property ID to use, defaults to 'qualifier of property constraint'
251     * @throws ConstraintParameterException if the parameter is invalid or missing
252     * @return ItemIdSnakValue[] array of values
253     */
254    public function parseItemsParameter(
255        array $constraintParameters,
256        string $constraintTypeItemId,
257        bool $required,
258        string $parameterId = null
259    ): array {
260        $this->checkError( $constraintParameters );
261        if ( $parameterId === null ) {
262            $parameterId = $this->config->get( 'WBQualityConstraintsQualifierOfPropertyConstraintId' );
263        }
264        if ( !array_key_exists( $parameterId, $constraintParameters ) ) {
265            if ( $required ) {
266                throw new ConstraintParameterException(
267                    ( new ViolationMessage( 'wbqc-violation-message-parameter-needed' ) )
268                        ->withEntityId( new ItemId( $constraintTypeItemId ), Role::CONSTRAINT_TYPE_ITEM )
269                        ->withEntityId( new NumericPropertyId( $parameterId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
270                );
271            } else {
272                return [];
273            }
274        }
275
276        $values = [];
277        foreach ( $constraintParameters[$parameterId] as $parameter ) {
278            $snak = $this->snakDeserializer->deserialize( $parameter );
279            switch ( true ) {
280                case $snak instanceof PropertyValueSnak:
281                    $values[] = $this->parseItemIdParameter( $snak, $parameterId );
282                    break;
283                case $snak instanceof PropertySomeValueSnak:
284                    $values[] = ItemIdSnakValue::someValue();
285                    break;
286                case $snak instanceof PropertyNoValueSnak:
287                    $values[] = ItemIdSnakValue::noValue();
288                    break;
289            }
290        }
291        return $values;
292    }
293
294    /**
295     * Parse a parameter that must contain item IDs.
296     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
297     * @param string $constraintTypeItemId used in error messages
298     * @param bool $required whether the parameter is required (error if absent) or not ([] if absent)
299     * @param string $parameterId the property ID to use
300     * @throws ConstraintParameterException
301     * @return ItemId[]
302     */
303    private function parseItemIdsParameter(
304        array $constraintParameters,
305        string $constraintTypeItemId,
306        bool $required,
307        string $parameterId
308    ): array {
309        return array_map( static function ( ItemIdSnakValue $value ) use ( $parameterId ): ItemId {
310            if ( $value->isValue() ) {
311                return $value->getItemId();
312            } else {
313                throw new ConstraintParameterException(
314                    ( new ViolationMessage( 'wbqc-violation-message-parameter-value' ) )
315                        ->withEntityId( new NumericPropertyId( $parameterId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
316                );
317            }
318        }, $this->parseItemsParameter(
319            $constraintParameters,
320            $constraintTypeItemId,
321            $required,
322            $parameterId
323        ) );
324    }
325
326    /**
327     * Map an item ID parameter to a well-known value or throw an appropriate error.
328     * @throws ConstraintParameterException
329     * @return mixed elements of $mapping
330     */
331    private function mapItemId( ItemId $itemId, array $mapping, string $parameterId ) {
332        $serialization = $itemId->getSerialization();
333        if ( array_key_exists( $serialization, $mapping ) ) {
334            return $mapping[$serialization];
335        } else {
336            $allowed = array_map( static function ( $id ) {
337                return new ItemId( $id );
338            }, array_keys( $mapping ) );
339            throw new ConstraintParameterException(
340                ( new ViolationMessage( 'wbqc-violation-message-parameter-oneof' ) )
341                    ->withEntityId( new NumericPropertyId( $parameterId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
342                    ->withEntityIdList( $allowed, Role::CONSTRAINT_PARAMETER_VALUE )
343            );
344        }
345    }
346
347    /**
348     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
349     * @param string $constraintTypeItemId used in error messages
350     * @throws ConstraintParameterException if the parameter is invalid or missing
351     * @return PropertyId[]
352     */
353    public function parsePropertiesParameter( array $constraintParameters, string $constraintTypeItemId ): array {
354        $this->checkError( $constraintParameters );
355        $propertyId = $this->config->get( 'WBQualityConstraintsPropertyId' );
356        if ( !array_key_exists( $propertyId, $constraintParameters ) ) {
357            throw new ConstraintParameterException(
358                ( new ViolationMessage( 'wbqc-violation-message-parameter-needed' ) )
359                    ->withEntityId( new ItemId( $constraintTypeItemId ), Role::CONSTRAINT_TYPE_ITEM )
360                    ->withEntityId( new NumericPropertyId( $propertyId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
361            );
362        }
363
364        $parameters = $constraintParameters[$propertyId];
365        if ( count( $parameters ) === 1 &&
366            $this->snakDeserializer->deserialize( $parameters[0] ) instanceof PropertyNoValueSnak
367        ) {
368            return [];
369        }
370
371        $properties = [];
372        foreach ( $parameters as $parameter ) {
373            $properties[] = $this->parsePropertyIdParameter( $parameter, $propertyId );
374        }
375        return $properties;
376    }
377
378    /**
379     * @throws ConstraintParameterException
380     */
381    private function parseValueOrNoValueParameter( array $snakSerialization, string $parameterId ): ?DataValue {
382        $snak = $this->snakDeserializer->deserialize( $snakSerialization );
383        if ( $snak instanceof PropertyValueSnak ) {
384            return $snak->getDataValue();
385        } elseif ( $snak instanceof PropertyNoValueSnak ) {
386            return null;
387        } else {
388            throw new ConstraintParameterException(
389                ( new ViolationMessage( 'wbqc-violation-message-parameter-value-or-novalue' ) )
390                    ->withEntityId( new NumericPropertyId( $parameterId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
391            );
392        }
393    }
394
395    private function parseValueOrNoValueOrNowParameter( array $snakSerialization, string $parameterId ): ?DataValue {
396        try {
397            return $this->parseValueOrNoValueParameter( $snakSerialization, $parameterId );
398        } catch ( ConstraintParameterException $e ) {
399            // unknown value means “now”
400            return new NowValue();
401        }
402    }
403
404    /**
405     * Checks whether there is exactly one non-null quantity with the given unit.
406     */
407    private function exactlyOneQuantityWithUnit( ?DataValue $min, ?DataValue $max, string $unit ): bool {
408        if ( !( $min instanceof UnboundedQuantityValue ) ||
409            !( $max instanceof UnboundedQuantityValue )
410        ) {
411            return false;
412        }
413
414        return ( $min->getUnit() === $unit ) !== ( $max->getUnit() === $unit );
415    }
416
417    /**
418     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
419     * @param string $minimumId
420     * @param string $maximumId
421     * @param string $constraintTypeItemId used in error messages
422     * @param string $type 'quantity' or 'time' (can be data type or data value type)
423     *
424     * @throws ConstraintParameterException if the parameter is invalid or missing
425     * @return DataValue[] if the parameter is invalid or missing
426     */
427    private function parseRangeParameter(
428        array $constraintParameters,
429        string $minimumId,
430        string $maximumId,
431        string $constraintTypeItemId,
432        string $type
433    ): array {
434        $this->checkError( $constraintParameters );
435        if ( !array_key_exists( $minimumId, $constraintParameters ) ||
436            !array_key_exists( $maximumId, $constraintParameters )
437        ) {
438            throw new ConstraintParameterException(
439                ( new ViolationMessage( 'wbqc-violation-message-range-parameters-needed' ) )
440                    ->withDataValueType( $type )
441                    ->withEntityId( new NumericPropertyId( $minimumId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
442                    ->withEntityId( new NumericPropertyId( $maximumId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
443                    ->withEntityId( new ItemId( $constraintTypeItemId ), Role::CONSTRAINT_TYPE_ITEM )
444            );
445        }
446
447        $this->requireSingleParameter( $constraintParameters, $minimumId );
448        $this->requireSingleParameter( $constraintParameters, $maximumId );
449        $parseFunction = $type === 'time' ? 'parseValueOrNoValueOrNowParameter' : 'parseValueOrNoValueParameter';
450        $min = $this->$parseFunction( $constraintParameters[$minimumId][0], $minimumId );
451        $max = $this->$parseFunction( $constraintParameters[$maximumId][0], $maximumId );
452
453        $yearUnit = $this->config->get( 'WBQualityConstraintsYearUnit' );
454        if ( $this->exactlyOneQuantityWithUnit( $min, $max, $yearUnit ) ) {
455            throw new ConstraintParameterException(
456                new ViolationMessage( 'wbqc-violation-message-range-parameters-one-year' )
457            );
458        }
459        if ( ( $min === null && $max === null ) ||
460            ( $min !== null && $max !== null && $min->equals( $max ) )
461        ) {
462            throw new ConstraintParameterException(
463                ( new ViolationMessage( 'wbqc-violation-message-range-parameters-same' ) )
464                    ->withEntityId( new NumericPropertyId( $minimumId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
465                    ->withEntityId( new NumericPropertyId( $maximumId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
466            );
467        }
468
469        return [ $min, $max ];
470    }
471
472    /**
473     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
474     * @param string $constraintTypeItemId used in error messages
475     *
476     * @throws ConstraintParameterException if the parameter is invalid or missing
477     * @return DataValue[] a pair of two data values, either of which may be null to signify an open range
478     */
479    public function parseQuantityRangeParameter( array $constraintParameters, string $constraintTypeItemId ): array {
480        return $this->parseRangeParameter(
481            $constraintParameters,
482            $this->config->get( 'WBQualityConstraintsMinimumQuantityId' ),
483            $this->config->get( 'WBQualityConstraintsMaximumQuantityId' ),
484            $constraintTypeItemId,
485            'quantity'
486        );
487    }
488
489    /**
490     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
491     * @param string $constraintTypeItemId used in error messages
492     *
493     * @throws ConstraintParameterException if the parameter is invalid or missing
494     * @return DataValue[] a pair of two data values, either of which may be null to signify an open range
495     */
496    public function parseTimeRangeParameter( array $constraintParameters, string $constraintTypeItemId ): array {
497        return $this->parseRangeParameter(
498            $constraintParameters,
499            $this->config->get( 'WBQualityConstraintsMinimumDateId' ),
500            $this->config->get( 'WBQualityConstraintsMaximumDateId' ),
501            $constraintTypeItemId,
502            'time'
503        );
504    }
505
506    /**
507     * Parse language parameter.
508     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
509     * @param string $constraintTypeItemId used in error messages
510     * @throws ConstraintParameterException
511     * @return string[]
512     */
513    public function parseLanguageParameter( array $constraintParameters, string $constraintTypeItemId ): array {
514        $this->checkError( $constraintParameters );
515        $languagePropertyId = $this->config->get( 'WBQualityConstraintsLanguagePropertyId' );
516        if ( !array_key_exists( $languagePropertyId, $constraintParameters ) ) {
517            throw new ConstraintParameterException(
518                ( new ViolationMessage( 'wbqc-violation-message-parameter-needed' ) )
519                    ->withEntityId( new ItemId( $constraintTypeItemId ), Role::CONSTRAINT_TYPE_ITEM )
520                    ->withEntityId( new NumericPropertyId( $languagePropertyId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
521            );
522        }
523
524        $languages = [];
525        foreach ( $constraintParameters[$languagePropertyId] as $snak ) {
526            $languages[] = $this->parseStringParameter( $snak, $languagePropertyId );
527        }
528        return $languages;
529    }
530
531    /**
532     * Parse a single string parameter.
533     * @throws ConstraintParameterException
534     */
535    private function parseStringParameter( array $snakSerialization, string $parameterId ): string {
536        $snak = $this->snakDeserializer->deserialize( $snakSerialization );
537        $this->requireValueParameter( $snak, $parameterId );
538        $value = $snak->getDataValue();
539        if ( $value instanceof StringValue ) {
540            return $value->getValue();
541        } else {
542            throw new ConstraintParameterException(
543                ( new ViolationMessage( 'wbqc-violation-message-parameter-string' ) )
544                    ->withEntityId( new NumericPropertyId( $parameterId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
545                    ->withDataValue( $value, Role::CONSTRAINT_PARAMETER_VALUE )
546            );
547        }
548    }
549
550    /**
551     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
552     * @param string $constraintTypeItemId used in error messages
553     * @throws ConstraintParameterException if the parameter is invalid or missing
554     * @return string
555     */
556    public function parseNamespaceParameter( array $constraintParameters, string $constraintTypeItemId ): string {
557        $this->checkError( $constraintParameters );
558        $namespaceId = $this->config->get( 'WBQualityConstraintsNamespaceId' );
559        if ( !array_key_exists( $namespaceId, $constraintParameters ) ) {
560            return '';
561        }
562
563        $this->requireSingleParameter( $constraintParameters, $namespaceId );
564        return $this->parseStringParameter( $constraintParameters[$namespaceId][0], $namespaceId );
565    }
566
567    /**
568     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
569     * @param string $constraintTypeItemId used in error messages
570     * @throws ConstraintParameterException if the parameter is invalid or missing
571     * @return string
572     */
573    public function parseFormatParameter( array $constraintParameters, string $constraintTypeItemId ): string {
574        $this->checkError( $constraintParameters );
575        $formatId = $this->config->get( 'WBQualityConstraintsFormatAsARegularExpressionId' );
576        if ( !array_key_exists( $formatId, $constraintParameters ) ) {
577            throw new ConstraintParameterException(
578                ( new ViolationMessage( 'wbqc-violation-message-parameter-needed' ) )
579                    ->withEntityId( new ItemId( $constraintTypeItemId ), Role::CONSTRAINT_TYPE_ITEM )
580                    ->withEntityId( new NumericPropertyId( $formatId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
581            );
582        }
583
584        $this->requireSingleParameter( $constraintParameters, $formatId );
585        return $this->parseStringParameter( $constraintParameters[$formatId][0], $formatId );
586    }
587
588    /**
589     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
590     * @throws ConstraintParameterException if the parameter is invalid
591     * @return EntityId[]
592     */
593    public function parseExceptionParameter( array $constraintParameters ): array {
594        $this->checkError( $constraintParameters );
595        $exceptionId = $this->config->get( 'WBQualityConstraintsExceptionToConstraintId' );
596        if ( !array_key_exists( $exceptionId, $constraintParameters ) ) {
597            return [];
598        }
599
600        return array_map(
601            function ( $snakSerialization ) use ( $exceptionId ) {
602                return $this->parseEntityIdParameter( $snakSerialization, $exceptionId );
603            },
604            $constraintParameters[$exceptionId]
605        );
606    }
607
608    /**
609     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
610     * @throws ConstraintParameterException if the parameter is invalid
611     * @return string|null 'mandatory', 'suggestion' or null
612     */
613    public function parseConstraintStatusParameter( array $constraintParameters ): ?string {
614        $this->checkError( $constraintParameters );
615        $constraintStatusId = $this->config->get( 'WBQualityConstraintsConstraintStatusId' );
616        if ( !array_key_exists( $constraintStatusId, $constraintParameters ) ) {
617            return null;
618        }
619
620        $mandatoryId = $this->config->get( 'WBQualityConstraintsMandatoryConstraintId' );
621        $supportedStatuses = [ new ItemId( $mandatoryId ) ];
622        if ( $this->config->get( 'WBQualityConstraintsEnableSuggestionConstraintStatus' ) ) {
623            $suggestionId = $this->config->get( 'WBQualityConstraintsSuggestionConstraintId' );
624            $supportedStatuses[] = new ItemId( $suggestionId );
625        } else {
626            $suggestionId = null;
627        }
628
629        $this->requireSingleParameter( $constraintParameters, $constraintStatusId );
630        $snak = $this->snakDeserializer->deserialize( $constraintParameters[$constraintStatusId][0] );
631        $this->requireValueParameter( $snak, $constraintStatusId );
632        '@phan-var \Wikibase\DataModel\Snak\PropertyValueSnak $snak';
633        $dataValue = $snak->getDataValue();
634        '@phan-var EntityIdValue $dataValue';
635        $entityId = $dataValue->getEntityId();
636        $statusId = $entityId->getSerialization();
637
638        if ( $statusId === $mandatoryId ) {
639            return 'mandatory';
640        } elseif ( $statusId === $suggestionId ) {
641            return 'suggestion';
642        } else {
643            throw new ConstraintParameterException(
644                ( new ViolationMessage( 'wbqc-violation-message-parameter-oneof' ) )
645                    ->withEntityId( new NumericPropertyId( $constraintStatusId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
646                    ->withEntityIdList(
647                        $supportedStatuses,
648                        Role::CONSTRAINT_PARAMETER_VALUE
649                    )
650            );
651        }
652    }
653
654    /**
655     * Require that $dataValue is a {@link MonolingualTextValue}.
656     * @throws ConstraintParameterException
657     */
658    private function requireMonolingualTextParameter( DataValue $dataValue, string $parameterId ): void {
659        if ( !( $dataValue instanceof MonolingualTextValue ) ) {
660            throw new ConstraintParameterException(
661                ( new ViolationMessage( 'wbqc-violation-message-parameter-monolingualtext' ) )
662                    ->withEntityId( new NumericPropertyId( $parameterId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
663                    ->withDataValue( $dataValue, Role::CONSTRAINT_PARAMETER_VALUE )
664            );
665        }
666    }
667
668    /**
669     * Parse a series of monolingual text snaks (serialized) into a map from language code to string.
670     *
671     * @throws ConstraintParameterException if invalid snaks are found or a language has multiple texts
672     */
673    private function parseMultilingualTextParameter( array $snakSerializations, string $parameterId ): MultilingualTextValue {
674        $result = [];
675
676        foreach ( $snakSerializations as $snakSerialization ) {
677            $snak = $this->snakDeserializer->deserialize( $snakSerialization );
678            $this->requireValueParameter( $snak, $parameterId );
679
680            $value = $snak->getDataValue();
681            $this->requireMonolingualTextParameter( $value, $parameterId );
682            /** @var MonolingualTextValue $value */
683            '@phan-var MonolingualTextValue $value';
684
685            $code = $value->getLanguageCode();
686            if ( array_key_exists( $code, $result ) ) {
687                throw new ConstraintParameterException(
688                    ( new ViolationMessage( 'wbqc-violation-message-parameter-single-per-language' ) )
689                        ->withEntityId( new NumericPropertyId( $parameterId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
690                        ->withLanguage( $code )
691                );
692            }
693
694            $result[$code] = $value;
695        }
696
697        return new MultilingualTextValue( $result );
698    }
699
700    /**
701     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
702     * @throws ConstraintParameterException if the parameter is invalid
703     * @return MultilingualTextValue
704     */
705    public function parseSyntaxClarificationParameter( array $constraintParameters ): MultilingualTextValue {
706        $syntaxClarificationId = $this->config->get( 'WBQualityConstraintsSyntaxClarificationId' );
707
708        if ( !array_key_exists( $syntaxClarificationId, $constraintParameters ) ) {
709            return new MultilingualTextValue( [] );
710        }
711
712        $syntaxClarifications = $this->parseMultilingualTextParameter(
713            $constraintParameters[$syntaxClarificationId],
714            $syntaxClarificationId
715        );
716
717        return $syntaxClarifications;
718    }
719
720    /**
721     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
722     * @throws ConstraintParameterException if the parameter is invalid
723     * @return MultilingualTextValue
724     */
725    public function parseConstraintClarificationParameter( array $constraintParameters ): MultilingualTextValue {
726        $constraintClarificationId = $this->config->get( 'WBQualityConstraintsConstraintClarificationId' );
727
728        if ( !array_key_exists( $constraintClarificationId, $constraintParameters ) ) {
729            return new MultilingualTextValue( [] );
730        }
731
732        $constraintClarifications = $this->parseMultilingualTextParameter(
733            $constraintParameters[$constraintClarificationId],
734            $constraintClarificationId
735        );
736
737        return $constraintClarifications;
738    }
739
740    /**
741     * Parse the constraint scope parameters:
742     * the context types and entity types where the constraint should be checked.
743     * Depending on configuration, this may be the same property ID or two different ones.
744     *
745     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
746     * @param string $constraintTypeItemId used in error messages
747     * @param string[] $validContextTypes a list of Context::TYPE_* constants which are valid for this constraint type.
748     * If one of the specified scopes is not in this list, a ConstraintParameterException is thrown.
749     * @param string[] $validEntityTypes a list of entity types which are valid for this constraint type.
750     * If one of the specified entity types is not in this list, a ConstraintParameterException is thrown.
751     * @throws ConstraintParameterException
752     * @return array [ string[]|null $contextTypes, string[]|null $entityTypes ]
753     * the context types and entity types in the parameters (each may be null if not specified)
754     * @suppress PhanTypeArraySuspicious
755     */
756    public function parseConstraintScopeParameters(
757        array $constraintParameters,
758        string $constraintTypeItemId,
759        array $validContextTypes,
760        array $validEntityTypes
761    ): array {
762        $contextTypeParameterId = $this->config->get( 'WBQualityConstraintsConstraintScopeId' );
763        $contextTypeItemIds = $this->parseItemIdsParameter(
764            $constraintParameters,
765            $constraintTypeItemId,
766            false,
767            $contextTypeParameterId
768        );
769        $entityTypeParameterId = $this->config->get( 'WBQualityConstraintsConstraintEntityTypesId' );
770        $entityTypeItemIds = $this->parseItemIdsParameter(
771            $constraintParameters,
772            $constraintTypeItemId,
773            false,
774            $entityTypeParameterId
775        );
776
777        $contextTypeMapping = $this->getConstraintScopeContextTypeMapping();
778        $entityTypeMapping = $this->getEntityTypeMapping();
779
780        // these nulls will turn into arrays the first time $contextTypes[] or $entityTypes[] is reached,
781        // so they’ll be returned as null iff the parameter was not specified
782        $contextTypes = null;
783        $entityTypes = null;
784
785        if ( $contextTypeParameterId === $entityTypeParameterId ) {
786            $itemIds = $contextTypeItemIds;
787            $mapping = $contextTypeMapping + $entityTypeMapping;
788            foreach ( $itemIds as $itemId ) {
789                $mapped = $this->mapItemId( $itemId, $mapping, $contextTypeParameterId );
790                if ( in_array( $mapped, $contextTypeMapping, true ) ) {
791                    $contextTypes[] = $mapped;
792                } else {
793                    $entityTypes[] = $mapped;
794                }
795            }
796        } else {
797            foreach ( $contextTypeItemIds as $contextTypeItemId ) {
798                $contextTypes[] = $this->mapItemId(
799                    $contextTypeItemId,
800                    $contextTypeMapping,
801                    $contextTypeParameterId
802                );
803            }
804            foreach ( $entityTypeItemIds as $entityTypeItemId ) {
805                $entityTypes[] = $this->mapItemId(
806                    $entityTypeItemId,
807                    $entityTypeMapping,
808                    $entityTypeParameterId
809                );
810            }
811        }
812
813        $this->checkValidScope( $constraintTypeItemId, $contextTypes, $validContextTypes );
814        $this->checkValidScope( $constraintTypeItemId, $entityTypes, $validEntityTypes );
815
816        return [ $contextTypes, $entityTypes ];
817    }
818
819    private function checkValidScope( string $constraintTypeItemId, ?array $types, array $validTypes ): void {
820        $invalidTypes = array_diff( $types ?: [], $validTypes );
821        if ( $invalidTypes !== [] ) {
822            $invalidType = array_pop( $invalidTypes );
823            throw new ConstraintParameterException(
824                ( new ViolationMessage( 'wbqc-violation-message-invalid-scope' ) )
825                    ->withConstraintScope( $invalidType, Role::CONSTRAINT_PARAMETER_VALUE )
826                    ->withEntityId( new ItemId( $constraintTypeItemId ), Role::CONSTRAINT_TYPE_ITEM )
827                    ->withConstraintScopeList( $validTypes, Role::CONSTRAINT_PARAMETER_VALUE )
828            );
829        }
830    }
831
832    /**
833     * Turn an item ID into a full unit string (using the concept URI).
834     */
835    private function parseUnitParameter( ItemId $unitId ): string {
836        return $this->unitItemConceptBaseUri . $unitId->getSerialization();
837    }
838
839    /**
840     * Turn an ItemIdSnakValue into a single unit parameter.
841     *
842     * @throws ConstraintParameterException
843     */
844    private function parseUnitItem( ItemIdSnakValue $item ): UnitsParameter {
845        switch ( true ) {
846            case $item->isValue():
847                $unit = $this->parseUnitParameter( $item->getItemId() );
848                return new UnitsParameter(
849                    [ $item->getItemId() ],
850                    [ UnboundedQuantityValue::newFromNumber( 1, $unit ) ],
851                    false
852                );
853            case $item->isSomeValue():
854                $qualifierId = $this->config->get( 'WBQualityConstraintsQualifierOfPropertyConstraintId' );
855                throw new ConstraintParameterException(
856                    ( new ViolationMessage( 'wbqc-violation-message-parameter-value-or-novalue' ) )
857                        ->withEntityId( new NumericPropertyId( $qualifierId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
858                );
859            case $item->isNoValue():
860                return new UnitsParameter( [], [], true );
861        }
862    }
863
864    /**
865     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
866     * @param string $constraintTypeItemId used in error messages
867     * @throws ConstraintParameterException if the parameter is invalid or missing
868     * @return UnitsParameter
869     */
870    public function parseUnitsParameter( array $constraintParameters, string $constraintTypeItemId ): UnitsParameter {
871        $items = $this->parseItemsParameter( $constraintParameters, $constraintTypeItemId, true );
872        $unitItems = [];
873        $unitQuantities = [];
874        $unitlessAllowed = false;
875
876        foreach ( $items as $item ) {
877            $unit = $this->parseUnitItem( $item );
878            $unitItems = array_merge( $unitItems, $unit->getUnitItemIds() );
879            $unitQuantities = array_merge( $unitQuantities, $unit->getUnitQuantities() );
880            $unitlessAllowed = $unitlessAllowed || $unit->getUnitlessAllowed();
881        }
882
883        if ( $unitQuantities === [] && !$unitlessAllowed ) {
884            throw new LogicException(
885                'The "units" parameter is required, and yet we seem to be missing any allowed unit'
886            );
887        }
888
889        return new UnitsParameter( $unitItems, $unitQuantities, $unitlessAllowed );
890    }
891
892    private function getEntityTypeMapping(): array {
893        return [
894            $this->config->get( 'WBQualityConstraintsWikibaseItemId' ) => 'item',
895            $this->config->get( 'WBQualityConstraintsWikibasePropertyId' ) => 'property',
896            $this->config->get( 'WBQualityConstraintsWikibaseLexemeId' ) => 'lexeme',
897            $this->config->get( 'WBQualityConstraintsWikibaseFormId' ) => 'form',
898            $this->config->get( 'WBQualityConstraintsWikibaseSenseId' ) => 'sense',
899            $this->config->get( 'WBQualityConstraintsWikibaseMediaInfoId' ) => 'mediainfo',
900        ];
901    }
902
903    /**
904     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
905     * @param string $constraintTypeItemId used in error messages
906     * @throws ConstraintParameterException if the parameter is invalid or missing
907     * @return EntityTypesParameter
908     */
909    public function parseEntityTypesParameter( array $constraintParameters, string $constraintTypeItemId ): EntityTypesParameter {
910        $entityTypes = [];
911        $entityTypeItemIds = [];
912        $parameterId = $this->config->get( 'WBQualityConstraintsQualifierOfPropertyConstraintId' );
913        $itemIds = $this->parseItemIdsParameter(
914            $constraintParameters,
915            $constraintTypeItemId,
916            true,
917            $parameterId
918        );
919
920        $mapping = $this->getEntityTypeMapping();
921        foreach ( $itemIds as $itemId ) {
922            $entityType = $this->mapItemId( $itemId, $mapping, $parameterId );
923            $entityTypes[] = $entityType;
924            $entityTypeItemIds[] = $itemId;
925        }
926
927        if ( $entityTypes === [] ) {
928            // @codeCoverageIgnoreStart
929            throw new LogicException(
930                'The "entity types" parameter is required, ' .
931                'and yet we seem to be missing any allowed entity type'
932            );
933            // @codeCoverageIgnoreEnd
934        }
935
936        return new EntityTypesParameter( $entityTypes, $entityTypeItemIds );
937    }
938
939    /**
940     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
941     * @throws ConstraintParameterException if the parameter is invalid
942     * @return PropertyId[]
943     */
944    public function parseSeparatorsParameter( array $constraintParameters ): array {
945        $separatorId = $this->config->get( 'WBQualityConstraintsSeparatorId' );
946
947        if ( !array_key_exists( $separatorId, $constraintParameters ) ) {
948            return [];
949        }
950
951        $parameters = $constraintParameters[$separatorId];
952        $separators = [];
953
954        foreach ( $parameters as $parameter ) {
955            $separators[] = $this->parsePropertyIdParameter( $parameter, $separatorId );
956        }
957
958        return $separators;
959    }
960
961    private function getConstraintScopeContextTypeMapping(): array {
962        return [
963            $this->config->get( 'WBQualityConstraintsConstraintCheckedOnMainValueId' ) => Context::TYPE_STATEMENT,
964            $this->config->get( 'WBQualityConstraintsConstraintCheckedOnQualifiersId' ) => Context::TYPE_QUALIFIER,
965            $this->config->get( 'WBQualityConstraintsConstraintCheckedOnReferencesId' ) => Context::TYPE_REFERENCE,
966        ];
967    }
968
969    private function getPropertyScopeContextTypeMapping(): array {
970        return [
971            $this->config->get( 'WBQualityConstraintsAsMainValueId' ) => Context::TYPE_STATEMENT,
972            $this->config->get( 'WBQualityConstraintsAsQualifiersId' ) => Context::TYPE_QUALIFIER,
973            $this->config->get( 'WBQualityConstraintsAsReferencesId' ) => Context::TYPE_REFERENCE,
974        ];
975    }
976
977    /**
978     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
979     * @param string $constraintTypeItemId used in error messages
980     * @throws ConstraintParameterException if the parameter is invalid or missing
981     * @return string[] list of Context::TYPE_* constants
982     */
983    public function parsePropertyScopeParameter( array $constraintParameters, string $constraintTypeItemId ): array {
984        $contextTypes = [];
985        $parameterId = $this->config->get( 'WBQualityConstraintsPropertyScopeId' );
986        $itemIds = $this->parseItemIdsParameter(
987            $constraintParameters,
988            $constraintTypeItemId,
989            true,
990            $parameterId
991        );
992
993        $mapping = $this->getPropertyScopeContextTypeMapping();
994        foreach ( $itemIds as $itemId ) {
995            $contextTypes[] = $this->mapItemId( $itemId, $mapping, $parameterId );
996        }
997
998        if ( $contextTypes === [] ) {
999            // @codeCoverageIgnoreStart
1000            throw new LogicException(
1001                'The "property scope" parameter is required, ' .
1002                'and yet we seem to be missing any allowed scope'
1003            );
1004            // @codeCoverageIgnoreEnd
1005        }
1006
1007        return $contextTypes;
1008    }
1009
1010}