Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.74% covered (success)
93.74%
434 / 463
87.80% covered (warning)
87.80%
36 / 41
CRAP
0.00% covered (danger)
0.00%
0 / 1
ConstraintParameterParser
93.74% covered (success)
93.74%
434 / 463
87.80% covered (warning)
87.80%
36 / 41
120.36
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%
23 / 23
100.00% covered (success)
100.00%
1 / 1
7
 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        $parameterId ??= $this->config->get( 'WBQualityConstraintsQualifierOfPropertyConstraintId' );
262        if ( !array_key_exists( $parameterId, $constraintParameters ) ) {
263            if ( $required ) {
264                throw new ConstraintParameterException(
265                    ( new ViolationMessage( 'wbqc-violation-message-parameter-needed' ) )
266                        ->withEntityId( new ItemId( $constraintTypeItemId ), Role::CONSTRAINT_TYPE_ITEM )
267                        ->withEntityId( new NumericPropertyId( $parameterId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
268                );
269            } else {
270                return [];
271            }
272        }
273
274        $values = [];
275        foreach ( $constraintParameters[$parameterId] as $parameter ) {
276            $snak = $this->snakDeserializer->deserialize( $parameter );
277            switch ( true ) {
278                case $snak instanceof PropertyValueSnak:
279                    $values[] = $this->parseItemIdParameter( $snak, $parameterId );
280                    break;
281                case $snak instanceof PropertySomeValueSnak:
282                    $values[] = ItemIdSnakValue::someValue();
283                    break;
284                case $snak instanceof PropertyNoValueSnak:
285                    $values[] = ItemIdSnakValue::noValue();
286                    break;
287            }
288        }
289        return $values;
290    }
291
292    /**
293     * Parse a parameter that must contain item IDs.
294     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
295     * @param string $constraintTypeItemId used in error messages
296     * @param bool $required whether the parameter is required (error if absent) or not ([] if absent)
297     * @param string $parameterId the property ID to use
298     * @throws ConstraintParameterException
299     * @return ItemId[]
300     */
301    private function parseItemIdsParameter(
302        array $constraintParameters,
303        string $constraintTypeItemId,
304        bool $required,
305        string $parameterId
306    ): array {
307        return array_map( static function ( ItemIdSnakValue $value ) use ( $parameterId ): ItemId {
308            if ( $value->isValue() ) {
309                return $value->getItemId();
310            } else {
311                throw new ConstraintParameterException(
312                    ( new ViolationMessage( 'wbqc-violation-message-parameter-value' ) )
313                        ->withEntityId( new NumericPropertyId( $parameterId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
314                );
315            }
316        }, $this->parseItemsParameter(
317            $constraintParameters,
318            $constraintTypeItemId,
319            $required,
320            $parameterId
321        ) );
322    }
323
324    /**
325     * Map an item ID parameter to a well-known value or throw an appropriate error.
326     * @throws ConstraintParameterException
327     * @return mixed elements of $mapping
328     */
329    private function mapItemId( ItemId $itemId, array $mapping, string $parameterId ) {
330        $serialization = $itemId->getSerialization();
331        if ( array_key_exists( $serialization, $mapping ) ) {
332            return $mapping[$serialization];
333        } else {
334            $allowed = array_map( static function ( $id ) {
335                return new ItemId( $id );
336            }, array_keys( $mapping ) );
337            throw new ConstraintParameterException(
338                ( new ViolationMessage( 'wbqc-violation-message-parameter-oneof' ) )
339                    ->withEntityId( new NumericPropertyId( $parameterId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
340                    ->withEntityIdList( $allowed, Role::CONSTRAINT_PARAMETER_VALUE )
341            );
342        }
343    }
344
345    /**
346     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
347     * @param string $constraintTypeItemId used in error messages
348     * @throws ConstraintParameterException if the parameter is invalid or missing
349     * @return PropertyId[]
350     */
351    public function parsePropertiesParameter( array $constraintParameters, string $constraintTypeItemId ): array {
352        $this->checkError( $constraintParameters );
353        $propertyId = $this->config->get( 'WBQualityConstraintsPropertyId' );
354        if ( !array_key_exists( $propertyId, $constraintParameters ) ) {
355            throw new ConstraintParameterException(
356                ( new ViolationMessage( 'wbqc-violation-message-parameter-needed' ) )
357                    ->withEntityId( new ItemId( $constraintTypeItemId ), Role::CONSTRAINT_TYPE_ITEM )
358                    ->withEntityId( new NumericPropertyId( $propertyId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
359            );
360        }
361
362        $parameters = $constraintParameters[$propertyId];
363        if ( count( $parameters ) === 1 &&
364            $this->snakDeserializer->deserialize( $parameters[0] ) instanceof PropertyNoValueSnak
365        ) {
366            return [];
367        }
368
369        $properties = [];
370        foreach ( $parameters as $parameter ) {
371            $properties[] = $this->parsePropertyIdParameter( $parameter, $propertyId );
372        }
373        return $properties;
374    }
375
376    /**
377     * @throws ConstraintParameterException
378     */
379    private function parseValueOrNoValueParameter( array $snakSerialization, string $parameterId ): ?DataValue {
380        $snak = $this->snakDeserializer->deserialize( $snakSerialization );
381        if ( $snak instanceof PropertyValueSnak ) {
382            return $snak->getDataValue();
383        } elseif ( $snak instanceof PropertyNoValueSnak ) {
384            return null;
385        } else {
386            throw new ConstraintParameterException(
387                ( new ViolationMessage( 'wbqc-violation-message-parameter-value-or-novalue' ) )
388                    ->withEntityId( new NumericPropertyId( $parameterId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
389            );
390        }
391    }
392
393    private function parseValueOrNoValueOrNowParameter( array $snakSerialization, string $parameterId ): ?DataValue {
394        try {
395            return $this->parseValueOrNoValueParameter( $snakSerialization, $parameterId );
396        } catch ( ConstraintParameterException $e ) {
397            // unknown value means “now”
398            return new NowValue();
399        }
400    }
401
402    /**
403     * Checks whether there is exactly one non-null quantity with the given unit.
404     */
405    private function exactlyOneQuantityWithUnit( ?DataValue $min, ?DataValue $max, string $unit ): bool {
406        if ( !( $min instanceof UnboundedQuantityValue ) ||
407            !( $max instanceof UnboundedQuantityValue )
408        ) {
409            return false;
410        }
411
412        return ( $min->getUnit() === $unit ) !== ( $max->getUnit() === $unit );
413    }
414
415    /**
416     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
417     * @param string $minimumId
418     * @param string $maximumId
419     * @param string $constraintTypeItemId used in error messages
420     * @param string $type 'quantity' or 'time' (can be data type or data value type)
421     *
422     * @throws ConstraintParameterException if the parameter is invalid or missing
423     * @return DataValue[] if the parameter is invalid or missing
424     */
425    private function parseRangeParameter(
426        array $constraintParameters,
427        string $minimumId,
428        string $maximumId,
429        string $constraintTypeItemId,
430        string $type
431    ): array {
432        $this->checkError( $constraintParameters );
433        if ( !array_key_exists( $minimumId, $constraintParameters ) ||
434            !array_key_exists( $maximumId, $constraintParameters )
435        ) {
436            throw new ConstraintParameterException(
437                ( new ViolationMessage( 'wbqc-violation-message-range-parameters-needed' ) )
438                    ->withDataValueType( $type )
439                    ->withEntityId( new NumericPropertyId( $minimumId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
440                    ->withEntityId( new NumericPropertyId( $maximumId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
441                    ->withEntityId( new ItemId( $constraintTypeItemId ), Role::CONSTRAINT_TYPE_ITEM )
442            );
443        }
444
445        $this->requireSingleParameter( $constraintParameters, $minimumId );
446        $this->requireSingleParameter( $constraintParameters, $maximumId );
447        $parseFunction = $type === 'time' ? 'parseValueOrNoValueOrNowParameter' : 'parseValueOrNoValueParameter';
448        $min = $this->$parseFunction( $constraintParameters[$minimumId][0], $minimumId );
449        $max = $this->$parseFunction( $constraintParameters[$maximumId][0], $maximumId );
450
451        $yearUnit = $this->config->get( 'WBQualityConstraintsYearUnit' );
452        if ( $this->exactlyOneQuantityWithUnit( $min, $max, $yearUnit ) ) {
453            throw new ConstraintParameterException(
454                new ViolationMessage( 'wbqc-violation-message-range-parameters-one-year' )
455            );
456        }
457        if ( ( $min === null && $max === null ) ||
458            ( $min !== null && $max !== null && $min->equals( $max ) )
459        ) {
460            throw new ConstraintParameterException(
461                ( new ViolationMessage( 'wbqc-violation-message-range-parameters-same' ) )
462                    ->withEntityId( new NumericPropertyId( $minimumId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
463                    ->withEntityId( new NumericPropertyId( $maximumId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
464            );
465        }
466
467        return [ $min, $max ];
468    }
469
470    /**
471     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
472     * @param string $constraintTypeItemId used in error messages
473     *
474     * @throws ConstraintParameterException if the parameter is invalid or missing
475     * @return DataValue[] a pair of two data values, either of which may be null to signify an open range
476     */
477    public function parseQuantityRangeParameter( array $constraintParameters, string $constraintTypeItemId ): array {
478        return $this->parseRangeParameter(
479            $constraintParameters,
480            $this->config->get( 'WBQualityConstraintsMinimumQuantityId' ),
481            $this->config->get( 'WBQualityConstraintsMaximumQuantityId' ),
482            $constraintTypeItemId,
483            'quantity'
484        );
485    }
486
487    /**
488     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
489     * @param string $constraintTypeItemId used in error messages
490     *
491     * @throws ConstraintParameterException if the parameter is invalid or missing
492     * @return DataValue[] a pair of two data values, either of which may be null to signify an open range
493     */
494    public function parseTimeRangeParameter( array $constraintParameters, string $constraintTypeItemId ): array {
495        return $this->parseRangeParameter(
496            $constraintParameters,
497            $this->config->get( 'WBQualityConstraintsMinimumDateId' ),
498            $this->config->get( 'WBQualityConstraintsMaximumDateId' ),
499            $constraintTypeItemId,
500            'time'
501        );
502    }
503
504    /**
505     * Parse language parameter.
506     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
507     * @param string $constraintTypeItemId used in error messages
508     * @throws ConstraintParameterException
509     * @return string[]
510     */
511    public function parseLanguageParameter( array $constraintParameters, string $constraintTypeItemId ): array {
512        $this->checkError( $constraintParameters );
513        $languagePropertyId = $this->config->get( 'WBQualityConstraintsLanguagePropertyId' );
514        if ( !array_key_exists( $languagePropertyId, $constraintParameters ) ) {
515            throw new ConstraintParameterException(
516                ( new ViolationMessage( 'wbqc-violation-message-parameter-needed' ) )
517                    ->withEntityId( new ItemId( $constraintTypeItemId ), Role::CONSTRAINT_TYPE_ITEM )
518                    ->withEntityId( new NumericPropertyId( $languagePropertyId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
519            );
520        }
521
522        $languages = [];
523        foreach ( $constraintParameters[$languagePropertyId] as $snak ) {
524            $languages[] = $this->parseStringParameter( $snak, $languagePropertyId );
525        }
526        return $languages;
527    }
528
529    /**
530     * Parse a single string parameter.
531     * @throws ConstraintParameterException
532     */
533    private function parseStringParameter( array $snakSerialization, string $parameterId ): string {
534        $snak = $this->snakDeserializer->deserialize( $snakSerialization );
535        $this->requireValueParameter( $snak, $parameterId );
536        $value = $snak->getDataValue();
537        if ( $value instanceof StringValue ) {
538            return $value->getValue();
539        } else {
540            throw new ConstraintParameterException(
541                ( new ViolationMessage( 'wbqc-violation-message-parameter-string' ) )
542                    ->withEntityId( new NumericPropertyId( $parameterId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
543                    ->withDataValue( $value, Role::CONSTRAINT_PARAMETER_VALUE )
544            );
545        }
546    }
547
548    /**
549     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
550     * @param string $constraintTypeItemId used in error messages
551     * @throws ConstraintParameterException if the parameter is invalid or missing
552     * @return string
553     */
554    public function parseNamespaceParameter( array $constraintParameters, string $constraintTypeItemId ): string {
555        $this->checkError( $constraintParameters );
556        $namespaceId = $this->config->get( 'WBQualityConstraintsNamespaceId' );
557        if ( !array_key_exists( $namespaceId, $constraintParameters ) ) {
558            return '';
559        }
560
561        $this->requireSingleParameter( $constraintParameters, $namespaceId );
562        return $this->parseStringParameter( $constraintParameters[$namespaceId][0], $namespaceId );
563    }
564
565    /**
566     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
567     * @param string $constraintTypeItemId used in error messages
568     * @throws ConstraintParameterException if the parameter is invalid or missing
569     * @return string
570     */
571    public function parseFormatParameter( array $constraintParameters, string $constraintTypeItemId ): string {
572        $this->checkError( $constraintParameters );
573        $formatId = $this->config->get( 'WBQualityConstraintsFormatAsARegularExpressionId' );
574        if ( !array_key_exists( $formatId, $constraintParameters ) ) {
575            throw new ConstraintParameterException(
576                ( new ViolationMessage( 'wbqc-violation-message-parameter-needed' ) )
577                    ->withEntityId( new ItemId( $constraintTypeItemId ), Role::CONSTRAINT_TYPE_ITEM )
578                    ->withEntityId( new NumericPropertyId( $formatId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
579            );
580        }
581
582        $this->requireSingleParameter( $constraintParameters, $formatId );
583        return $this->parseStringParameter( $constraintParameters[$formatId][0], $formatId );
584    }
585
586    /**
587     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
588     * @throws ConstraintParameterException if the parameter is invalid
589     * @return EntityId[]
590     */
591    public function parseExceptionParameter( array $constraintParameters ): array {
592        $this->checkError( $constraintParameters );
593        $exceptionId = $this->config->get( 'WBQualityConstraintsExceptionToConstraintId' );
594        if ( !array_key_exists( $exceptionId, $constraintParameters ) ) {
595            return [];
596        }
597
598        return array_map(
599            function ( $snakSerialization ) use ( $exceptionId ) {
600                return $this->parseEntityIdParameter( $snakSerialization, $exceptionId );
601            },
602            $constraintParameters[$exceptionId]
603        );
604    }
605
606    /**
607     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
608     * @throws ConstraintParameterException if the parameter is invalid
609     * @return string|null 'mandatory', 'suggestion' or null
610     */
611    public function parseConstraintStatusParameter( array $constraintParameters ): ?string {
612        $this->checkError( $constraintParameters );
613        $constraintStatusId = $this->config->get( 'WBQualityConstraintsConstraintStatusId' );
614        if ( !array_key_exists( $constraintStatusId, $constraintParameters ) ) {
615            return null;
616        }
617
618        $mandatoryId = $this->config->get( 'WBQualityConstraintsMandatoryConstraintId' );
619        $supportedStatuses = [ new ItemId( $mandatoryId ) ];
620        if ( $this->config->get( 'WBQualityConstraintsEnableSuggestionConstraintStatus' ) ) {
621            $suggestionId = $this->config->get( 'WBQualityConstraintsSuggestionConstraintId' );
622            $supportedStatuses[] = new ItemId( $suggestionId );
623        } else {
624            $suggestionId = null;
625        }
626
627        $this->requireSingleParameter( $constraintParameters, $constraintStatusId );
628        $snak = $this->snakDeserializer->deserialize( $constraintParameters[$constraintStatusId][0] );
629        $this->requireValueParameter( $snak, $constraintStatusId );
630        '@phan-var \Wikibase\DataModel\Snak\PropertyValueSnak $snak';
631        $dataValue = $snak->getDataValue();
632        '@phan-var EntityIdValue $dataValue';
633        $entityId = $dataValue->getEntityId();
634        $statusId = $entityId->getSerialization();
635
636        if ( $statusId === $mandatoryId ) {
637            return 'mandatory';
638        } elseif ( $statusId === $suggestionId ) {
639            return 'suggestion';
640        } else {
641            throw new ConstraintParameterException(
642                ( new ViolationMessage( 'wbqc-violation-message-parameter-oneof' ) )
643                    ->withEntityId( new NumericPropertyId( $constraintStatusId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
644                    ->withEntityIdList(
645                        $supportedStatuses,
646                        Role::CONSTRAINT_PARAMETER_VALUE
647                    )
648            );
649        }
650    }
651
652    /**
653     * Require that $dataValue is a {@link MonolingualTextValue}.
654     * @throws ConstraintParameterException
655     */
656    private function requireMonolingualTextParameter( DataValue $dataValue, string $parameterId ): void {
657        if ( !( $dataValue instanceof MonolingualTextValue ) ) {
658            throw new ConstraintParameterException(
659                ( new ViolationMessage( 'wbqc-violation-message-parameter-monolingualtext' ) )
660                    ->withEntityId( new NumericPropertyId( $parameterId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
661                    ->withDataValue( $dataValue, Role::CONSTRAINT_PARAMETER_VALUE )
662            );
663        }
664    }
665
666    /**
667     * Parse a series of monolingual text snaks (serialized) into a map from language code to string.
668     *
669     * @throws ConstraintParameterException if invalid snaks are found or a language has multiple texts
670     */
671    private function parseMultilingualTextParameter( array $snakSerializations, string $parameterId ): MultilingualTextValue {
672        $result = [];
673
674        foreach ( $snakSerializations as $snakSerialization ) {
675            $snak = $this->snakDeserializer->deserialize( $snakSerialization );
676            $this->requireValueParameter( $snak, $parameterId );
677
678            $value = $snak->getDataValue();
679            $this->requireMonolingualTextParameter( $value, $parameterId );
680            /** @var MonolingualTextValue $value */
681            '@phan-var MonolingualTextValue $value';
682
683            $code = $value->getLanguageCode();
684            if ( array_key_exists( $code, $result ) ) {
685                throw new ConstraintParameterException(
686                    ( new ViolationMessage( 'wbqc-violation-message-parameter-single-per-language' ) )
687                        ->withEntityId( new NumericPropertyId( $parameterId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
688                        ->withLanguage( $code )
689                );
690            }
691
692            $result[$code] = $value;
693        }
694
695        return new MultilingualTextValue( $result );
696    }
697
698    /**
699     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
700     * @throws ConstraintParameterException if the parameter is invalid
701     * @return MultilingualTextValue
702     */
703    public function parseSyntaxClarificationParameter( array $constraintParameters ): MultilingualTextValue {
704        $syntaxClarificationId = $this->config->get( 'WBQualityConstraintsSyntaxClarificationId' );
705
706        if ( !array_key_exists( $syntaxClarificationId, $constraintParameters ) ) {
707            return new MultilingualTextValue( [] );
708        }
709
710        $syntaxClarifications = $this->parseMultilingualTextParameter(
711            $constraintParameters[$syntaxClarificationId],
712            $syntaxClarificationId
713        );
714
715        return $syntaxClarifications;
716    }
717
718    /**
719     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
720     * @throws ConstraintParameterException if the parameter is invalid
721     * @return MultilingualTextValue
722     */
723    public function parseConstraintClarificationParameter( array $constraintParameters ): MultilingualTextValue {
724        $constraintClarificationId = $this->config->get( 'WBQualityConstraintsConstraintClarificationId' );
725
726        if ( !array_key_exists( $constraintClarificationId, $constraintParameters ) ) {
727            return new MultilingualTextValue( [] );
728        }
729
730        $constraintClarifications = $this->parseMultilingualTextParameter(
731            $constraintParameters[$constraintClarificationId],
732            $constraintClarificationId
733        );
734
735        return $constraintClarifications;
736    }
737
738    /**
739     * Parse the constraint scope parameters:
740     * the context types and entity types where the constraint should be checked.
741     * Depending on configuration, this may be the same property ID or two different ones.
742     *
743     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
744     * @param string $constraintTypeItemId used in error messages
745     * @param string[] $validContextTypes a list of Context::TYPE_* constants which are valid for this constraint type.
746     * If one of the specified scopes is not in this list, a ConstraintParameterException is thrown.
747     * @param string[] $validEntityTypes a list of entity types which are valid for this constraint type.
748     * If one of the specified entity types is not in this list, a ConstraintParameterException is thrown.
749     * @throws ConstraintParameterException
750     * @return array [ string[]|null $contextTypes, string[]|null $entityTypes ]
751     * the context types and entity types in the parameters (each may be null if not specified)
752     * @suppress PhanTypeArraySuspicious
753     */
754    public function parseConstraintScopeParameters(
755        array $constraintParameters,
756        string $constraintTypeItemId,
757        array $validContextTypes,
758        array $validEntityTypes
759    ): array {
760        $contextTypeParameterId = $this->config->get( 'WBQualityConstraintsConstraintScopeId' );
761        $contextTypeItemIds = $this->parseItemIdsParameter(
762            $constraintParameters,
763            $constraintTypeItemId,
764            false,
765            $contextTypeParameterId
766        );
767        $entityTypeParameterId = $this->config->get( 'WBQualityConstraintsConstraintEntityTypesId' );
768        $entityTypeItemIds = $this->parseItemIdsParameter(
769            $constraintParameters,
770            $constraintTypeItemId,
771            false,
772            $entityTypeParameterId
773        );
774
775        $contextTypeMapping = $this->getConstraintScopeContextTypeMapping();
776        $entityTypeMapping = $this->getEntityTypeMapping();
777
778        // these nulls will turn into arrays the first time $contextTypes[] or $entityTypes[] is reached,
779        // so they’ll be returned as null iff the parameter was not specified
780        $contextTypes = null;
781        $entityTypes = null;
782
783        if ( $contextTypeParameterId === $entityTypeParameterId ) {
784            $itemIds = $contextTypeItemIds;
785            $mapping = $contextTypeMapping + $entityTypeMapping;
786            foreach ( $itemIds as $itemId ) {
787                $mapped = $this->mapItemId( $itemId, $mapping, $contextTypeParameterId );
788                if ( in_array( $mapped, $contextTypeMapping, true ) ) {
789                    $contextTypes[] = $mapped;
790                } else {
791                    $entityTypes[] = $mapped;
792                }
793            }
794        } else {
795            foreach ( $contextTypeItemIds as $contextTypeItemId ) {
796                $contextTypes[] = $this->mapItemId(
797                    $contextTypeItemId,
798                    $contextTypeMapping,
799                    $contextTypeParameterId
800                );
801            }
802            foreach ( $entityTypeItemIds as $entityTypeItemId ) {
803                $entityTypes[] = $this->mapItemId(
804                    $entityTypeItemId,
805                    $entityTypeMapping,
806                    $entityTypeParameterId
807                );
808            }
809        }
810
811        $this->checkValidScope( $constraintTypeItemId, $contextTypes, $validContextTypes );
812        $this->checkValidScope( $constraintTypeItemId, $entityTypes, $validEntityTypes );
813
814        return [ $contextTypes, $entityTypes ];
815    }
816
817    private function checkValidScope( string $constraintTypeItemId, ?array $types, array $validTypes ): void {
818        $invalidTypes = array_diff( $types ?: [], $validTypes );
819        if ( $invalidTypes !== [] ) {
820            $invalidType = array_pop( $invalidTypes );
821            throw new ConstraintParameterException(
822                ( new ViolationMessage( 'wbqc-violation-message-invalid-scope' ) )
823                    ->withConstraintScope( $invalidType, Role::CONSTRAINT_PARAMETER_VALUE )
824                    ->withEntityId( new ItemId( $constraintTypeItemId ), Role::CONSTRAINT_TYPE_ITEM )
825                    ->withConstraintScopeList( $validTypes, Role::CONSTRAINT_PARAMETER_VALUE )
826            );
827        }
828    }
829
830    /**
831     * Turn an item ID into a full unit string (using the concept URI).
832     */
833    private function parseUnitParameter( ItemId $unitId ): string {
834        return $this->unitItemConceptBaseUri . $unitId->getSerialization();
835    }
836
837    /**
838     * Turn an ItemIdSnakValue into a single unit parameter.
839     *
840     * @throws ConstraintParameterException
841     */
842    private function parseUnitItem( ItemIdSnakValue $item ): UnitsParameter {
843        switch ( true ) {
844            case $item->isValue():
845                $unit = $this->parseUnitParameter( $item->getItemId() );
846                return new UnitsParameter(
847                    [ $item->getItemId() ],
848                    [ UnboundedQuantityValue::newFromNumber( 1, $unit ) ],
849                    false
850                );
851            case $item->isSomeValue():
852                $qualifierId = $this->config->get( 'WBQualityConstraintsQualifierOfPropertyConstraintId' );
853                throw new ConstraintParameterException(
854                    ( new ViolationMessage( 'wbqc-violation-message-parameter-value-or-novalue' ) )
855                        ->withEntityId( new NumericPropertyId( $qualifierId ), Role::CONSTRAINT_PARAMETER_PROPERTY )
856                );
857            case $item->isNoValue():
858                return new UnitsParameter( [], [], true );
859        }
860    }
861
862    /**
863     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
864     * @param string $constraintTypeItemId used in error messages
865     * @throws ConstraintParameterException if the parameter is invalid or missing
866     * @return UnitsParameter
867     */
868    public function parseUnitsParameter( array $constraintParameters, string $constraintTypeItemId ): UnitsParameter {
869        $items = $this->parseItemsParameter( $constraintParameters, $constraintTypeItemId, true );
870        $unitItems = [];
871        $unitQuantities = [];
872        $unitlessAllowed = false;
873
874        foreach ( $items as $item ) {
875            $unit = $this->parseUnitItem( $item );
876            $unitItems = array_merge( $unitItems, $unit->getUnitItemIds() );
877            $unitQuantities = array_merge( $unitQuantities, $unit->getUnitQuantities() );
878            $unitlessAllowed = $unitlessAllowed || $unit->getUnitlessAllowed();
879        }
880
881        if ( $unitQuantities === [] && !$unitlessAllowed ) {
882            throw new LogicException(
883                'The "units" parameter is required, and yet we seem to be missing any allowed unit'
884            );
885        }
886
887        return new UnitsParameter( $unitItems, $unitQuantities, $unitlessAllowed );
888    }
889
890    private function getEntityTypeMapping(): array {
891        return [
892            $this->config->get( 'WBQualityConstraintsWikibaseItemId' ) => 'item',
893            $this->config->get( 'WBQualityConstraintsWikibasePropertyId' ) => 'property',
894            $this->config->get( 'WBQualityConstraintsWikibaseLexemeId' ) => 'lexeme',
895            $this->config->get( 'WBQualityConstraintsWikibaseFormId' ) => 'form',
896            $this->config->get( 'WBQualityConstraintsWikibaseSenseId' ) => 'sense',
897            $this->config->get( 'WBQualityConstraintsWikibaseMediaInfoId' ) => 'mediainfo',
898        ];
899    }
900
901    /**
902     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
903     * @param string $constraintTypeItemId used in error messages
904     * @throws ConstraintParameterException if the parameter is invalid or missing
905     * @return EntityTypesParameter
906     */
907    public function parseEntityTypesParameter( array $constraintParameters, string $constraintTypeItemId ): EntityTypesParameter {
908        $entityTypes = [];
909        $entityTypeItemIds = [];
910        $parameterId = $this->config->get( 'WBQualityConstraintsQualifierOfPropertyConstraintId' );
911        $itemIds = $this->parseItemIdsParameter(
912            $constraintParameters,
913            $constraintTypeItemId,
914            true,
915            $parameterId
916        );
917
918        $mapping = $this->getEntityTypeMapping();
919        foreach ( $itemIds as $itemId ) {
920            $entityType = $this->mapItemId( $itemId, $mapping, $parameterId );
921            $entityTypes[] = $entityType;
922            $entityTypeItemIds[] = $itemId;
923        }
924
925        if ( $entityTypes === [] ) {
926            // @codeCoverageIgnoreStart
927            throw new LogicException(
928                'The "entity types" parameter is required, ' .
929                'and yet we seem to be missing any allowed entity type'
930            );
931            // @codeCoverageIgnoreEnd
932        }
933
934        return new EntityTypesParameter( $entityTypes, $entityTypeItemIds );
935    }
936
937    /**
938     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
939     * @throws ConstraintParameterException if the parameter is invalid
940     * @return PropertyId[]
941     */
942    public function parseSeparatorsParameter( array $constraintParameters ): array {
943        $separatorId = $this->config->get( 'WBQualityConstraintsSeparatorId' );
944
945        if ( !array_key_exists( $separatorId, $constraintParameters ) ) {
946            return [];
947        }
948
949        $parameters = $constraintParameters[$separatorId];
950        $separators = [];
951
952        foreach ( $parameters as $parameter ) {
953            $separators[] = $this->parsePropertyIdParameter( $parameter, $separatorId );
954        }
955
956        return $separators;
957    }
958
959    private function getConstraintScopeContextTypeMapping(): array {
960        return [
961            $this->config->get( 'WBQualityConstraintsConstraintCheckedOnMainValueId' ) => Context::TYPE_STATEMENT,
962            $this->config->get( 'WBQualityConstraintsConstraintCheckedOnQualifiersId' ) => Context::TYPE_QUALIFIER,
963            $this->config->get( 'WBQualityConstraintsConstraintCheckedOnReferencesId' ) => Context::TYPE_REFERENCE,
964        ];
965    }
966
967    private function getPropertyScopeContextTypeMapping(): array {
968        return [
969            $this->config->get( 'WBQualityConstraintsAsMainValueId' ) => Context::TYPE_STATEMENT,
970            $this->config->get( 'WBQualityConstraintsAsQualifiersId' ) => Context::TYPE_QUALIFIER,
971            $this->config->get( 'WBQualityConstraintsAsReferencesId' ) => Context::TYPE_REFERENCE,
972        ];
973    }
974
975    /**
976     * @param array $constraintParameters see {@link \WikibaseQuality\ConstraintReport\Constraint::getConstraintParameters()}
977     * @param string $constraintTypeItemId used in error messages
978     * @throws ConstraintParameterException if the parameter is invalid or missing
979     * @return string[] list of Context::TYPE_* constants
980     */
981    public function parsePropertyScopeParameter( array $constraintParameters, string $constraintTypeItemId ): array {
982        $contextTypes = [];
983        $parameterId = $this->config->get( 'WBQualityConstraintsPropertyScopeId' );
984        $itemIds = $this->parseItemIdsParameter(
985            $constraintParameters,
986            $constraintTypeItemId,
987            true,
988            $parameterId
989        );
990
991        $mapping = $this->getPropertyScopeContextTypeMapping();
992        foreach ( $itemIds as $itemId ) {
993            $contextTypes[] = $this->mapItemId( $itemId, $mapping, $parameterId );
994        }
995
996        if ( $contextTypes === [] ) {
997            // @codeCoverageIgnoreStart
998            throw new LogicException(
999                'The "property scope" parameter is required, ' .
1000                'and yet we seem to be missing any allowed scope'
1001            );
1002            // @codeCoverageIgnoreEnd
1003        }
1004
1005        return $contextTypes;
1006    }
1007
1008}