|
| 1 | +<?php |
| 2 | +/** |
| 3 | + * Collect messages for display at a later point in the process flow. |
| 4 | + * |
| 5 | + * If any message with type "error" is passed in, displaying the errors will result in halting the program |
| 6 | + * with a non-zero exit code. |
| 7 | + * If only messages with a lower severity are passed in, displaying the errors will be non-blocking |
| 8 | + * and will not affect the exit code. |
| 9 | + * |
| 10 | + * --------------------------------------------------------------------------------------------- |
| 11 | + * This class is intended for internal use only and is not part of the public API. |
| 12 | + * This also means that it has no promise of backward compatibility. Use at your own risk. |
| 13 | + * --------------------------------------------------------------------------------------------- |
| 14 | + * |
| 15 | + * @internal |
| 16 | + * |
| 17 | + * @author Juliette Reinders Folmer <[email protected]> |
| 18 | + * @copyright 2025 PHPCSStandards and contributors |
| 19 | + * @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence |
| 20 | + */ |
| 21 | + |
| 22 | +namespace PHP_CodeSniffer\Util; |
| 23 | + |
| 24 | +use InvalidArgumentException; |
| 25 | +use PHP_CodeSniffer\Exceptions\RuntimeException; |
| 26 | + |
| 27 | +final class MessageCollector |
| 28 | +{ |
| 29 | + |
| 30 | + /** |
| 31 | + * Indicator for a (blocking) error. |
| 32 | + * |
| 33 | + * @var int |
| 34 | + */ |
| 35 | + const ERROR = 1; |
| 36 | + |
| 37 | + /** |
| 38 | + * Indicator for a warning. |
| 39 | + * |
| 40 | + * @var int |
| 41 | + */ |
| 42 | + const WARNING = 2; |
| 43 | + |
| 44 | + /** |
| 45 | + * Indicator for a notice. |
| 46 | + * |
| 47 | + * @var int |
| 48 | + */ |
| 49 | + const NOTICE = 4; |
| 50 | + |
| 51 | + /** |
| 52 | + * Indicator for a deprecation notice. |
| 53 | + * |
| 54 | + * @var int |
| 55 | + */ |
| 56 | + const DEPRECATED = 8; |
| 57 | + |
| 58 | + /** |
| 59 | + * Indicator for ordering the messages based on severity first, order received second. |
| 60 | + * |
| 61 | + * @var string |
| 62 | + */ |
| 63 | + const ORDERBY_SEVERITY = 'severity'; |
| 64 | + |
| 65 | + /** |
| 66 | + * Indicator for ordering the messages based on the order in which they were received. |
| 67 | + * |
| 68 | + * @var string |
| 69 | + */ |
| 70 | + const ORDERBY_RECEIVED = 'received'; |
| 71 | + |
| 72 | + /** |
| 73 | + * Collected messages. |
| 74 | + * |
| 75 | + * @var array<array<string, string|int>> The value for each array entry is an associative array |
| 76 | + * which holds two keys: |
| 77 | + * - 'message' string The message text. |
| 78 | + * - 'type' int The type of the message based on the |
| 79 | + * above declared error level constants. |
| 80 | + */ |
| 81 | + private $cache = []; |
| 82 | + |
| 83 | + |
| 84 | + /** |
| 85 | + * Add a new message. |
| 86 | + * |
| 87 | + * @param string $message The message text. |
| 88 | + * @param int $type The type of message. Should be one of the following constants: |
| 89 | + * MessageCollector::ERROR, MessageCollector::WARNING, MessageCollector::NOTICE |
| 90 | + * or MessageCollector::DEPRECATED. |
| 91 | + * Defaults to MessageCollector::NOTICE. |
| 92 | + * |
| 93 | + * @return void |
| 94 | + * |
| 95 | + * @throws \InvalidArgumentException If the message text is not a string. |
| 96 | + * @throws \InvalidArgumentException If the message type is not one of the accepted types. |
| 97 | + */ |
| 98 | + public function add($message, $type=self::NOTICE) |
| 99 | + { |
| 100 | + if (is_string($message) === false) { |
| 101 | + throw new InvalidArgumentException('The $message should be of type string. Received: '.gettype($message).'.'); |
| 102 | + } |
| 103 | + |
| 104 | + if ($type !== self::ERROR |
| 105 | + && $type !== self::WARNING |
| 106 | + && $type !== self::NOTICE |
| 107 | + && $type !== self::DEPRECATED |
| 108 | + ) { |
| 109 | + throw new InvalidArgumentException('The message $type should be one of the predefined MessageCollector constants. Received: '.$type.'.'); |
| 110 | + } |
| 111 | + |
| 112 | + $this->cache[] = [ |
| 113 | + 'message' => $message, |
| 114 | + 'type' => $type, |
| 115 | + ]; |
| 116 | + |
| 117 | + }//end add() |
| 118 | + |
| 119 | + |
| 120 | + /** |
| 121 | + * Determine whether or not the currently cached errors include blocking errors. |
| 122 | + * |
| 123 | + * @return bool |
| 124 | + */ |
| 125 | + public function containsBlockingErrors() |
| 126 | + { |
| 127 | + $seenTypes = $this->arrayColumn($this->cache, 'type'); |
| 128 | + $typeFrequency = array_count_values($seenTypes); |
| 129 | + return isset($typeFrequency[self::ERROR]); |
| 130 | + |
| 131 | + }//end containsBlockingErrors() |
| 132 | + |
| 133 | + |
| 134 | + /** |
| 135 | + * Display the cached messages. |
| 136 | + * |
| 137 | + * Displaying the messages will also clear the message cache. |
| 138 | + * |
| 139 | + * @param string $order Optional. The order in which to display the messages. |
| 140 | + * Should be one of the following constants: MessageCollector::ORDERBY_SEVERITY, |
| 141 | + * MessageCollector::ORDERBY_RECEIVED. |
| 142 | + * Defaults to MessageCollector::ORDERBY_SEVERITY. |
| 143 | + * |
| 144 | + * @return void |
| 145 | + * |
| 146 | + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException When there are blocking errors. |
| 147 | + */ |
| 148 | + public function display($order=self::ORDERBY_SEVERITY) |
| 149 | + { |
| 150 | + if ($this->cache === []) { |
| 151 | + return; |
| 152 | + } |
| 153 | + |
| 154 | + $blocking = $this->containsBlockingErrors(); |
| 155 | + $messageInfo = $this->prefixAll($this->cache); |
| 156 | + $this->clearCache(); |
| 157 | + |
| 158 | + if ($order === self::ORDERBY_RECEIVED) { |
| 159 | + $messages = $this->arrayColumn($messageInfo, 'message'); |
| 160 | + } else { |
| 161 | + $messages = $this->sortBySeverity($messageInfo); |
| 162 | + } |
| 163 | + |
| 164 | + $allMessages = implode(PHP_EOL, $messages).PHP_EOL.PHP_EOL; |
| 165 | + |
| 166 | + if ($blocking === true) { |
| 167 | + throw new RuntimeException($allMessages); |
| 168 | + } else { |
| 169 | + echo $allMessages; |
| 170 | + } |
| 171 | + |
| 172 | + }//end display() |
| 173 | + |
| 174 | + |
| 175 | + /** |
| 176 | + * Label all messages based on their type. |
| 177 | + * |
| 178 | + * @param array<array<string, string|int>> $messages A multi-dimensional array of messages with their severity. |
| 179 | + * |
| 180 | + * @return array<array<string, string|int>> |
| 181 | + */ |
| 182 | + private function prefixAll($messages) |
| 183 | + { |
| 184 | + foreach ($messages as $i => $details) { |
| 185 | + $messages[$i]['message'] = $this->prefix($details['message'], $details['type']); |
| 186 | + } |
| 187 | + |
| 188 | + return $messages; |
| 189 | + |
| 190 | + }//end prefixAll() |
| 191 | + |
| 192 | + |
| 193 | + /** |
| 194 | + * Add a message type prefix to a message. |
| 195 | + * |
| 196 | + * @param string $message The message text. |
| 197 | + * @param int $type The type of message. |
| 198 | + * |
| 199 | + * @return string |
| 200 | + */ |
| 201 | + private function prefix($message, $type) |
| 202 | + { |
| 203 | + switch ($type) { |
| 204 | + case self::ERROR: |
| 205 | + $message = 'ERROR: '.$message; |
| 206 | + break; |
| 207 | + |
| 208 | + case self::WARNING: |
| 209 | + $message = 'WARNING: '.$message; |
| 210 | + break; |
| 211 | + |
| 212 | + case self::DEPRECATED: |
| 213 | + $message = 'DEPRECATED: '.$message; |
| 214 | + break; |
| 215 | + |
| 216 | + default: |
| 217 | + $message = 'NOTICE: '.$message; |
| 218 | + break; |
| 219 | + } |
| 220 | + |
| 221 | + return $message; |
| 222 | + |
| 223 | + }//end prefix() |
| 224 | + |
| 225 | + |
| 226 | + /** |
| 227 | + * Sort an array of messages by severity. |
| 228 | + * |
| 229 | + * @param array<array<string, string|int>> $messages A multi-dimensional array of messages with their severity. |
| 230 | + * |
| 231 | + * @return array<string> A single dimensional array of only messages, sorted by severity. |
| 232 | + */ |
| 233 | + private function sortBySeverity($messages) |
| 234 | + { |
| 235 | + if (count($messages) === 1) { |
| 236 | + return [$messages[0]['message']]; |
| 237 | + } |
| 238 | + |
| 239 | + $errors = []; |
| 240 | + $warnings = []; |
| 241 | + $notices = []; |
| 242 | + $deprecations = []; |
| 243 | + |
| 244 | + foreach ($messages as $details) { |
| 245 | + switch ($details['type']) { |
| 246 | + case self::ERROR: |
| 247 | + $errors[] = $details['message']; |
| 248 | + break; |
| 249 | + |
| 250 | + case self::WARNING: |
| 251 | + $warnings[] = $details['message']; |
| 252 | + break; |
| 253 | + |
| 254 | + case self::DEPRECATED: |
| 255 | + $deprecations[] = $details['message']; |
| 256 | + break; |
| 257 | + |
| 258 | + default: |
| 259 | + $notices[] = $details['message']; |
| 260 | + break; |
| 261 | + } |
| 262 | + } |
| 263 | + |
| 264 | + return array_merge($errors, $warnings, $notices, $deprecations); |
| 265 | + |
| 266 | + }//end sortBySeverity() |
| 267 | + |
| 268 | + |
| 269 | + /** |
| 270 | + * Clear the message cache. |
| 271 | + * |
| 272 | + * @return void |
| 273 | + */ |
| 274 | + private function clearCache() |
| 275 | + { |
| 276 | + $this->cache = []; |
| 277 | + |
| 278 | + }//end clearCache() |
| 279 | + |
| 280 | + |
| 281 | + /** |
| 282 | + * Return the values from a single column in the input array. |
| 283 | + * |
| 284 | + * Polyfill for the PHP 5.5+ native array_column() function (for the functionality needed here). |
| 285 | + * |
| 286 | + * @param array<array<string, string|int>> $input A multi-dimensional array from which to pull a column of values. |
| 287 | + * @param string $columnKey The name of the column of values to return. |
| 288 | + * |
| 289 | + * @link https://www.php.net/function.array-column |
| 290 | + * |
| 291 | + * @return array<string|int> |
| 292 | + */ |
| 293 | + private function arrayColumn(array $input, $columnKey) |
| 294 | + { |
| 295 | + if (function_exists('array_column') === true) { |
| 296 | + // PHP 5.5+. |
| 297 | + return array_column($input, $columnKey); |
| 298 | + } |
| 299 | + |
| 300 | + // PHP 5.4. |
| 301 | + $callback = function ($row) use ($columnKey) { |
| 302 | + return $row[$columnKey]; |
| 303 | + }; |
| 304 | + |
| 305 | + return array_map($callback, $input); |
| 306 | + |
| 307 | + }//end arrayColumn() |
| 308 | + |
| 309 | + |
| 310 | +}//end class |
0 commit comments