Translated using Weblate (Portuguese)
[phpmyadmin.git] / src / Linter.php
blobde6ce7650e89c20f5afd0bbc24ecd5b89e38dffd
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 int[]
32 public static function getLines(string|UtfString $str): array
34 if (! $str instanceof UtfString) {
35 // If the lexer uses UtfString for processing then the position will
36 // represent the position of the character and not the position of
37 // the byte.
38 $str = new UtfString($str);
41 // The reason for using the strlen is that the length
42 // required is the length in bytes, not characters.
44 // Given the following string: `????+`, where `?` represents a
45 // multi-byte character (lets assume that every `?` is a 2-byte
46 // character) and `+` is a newline, the first value of `$i` is `0`
47 // and the last one is `4` (because there are 5 characters). Bytes
48 // `$str[0]` and `$str[1]` are the first character, `$str[2]` and
49 // `$str[3]` are the second one and `$str[4]` is going to be the
50 // first byte of the third character. The fourth and the last one
51 // (which is actually a new line) aren't going to be processed at
52 // all.
53 $len = $str->length();
55 $lines = [0];
56 /** @infection-ignore-all */
57 for ($i = 0; $i < $len; ++$i) {
58 if ($str[$i] !== "\n") {
59 continue;
62 $lines[] = $i + 1;
65 return $lines;
68 /**
69 * Computes the number of the line and column given an absolute position.
71 * @param list<int> $lines The starting position of each line.
72 * @param int $pos The absolute position
74 * @return array{int, int}
76 public static function findLineNumberAndColumn(array $lines, int $pos): array
78 $line = 0;
79 foreach ($lines as $lineNo => $lineStart) {
80 if ($lineStart > $pos) {
81 break;
84 $line = $lineNo;
87 return [$line, $pos - $lines[$line]];
90 /**
91 * Runs the linting process.
93 * @param string $query The query to be checked.
95 * @return (string|int)[][]
97 public static function lint(string $query): array
99 // Disabling lint for huge queries to save some resources.
100 if (mb_strlen($query) > 10000) {
101 return [
103 'message' => __('Linting is disabled for this query because it exceeds the maximum length.'),
104 'fromLine' => 0,
105 'fromColumn' => 0,
106 'toLine' => 0,
107 'toColumn' => 0,
108 'severity' => 'warning',
114 * Lexer used for tokenizing the query.
116 $lexer = new Lexer($query);
119 * Parsed used for analysing the query.
121 $parser = new Parser($lexer->list);
124 * Array containing all errors.
126 $errors = ParserError::get([$lexer, $parser]);
129 * The response containing of all errors.
131 $response = [];
134 * The starting position for each line.
136 * CodeMirror requires relative position to line, but the parser stores
137 * only the absolute position of the character in string.
139 $lines = static::getLines($query);
141 // Building the response.
142 foreach ($errors as $error) {
143 // Starting position of the string that caused the error.
144 [$fromLine, $fromColumn] = static::findLineNumberAndColumn($lines, (int) $error[3]);
146 // Ending position of the string that caused the error.
147 [$toLine, $toColumn] = static::findLineNumberAndColumn(
148 $lines,
149 $error[3] + mb_strlen((string) $error[2]),
152 // Building the response.
153 $response[] = [
154 'message' => sprintf(
155 __('%1$s (near <code>%2$s</code>)'),
156 htmlspecialchars((string) $error[0]),
157 htmlspecialchars((string) $error[2]),
159 'fromLine' => $fromLine,
160 'fromColumn' => $fromColumn,
161 'toLine' => $toLine,
162 'toColumn' => $toColumn,
163 'severity' => 'error',
167 // Sending back the answer.
168 return $response;