From 9d98fa25ec904c5702959d7d06b6f556a707f796 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 1 Sep 2013 21:41:49 +0200 Subject: [PATCH] [ExpressionLanguage] added the component --- composer.json | 1 + .../Component/ExpressionLanguage/.gitignore | 3 + .../Component/ExpressionLanguage/CHANGELOG.md | 7 + .../Component/ExpressionLanguage/Compiler.php | 148 ++++++++ .../ExpressionLanguage/Expression.php | 42 +++ .../ExpressionLanguage/ExpressionLanguage.php | 103 +++++ .../Component/ExpressionLanguage/LICENSE | 19 + .../Component/ExpressionLanguage/Lexer.php | 119 ++++++ .../ExpressionLanguage/Node/ArgumentsNode.php | 22 ++ .../ExpressionLanguage/Node/ArrayNode.php | 85 +++++ .../ExpressionLanguage/Node/BinaryNode.php | 129 +++++++ .../Node/ConditionalNode.php | 44 +++ .../ExpressionLanguage/Node/ConstantNode.php | 32 ++ .../ExpressionLanguage/Node/FunctionNode.php | 45 +++ .../ExpressionLanguage/Node/GetAttrNode.php | 90 +++++ .../ExpressionLanguage/Node/NameNode.php | 32 ++ .../ExpressionLanguage/Node/Node.php | 78 ++++ .../ExpressionLanguage/Node/UnaryNode.php | 54 +++ .../Component/ExpressionLanguage/Parser.php | 356 ++++++++++++++++++ .../Component/ExpressionLanguage/README.md | 43 +++ .../ExpressionLanguage/SyntaxError.php | 20 + .../ExpressionLanguage/Tests/LexerTest.php | 75 ++++ .../Tests/Node/AbstractNodeTest.php | 39 ++ .../Tests/Node/ArgumentsNodeTest.php | 30 ++ .../Tests/Node/ArrayNodeTest.php | 46 +++ .../Tests/Node/BinaryNodeTest.php | 113 ++++++ .../Tests/Node/ConditionalNodeTest.php | 34 ++ .../Tests/Node/ConstantNodeTest.php | 43 +++ .../Tests/Node/FunctionNodeTest.php | 45 +++ .../Tests/Node/GetAttrNodeTest.php | 64 ++++ .../Tests/Node/NameNodeTest.php | 31 ++ .../Tests/Node/NodeTest.php | 30 ++ .../Tests/Node/UnaryNodeTest.php | 38 ++ .../ExpressionLanguage/Tests/ParserTest.php | 143 +++++++ .../Component/ExpressionLanguage/Token.php | 68 ++++ .../ExpressionLanguage/TokenStream.php | 83 ++++ .../ExpressionLanguage/composer.json | 31 ++ .../ExpressionLanguage/phpunit.xml.dist | 29 ++ 38 files changed, 2414 insertions(+) create mode 100644 src/Symfony/Component/ExpressionLanguage/.gitignore create mode 100644 src/Symfony/Component/ExpressionLanguage/CHANGELOG.md create mode 100644 src/Symfony/Component/ExpressionLanguage/Compiler.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Expression.php create mode 100644 src/Symfony/Component/ExpressionLanguage/ExpressionLanguage.php create mode 100644 src/Symfony/Component/ExpressionLanguage/LICENSE create mode 100644 src/Symfony/Component/ExpressionLanguage/Lexer.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Node/ArgumentsNode.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Node/ArrayNode.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Node/BinaryNode.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Node/ConditionalNode.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Node/ConstantNode.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Node/FunctionNode.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Node/GetAttrNode.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Node/NameNode.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Node/Node.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Node/UnaryNode.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Parser.php create mode 100644 src/Symfony/Component/ExpressionLanguage/README.md create mode 100644 src/Symfony/Component/ExpressionLanguage/SyntaxError.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Tests/LexerTest.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Tests/Node/AbstractNodeTest.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Tests/Node/ArgumentsNodeTest.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Tests/Node/ArrayNodeTest.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Tests/Node/BinaryNodeTest.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Tests/Node/ConditionalNodeTest.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Tests/Node/ConstantNodeTest.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Tests/Node/FunctionNodeTest.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Tests/Node/GetAttrNodeTest.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Tests/Node/NameNodeTest.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Tests/Node/NodeTest.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Tests/Node/UnaryNodeTest.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Tests/ParserTest.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Token.php create mode 100644 src/Symfony/Component/ExpressionLanguage/TokenStream.php create mode 100644 src/Symfony/Component/ExpressionLanguage/composer.json create mode 100644 src/Symfony/Component/ExpressionLanguage/phpunit.xml.dist diff --git a/composer.json b/composer.json index a3098c8585..4e75824493 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,7 @@ "symfony/doctrine-bridge": "self.version", "symfony/dom-crawler": "self.version", "symfony/event-dispatcher": "self.version", + "symfony/expression-language": "self.version", "symfony/filesystem": "self.version", "symfony/finder": "self.version", "symfony/form": "self.version", diff --git a/src/Symfony/Component/ExpressionLanguage/.gitignore b/src/Symfony/Component/ExpressionLanguage/.gitignore new file mode 100644 index 0000000000..c49a5d8df5 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/ExpressionLanguage/CHANGELOG.md b/src/Symfony/Component/ExpressionLanguage/CHANGELOG.md new file mode 100644 index 0000000000..eacb5a60fb --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +2.4.0 +----- + + * added the component diff --git a/src/Symfony/Component/ExpressionLanguage/Compiler.php b/src/Symfony/Component/ExpressionLanguage/Compiler.php new file mode 100644 index 0000000000..d9f2969647 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Compiler.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage; + +/** + * Compiles a node to PHP code. + * + * @author Fabien Potencier + */ +class Compiler +{ + private $source; + private $functions; + + public function __construct(array $functions) + { + $this->functions = $functions; + } + + public function getFunction($name) + { + return $this->functions[$name]; + } + + /** + * Gets the current PHP code after compilation. + * + * @return string The PHP code + */ + public function getSource() + { + return $this->source; + } + + public function reset() + { + $this->source = ''; + + return $this; + } + + /** + * Compiles a node. + * + * @param Node\Node $node The node to compile + * + * @return Compiler The current compiler instance + */ + public function compile(Node\Node $node) + { + $node->compile($this); + + return $this; + } + + public function subcompile(Node\Node $node) + { + $current = $this->source; + $this->source = ''; + + $node->compile($this); + + $source = $this->source; + $this->source = $current; + + return $source; + } + + /** + * Adds a raw string to the compiled code. + * + * @param string $string The string + * + * @return Compiler The current compiler instance + */ + public function raw($string) + { + $this->source .= $string; + + return $this; + } + + /** + * Adds a quoted string to the compiled code. + * + * @param string $value The string + * + * @return Compiler The current compiler instance + */ + public function string($value) + { + $this->source .= sprintf('"%s"', addcslashes($value, "\0\t\"\$\\")); + + return $this; + } + + /** + * Returns a PHP representation of a given value. + * + * @param mixed $value The value to convert + * + * @return Compiler The current compiler instance + */ + public function repr($value) + { + if (is_int($value) || is_float($value)) { + if (false !== $locale = setlocale(LC_NUMERIC, 0)) { + setlocale(LC_NUMERIC, 'C'); + } + + $this->raw($value); + + if (false !== $locale) { + setlocale(LC_NUMERIC, $locale); + } + } elseif (null === $value) { + $this->raw('null'); + } elseif (is_bool($value)) { + $this->raw($value ? 'true' : 'false'); + } elseif (is_array($value)) { + $this->raw('array('); + $first = true; + foreach ($value as $key => $value) { + if (!$first) { + $this->raw(', '); + } + $first = false; + $this->repr($key); + $this->raw(' => '); + $this->repr($value); + } + $this->raw(')'); + } else { + $this->string($value); + } + + return $this; + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Expression.php b/src/Symfony/Component/ExpressionLanguage/Expression.php new file mode 100644 index 0000000000..a1ddc362bc --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Expression.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage; + +/** + * Represents an expression. + * + * @author Fabien Potencier + */ +class Expression +{ + private $expression; + + /** + * Constructor. + * + * @param string $expression An expression + */ + public function __construct($expression) + { + $this->expression = (string) $expression; + } + + /** + * Gets the expression. + * + * @return string The expression + */ + public function __toString() + { + return $this->expression; + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/ExpressionLanguage.php b/src/Symfony/Component/ExpressionLanguage/ExpressionLanguage.php new file mode 100644 index 0000000000..dfd8018c57 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/ExpressionLanguage.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage; + +/** + * Allows to compile and evaluate expressions written in your own DSL. + * + * @author Fabien Potencier + */ +class ExpressionLanguage +{ + private $lexer; + private $parser; + private $compiler; + private $cache; + + protected $functions; + + public function __construct() + { + $this->functions = array(); + $this->registerFunctions(); + } + + /** + * Compiles an expression source code. + * + * @param string $expression The expression to compile + * @param array $names An array of valid names + * + * @return string The compiled PHP source code + */ + public function compile($expression, $names = array()) + { + return $this->getCompiler()->compile($this->parse($expression, $names))->getSource(); + } + + public function evaluate($expression, $values = array()) + { + return $this->parse($expression, array_keys($values))->evaluate($this->functions, $values); + } + + public function addFunction($name, $compiler, $evaluator) + { + $this->functions[$name] = array('compiler' => $compiler, 'evaluator' => $evaluator); + } + + protected function registerFunctions() + { + $this->addFunction('constant', function ($constant) { + return sprintf('constant(%s)', $constant); + }, function (array $values, $constant) { + return constant($constant); + }); + } + + private function getLexer() + { + if (null === $this->lexer) { + $this->lexer = new Lexer(); + } + + return $this->lexer; + } + + private function getParser() + { + if (null === $this->parser) { + $this->parser = new Parser($this->functions); + } + + return $this->parser; + } + + private function getCompiler() + { + if (null === $this->compiler) { + $this->compiler = new Compiler($this->functions); + } + + return $this->compiler->reset(); + } + + private function parse($expression, $names) + { + $key = $expression.'//'.implode('-', $names); + + if (!isset($this->cache[$key])) { + $this->cache[$key] = $this->getParser()->parse($this->getLexer()->tokenize((string) $expression), $names); + } + + return $this->cache[$key]; + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/LICENSE b/src/Symfony/Component/ExpressionLanguage/LICENSE new file mode 100644 index 0000000000..88a57f8d8d --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2013 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/ExpressionLanguage/Lexer.php b/src/Symfony/Component/ExpressionLanguage/Lexer.php new file mode 100644 index 0000000000..1c573e8f59 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Lexer.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage; + +/** + * Lexes an expression. + * + * @author Fabien Potencier + */ +class Lexer +{ + /** + * Tokenizes an expression. + * + * @param string $expression The expression to tokenize + * + * @return TokenStream A token stream instance + */ + public function tokenize($expression) + { + $expression = str_replace(array("\r\n", "\r"), "\n", $expression); + $cursor = 0; + $tokens = array(); + $brackets = array(); + $operatorRegex = $this->getOperatorRegex(); + $end = strlen($expression); + + while ($cursor < $end) { + if (preg_match('/\s+/A', $expression, $match, null, $cursor)) { + // whitespace + $cursor += strlen($match[0]); + } elseif (preg_match($operatorRegex, $expression, $match, null, $cursor)) { + // operators + $tokens[] = new Token(Token::OPERATOR_TYPE, $match[0], $cursor + 1); + $cursor += strlen($match[0]); + } elseif (preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A', $expression, $match, null, $cursor)) { + // names + $tokens[] = new Token(Token::NAME_TYPE, $match[0], $cursor + 1); + $cursor += strlen($match[0]); + } elseif (preg_match('/[0-9]+(?:\.[0-9]+)?/A', $expression, $match, null, $cursor)) { + // numbers + $number = (float) $match[0]; // floats + if (ctype_digit($match[0]) && $number <= PHP_INT_MAX) { + $number = (int) $match[0]; // integers lower than the maximum + } + $tokens[] = new Token(Token::NUMBER_TYPE, $number, $cursor + 1); + $cursor += strlen($match[0]); + } elseif (false !== strpos('([{', $expression[$cursor])) { + // opening bracket + $brackets[] = array($expression[$cursor], $cursor); + + $tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1); + ++$cursor; + } elseif (false !== strpos(')]}', $expression[$cursor])) { + // closing bracket + if (empty($brackets)) { + throw new SyntaxError(sprintf('Unexpected "%s"', $expression[$cursor]), $cursor); + } + + list($expect, $cur) = array_pop($brackets); + if ($expression[$cursor] != strtr($expect, '([{', ')]}')) { + throw new SyntaxError(sprintf('Unclosed "%s"', $expect), $cur); + } + + $tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1); + ++$cursor; + } elseif (false !== strpos('.,?:', $expression[$cursor])) { + // punctuation + $tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1); + ++$cursor; + } elseif (preg_match('/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As', $expression, $match, null, $cursor)) { + // strings + $tokens[] = new Token(Token::STRING_TYPE, stripcslashes(substr($match[0], 1, -1)), $cursor + 1); + $cursor += strlen($match[0]); + } else { + // unlexable + throw new SyntaxError(sprintf('Unexpected character "%s"', $expression[$cursor]), $cursor); + } + } + + $tokens[] = new Token(Token::EOF_TYPE, null, $cursor + 1); + + if (!empty($brackets)) { + list($expect, $cur) = array_pop($brackets); + throw new SyntaxError(sprintf('Unclosed "%s"', $expect), $cur); + } + + return new TokenStream($tokens); + } + + private function getOperatorRegex() + { + $operators = array( + 'not', '!', '-', '+', + 'or', '||', '&&', 'and', '|', '^', '&', '==', '===', '!=', '!==', '<', '>', '>=', '<=', 'not in', 'in', '..', '+', '-', '~', '*', '/', '%', '**', + ); + + $operators = array_combine($operators, array_map('strlen', $operators)); + arsort($operators); + + $regex = array(); + foreach ($operators as $operator => $length) { + // an operator that ends with a character must be followed by + // a whitespace or a parenthesis + $regex[] = preg_quote($operator, '/').(ctype_alpha($operator[$length - 1]) ? '(?=[\s()])' : ''); + } + + return '/'.implode('|', $regex).'/A'; + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Node/ArgumentsNode.php b/src/Symfony/Component/ExpressionLanguage/Node/ArgumentsNode.php new file mode 100644 index 0000000000..f440101684 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Node/ArgumentsNode.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +class ArgumentsNode extends ArrayNode +{ + public function compile(Compiler $compiler) + { + $this->compileArguments($compiler, false); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Node/ArrayNode.php b/src/Symfony/Component/ExpressionLanguage/Node/ArrayNode.php new file mode 100644 index 0000000000..e4084f5b82 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Node/ArrayNode.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +class ArrayNode extends Node +{ + protected $index; + + public function __construct() + { + $this->index = -1; + } + + public function addElement(Node $value, Node $key = null) + { + if (null === $key) { + $key = new ConstantNode(++$this->index); + } + + array_push($this->nodes, $key, $value); + } + + /** + * Compiles the node to PHP. + * + * @param Compiler A Compiler instance + */ + public function compile(Compiler $compiler) + { + $compiler->raw('array('); + $this->compileArguments($compiler); + $compiler->raw(')'); + } + + public function evaluate($functions, $values) + { + $result = array(); + foreach ($this->getKeyValuePairs() as $pair) { + $result[$pair['key']->evaluate($functions, $values)] = $pair['value']->evaluate($functions, $values); + } + + return $result; + } + + protected function getKeyValuePairs() + { + $pairs = array(); + foreach (array_chunk($this->nodes, 2) as $pair) { + $pairs[] = array('key' => $pair[0], 'value' => $pair[1]); + } + + return $pairs; + } + + protected function compileArguments(Compiler $compiler, $withKeys = true) + { + $first = true; + foreach ($this->getKeyValuePairs() as $pair) { + if (!$first) { + $compiler->raw(', '); + } + $first = false; + + if ($withKeys) { + $compiler + ->compile($pair['key']) + ->raw(' => ') + ; + } + + $compiler->compile($pair['value']); + } + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Node/BinaryNode.php b/src/Symfony/Component/ExpressionLanguage/Node/BinaryNode.php new file mode 100644 index 0000000000..850376b50f --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Node/BinaryNode.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +class BinaryNode extends Node +{ + private $operators = array( + '~' => '.', + 'and' => '&&', + 'or' => '||', + ); + + private $functions = array( + '**' => 'pow', + '..' => 'range', + 'in' => 'in_array', + 'not in' => '!in_array', + ); + + public function __construct($operator, Node $left, Node $right) + { + $this->nodes = array('left' => $left, 'right' => $right); + $this->attributes = array('operator' => $operator); + } + + public function compile(Compiler $compiler) + { + $operator = $this->attributes['operator']; + + if (isset($this->functions[$operator])) { + $compiler + ->raw(sprintf('%s(', $this->functions[$operator])) + ->compile($this->nodes['left']) + ->raw(', ') + ->compile($this->nodes['right']) + ->raw(')') + ; + + return; + } + + if (isset($this->operators[$operator])) { + $operator = $this->operators[$operator]; + } + + $compiler + ->raw('(') + ->compile($this->nodes['left']) + ->raw(' ') + ->raw($operator) + ->raw(' ') + ->compile($this->nodes['right']) + ->raw(')') + ; + } + + public function evaluate($functions, $values) + { + $operator = $this->attributes['operator']; + $left = $this->nodes['left']->evaluate($functions, $values); + $right = $this->nodes['right']->evaluate($functions, $values); + + if (isset($this->functions[$operator])) { + if ('not in' == $operator) { + return !call_user_func('in_array', $left, $right); + } + + return call_user_func($this->functions[$operator], $left, $right); + } + + switch ($operator) { + case 'or': + case '||': + return $left || $right; + case 'and': + case '&&': + return $left && $right; + case '|': + return $left | $right; + case '^': + return $left ^ $right; + case '&': + return $left & $right; + case '==': + return $left == $right; + case '===': + return $left === $right; + case '!=': + return $left != $right; + case '!==': + return $left !== $right; + case '<': + return $left < $right; + case '>': + return $left > $right; + case '>=': + return $left >= $right; + case '<=': + return $left <= $right; + case 'not in': + return !in_array($left, $right); + case 'in': + return in_array($left, $right); + case '+': + return $left + $right; + case '-': + return $left - $right; + case '~': + return $left.$right; + case '*': + return $left * $right; + case '/': + return $left / $right; + case '%': + return $left % $right; + } + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Node/ConditionalNode.php b/src/Symfony/Component/ExpressionLanguage/Node/ConditionalNode.php new file mode 100644 index 0000000000..b77dcc5aad --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Node/ConditionalNode.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +class ConditionalNode extends Node +{ + public function __construct(Node $expr1, Node $expr2, Node $expr3) + { + $this->nodes = array('expr1' => $expr1, 'expr2' => $expr2, 'expr3' => $expr3); + } + + public function compile(Compiler $compiler) + { + $compiler + ->raw('((') + ->compile($this->nodes['expr1']) + ->raw(') ? (') + ->compile($this->nodes['expr2']) + ->raw(') : (') + ->compile($this->nodes['expr3']) + ->raw('))') + ; + } + + public function evaluate($functions, $values) + { + if ($this->nodes['expr1']->evaluate($functions, $values)) { + return $this->nodes['expr2']->evaluate($functions, $values); + } + + return $this->nodes['expr3']->evaluate($functions, $values); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Node/ConstantNode.php b/src/Symfony/Component/ExpressionLanguage/Node/ConstantNode.php new file mode 100644 index 0000000000..fcc1ce6fb6 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Node/ConstantNode.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +class ConstantNode extends Node +{ + public function __construct($value) + { + $this->attributes = array('value' => $value); + } + + public function compile(Compiler $compiler) + { + $compiler->repr($this->attributes['value']); + } + + public function evaluate($functions, $values) + { + return $this->attributes['value']; + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Node/FunctionNode.php b/src/Symfony/Component/ExpressionLanguage/Node/FunctionNode.php new file mode 100644 index 0000000000..a7b090591a --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Node/FunctionNode.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +class FunctionNode extends Node +{ + public function __construct($name, Node $arguments) + { + $this->nodes = array('arguments' => $arguments); + $this->attributes = array('name' => $name); + } + + public function compile(Compiler $compiler) + { + $arguments = array(); + foreach ($this->nodes['arguments']->nodes as $node) { + $arguments[] = $compiler->subcompile($node); + } + + $function = $compiler->getFunction($this->attributes['name']); + + $compiler->raw(call_user_func_array($function['compiler'], $arguments)); + } + + public function evaluate($functions, $values) + { + $arguments = array($values); + foreach ($this->nodes['arguments']->nodes as $node) { + $arguments[] = $node->evaluate($functions, $values); + } + + return call_user_func_array($functions[$this->attributes['name']]['evaluator'], $arguments); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Node/GetAttrNode.php b/src/Symfony/Component/ExpressionLanguage/Node/GetAttrNode.php new file mode 100644 index 0000000000..2f1156ca76 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Node/GetAttrNode.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +class GetAttrNode extends Node +{ + const PROPERTY_CALL = 1; + const METHOD_CALL = 2; + const ARRAY_CALL = 3; + + public function __construct(Node $node, Node $attribute, ArrayNode $arguments, $type) + { + $this->nodes = array('node' => $node, 'attribute' => $attribute, 'arguments' => $arguments); + $this->attributes = array('type' => $type); + } + + public function compile(Compiler $compiler) + { + switch ($this->attributes['type']) { + case self::PROPERTY_CALL: + $compiler + ->compile($this->nodes['node']) + ->raw('->') + ->raw($this->nodes['attribute']->attributes['value']) + ; + break; + + case self::METHOD_CALL: + $compiler + ->compile($this->nodes['node']) + ->raw('->') + ->raw($this->nodes['attribute']->attributes['value']) + ->raw('(') + ->compile($this->nodes['arguments']) + ->raw(')') + ; + break; + + case self::ARRAY_CALL: + $compiler + ->compile($this->nodes['node']) + ->raw('[') + ->compile($this->nodes['attribute'])->raw(']') + ; + break; + } + } + + public function evaluate($functions, $values) + { + switch ($this->attributes['type']) { + case self::PROPERTY_CALL: + $obj = $this->nodes['node']->evaluate($functions, $values); + if (!is_object($obj)) { + throw new \RuntimeException('Unable to get a property on a non-object.'); + } + + $property = $this->nodes['attribute']->attributes['value']; + + return $obj->$property; + + case self::METHOD_CALL: + $obj = $this->nodes['node']->evaluate($functions, $values); + if (!is_object($obj)) { + throw new \RuntimeException('Unable to get a property on a non-object.'); + } + + return call_user_func_array(array($obj, $this->nodes['attribute']->evaluate($functions, $values)), $this->nodes['arguments']->evaluate($functions, $values)); + + case self::ARRAY_CALL: + $values = $this->nodes['node']->evaluate($functions, $values); + if (!is_array($values) && !$values instanceof \ArrayAccess) { + throw new \RuntimeException('Unable to get an item on a non-array.'); + } + + return $values[$this->nodes['attribute']->evaluate($functions, $values)]; + } + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Node/NameNode.php b/src/Symfony/Component/ExpressionLanguage/Node/NameNode.php new file mode 100644 index 0000000000..dbd0c780d9 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Node/NameNode.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +class NameNode extends Node +{ + public function __construct($name) + { + $this->attributes = array('name' => $name); + } + + public function compile(Compiler $compiler) + { + $compiler->raw('$'.$this->attributes['name']); + } + + public function evaluate($functions, $values) + { + return $values[$this->attributes['name']]; + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Node/Node.php b/src/Symfony/Component/ExpressionLanguage/Node/Node.php new file mode 100644 index 0000000000..da49d6b4b2 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Node/Node.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +/** + * Represents a node in the AST. + * + * @author Fabien Potencier + */ +class Node +{ + public $nodes = array(); + public $attributes = array(); + + /** + * Constructor. + * + * @param array $nodes An array of nodes + * @param array $attributes An array of attributes + */ + public function __construct(array $nodes = array(), array $attributes = array()) + { + $this->nodes = $nodes; + $this->attributes = $attributes; + } + + public function __toString() + { + $attributes = array(); + foreach ($this->attributes as $name => $value) { + $attributes[] = sprintf('%s: %s', $name, str_replace("\n", '', var_export($value, true))); + } + + $repr = array(str_replace('Symfony\Component\ExpressionLanguage\Node\\', '', get_class($this)).'('.implode(', ', $attributes)); + + if (count($this->nodes)) { + foreach ($this->nodes as $node) { + foreach (explode("\n", (string) $node) as $line) { + $repr[] = ' '.$line; + } + } + + $repr[] = ')'; + } else { + $repr[0] .= ')'; + } + + return implode("\n", $repr); + } + + public function compile(Compiler $compiler) + { + foreach ($this->nodes as $node) { + $node->compile($compiler); + } + } + + public function evaluate($functions, $values) + { + $results = array(); + foreach ($this->nodes as $node) { + $results[] = $node->evaluate($functions, $values); + } + + return $results; + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Node/UnaryNode.php b/src/Symfony/Component/ExpressionLanguage/Node/UnaryNode.php new file mode 100644 index 0000000000..3ec88d1179 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Node/UnaryNode.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +class UnaryNode extends Node +{ + private $operators = array( + '!' => '!', + 'not' => '!', + '+' => '+', + '-' => '-', + ); + + public function __construct($operator, Node $node) + { + $this->nodes = array('node' => $node); + $this->attributes = array('operator' => $operator); + } + + public function compile(Compiler $compiler) + { + $compiler + ->raw('(') + ->raw($this->operators[$this->attributes['operator']]) + ->compile($this->nodes['node']) + ->raw(')') + ; + } + + public function evaluate($functions, $values) + { + $value = $this->nodes['node']->evaluate($functions, $values); + switch ($this->attributes['operator']) { + case 'not': + case '!': + return !$value; + case '-': + return -$value; + } + + return $value; + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Parser.php b/src/Symfony/Component/ExpressionLanguage/Parser.php new file mode 100644 index 0000000000..6603dd42fc --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Parser.php @@ -0,0 +1,356 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage; + +/** + * Parsers a token stream. + * + * This parser implements a "Precedence climbing" algorithm. + * + * @see http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm + * @see http://en.wikipedia.org/wiki/Operator-precedence_parser + * + * @author Fabien Potencier + */ +class Parser +{ + const OPERATOR_LEFT = 1; + const OPERATOR_RIGHT = 2; + + private $stream; + private $unaryOperators; + private $binaryOperators; + private $functions; + private $names; + + public function __construct(array $functions) + { + $this->functions = $functions; + + $this->unaryOperators = array( + 'not' => array('precedence' => 50), + '!' => array('precedence' => 50), + '-' => array('precedence' => 500), + '+' => array('precedence' => 500), + ); + $this->binaryOperators = array( + 'or' => array('precedence' => 10, 'associativity' => Parser::OPERATOR_LEFT), + '||' => array('precedence' => 10, 'associativity' => Parser::OPERATOR_LEFT), + 'and' => array('precedence' => 15, 'associativity' => Parser::OPERATOR_LEFT), + '&&' => array('precedence' => 15, 'associativity' => Parser::OPERATOR_LEFT), + '|' => array('precedence' => 16, 'associativity' => Parser::OPERATOR_LEFT), + '^' => array('precedence' => 17, 'associativity' => Parser::OPERATOR_LEFT), + '&' => array('precedence' => 18, 'associativity' => Parser::OPERATOR_LEFT), + '==' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + '===' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + '!=' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + '!==' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + '<' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + '>' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + '>=' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + '<=' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + 'not in' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + 'in' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + '..' => array('precedence' => 25, 'associativity' => Parser::OPERATOR_LEFT), + '+' => array('precedence' => 30, 'associativity' => Parser::OPERATOR_LEFT), + '-' => array('precedence' => 30, 'associativity' => Parser::OPERATOR_LEFT), + '~' => array('precedence' => 40, 'associativity' => Parser::OPERATOR_LEFT), + '*' => array('precedence' => 60, 'associativity' => Parser::OPERATOR_LEFT), + '/' => array('precedence' => 60, 'associativity' => Parser::OPERATOR_LEFT), + '%' => array('precedence' => 60, 'associativity' => Parser::OPERATOR_LEFT), + '**' => array('precedence' => 200, 'associativity' => Parser::OPERATOR_RIGHT), + ); + } + + /** + * Converts a token stream to a node tree. + * + * @param TokenStream $stream A token stream instance + * @param array $names An array of valid names + * + * @return Node A node tree + */ + public function parse(TokenStream $stream, $names = array()) + { + $this->stream = $stream; + $this->names = $names; + + $node = $this->parseExpression(); + if (!$stream->isEOF()) { + throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s"', $stream->current->type, $stream->current->value), $stream->current->cursor); + } + + return $node; + } + + public function parseExpression($precedence = 0) + { + $expr = $this->getPrimary(); + $token = $this->stream->current; + while ($token->test(Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->value]) && $this->binaryOperators[$token->value]['precedence'] >= $precedence) { + $op = $this->binaryOperators[$token->value]; + $this->stream->next(); + + $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']); + $expr = new Node\BinaryNode($token->value, $expr, $expr1); + + $token = $this->stream->current; + } + + if (0 === $precedence) { + return $this->parseConditionalExpression($expr); + } + + return $expr; + } + + protected function getPrimary() + { + $token = $this->stream->current; + + if ($token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->value])) { + $operator = $this->unaryOperators[$token->value]; + $this->stream->next(); + $expr = $this->parseExpression($operator['precedence']); + + return $this->parsePostfixExpression(new Node\UnaryNode($token->value, $expr)); + } + + if ($token->test(Token::PUNCTUATION_TYPE, '(')) { + $this->stream->next(); + $expr = $this->parseExpression(); + $this->stream->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed'); + + return $this->parsePostfixExpression($expr); + } + + return $this->parsePrimaryExpression(); + } + + protected function parseConditionalExpression($expr) + { + while ($this->stream->current->test(Token::PUNCTUATION_TYPE, '?')) { + $this->stream->next(); + if (!$this->stream->current->test(Token::PUNCTUATION_TYPE, ':')) { + $expr2 = $this->parseExpression(); + if ($this->stream->current->test(Token::PUNCTUATION_TYPE, ':')) { + $this->stream->next(); + $expr3 = $this->parseExpression(); + } else { + $expr3 = new Node\ConstantNode(null); + } + } else { + $this->stream->next(); + $expr2 = $expr; + $expr3 = $this->parseExpression(); + } + + $expr = new Node\ConditionalNode($expr, $expr2, $expr3); + } + + return $expr; + } + + public function parsePrimaryExpression() + { + $token = $this->stream->current; + switch ($token->type) { + case Token::NAME_TYPE: + $this->stream->next(); + switch ($token->value) { + case 'true': + case 'TRUE': + return new Node\ConstantNode(true); + + case 'false': + case 'FALSE': + return new Node\ConstantNode(false); + + case 'null': + case 'NULL': + return new Node\ConstantNode(null); + + default: + if ('(' === $this->stream->current->value) { + if (false === isset($this->functions[$token->value])) { + throw new SyntaxError(sprintf('The function "%s" does not exist', $token->value), $token->cursor); + } + + $node = new Node\FunctionNode($token->value, $this->parseArguments()); + } else { + if (!in_array($token->value, $this->names)) { + throw new SyntaxError(sprintf('Variable "%s" is not valid', $token->value), $token->cursor); + } + + $node = new Node\NameNode($token->value); + } + } + break; + + case Token::NUMBER_TYPE: + case Token::STRING_TYPE: + $this->stream->next(); + return new Node\ConstantNode($token->value); + + default: + if ($token->test(Token::PUNCTUATION_TYPE, '[')) { + $node = $this->parseArrayExpression(); + } elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) { + $node = $this->parseHashExpression(); + } else { + throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s"', $token->type, $token->value), $token->cursor); + } + } + + return $this->parsePostfixExpression($node); + } + + public function parseArrayExpression() + { + $this->stream->expect(Token::PUNCTUATION_TYPE, '[', 'An array element was expected'); + + $node = new Node\ArrayNode(); + $first = true; + while (!$this->stream->current->test(Token::PUNCTUATION_TYPE, ']')) { + if (!$first) { + $this->stream->expect(Token::PUNCTUATION_TYPE, ',', 'An array element must be followed by a comma'); + + // trailing ,? + if ($this->stream->current->test(Token::PUNCTUATION_TYPE, ']')) { + break; + } + } + $first = false; + + $node->addElement($this->parseExpression()); + } + $this->stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened array is not properly closed'); + + return $node; + } + + public function parseHashExpression() + { + $this->stream->expect(Token::PUNCTUATION_TYPE, '{', 'A hash element was expected'); + + $node = new Node\ArrayNode(); + $first = true; + while (!$this->stream->current->test(Token::PUNCTUATION_TYPE, '}')) { + if (!$first) { + $this->stream->expect(Token::PUNCTUATION_TYPE, ',', 'A hash value must be followed by a comma'); + + // trailing ,? + if ($this->stream->current->test(Token::PUNCTUATION_TYPE, '}')) { + break; + } + } + $first = false; + + // a hash key can be: + // + // * a number -- 12 + // * a string -- 'a' + // * a name, which is equivalent to a string -- a + // * an expression, which must be enclosed in parentheses -- (1 + 2) + if ($this->stream->current->test(Token::STRING_TYPE) || $this->stream->current->test(Token::NAME_TYPE) || $this->stream->current->test(Token::NUMBER_TYPE)) { + $key = new Node\ConstantNode($this->stream->current->value); + $this->stream->next(); + } elseif ($this->stream->current->test(Token::PUNCTUATION_TYPE, '(')) { + $key = $this->parseExpression(); + } else { + $current = $this->stream->current; + + throw new SyntaxError(sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s"', $current->type, $current->value), $current->cursor); + } + + $this->stream->expect(Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)'); + $value = $this->parseExpression(); + + $node->addElement($value, $key); + } + $this->stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened hash is not properly closed'); + + return $node; + } + + public function parsePostfixExpression($node) + { + $token = $this->stream->current; + while ($token->type == Token::PUNCTUATION_TYPE) { + if ('.' === $token->value) { + $this->stream->next(); + $token = $this->stream->current; + $this->stream->next(); + + if ( + $token->type !== Token::NAME_TYPE + && + $token->type !== Token::NUMBER_TYPE + && + // operators line "not" are valid method or property names + ($token->type !== Token::OPERATOR_TYPE && preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A', $token->value)) + ) { + throw new SyntaxError('Expected name or number', $token->cursor); + } + + $arg = new Node\ConstantNode($token->value); + + $arguments = new Node\ArgumentsNode(); + if ($this->stream->current->test(Token::PUNCTUATION_TYPE, '(')) { + $type = Node\GetAttrNode::METHOD_CALL; + foreach ($this->parseArguments()->nodes as $n) { + $arguments->addElement($n); + } + } else { + $type = Node\GetAttrNode::PROPERTY_CALL; + } + + $node = new Node\GetAttrNode($node, $arg, $arguments, $type); + } elseif ('[' === $token->value) { + if ($node instanceof Node\GetAttrNode && Node\GetAttrNode::METHOD_CALL === $node->attributes['type'] && version_compare(PHP_VERSION, '5.4.0', '<')) { + throw new SyntaxError('Array calls on a method call is only supported on PHP 5.4+', $token->cursor); + } + + $this->stream->next(); + $arg = $this->parseExpression(); + $this->stream->expect(Token::PUNCTUATION_TYPE, ']'); + + $node = new Node\GetAttrNode($node, $arg, new Node\ArgumentsNode(), Node\GetAttrNode::ARRAY_CALL); + } else { + break; + } + + $token = $this->stream->current; + } + + return $node; + } + + /** + * Parses arguments. + */ + public function parseArguments() + { + $args = array(); + $this->stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + while (!$this->stream->current->test(Token::PUNCTUATION_TYPE, ')')) { + if (!empty($args)) { + $this->stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); + } + + $args[] = $this->parseExpression(); + } + $this->stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); + + return new Node\Node($args); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/README.md b/src/Symfony/Component/ExpressionLanguage/README.md new file mode 100644 index 0000000000..648dedcbcf --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/README.md @@ -0,0 +1,43 @@ +ExpressionLanguage Component +============================ + +The ExpressionLanguage component provides an engine that can compile and +evaluate expressions: + + use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + + $language = new ExpressionLanguage(); + + echo $language->evaluate('1 + foo', array('foo' => 2)); + // would output 3 + + echo $language->compile('1 + foo'); + // would output (1 + $foo) + +By default, the engine implements simple math and logic functions, method +calls, property accesses, and array accesses. + +You can extend your DSL with functions: + + $compiler = function ($arg) { + return sprintf('strtoupper(%s)', $arg); + }; + $evaluator = function (array $variables, $value) { + return strtoupper($value); + }; + $language->addFunction('upper', $compiler, $evaluator); + + echo $language->evaluate('"foo" ~ upper(foo)', array('foo' => 'bar')); + // would output fooBAR + + echo $language->compile('"foo" ~ upper(foo)'); + // would output ("foo" . strtoupper($foo)) + +Resources +--------- + +You can run the unit tests with the following command: + + $ cd path/to/Symfony/Component/ExpressionLanguage/ + $ composer.phar install --dev + $ phpunit diff --git a/src/Symfony/Component/ExpressionLanguage/SyntaxError.php b/src/Symfony/Component/ExpressionLanguage/SyntaxError.php new file mode 100644 index 0000000000..d149c00768 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/SyntaxError.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage; + +class SyntaxError extends \LogicException +{ + public function __construct($message, $cursor = 0) + { + parent::__construct(sprintf('%s around position %d.', $message, $cursor)); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/LexerTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/LexerTest.php new file mode 100644 index 0000000000..a69eb34809 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/LexerTest.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Lexer; +use Symfony\Component\ExpressionLanguage\Token; +use Symfony\Component\ExpressionLanguage\TokenStream; + +class LexerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider getTokenizeData + */ + public function testTokenize($tokens, $expression) + { + $tokens[] = new Token('end of expression', null, strlen($expression) + 1); + $lexer = new Lexer(); + $this->assertEquals(new TokenStream($tokens), $lexer->tokenize($expression)); + } + + public function getTokenizeData() + { + return array( + array( + array(new Token('name', 'a', 1)), + 'a', + ), + array( + array(new Token('string', 'foo', 1)), + '"foo"', + ), + array( + array(new Token('number', '3', 1)), + '3', + ), + array( + array(new Token('operator', '+', 1)), + '+', + ), + array( + array(new Token('punctuation', '.', 1)), + '.', + ), + array( + array( + new Token('punctuation', '(', 1), + new Token('number', '3', 2), + new Token('operator', '+', 4), + new Token('number', '5', 6), + new Token('punctuation', ')', 7), + new Token('operator', '~', 9), + new Token('name', 'foo', 11), + new Token('punctuation', '(', 14), + new Token('string', 'bar', 15), + new Token('punctuation', ')', 20), + new Token('punctuation', '.', 21), + new Token('name', 'baz', 22), + new Token('punctuation', '[', 25), + new Token('number', '4', 26), + new Token('punctuation', ']', 27), + ), + '(3 + 5) ~ foo("bar").baz[4]', + ), + ); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/AbstractNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/AbstractNodeTest.php new file mode 100644 index 0000000000..58b0e177e8 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/AbstractNodeTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +abstract class AbstractNodeTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider getEvaluateData + */ + public function testEvaluate($expected, $node, $variables = array(), $functions = array()) + { + $this->assertSame($expected, $node->evaluate($functions, $variables)); + } + + abstract public function getEvaluateData(); + + /** + * @dataProvider getCompileData + */ + public function testCompile($expected, $node, $functions = array()) + { + $compiler = new Compiler($functions); + $node->compile($compiler); + $this->assertSame($expected, $compiler->getSource()); + } + + abstract public function getCompileData(); +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/ArgumentsNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/ArgumentsNodeTest.php new file mode 100644 index 0000000000..42475b0276 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/ArgumentsNodeTest.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\ArgumentsNode; +use Symfony\Component\ExpressionLanguage\Node\ConstantNode; + +class ArgumentsNodeTest extends ArrayNodeTest +{ + public function getCompileData() + { + return array( + array('"a", "b"', $this->getArrayNode()), + ); + } + + protected function createArrayNode() + { + return new ArgumentsNode(); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/ArrayNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/ArrayNodeTest.php new file mode 100644 index 0000000000..24d1bb06fb --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/ArrayNodeTest.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\ArrayNode; +use Symfony\Component\ExpressionLanguage\Node\ConstantNode; + +class ArrayNodeTest extends AbstractNodeTest +{ + public function getEvaluateData() + { + return array( + array(array('b' => 'a', 'b'), $this->getArrayNode()), + ); + } + + public function getCompileData() + { + return array( + array('array("b" => "a", 0 => "b")', $this->getArrayNode()), + ); + } + + protected function getArrayNode() + { + $array = $this->createArrayNode(); + $array->addElement(new ConstantNode('a'), new ConstantNode('b')); + $array->addElement(new ConstantNode('b')); + + return $array; + } + + protected function createArrayNode() + { + return new ArrayNode(); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/BinaryNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/BinaryNodeTest.php new file mode 100644 index 0000000000..ecf3c7728b --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/BinaryNodeTest.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\BinaryNode; +use Symfony\Component\ExpressionLanguage\Node\ArrayNode; +use Symfony\Component\ExpressionLanguage\Node\ConstantNode; + +class BinaryNodeTest extends AbstractNodeTest +{ + public function getEvaluateData() + { + $array = new ArrayNode(); + $array->addElement(new ConstantNode('a')); + $array->addElement(new ConstantNode('b')); + + return array( + array(true, new BinaryNode('or', new ConstantNode(true), new ConstantNode(false))), + array(true, new BinaryNode('||', new ConstantNode(true), new ConstantNode(false))), + array(false, new BinaryNode('and', new ConstantNode(true), new ConstantNode(false))), + array(false, new BinaryNode('&&', new ConstantNode(true), new ConstantNode(false))), + + array(0, new BinaryNode('&', new ConstantNode(2), new ConstantNode(4))), + array(6, new BinaryNode('|', new ConstantNode(2), new ConstantNode(4))), + array(6, new BinaryNode('^', new ConstantNode(2), new ConstantNode(4))), + + array(true, new BinaryNode('<', new ConstantNode(1), new ConstantNode(2))), + array(true, new BinaryNode('<=', new ConstantNode(1), new ConstantNode(2))), + array(true, new BinaryNode('<=', new ConstantNode(1), new ConstantNode(1))), + + array(false, new BinaryNode('>', new ConstantNode(1), new ConstantNode(2))), + array(false, new BinaryNode('>=', new ConstantNode(1), new ConstantNode(2))), + array(true, new BinaryNode('>=', new ConstantNode(1), new ConstantNode(1))), + + array(true, new BinaryNode('===', new ConstantNode(true), new ConstantNode(true))), + array(false, new BinaryNode('!==', new ConstantNode(true), new ConstantNode(true))), + + array(false, new BinaryNode('==', new ConstantNode(2), new ConstantNode(1))), + array(true, new BinaryNode('!=', new ConstantNode(2), new ConstantNode(1))), + + array(-1, new BinaryNode('-', new ConstantNode(1), new ConstantNode(2))), + array(3, new BinaryNode('+', new ConstantNode(1), new ConstantNode(2))), + array(4, new BinaryNode('*', new ConstantNode(2), new ConstantNode(2))), + array(1, new BinaryNode('/', new ConstantNode(2), new ConstantNode(2))), + array(1, new BinaryNode('%', new ConstantNode(5), new ConstantNode(2))), + array(25, new BinaryNode('**', new ConstantNode(5), new ConstantNode(2))), + array('ab', new BinaryNode('~', new ConstantNode('a'), new ConstantNode('b'))), + + array(true, new BinaryNode('in', new ConstantNode('a'), $array)), + array(false, new BinaryNode('in', new ConstantNode('c'), $array)), + array(true, new BinaryNode('not in', new ConstantNode('c'), $array)), + array(false, new BinaryNode('not in', new ConstantNode('a'), $array)), + + array(array(1, 2, 3), new BinaryNode('..', new ConstantNode(1), new ConstantNode(3))), + ); + } + + public function getCompileData() + { + $array = new ArrayNode(); + $array->addElement(new ConstantNode('a')); + $array->addElement(new ConstantNode('b')); + + return array( + array('(true || false)', new BinaryNode('or', new ConstantNode(true), new ConstantNode(false))), + array('(true || false)', new BinaryNode('||', new ConstantNode(true), new ConstantNode(false))), + array('(true && false)', new BinaryNode('and', new ConstantNode(true), new ConstantNode(false))), + array('(true && false)', new BinaryNode('&&', new ConstantNode(true), new ConstantNode(false))), + + array('(2 & 4)', new BinaryNode('&', new ConstantNode(2), new ConstantNode(4))), + array('(2 | 4)', new BinaryNode('|', new ConstantNode(2), new ConstantNode(4))), + array('(2 ^ 4)', new BinaryNode('^', new ConstantNode(2), new ConstantNode(4))), + + array('(1 < 2)', new BinaryNode('<', new ConstantNode(1), new ConstantNode(2))), + array('(1 <= 2)', new BinaryNode('<=', new ConstantNode(1), new ConstantNode(2))), + array('(1 <= 1)', new BinaryNode('<=', new ConstantNode(1), new ConstantNode(1))), + + array('(1 > 2)', new BinaryNode('>', new ConstantNode(1), new ConstantNode(2))), + array('(1 >= 2)', new BinaryNode('>=', new ConstantNode(1), new ConstantNode(2))), + array('(1 >= 1)', new BinaryNode('>=', new ConstantNode(1), new ConstantNode(1))), + + array('(true === true)', new BinaryNode('===', new ConstantNode(true), new ConstantNode(true))), + array('(true !== true)', new BinaryNode('!==', new ConstantNode(true), new ConstantNode(true))), + + array('(2 == 1)', new BinaryNode('==', new ConstantNode(2), new ConstantNode(1))), + array('(2 != 1)', new BinaryNode('!=', new ConstantNode(2), new ConstantNode(1))), + + array('(1 - 2)', new BinaryNode('-', new ConstantNode(1), new ConstantNode(2))), + array('(1 + 2)', new BinaryNode('+', new ConstantNode(1), new ConstantNode(2))), + array('(2 * 2)', new BinaryNode('*', new ConstantNode(2), new ConstantNode(2))), + array('(2 / 2)', new BinaryNode('/', new ConstantNode(2), new ConstantNode(2))), + array('(5 % 2)', new BinaryNode('%', new ConstantNode(5), new ConstantNode(2))), + array('pow(5, 2)', new BinaryNode('**', new ConstantNode(5), new ConstantNode(2))), + array('("a" . "b")', new BinaryNode('~', new ConstantNode('a'), new ConstantNode('b'))), + + array('in_array("a", array(0 => "a", 1 => "b"))', new BinaryNode('in', new ConstantNode('a'), $array)), + array('in_array("c", array(0 => "a", 1 => "b"))', new BinaryNode('in', new ConstantNode('c'), $array)), + array('!in_array("c", array(0 => "a", 1 => "b"))', new BinaryNode('not in', new ConstantNode('c'), $array)), + array('!in_array("a", array(0 => "a", 1 => "b"))', new BinaryNode('not in', new ConstantNode('a'), $array)), + + array('range(1, 3)', new BinaryNode('..', new ConstantNode(1), new ConstantNode(3))), + ); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/ConditionalNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/ConditionalNodeTest.php new file mode 100644 index 0000000000..9b9f7a2724 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/ConditionalNodeTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\ConditionalNode; +use Symfony\Component\ExpressionLanguage\Node\ConstantNode; + +class ConditionalNodeTest extends AbstractNodeTest +{ + public function getEvaluateData() + { + return array( + array(1, new ConditionalNode(new ConstantNode(true), new ConstantNode(1), new ConstantNode(2))), + array(2, new ConditionalNode(new ConstantNode(false), new ConstantNode(1), new ConstantNode(2))), + ); + } + + public function getCompileData() + { + return array( + array('((true) ? (1) : (2))', new ConditionalNode(new ConstantNode(true), new ConstantNode(1), new ConstantNode(2))), + array('((false) ? (1) : (2))', new ConditionalNode(new ConstantNode(false), new ConstantNode(1), new ConstantNode(2))), + ); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/ConstantNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/ConstantNodeTest.php new file mode 100644 index 0000000000..c1a67a8603 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/ConstantNodeTest.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\ConstantNode; + +class ConstantNodeTest extends AbstractNodeTest +{ + public function getEvaluateData() + { + return array( + array(false, new ConstantNode(false)), + array(true, new ConstantNode(true)), + array(null, new ConstantNode(null)), + array(3, new ConstantNode(3)), + array(3.3, new ConstantNode(3.3)), + array('foo', new ConstantNode('foo')), + array(array(1, 'b' => 'a'), new ConstantNode(array(1, 'b' => 'a'))), + ); + } + + public function getCompileData() + { + return array( + array('false', new ConstantNode(false)), + array('true', new ConstantNode(true)), + array('null', new ConstantNode(null)), + array('3', new ConstantNode(3)), + array('3.3', new ConstantNode(3.3)), + array('"foo"', new ConstantNode('foo')), + array('array(0 => 1, "b" => "a")', new ConstantNode(array(1, 'b' => 'a'))), + ); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/FunctionNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/FunctionNodeTest.php new file mode 100644 index 0000000000..ecdc3d6371 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/FunctionNodeTest.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\FunctionNode; +use Symfony\Component\ExpressionLanguage\Node\ConstantNode; +use Symfony\Component\ExpressionLanguage\Node\Node; + +class FunctionNodeTest extends AbstractNodeTest +{ + public function getEvaluateData() + { + return array( + array('bar', new FunctionNode('foo', new Node(array(new ConstantNode('bar')))), array(), array('foo' => $this->getCallables())), + ); + } + + public function getCompileData() + { + return array( + array('foo("bar")', new FunctionNode('foo', new Node(array(new ConstantNode('bar')))), array('foo' => $this->getCallables())), + ); + } + + protected function getCallables() + { + return array( + 'compiler' => function ($arg) { + return sprintf('foo(%s)', $arg); + }, + 'evaluator' => function ($variables, $arg) { + return $arg; + }, + ); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/GetAttrNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/GetAttrNodeTest.php new file mode 100644 index 0000000000..ec0b69281f --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/GetAttrNodeTest.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\ArrayNode; +use Symfony\Component\ExpressionLanguage\Node\NameNode; +use Symfony\Component\ExpressionLanguage\Node\GetAttrNode; +use Symfony\Component\ExpressionLanguage\Node\ConstantNode; + +class GetAttrNodeTest extends AbstractNodeTest +{ + public function getEvaluateData() + { + return array( + array('b', new GetAttrNode(new NameNode('foo'), new ConstantNode(0), $this->getArrayNode(), GetAttrNode::ARRAY_CALL), array('foo' => array('b' => 'a', 'b'))), + array('a', new GetAttrNode(new NameNode('foo'), new ConstantNode('b'), $this->getArrayNode(), GetAttrNode::ARRAY_CALL), array('foo' => array('b' => 'a', 'b'))), + + array('bar', new GetAttrNode(new NameNode('foo'), new ConstantNode('foo'), $this->getArrayNode(), GetAttrNode::PROPERTY_CALL), array('foo' => new Obj())), + + array('baz', new GetAttrNode(new NameNode('foo'), new ConstantNode('foo'), $this->getArrayNode(), GetAttrNode::METHOD_CALL), array('foo' => new Obj())), + ); + } + + public function getCompileData() + { + return array( + array('$foo[0]', new GetAttrNode(new NameNode('foo'), new ConstantNode(0), $this->getArrayNode(), GetAttrNode::ARRAY_CALL)), + array('$foo["b"]', new GetAttrNode(new NameNode('foo'), new ConstantNode('b'), $this->getArrayNode(), GetAttrNode::ARRAY_CALL)), + + array('$foo->foo', new GetAttrNode(new NameNode('foo'), new ConstantNode('foo'), $this->getArrayNode(), GetAttrNode::PROPERTY_CALL), array('foo' => new Obj())), + + array('$foo->foo(array("b" => "a", 0 => "b"))', new GetAttrNode(new NameNode('foo'), new ConstantNode('foo'), $this->getArrayNode(), GetAttrNode::METHOD_CALL), array('foo' => new Obj())), + ); + } + + protected function getArrayNode() + { + $array = new ArrayNode(); + $array->addElement(new ConstantNode('a'), new ConstantNode('b')); + $array->addElement(new ConstantNode('b')); + + return $array; + } +} + +class Obj +{ + public $foo = 'bar'; + + public function foo() + { + return 'baz'; + } +} + diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/NameNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/NameNodeTest.php new file mode 100644 index 0000000000..b645a6bfff --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/NameNodeTest.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\NameNode; + +class NameNodeTest extends AbstractNodeTest +{ + public function getEvaluateData() + { + return array( + array('bar', new NameNode('foo'), array('foo' => 'bar')), + ); + } + + public function getCompileData() + { + return array( + array('$foo', new NameNode('foo')), + ); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/NodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/NodeTest.php new file mode 100644 index 0000000000..4365e8a1f6 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/NodeTest.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\Node; +use Symfony\Component\ExpressionLanguage\Node\ConstantNode; + +class NodeTest extends \PHPUnit_Framework_TestCase +{ + public function testToString() + { + $node = new Node(array(new ConstantNode('foo'))); + + $this->assertEquals(<< + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\UnaryNode; +use Symfony\Component\ExpressionLanguage\Node\ConstantNode; + +class UnaryNodeTest extends AbstractNodeTest +{ + public function getEvaluateData() + { + return array( + array(-1, new UnaryNode('-', new ConstantNode(1))), + array(3, new UnaryNode('+', new ConstantNode(3))), + array(false, new UnaryNode('!', new ConstantNode(true))), + array(false, new UnaryNode('not', new ConstantNode(true))), + ); + } + + public function getCompileData() + { + return array( + array('(-1)', new UnaryNode('-', new ConstantNode(1))), + array('(+3)', new UnaryNode('+', new ConstantNode(3))), + array('(!true)', new UnaryNode('!', new ConstantNode(true))), + array('(!true)', new UnaryNode('not', new ConstantNode(true))), + ); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/ParserTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/ParserTest.php new file mode 100644 index 0000000000..c1c33dd426 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/ParserTest.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Parser; +use Symfony\Component\ExpressionLanguage\Lexer; +use Symfony\Component\ExpressionLanguage\Node; + +class ParserTest extends \PHPUnit_Framework_TestCase +{ + /** + * @expectedException \Symfony\Component\ExpressionLanguage\SyntaxError + * @expectedExceptionMessage Variable "foo" is not valid around position 1. + */ + public function testParseWithInvalidName() + { + $lexer = new Lexer(); + $parser = new Parser(array()); + $parser->parse($lexer->tokenize('foo')); + } + + /** + * @dataProvider getParseData + */ + public function testParse($node, $expression, $names = array()) + { + $lexer = new Lexer(); + $parser = new Parser(array()); + $this->assertEquals($node, $parser->parse($lexer->tokenize($expression), $names)); + } + + public function getParseData() + { + $arguments = new Node\ArgumentsNode(); + $arguments->addElement(new Node\ConstantNode('arg1')); + $arguments->addElement(new Node\ConstantNode(2)); + $arguments->addElement(new Node\ConstantNode(true)); + + return array( + array( + new Node\NameNode('a'), + 'a', + array('a'), + ), + array( + new Node\ConstantNode('a'), + '"a"', + ), + array( + new Node\ConstantNode(3), + '3', + ), + array( + new Node\ConstantNode(false), + 'false', + ), + array( + new Node\ConstantNode(true), + 'true', + ), + array( + new Node\ConstantNode(null), + 'null', + ), + array( + new Node\UnaryNode('-', new Node\ConstantNode(3)), + '-3', + ), + array( + new Node\BinaryNode('-', new Node\ConstantNode(3), new Node\ConstantNode(3)), + '3 - 3', + ), + array( + new Node\BinaryNode('*', + new Node\BinaryNode('-', new Node\ConstantNode(3), new Node\ConstantNode(3)), + new Node\ConstantNode(2) + ), + '(3 - 3) * 2', + ), + array( + new Node\GetAttrNode(new Node\NameNode('foo'), new Node\ConstantNode('bar'), new Node\ArgumentsNode(), Node\GetAttrNode::PROPERTY_CALL), + 'foo.bar', + array('foo'), + ), + array( + new Node\GetAttrNode(new Node\NameNode('foo'), new Node\ConstantNode('bar'), new Node\ArgumentsNode(), Node\GetAttrNode::METHOD_CALL), + 'foo.bar()', + array('foo'), + ), + array( + new Node\GetAttrNode(new Node\NameNode('foo'), new Node\ConstantNode('not'), new Node\ArgumentsNode(), Node\GetAttrNode::METHOD_CALL), + 'foo.not()', + array('foo'), + ), + array( + new Node\GetAttrNode( + new Node\NameNode('foo'), + new Node\ConstantNode('bar'), + $arguments, + Node\GetAttrNode::METHOD_CALL + ), + 'foo.bar("arg1", 2, true)', + array('foo'), + ), + array( + new Node\GetAttrNode(new Node\NameNode('foo'), new Node\ConstantNode(3), new Node\ArgumentsNode(), Node\GetAttrNode::ARRAY_CALL), + 'foo[3]', + array('foo'), + ), + array( + new Node\ConditionalNode(new Node\ConstantNode(true), new Node\ConstantNode(true), new Node\ConstantNode(false)), + 'true ? true : false', + ), + + // chained calls + array( + $this->createGetAttrNode( + $this->createGetAttrNode( + $this->createGetAttrNode( + $this->createGetAttrNode(new Node\NameNode('foo'), 'bar', Node\GetAttrNode::METHOD_CALL), + 'foo', Node\GetAttrNode::METHOD_CALL), + 'baz', Node\GetAttrNode::PROPERTY_CALL), + '3', Node\GetAttrNode::ARRAY_CALL), + 'foo.bar().foo().baz[3]', + array('foo'), + ), + ); + } + + private function createGetAttrNode($node, $item, $type) + { + return new Node\GetAttrNode($node, new Node\ConstantNode($item), new Node\ArgumentsNode(), $type); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Token.php b/src/Symfony/Component/ExpressionLanguage/Token.php new file mode 100644 index 0000000000..4b9e184551 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Token.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage; + +/** + * Represents a Token. + * + * @author Fabien Potencier + */ +class Token +{ + public $value; + public $type; + public $cursor; + + const EOF_TYPE = 'end of expression'; + const NAME_TYPE = 'name'; + const NUMBER_TYPE = 'number'; + const STRING_TYPE = 'string'; + const OPERATOR_TYPE = 'operator'; + const PUNCTUATION_TYPE = 'punctuation'; + + /** + * Constructor. + * + * @param integer $type The type of the token + * @param string $value The token value + * @param integer $cursor The cursor position in the source + */ + public function __construct($type, $value, $cursor) + { + $this->type = $type; + $this->value = $value; + $this->cursor = $cursor; + } + + /** + * Returns a string representation of the token. + * + * @return string A string representation of the token + */ + public function __toString() + { + return sprintf('%3d %-11s %s', $this->cursor, strtoupper($this->type), $this->value); + } + + /** + * Tests the current token for a type and/or a value. + * + * @param array|integer $type The type to test + * @param string|null $value The token value + * + * @return Boolean + */ + public function test($type, $value = null) + { + return $this->type === $type && (null === $value || $this->value == $value); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/TokenStream.php b/src/Symfony/Component/ExpressionLanguage/TokenStream.php new file mode 100644 index 0000000000..7a75f96648 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/TokenStream.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage; + +/** + * Represents a token stream. + * + * @author Fabien Potencier + */ +class TokenStream +{ + public $current; + + private $tokens; + private $position; + + /** + * Constructor. + * + * @param array $tokens An array of tokens + */ + public function __construct(array $tokens) + { + $this->tokens = $tokens; + $this->position = 0; + $this->current = $tokens[0]; + } + + /** + * Returns a string representation of the token stream. + * + * @return string + */ + public function __toString() + { + return implode("\n", $this->tokens); + } + + /** + * Sets the pointer to the next token and returns the old one. + */ + public function next() + { + if (!isset($this->tokens[$this->position])) { + throw new SyntaxError('Unexpected end of expression', $this->current->cursor); + } + + ++$this->position; + + $this->current = $this->tokens[$this->position]; + } + + /** + * Tests a token. + */ + public function expect($type, $value = null, $message = null) + { + $token = $this->current; + if (!$token->test($type, $value)) { + throw new SyntaxError(sprintf('%sUnexpected token "%s" of value "%s" ("%s" expected%s)', $message ? $message.'. ' : '', $token->type, $token->value, $type, $value ? sprintf(' with value "%s"', $value) : ''), $token->cursor); + } + $this->next(); + } + + /** + * Checks if end of stream was reached + * + * @return bool + */ + public function isEOF() + { + return $this->current->type === Token::EOF_TYPE; + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/composer.json b/src/Symfony/Component/ExpressionLanguage/composer.json new file mode 100644 index 0000000000..d939a75883 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/composer.json @@ -0,0 +1,31 @@ +{ + "name": "symfony/expression-language", + "type": "library", + "description": "Symfony ExpressionLanguage Component", + "keywords": [], + "homepage": "http://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.3.3" + }, + "autoload": { + "psr-0": { "Symfony\\Component\\ExpressionLanguage\\": "" } + }, + "target-dir": "Symfony/Component/ExpressionLanguage", + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + } + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/phpunit.xml.dist b/src/Symfony/Component/ExpressionLanguage/phpunit.xml.dist new file mode 100644 index 0000000000..41d9128824 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./vendor + + + +