Linter.php 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. <?php
  2. /* vim: set expandtab sw=4 ts=4 sts=4: */
  3. /**
  4. * Analyzes a query and gives user feedback.
  5. *
  6. * @package PhpMyAdmin
  7. */
  8. declare(strict_types=1);
  9. 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;
  14. /**
  15. * The linter itself.
  16. *
  17. * @package PhpMyAdmin
  18. */
  19. class Linter
  20. {
  21. /**
  22. * Gets the starting position of each line.
  23. *
  24. * @param string $str String to be analyzed.
  25. *
  26. * @return array
  27. */
  28. public static function getLines($str)
  29. {
  30. if ((! ($str instanceof UtfString))
  31. && defined('USE_UTF_STRINGS')
  32. && USE_UTF_STRINGS
  33. ) {
  34. // If the lexer uses UtfString for processing then the position will
  35. // represent the position of the character and not the position of
  36. // the byte.
  37. $str = new UtfString($str);
  38. }
  39. // The reason for using the strlen is that the length
  40. // required is the length in bytes, not characters.
  41. //
  42. // Given the following string: `????+`, where `?` represents a
  43. // multi-byte character (lets assume that every `?` is a 2-byte
  44. // character) and `+` is a newline, the first value of `$i` is `0`
  45. // and the last one is `4` (because there are 5 characters). Bytes
  46. // `$str[0]` and `$str[1]` are the first character, `$str[2]` and
  47. // `$str[3]` are the second one and `$str[4]` is going to be the
  48. // first byte of the third character. The fourth and the last one
  49. // (which is actually a new line) aren't going to be processed at
  50. // all.
  51. $len = ($str instanceof UtfString) ?
  52. $str->length() : strlen($str);
  53. $lines = [0];
  54. for ($i = 0; $i < $len; ++$i) {
  55. if ($str[$i] === "\n") {
  56. $lines[] = $i + 1;
  57. }
  58. }
  59. return $lines;
  60. }
  61. /**
  62. * Computes the number of the line and column given an absolute position.
  63. *
  64. * @param array $lines The starting position of each line.
  65. * @param int $pos The absolute position
  66. *
  67. * @return array
  68. */
  69. public static function findLineNumberAndColumn(array $lines, $pos)
  70. {
  71. $line = 0;
  72. foreach ($lines as $lineNo => $lineStart) {
  73. if ($lineStart > $pos) {
  74. break;
  75. }
  76. $line = $lineNo;
  77. }
  78. return [
  79. $line,
  80. $pos - $lines[$line],
  81. ];
  82. }
  83. /**
  84. * Runs the linting process.
  85. *
  86. * @param string $query The query to be checked.
  87. *
  88. * @return array
  89. */
  90. public static function lint($query)
  91. {
  92. // Disabling lint for huge queries to save some resources.
  93. if (mb_strlen($query) > 10000) {
  94. return [
  95. [
  96. 'message' => __(
  97. 'Linting is disabled for this query because it exceeds the '
  98. . 'maximum length.'
  99. ),
  100. 'fromLine' => 0,
  101. 'fromColumn' => 0,
  102. 'toLine' => 0,
  103. 'toColumn' => 0,
  104. 'severity' => 'warning',
  105. ],
  106. ];
  107. }
  108. /**
  109. * Lexer used for tokenizing the query.
  110. *
  111. * @var Lexer
  112. */
  113. $lexer = new Lexer($query);
  114. /**
  115. * Parsed used for analysing the query.
  116. *
  117. * @var Parser
  118. */
  119. $parser = new Parser($lexer->list);
  120. /**
  121. * Array containing all errors.
  122. *
  123. * @var array
  124. */
  125. $errors = ParserError::get([$lexer, $parser]);
  126. /**
  127. * The response containing of all errors.
  128. *
  129. * @var array
  130. */
  131. $response = [];
  132. /**
  133. * The starting position for each line.
  134. *
  135. * CodeMirror requires relative position to line, but the parser stores
  136. * only the absolute position of the character in string.
  137. *
  138. * @var array
  139. */
  140. $lines = static::getLines($query);
  141. // Building the response.
  142. foreach ($errors as $idx => $error) {
  143. // Starting position of the string that caused the error.
  144. list($fromLine, $fromColumn) = static::findLineNumberAndColumn(
  145. $lines,
  146. $error[3]
  147. );
  148. // Ending position of the string that caused the error.
  149. list($toLine, $toColumn) = static::findLineNumberAndColumn(
  150. $lines,
  151. $error[3] + mb_strlen((string) $error[2])
  152. );
  153. // Building the response.
  154. $response[] = [
  155. 'message' => sprintf(
  156. __('%1$s (near <code>%2$s</code>)'),
  157. htmlspecialchars((string) $error[0]),
  158. htmlspecialchars((string) $error[2])
  159. ),
  160. 'fromLine' => $fromLine,
  161. 'fromColumn' => $fromColumn,
  162. 'toLine' => $toLine,
  163. 'toColumn' => $toColumn,
  164. 'severity' => 'error',
  165. ];
  166. }
  167. // Sending back the answer.
  168. return $response;
  169. }
  170. }