3 * Analyzes a query and gives user feedback.
6 declare(strict_types
=1);
10 use PhpMyAdmin\SqlParser\Lexer
;
11 use PhpMyAdmin\SqlParser\Parser
;
12 use PhpMyAdmin\SqlParser\UtfString
;
13 use PhpMyAdmin\SqlParser\Utils\Error
as ParserError
;
16 use function htmlspecialchars
;
17 use function mb_strlen
;
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
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
54 $len = $str->length();
57 /** @infection-ignore-all */
58 for ($i = 0; $i < $len; ++
$i) {
59 if ($str[$i] !== "\n") {
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
80 foreach ($lines as $lineNo => $lineStart) {
81 if ($lineStart > $pos) {
88 return [$line, $pos - $lines[$line]];
92 * Runs the linting process.
94 * @param string $query The query to be checked.
96 * @return (string|int)[][]
97 * @psalm-return list<array{
106 public static function lint(string $query): array
108 // Disabling lint for huge queries to save some resources.
109 if (mb_strlen($query) > 10000) {
112 'message' => __('Linting is disabled for this query because it exceeds the maximum length.'),
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.
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(
158 $error[3] +
mb_strlen((string) $error[2]),
161 // Building the 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,
171 'toColumn' => $toColumn,
172 'severity' => 'error',
176 // Sending back the answer.