Merge branch 'QA_5_2'
[phpmyadmin.git] / src / Linter.php
bloba3b1fbdf52839f950d28b9cd30dc0787158955ef
1 <?php
2 /**
3 * Analyzes a query and gives user feedback.
4 */
6 declare(strict_types=1);
8 namespace PhpMyAdmin;
10 use PhpMyAdmin\SqlParser\Lexer;
11 use PhpMyAdmin\SqlParser\Parser;
12 use PhpMyAdmin\SqlParser\UtfString;
13 use PhpMyAdmin\SqlParser\Utils\Error as ParserError;
15 use function __;
16 use function htmlspecialchars;
17 use function mb_strlen;
18 use function sprintf;
20 /**
21 * The linter itself.
23 class Linter
25 /**
26 * Gets the starting position of each line.
28 * @param string|UtfString $str String to be analyzed.
30 * @return array<int,int>
31 * @psalm-return list<int>
33 public static function getLines(string|UtfString $str): array
35 if (! $str instanceof UtfString) {
36 // If the lexer uses UtfString for processing then the position will
37 // represent the position of the character and not the position of
38 // the byte.
39 $str = new UtfString($str);
42 // The reason for using the strlen is that the length
43 // required is the length in bytes, not characters.
45 // Given the following string: `????+`, where `?` represents a
46 // multi-byte character (lets assume that every `?` is a 2-byte
47 // character) and `+` is a newline, the first value of `$i` is `0`
48 // and the last one is `4` (because there are 5 characters). Bytes
49 // `$str[0]` and `$str[1]` are the first character, `$str[2]` and
50 // `$str[3]` are the second one and `$str[4]` is going to be the
51 // first byte of the third character. The fourth and the last one
52 // (which is actually a new line) aren't going to be processed at
53 // all.
54 $len = $str->length();
56 $lines = [0];
57 /** @infection-ignore-all */
58 for ($i = 0; $i < $len; ++$i) {
59 if ($str[$i] !== "\n") {
60 continue;
63 $lines[] = $i + 1;
66 return $lines;
69 /**
70 * Computes the number of the line and column given an absolute position.
72 * @param list<int> $lines The starting position of each line.
73 * @param int $pos The absolute position
75 * @return array{int, int}
77 public static function findLineNumberAndColumn(array $lines, int $pos): array
79 $line = 0;
80 foreach ($lines as $lineNo => $lineStart) {
81 if ($lineStart > $pos) {
82 break;
85 $line = $lineNo;
88 return [$line, $pos - $lines[$line]];
91 /**
92 * Runs the linting process.
94 * @param string $query The query to be checked.
96 * @return (string|int)[][]
97 * @psalm-return list<array{
98 * message: string,
99 * fromLine: int,
100 * fromColumn: int,
101 * toLine: int,
102 * toColumn: int,
103 * severity: string,
104 * }>
106 public static function lint(string $query): array
108 // Disabling lint for huge queries to save some resources.
109 if (mb_strlen($query) > 10000) {
110 return [
112 'message' => __('Linting is disabled for this query because it exceeds the maximum length.'),
113 'fromLine' => 0,
114 'fromColumn' => 0,
115 'toLine' => 0,
116 'toColumn' => 0,
117 'severity' => 'warning',
123 * Lexer used for tokenizing the query.
125 $lexer = new Lexer($query);
128 * Parsed used for analysing the query.
130 $parser = new Parser($lexer->list);
133 * Array containing all errors.
135 $errors = ParserError::get([$lexer, $parser]);
138 * The response containing of all errors.
140 $response = [];
143 * The starting position for each line.
145 * CodeMirror requires relative position to line, but the parser stores
146 * only the absolute position of the character in string.
148 $lines = static::getLines($query);
150 // Building the response.
151 foreach ($errors as $error) {
152 // Starting position of the string that caused the error.
153 [$fromLine, $fromColumn] = static::findLineNumberAndColumn($lines, (int) $error[3]);
155 // Ending position of the string that caused the error.
156 [$toLine, $toColumn] = static::findLineNumberAndColumn(
157 $lines,
158 $error[3] + mb_strlen((string) $error[2]),
161 // Building the response.
162 $response[] = [
163 'message' => sprintf(
164 __('%1$s (near <code>%2$s</code>)'),
165 htmlspecialchars((string) $error[0]),
166 htmlspecialchars((string) $error[2]),
168 'fromLine' => $fromLine,
169 'fromColumn' => $fromColumn,
170 'toLine' => $toLine,
171 'toColumn' => $toColumn,
172 'severity' => 'error',
176 // Sending back the answer.
177 return $response;