Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.84% covered (warning)
87.84%
130 / 148
73.68% covered (warning)
73.68%
14 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
ViolationMessageRenderer
87.84% covered (warning)
87.84%
130 / 148
73.68% covered (warning)
73.68%
14 / 19
40.60
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 render
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 addRole
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 msgEscaped
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 renderArgument
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
2
 renderList
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
4
 renderEntityId
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 renderEntityIdList
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 renderItemIdSnakValue
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
5
 renderItemIdSnakValueList
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 renderDataValue
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 renderDataValueType
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 renderInlineCode
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 renderConstraintScope
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 renderConstraintScopeList
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 renderPropertyScope
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 renderPropertyScopeList
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 renderLanguage
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 renderLanguageList
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare( strict_types = 1 );
4
5namespace WikibaseQuality\ConstraintReport\ConstraintCheck\Message;
6
7use DataValues\DataValue;
8use InvalidArgumentException;
9use LogicException;
10use MediaWiki\Config\Config;
11use MediaWiki\Languages\LanguageNameUtils;
12use Message;
13use MessageLocalizer;
14use ValueFormatters\ValueFormatter;
15use Wikibase\DataModel\Entity\EntityId;
16use Wikibase\DataModel\Entity\ItemId;
17use Wikibase\DataModel\Services\EntityId\EntityIdFormatter;
18use Wikibase\Lib\TermLanguageFallbackChain;
19use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\Context;
20use WikibaseQuality\ConstraintReport\ConstraintCheck\ItemIdSnakValue;
21
22/**
23 * Render a {@link ViolationMessage} into a localized string.
24 *
25 * Note: This class does <em>not</em> support multilingual text arguments –
26 * for that, use {@link MultilingualTextViolationMessageRenderer}.
27 *
28 * @license GPL-2.0-or-later
29 */
30class ViolationMessageRenderer {
31
32    private EntityIdFormatter $entityIdFormatter;
33    private ValueFormatter $dataValueFormatter;
34    private LanguageNameUtils $languageNameUtils;
35    private string $userLanguageCode;
36    protected TermLanguageFallbackChain $languageFallbackChain;
37    protected MessageLocalizer $messageLocalizer;
38    private Config $config;
39    private int $maxListLength;
40
41    /**
42     * @param EntityIdFormatter $entityIdFormatter
43     * @param ValueFormatter $dataValueFormatter
44     * @param MessageLocalizer $messageLocalizer
45     * @param Config $config
46     * @param int $maxListLength The maximum number of elements to be rendered in a list parameter.
47     * Longer lists are truncated to this length and then rendered with an ellipsis in the HMTL list.
48     */
49    public function __construct(
50        EntityIdFormatter $entityIdFormatter,
51        ValueFormatter $dataValueFormatter,
52        LanguageNameUtils $languageNameUtils,
53        string $userLanguageCode,
54        TermLanguageFallbackChain $languageFallbackChain,
55        MessageLocalizer $messageLocalizer,
56        Config $config,
57        int $maxListLength = 10
58    ) {
59        $this->entityIdFormatter = $entityIdFormatter;
60        $this->dataValueFormatter = $dataValueFormatter;
61        $this->languageNameUtils = $languageNameUtils;
62        $this->userLanguageCode = $userLanguageCode;
63        $this->languageFallbackChain = $languageFallbackChain;
64        $this->messageLocalizer = $messageLocalizer;
65        $this->config = $config;
66        $this->maxListLength = $maxListLength;
67    }
68
69    public function render( ViolationMessage $violationMessage ): string {
70        $messageKey = $violationMessage->getMessageKey();
71        $paramsLists = [ [] ];
72        foreach ( $violationMessage->getArguments() as $argument ) {
73            $params = $this->renderArgument( $argument );
74            $paramsLists[] = $params;
75        }
76        $allParams = call_user_func_array( 'array_merge', $paramsLists );
77        return $this->messageLocalizer
78            ->msg( $messageKey )
79            ->params( $allParams )
80            ->escaped();
81    }
82
83    /**
84     * @param string $value HTML
85     * @param string|null $role one of the Role::* constants
86     * @return string HTML
87     */
88    protected function addRole( string $value, ?string $role ): string {
89        if ( $role === null ) {
90            return $value;
91        }
92
93        return '<span class="wbqc-role wbqc-role-' . htmlspecialchars( $role ) . '">' .
94            $value .
95            '</span>';
96    }
97
98    /**
99     * @param string $key message key
100     * @return string HTML
101     */
102    protected function msgEscaped( string $key ): string {
103        return $this->messageLocalizer->msg( $key )->escaped();
104    }
105
106    /**
107     * @param array $argument
108     * @return array[] params (for Message::params)
109     */
110    protected function renderArgument( array $argument ): array {
111        $methods = [
112            ViolationMessage::TYPE_ENTITY_ID => 'renderEntityId',
113            ViolationMessage::TYPE_ENTITY_ID_LIST => 'renderEntityIdList',
114            ViolationMessage::TYPE_ITEM_ID_SNAK_VALUE => 'renderItemIdSnakValue',
115            ViolationMessage::TYPE_ITEM_ID_SNAK_VALUE_LIST => 'renderItemIdSnakValueList',
116            ViolationMessage::TYPE_DATA_VALUE => 'renderDataValue',
117            ViolationMessage::TYPE_DATA_VALUE_TYPE => 'renderDataValueType',
118            ViolationMessage::TYPE_INLINE_CODE => 'renderInlineCode',
119            ViolationMessage::TYPE_CONSTRAINT_SCOPE => 'renderConstraintScope',
120            ViolationMessage::TYPE_CONSTRAINT_SCOPE_LIST => 'renderConstraintScopeList',
121            ViolationMessage::TYPE_PROPERTY_SCOPE => 'renderPropertyScope',
122            ViolationMessage::TYPE_PROPERTY_SCOPE_LIST => 'renderPropertyScopeList',
123            ViolationMessage::TYPE_LANGUAGE => 'renderLanguage',
124            ViolationMessage::TYPE_LANGUAGE_LIST => 'renderLanguageList',
125        ];
126
127        $type = $argument['type'];
128        $value = $argument['value'];
129        $role = $argument['role'];
130
131        if ( array_key_exists( $type, $methods ) ) {
132            $method = $methods[$type];
133            $params = $this->$method( $value, $role );
134        } else {
135            throw new InvalidArgumentException(
136                'Unknown ViolationMessage argument type ' . $type . '!'
137            );
138        }
139
140        return $params;
141    }
142
143    /**
144     * @param array $list
145     * @param string|null $role one of the Role::* constants
146     * @param callable $render must accept $list elements and $role as parameters
147     * and return a single-element array with a raw message param (i. e. [ Message::rawParam( … ) ])
148     * @return array[] list of parameters as accepted by Message::params()
149     */
150    private function renderList( array $list, ?string $role, callable $render ): array {
151        if ( $list === [] ) {
152            return [
153                Message::numParam( 0 ),
154                Message::rawParam( '<ul></ul>' ),
155            ];
156        }
157
158        if ( count( $list ) > $this->maxListLength ) {
159            $list = array_slice( $list, 0, $this->maxListLength );
160            $truncated = true;
161        }
162
163        $renderedParamsLists = array_map(
164            $render,
165            $list,
166            array_fill( 0, count( $list ), $role )
167        );
168        $renderedParams = array_column( $renderedParamsLists, 0 );
169        $renderedElements = array_column( $renderedParams, 'raw' );
170        if ( isset( $truncated ) ) {
171            $renderedElements[] = $this->msgEscaped( 'ellipsis' );
172        }
173
174        return array_merge(
175            [
176                Message::numParam( count( $list ) ),
177                Message::rawParam(
178                    '<ul><li>' .
179                    implode( '</li><li>', $renderedElements ) .
180                    '</li></ul>'
181                ),
182            ],
183            $renderedParams
184        );
185    }
186
187    /**
188     * @param EntityId $entityId
189     * @param string|null $role one of the Role::* constants
190     * @return array[] list of a single raw message param (i. e. [ Message::rawParam( … ) ])
191     */
192    private function renderEntityId( EntityId $entityId, ?string $role ): array {
193        return [ Message::rawParam( $this->addRole(
194            $this->entityIdFormatter->formatEntityId( $entityId ),
195            $role
196        ) ) ];
197    }
198
199    /**
200     * @param EntityId[] $entityIdList
201     * @param string|null $role one of the Role::* constants
202     * @return array[] list of parameters as accepted by Message::params()
203     */
204    private function renderEntityIdList( array $entityIdList, ?string $role ): array {
205        return $this->renderList( $entityIdList, $role, [ $this, 'renderEntityId' ] );
206    }
207
208    /**
209     * @param ItemIdSnakValue $value
210     * @param string|null $role one of the Role::* constants
211     * @return array[] list of a single raw message param (i. e. [ Message::rawParam( … ) ])
212     */
213    private function renderItemIdSnakValue( ItemIdSnakValue $value, ?string $role ): array {
214        switch ( true ) {
215            case $value->isValue():
216                return $this->renderEntityId( $value->getItemId(), $role );
217            case $value->isSomeValue():
218                return [ Message::rawParam( $this->addRole(
219                    '<span class="wikibase-snakview-variation-somevaluesnak">' .
220                        $this->msgEscaped( 'wikibase-snakview-snaktypeselector-somevalue' ) .
221                        '</span>',
222                    $role
223                ) ) ];
224            case $value->isNoValue():
225                return [ Message::rawParam( $this->addRole(
226                    '<span class="wikibase-snakview-variation-novaluesnak">' .
227                    $this->msgEscaped( 'wikibase-snakview-snaktypeselector-novalue' ) .
228                        '</span>',
229                    $role
230                ) ) ];
231            default:
232                // @codeCoverageIgnoreStart
233                throw new LogicException(
234                    'ItemIdSnakValue should guarantee that one of is{,Some,No}Value() is true'
235                );
236                // @codeCoverageIgnoreEnd
237        }
238    }
239
240    /**
241     * @param ItemIdSnakValue[] $valueList
242     * @param string|null $role one of the Role::* constants
243     * @return array[] list of parameters as accepted by Message::params()
244     */
245    private function renderItemIdSnakValueList( array $valueList, ?string $role ): array {
246        return $this->renderList( $valueList, $role, [ $this, 'renderItemIdSnakValue' ] );
247    }
248
249    /**
250     * @param DataValue $dataValue
251     * @param string|null $role one of the Role::* constants
252     * @return array[] list of parameters as accepted by Message::params()
253     */
254    private function renderDataValue( DataValue $dataValue, ?string $role ): array {
255        return [ Message::rawParam( $this->addRole(
256            $this->dataValueFormatter->format( $dataValue ),
257            $role
258        ) ) ];
259    }
260
261    /**
262     * @param string $dataValueType
263     * @param string|null $role one of the Role::* constants
264     * @return array[] list of parameters as accepted by Message::params()
265     */
266    private function renderDataValueType( string $dataValueType, ?string $role ): array {
267        $messageKeys = [
268            'string' => 'datatypes-type-string',
269            'monolingualtext' => 'datatypes-type-monolingualtext',
270            'time' => 'datatypes-type-time',
271            'quantity' => 'datatypes-type-quantity',
272            'wikibase-entityid' => 'wbqc-dataValueType-wikibase-entityid',
273        ];
274
275        if ( array_key_exists( $dataValueType, $messageKeys ) ) {
276            return [ Message::rawParam( $this->addRole(
277                $this->msgEscaped( $messageKeys[$dataValueType] ),
278                $role
279            ) ) ];
280        } else {
281            // @codeCoverageIgnoreStart
282            throw new LogicException(
283                'Unknown data value type ' . $dataValueType
284            );
285            // @codeCoverageIgnoreEnd
286        }
287    }
288
289    /**
290     * @param string $code (not yet HTML-escaped)
291     * @param string|null $role one of the Role::* constants
292     * @return array[] list of parameters as accepted by Message::params()
293     */
294    private function renderInlineCode( string $code, ?string $role ): array {
295        return [ Message::rawParam( $this->addRole(
296            '<code>' . htmlspecialchars( $code ) . '</code>',
297            $role
298        ) ) ];
299    }
300
301    /**
302     * @param string $scope one of the Context::TYPE_* constants
303     * @param string|null $role one of the Role::* constants
304     * @return array[] list of a single raw message param (i. e. [ Message::rawParam( … ) ])
305     */
306    private function renderConstraintScope( string $scope, ?string $role ): array {
307        switch ( $scope ) {
308            case Context::TYPE_STATEMENT:
309                $itemId = $this->config->get(
310                    'WBQualityConstraintsConstraintCheckedOnMainValueId'
311                );
312                break;
313            case Context::TYPE_QUALIFIER:
314                $itemId = $this->config->get(
315                    'WBQualityConstraintsConstraintCheckedOnQualifiersId'
316                );
317                break;
318            case Context::TYPE_REFERENCE:
319                $itemId = $this->config->get(
320                    'WBQualityConstraintsConstraintCheckedOnReferencesId'
321                );
322                break;
323            default:
324                // callers should never let this happen, but if it does happen,
325                // showing “unknown value” seems reasonable
326                // @codeCoverageIgnoreStart
327                return $this->renderItemIdSnakValue( ItemIdSnakValue::someValue(), $role );
328                // @codeCoverageIgnoreEnd
329        }
330        return $this->renderEntityId( new ItemId( $itemId ), $role );
331    }
332
333    /**
334     * @param string[] $scopeList Context::TYPE_* constants
335     * @param string|null $role one of the Role::* constants
336     * @return array[] list of parameters as accepted by Message::params()
337     */
338    private function renderConstraintScopeList( array $scopeList, ?string $role ): array {
339        return $this->renderList( $scopeList, $role, [ $this, 'renderConstraintScope' ] );
340    }
341
342    /**
343     * @param string $scope one of the Context::TYPE_* constants
344     * @param string|null $role one of the Role::* constants
345     * @return array[] list of a single raw message param (i. e. [ Message::rawParam( … ) ])
346     */
347    private function renderPropertyScope( string $scope, ?string $role ): array {
348        switch ( $scope ) {
349            case Context::TYPE_STATEMENT:
350                $itemId = $this->config->get( 'WBQualityConstraintsAsMainValueId' );
351                break;
352            case Context::TYPE_QUALIFIER:
353                $itemId = $this->config->get( 'WBQualityConstraintsAsQualifiersId' );
354                break;
355            case Context::TYPE_REFERENCE:
356                $itemId = $this->config->get( 'WBQualityConstraintsAsReferencesId' );
357                break;
358            default:
359                // callers should never let this happen, but if it does happen,
360                // showing “unknown value” seems reasonable
361                // @codeCoverageIgnoreStart
362                return $this->renderItemIdSnakValue( ItemIdSnakValue::someValue(), $role );
363                // @codeCoverageIgnoreEnd
364        }
365        return $this->renderEntityId( new ItemId( $itemId ), $role );
366    }
367
368    /**
369     * @param string[] $scopeList Context::TYPE_* constants
370     * @param string|null $role one of the Role::* constants
371     * @return array[] list of parameters as accepted by Message::params()
372     */
373    private function renderPropertyScopeList( array $scopeList, ?string $role ): array {
374        return $this->renderList( $scopeList, $role, [ $this, 'renderPropertyScope' ] );
375    }
376
377    /**
378     * @param string $languageCode MediaWiki language code
379     * @param string|null $role one of the Role::* constants
380     * @return array[] list of parameters as accepted by Message::params()
381     */
382    private function renderLanguage( string $languageCode, ?string $role ): array {
383        return [
384            // ::renderList (through ::renderLanguageList) requires 'raw' parameter
385            // so we effectively build Message::plaintextParam here
386            Message::rawParam( htmlspecialchars(
387                $this->languageNameUtils->getLanguageName( $languageCode, $this->userLanguageCode )
388            ) ),
389            Message::plaintextParam( $languageCode ),
390        ];
391    }
392
393    /**
394     * @param string[] $languageCodes MediaWiki language codes
395     * @param string|null $role one of the Role::* constants
396     * @return array[] list of parameters as accepted by Message::params()
397     */
398    private function renderLanguageList( array $languageCodes, ?string $role ): array {
399        return $this->renderList( $languageCodes, $role, [ $this, 'renderLanguage' ] );
400    }
401
402}