Parser.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. <?php
  2. namespace JmesPath;
  3. use JmesPath\Lexer as T;
  4. /**
  5. * JMESPath Pratt parser
  6. * @link http://hall.org.ua/halls/wizzard/pdf/Vaughan.Pratt.TDOP.pdf
  7. */
  8. class Parser
  9. {
  10. /** @var Lexer */
  11. private $lexer;
  12. private $tokens;
  13. private $token;
  14. private $tpos;
  15. private $expression;
  16. private static $nullToken = ['type' => T::T_EOF];
  17. private static $currentNode = ['type' => T::T_CURRENT];
  18. private static $bp = [
  19. T::T_EOF => 0,
  20. T::T_QUOTED_IDENTIFIER => 0,
  21. T::T_IDENTIFIER => 0,
  22. T::T_RBRACKET => 0,
  23. T::T_RPAREN => 0,
  24. T::T_COMMA => 0,
  25. T::T_RBRACE => 0,
  26. T::T_NUMBER => 0,
  27. T::T_CURRENT => 0,
  28. T::T_EXPREF => 0,
  29. T::T_COLON => 0,
  30. T::T_PIPE => 1,
  31. T::T_OR => 2,
  32. T::T_AND => 3,
  33. T::T_COMPARATOR => 5,
  34. T::T_FLATTEN => 9,
  35. T::T_STAR => 20,
  36. T::T_FILTER => 21,
  37. T::T_DOT => 40,
  38. T::T_NOT => 45,
  39. T::T_LBRACE => 50,
  40. T::T_LBRACKET => 55,
  41. T::T_LPAREN => 60,
  42. ];
  43. /** @var array Acceptable tokens after a dot token */
  44. private static $afterDot = [
  45. T::T_IDENTIFIER => true, // foo.bar
  46. T::T_QUOTED_IDENTIFIER => true, // foo."bar"
  47. T::T_STAR => true, // foo.*
  48. T::T_LBRACE => true, // foo[1]
  49. T::T_LBRACKET => true, // foo{a: 0}
  50. T::T_FILTER => true, // foo.[?bar==10]
  51. ];
  52. /**
  53. * @param Lexer $lexer Lexer used to tokenize expressions
  54. */
  55. public function __construct(Lexer $lexer = null)
  56. {
  57. $this->lexer = $lexer ?: new Lexer();
  58. }
  59. /**
  60. * Parses a JMESPath expression into an AST
  61. *
  62. * @param string $expression JMESPath expression to compile
  63. *
  64. * @return array Returns an array based AST
  65. * @throws SyntaxErrorException
  66. */
  67. public function parse($expression)
  68. {
  69. $this->expression = $expression;
  70. $this->tokens = $this->lexer->tokenize($expression);
  71. $this->tpos = -1;
  72. $this->next();
  73. $result = $this->expr();
  74. if ($this->token['type'] === T::T_EOF) {
  75. return $result;
  76. }
  77. throw $this->syntax('Did not reach the end of the token stream');
  78. }
  79. /**
  80. * Parses an expression while rbp < lbp.
  81. *
  82. * @param int $rbp Right bound precedence
  83. *
  84. * @return array
  85. */
  86. private function expr($rbp = 0)
  87. {
  88. $left = $this->{"nud_{$this->token['type']}"}();
  89. while ($rbp < self::$bp[$this->token['type']]) {
  90. $left = $this->{"led_{$this->token['type']}"}($left);
  91. }
  92. return $left;
  93. }
  94. private function nud_identifier()
  95. {
  96. $token = $this->token;
  97. $this->next();
  98. return ['type' => 'field', 'value' => $token['value']];
  99. }
  100. private function nud_quoted_identifier()
  101. {
  102. $token = $this->token;
  103. $this->next();
  104. $this->assertNotToken(T::T_LPAREN);
  105. return ['type' => 'field', 'value' => $token['value']];
  106. }
  107. private function nud_current()
  108. {
  109. $this->next();
  110. return self::$currentNode;
  111. }
  112. private function nud_literal()
  113. {
  114. $token = $this->token;
  115. $this->next();
  116. return ['type' => 'literal', 'value' => $token['value']];
  117. }
  118. private function nud_expref()
  119. {
  120. $this->next();
  121. return ['type' => T::T_EXPREF, 'children' => [$this->expr(self::$bp[T::T_EXPREF])]];
  122. }
  123. private function nud_not()
  124. {
  125. $this->next();
  126. return ['type' => T::T_NOT, 'children' => [$this->expr(self::$bp[T::T_NOT])]];
  127. }
  128. private function nud_lparen() {
  129. $this->next();
  130. $result = $this->expr(0);
  131. if ($this->token['type'] !== T::T_RPAREN) {
  132. throw $this->syntax('Unclosed `(`');
  133. }
  134. $this->next();
  135. return $result;
  136. }
  137. private function nud_lbrace()
  138. {
  139. static $validKeys = [T::T_QUOTED_IDENTIFIER => true, T::T_IDENTIFIER => true];
  140. $this->next($validKeys);
  141. $pairs = [];
  142. do {
  143. $pairs[] = $this->parseKeyValuePair();
  144. if ($this->token['type'] == T::T_COMMA) {
  145. $this->next($validKeys);
  146. }
  147. } while ($this->token['type'] !== T::T_RBRACE);
  148. $this->next();
  149. return['type' => 'multi_select_hash', 'children' => $pairs];
  150. }
  151. private function nud_flatten()
  152. {
  153. return $this->led_flatten(self::$currentNode);
  154. }
  155. private function nud_filter()
  156. {
  157. return $this->led_filter(self::$currentNode);
  158. }
  159. private function nud_star()
  160. {
  161. return $this->parseWildcardObject(self::$currentNode);
  162. }
  163. private function nud_lbracket()
  164. {
  165. $this->next();
  166. $type = $this->token['type'];
  167. if ($type == T::T_NUMBER || $type == T::T_COLON) {
  168. return $this->parseArrayIndexExpression();
  169. } elseif ($type == T::T_STAR && $this->lookahead() == T::T_RBRACKET) {
  170. return $this->parseWildcardArray();
  171. } else {
  172. return $this->parseMultiSelectList();
  173. }
  174. }
  175. private function led_lbracket(array $left)
  176. {
  177. static $nextTypes = [T::T_NUMBER => true, T::T_COLON => true, T::T_STAR => true];
  178. $this->next($nextTypes);
  179. switch ($this->token['type']) {
  180. case T::T_NUMBER:
  181. case T::T_COLON:
  182. return [
  183. 'type' => 'subexpression',
  184. 'children' => [$left, $this->parseArrayIndexExpression()]
  185. ];
  186. default:
  187. return $this->parseWildcardArray($left);
  188. }
  189. }
  190. private function led_flatten(array $left)
  191. {
  192. $this->next();
  193. return [
  194. 'type' => 'projection',
  195. 'from' => 'array',
  196. 'children' => [
  197. ['type' => T::T_FLATTEN, 'children' => [$left]],
  198. $this->parseProjection(self::$bp[T::T_FLATTEN])
  199. ]
  200. ];
  201. }
  202. private function led_dot(array $left)
  203. {
  204. $this->next(self::$afterDot);
  205. if ($this->token['type'] == T::T_STAR) {
  206. return $this->parseWildcardObject($left);
  207. }
  208. return [
  209. 'type' => 'subexpression',
  210. 'children' => [$left, $this->parseDot(self::$bp[T::T_DOT])]
  211. ];
  212. }
  213. private function led_or(array $left)
  214. {
  215. $this->next();
  216. return [
  217. 'type' => T::T_OR,
  218. 'children' => [$left, $this->expr(self::$bp[T::T_OR])]
  219. ];
  220. }
  221. private function led_and(array $left)
  222. {
  223. $this->next();
  224. return [
  225. 'type' => T::T_AND,
  226. 'children' => [$left, $this->expr(self::$bp[T::T_AND])]
  227. ];
  228. }
  229. private function led_pipe(array $left)
  230. {
  231. $this->next();
  232. return [
  233. 'type' => T::T_PIPE,
  234. 'children' => [$left, $this->expr(self::$bp[T::T_PIPE])]
  235. ];
  236. }
  237. private function led_lparen(array $left)
  238. {
  239. $args = [];
  240. $this->next();
  241. while ($this->token['type'] != T::T_RPAREN) {
  242. $args[] = $this->expr(0);
  243. if ($this->token['type'] == T::T_COMMA) {
  244. $this->next();
  245. }
  246. }
  247. $this->next();
  248. return [
  249. 'type' => 'function',
  250. 'value' => $left['value'],
  251. 'children' => $args
  252. ];
  253. }
  254. private function led_filter(array $left)
  255. {
  256. $this->next();
  257. $expression = $this->expr();
  258. if ($this->token['type'] != T::T_RBRACKET) {
  259. throw $this->syntax('Expected a closing rbracket for the filter');
  260. }
  261. $this->next();
  262. $rhs = $this->parseProjection(self::$bp[T::T_FILTER]);
  263. return [
  264. 'type' => 'projection',
  265. 'from' => 'array',
  266. 'children' => [
  267. $left ?: self::$currentNode,
  268. [
  269. 'type' => 'condition',
  270. 'children' => [$expression, $rhs]
  271. ]
  272. ]
  273. ];
  274. }
  275. private function led_comparator(array $left)
  276. {
  277. $token = $this->token;
  278. $this->next();
  279. return [
  280. 'type' => T::T_COMPARATOR,
  281. 'value' => $token['value'],
  282. 'children' => [$left, $this->expr(self::$bp[T::T_COMPARATOR])]
  283. ];
  284. }
  285. private function parseProjection($bp)
  286. {
  287. $type = $this->token['type'];
  288. if (self::$bp[$type] < 10) {
  289. return self::$currentNode;
  290. } elseif ($type == T::T_DOT) {
  291. $this->next(self::$afterDot);
  292. return $this->parseDot($bp);
  293. } elseif ($type == T::T_LBRACKET || $type == T::T_FILTER) {
  294. return $this->expr($bp);
  295. }
  296. throw $this->syntax('Syntax error after projection');
  297. }
  298. private function parseDot($bp)
  299. {
  300. if ($this->token['type'] == T::T_LBRACKET) {
  301. $this->next();
  302. return $this->parseMultiSelectList();
  303. }
  304. return $this->expr($bp);
  305. }
  306. private function parseKeyValuePair()
  307. {
  308. static $validColon = [T::T_COLON => true];
  309. $key = $this->token['value'];
  310. $this->next($validColon);
  311. $this->next();
  312. return [
  313. 'type' => 'key_val_pair',
  314. 'value' => $key,
  315. 'children' => [$this->expr()]
  316. ];
  317. }
  318. private function parseWildcardObject(array $left = null)
  319. {
  320. $this->next();
  321. return [
  322. 'type' => 'projection',
  323. 'from' => 'object',
  324. 'children' => [
  325. $left ?: self::$currentNode,
  326. $this->parseProjection(self::$bp[T::T_STAR])
  327. ]
  328. ];
  329. }
  330. private function parseWildcardArray(array $left = null)
  331. {
  332. static $getRbracket = [T::T_RBRACKET => true];
  333. $this->next($getRbracket);
  334. $this->next();
  335. return [
  336. 'type' => 'projection',
  337. 'from' => 'array',
  338. 'children' => [
  339. $left ?: self::$currentNode,
  340. $this->parseProjection(self::$bp[T::T_STAR])
  341. ]
  342. ];
  343. }
  344. /**
  345. * Parses an array index expression (e.g., [0], [1:2:3]
  346. */
  347. private function parseArrayIndexExpression()
  348. {
  349. static $matchNext = [
  350. T::T_NUMBER => true,
  351. T::T_COLON => true,
  352. T::T_RBRACKET => true
  353. ];
  354. $pos = 0;
  355. $parts = [null, null, null];
  356. $expected = $matchNext;
  357. do {
  358. if ($this->token['type'] == T::T_COLON) {
  359. $pos++;
  360. $expected = $matchNext;
  361. } elseif ($this->token['type'] == T::T_NUMBER) {
  362. $parts[$pos] = $this->token['value'];
  363. $expected = [T::T_COLON => true, T::T_RBRACKET => true];
  364. }
  365. $this->next($expected);
  366. } while ($this->token['type'] != T::T_RBRACKET);
  367. // Consume the closing bracket
  368. $this->next();
  369. if ($pos === 0) {
  370. // No colons were found so this is a simple index extraction
  371. return ['type' => 'index', 'value' => $parts[0]];
  372. }
  373. if ($pos > 2) {
  374. throw $this->syntax('Invalid array slice syntax: too many colons');
  375. }
  376. // Sliced array from start (e.g., [2:])
  377. return [
  378. 'type' => 'projection',
  379. 'from' => 'array',
  380. 'children' => [
  381. ['type' => 'slice', 'value' => $parts],
  382. $this->parseProjection(self::$bp[T::T_STAR])
  383. ]
  384. ];
  385. }
  386. private function parseMultiSelectList()
  387. {
  388. $nodes = [];
  389. do {
  390. $nodes[] = $this->expr();
  391. if ($this->token['type'] == T::T_COMMA) {
  392. $this->next();
  393. $this->assertNotToken(T::T_RBRACKET);
  394. }
  395. } while ($this->token['type'] !== T::T_RBRACKET);
  396. $this->next();
  397. return ['type' => 'multi_select_list', 'children' => $nodes];
  398. }
  399. private function syntax($msg)
  400. {
  401. return new SyntaxErrorException($msg, $this->token, $this->expression);
  402. }
  403. private function lookahead()
  404. {
  405. return (!isset($this->tokens[$this->tpos + 1]))
  406. ? T::T_EOF
  407. : $this->tokens[$this->tpos + 1]['type'];
  408. }
  409. private function next(array $match = null)
  410. {
  411. if (!isset($this->tokens[$this->tpos + 1])) {
  412. $this->token = self::$nullToken;
  413. } else {
  414. $this->token = $this->tokens[++$this->tpos];
  415. }
  416. if ($match && !isset($match[$this->token['type']])) {
  417. throw $this->syntax($match);
  418. }
  419. }
  420. private function assertNotToken($type)
  421. {
  422. if ($this->token['type'] == $type) {
  423. throw $this->syntax("Token {$this->tpos} not allowed to be $type");
  424. }
  425. }
  426. /**
  427. * @internal Handles undefined tokens without paying the cost of validation
  428. */
  429. public function __call($method, $args)
  430. {
  431. $prefix = substr($method, 0, 4);
  432. if ($prefix == 'nud_' || $prefix == 'led_') {
  433. $token = substr($method, 4);
  434. $message = "Unexpected \"$token\" token ($method). Expected one of"
  435. . " the following tokens: "
  436. . implode(', ', array_map(function ($i) {
  437. return '"' . substr($i, 4) . '"';
  438. }, array_filter(
  439. get_class_methods($this),
  440. function ($i) use ($prefix) {
  441. return strpos($i, $prefix) === 0;
  442. }
  443. )));
  444. throw $this->syntax($message);
  445. }
  446. throw new \BadMethodCallException("Call to undefined method $method");
  447. }
  448. }