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