.doctrine-project.json000064400000000513144760110620010770 0ustar00{ "active": true, "name": "Lexer", "slug": "lexer", "docsSlug": "doctrine-lexer", "versions": [ { "name": "master", "branchName": "master", "slug": "latest", "aliases": [ "current", "stable" ] } ] } .github/FUNDING.yml000064400000000164144760110620007723 0ustar00patreon: phpdoctrine tidelift: packagist/doctrine%2Flexer custom: https://www.doctrine-project.org/sponsorship.html .gitignore000064400000000025144760110620006532 0ustar00vendor composer.lock LICENSE000064400000002051144760110620005550 0ustar00Copyright (c) 2006-2018 Doctrine Project 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. README.md000064400000000341144760110620006022 0ustar00# Doctrine Lexer Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers. This lexer is used in Doctrine Annotations and in Doctrine ORM (DQL). https://www.doctrine-project.org/projects/lexer.html composer.json000064400000001773144760110620007277 0ustar00{ "name": "doctrine/lexer", "type": "library", "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", "keywords": [ "php", "parser", "lexer", "annotations", "docblock" ], "homepage": "https://www.doctrine-project.org/projects/lexer.html", "license": "MIT", "authors": [ {"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"}, {"name": "Roman Borschel", "email": "roman@code-factory.org"}, {"name": "Johannes Schmitt", "email": "schmittjoh@gmail.com"} ], "require": { "php": ">=5.3.2" }, "require-dev": { "phpunit/phpunit": "^4.5" }, "autoload": { "psr-4": { "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" } }, "autoload-dev": { "psr-4": { "Doctrine\\Tests\\": "tests/Doctrine" } }, "extra": { "branch-alias": { "dev-master": "1.0.x-dev" } } } docs/en/dql-parser.rst000064400000023224144760110620010706 0ustar00DQL Lexer ========= Here is a more complicated example from the Doctrine ORM project. The ``Doctrine\ORM\Query\Lexer`` implementation for DQL looks something like the following: .. code-block:: php use Doctrine\Common\Lexer\AbstractLexer; class Lexer extends AbstractLexer { // All tokens that are not valid identifiers must be < 100 public const T_NONE = 1; public const T_INTEGER = 2; public const T_STRING = 3; public const T_INPUT_PARAMETER = 4; public const T_FLOAT = 5; public const T_CLOSE_PARENTHESIS = 6; public const T_OPEN_PARENTHESIS = 7; public const T_COMMA = 8; public const T_DIVIDE = 9; public const T_DOT = 10; public const T_EQUALS = 11; public const T_GREATER_THAN = 12; public const T_LOWER_THAN = 13; public const T_MINUS = 14; public const T_MULTIPLY = 15; public const T_NEGATE = 16; public const T_PLUS = 17; public const T_OPEN_CURLY_BRACE = 18; public const T_CLOSE_CURLY_BRACE = 19; // All tokens that are identifiers or keywords that could be considered as identifiers should be >= 100 public const T_ALIASED_NAME = 100; public const T_FULLY_QUALIFIED_NAME = 101; public const T_IDENTIFIER = 102; // All keyword tokens should be >= 200 public const T_ALL = 200; public const T_AND = 201; public const T_ANY = 202; public const T_AS = 203; public const T_ASC = 204; public const T_AVG = 205; public const T_BETWEEN = 206; public const T_BOTH = 207; public const T_BY = 208; public const T_CASE = 209; public const T_COALESCE = 210; public const T_COUNT = 211; public const T_DELETE = 212; public const T_DESC = 213; public const T_DISTINCT = 214; public const T_ELSE = 215; public const T_EMPTY = 216; public const T_END = 217; public const T_ESCAPE = 218; public const T_EXISTS = 219; public const T_FALSE = 220; public const T_FROM = 221; public const T_GROUP = 222; public const T_HAVING = 223; public const T_HIDDEN = 224; public const T_IN = 225; public const T_INDEX = 226; public const T_INNER = 227; public const T_INSTANCE = 228; public const T_IS = 229; public const T_JOIN = 230; public const T_LEADING = 231; public const T_LEFT = 232; public const T_LIKE = 233; public const T_MAX = 234; public const T_MEMBER = 235; public const T_MIN = 236; public const T_NEW = 237; public const T_NOT = 238; public const T_NULL = 239; public const T_NULLIF = 240; public const T_OF = 241; public const T_OR = 242; public const T_ORDER = 243; public const T_OUTER = 244; public const T_PARTIAL = 245; public const T_SELECT = 246; public const T_SET = 247; public const T_SOME = 248; public const T_SUM = 249; public const T_THEN = 250; public const T_TRAILING = 251; public const T_TRUE = 252; public const T_UPDATE = 253; public const T_WHEN = 254; public const T_WHERE = 255; public const T_WITH = 256; /** * Creates a new query scanner object. * * @param string $input A query string. */ public function __construct($input) { $this->setInput($input); } /** * {@inheritdoc} */ protected function getCatchablePatterns() { return [ '[a-z_][a-z0-9_]*\:[a-z_][a-z0-9_]*(?:\\\[a-z_][a-z0-9_]*)*', // aliased name '[a-z_\\\][a-z0-9_]*(?:\\\[a-z_][a-z0-9_]*)*', // identifier or qualified name '(?:[0-9]+(?:[\.][0-9]+)*)(?:e[+-]?[0-9]+)?', // numbers "'(?:[^']|'')*'", // quoted strings '\?[0-9]*|:[a-z_][a-z0-9_]*', // parameters ]; } /** * {@inheritdoc} */ protected function getNonCatchablePatterns() { return ['\s+', '(.)']; } /** * {@inheritdoc} */ protected function getType(&$value) { $type = self::T_NONE; switch (true) { // Recognize numeric values case (is_numeric($value)): if (strpos($value, '.') !== false || stripos($value, 'e') !== false) { return self::T_FLOAT; } return self::T_INTEGER; // Recognize quoted strings case ($value[0] === "'"): $value = str_replace("''", "'", substr($value, 1, strlen($value) - 2)); return self::T_STRING; // Recognize identifiers, aliased or qualified names case (ctype_alpha($value[0]) || $value[0] === '_' || $value[0] === '\\'): $name = 'Doctrine\ORM\Query\Lexer::T_' . strtoupper($value); if (defined($name)) { $type = constant($name); if ($type > 100) { return $type; } } if (strpos($value, ':') !== false) { return self::T_ALIASED_NAME; } if (strpos($value, '\\') !== false) { return self::T_FULLY_QUALIFIED_NAME; } return self::T_IDENTIFIER; // Recognize input parameters case ($value[0] === '?' || $value[0] === ':'): return self::T_INPUT_PARAMETER; // Recognize symbols case ($value === '.'): return self::T_DOT; case ($value === ','): return self::T_COMMA; case ($value === '('): return self::T_OPEN_PARENTHESIS; case ($value === ')'): return self::T_CLOSE_PARENTHESIS; case ($value === '='): return self::T_EQUALS; case ($value === '>'): return self::T_GREATER_THAN; case ($value === '<'): return self::T_LOWER_THAN; case ($value === '+'): return self::T_PLUS; case ($value === '-'): return self::T_MINUS; case ($value === '*'): return self::T_MULTIPLY; case ($value === '/'): return self::T_DIVIDE; case ($value === '!'): return self::T_NEGATE; case ($value === '{'): return self::T_OPEN_CURLY_BRACE; case ($value === '}'): return self::T_CLOSE_CURLY_BRACE; // Default default: // Do nothing } return $type; } } This is roughly what the DQL Parser looks like that uses the above Lexer implementation: .. note:: You can see the full implementation `here `_. .. code-block:: php class Parser { private $lexer; public function __construct($dql) { $this->lexer = new Lexer(); $this->lexer->setInput($dql); } // ... public function getAST() { // Parse & build AST $AST = $this->QueryLanguage(); // ... return $AST; } public function QueryLanguage() { $this->lexer->moveNext(); switch ($this->lexer->lookahead['type']) { case Lexer::T_SELECT: $statement = $this->SelectStatement(); break; case Lexer::T_UPDATE: $statement = $this->UpdateStatement(); break; case Lexer::T_DELETE: $statement = $this->DeleteStatement(); break; default: $this->syntaxError('SELECT, UPDATE or DELETE'); break; } // Check for end of string if ($this->lexer->lookahead !== null) { $this->syntaxError('end of string'); } return $statement; } // ... } Now the AST is used to transform the DQL query in to portable SQL for whatever relational database you are using! .. code-block:: php $parser = new Parser('SELECT u FROM User u'); $AST = $parser->getAST(); // returns \Doctrine\ORM\Query\AST\SelectStatement What is an AST? =============== AST stands for `Abstract syntax tree `_. In computer science, an abstract syntax tree (AST), or just syntax tree, is a tree representation of the abstract syntactic structure of source code written in a programming language. Each node of the tree denotes a construct occurring in the source code. docs/en/index.rst000064400000004152144760110620007742 0ustar00Introduction ============ Doctrine Lexer is a library that can be used in Top-Down, Recursive Descent Parsers. This lexer is used in Doctrine Annotations and in Doctrine ORM (DQL). To write your own parser you just need to extend ``Doctrine\Common\Lexer\AbstractLexer`` and implement the following three abstract methods. .. code-block:: php /** * Lexical catchable patterns. * * @return array */ abstract protected function getCatchablePatterns(); /** * Lexical non-catchable patterns. * * @return array */ abstract protected function getNonCatchablePatterns(); /** * Retrieve token type. Also processes the token value if necessary. * * @param string $value * @return integer */ abstract protected function getType(&$value); These methods define the `lexical `_ catchable and non-catchable patterns and a method for returning the type of a token and filtering the value if necessary. The Lexer is responsible for giving you an API to walk across a string one character at a time and analyze the type of each character, value and position of each token in the string. The low level API of the lexer is pretty simple: - ``setInput($input)`` - Sets the input data to be tokenized. The Lexer is immediately reset and the new input tokenized. - ``reset()`` - Resets the lexer. - ``resetPeek()`` - Resets the peek pointer to 0. - ``resetPosition($position = 0)`` - Resets the lexer position on the input to the given position. - ``isNextToken($token)`` - Checks whether a given token matches the current lookahead. - ``isNextTokenAny(array $tokens)`` - Checks whether any of the given tokens matches the current lookahead. - ``moveNext()`` - Moves to the next token in the input string. - ``skipUntil($type)`` - Tells the lexer to skip input tokens until it sees a token with the given value. - ``isA($value, $token)`` - Checks if given value is identical to the given token. - ``peek()`` - Moves the lookahead token forward. - ``glimpse()`` - Peeks at the next token, returns it and immediately resets the peek. docs/en/sidebar.rst000064400000000117144760110620010241 0ustar00.. toctree:: :depth: 3 index simple-parser-example dql-parser docs/en/simple-parser-example.rst000064400000005062144760110620013050 0ustar00Simple Parser Example ===================== Extend the ``Doctrine\Common\Lexer\AbstractLexer`` class and implement the ``getCatchablePatterns``, ``getNonCatchablePatterns``, and ``getType`` methods. Here is a very simple example lexer implementation named ``CharacterTypeLexer``. It tokenizes a string to ``T_UPPER``, ``T_LOWER`` and``T_NUMBER`` tokens: .. code-block:: php lexer = $lexer; } public function getUpperCaseCharacters($string) { $this->lexer->setInput($string); $this->lexer->moveNext(); $upperCaseChars = array(); while (true) { if (!$this->lexer->lookahead) { break; } $this->lexer->moveNext(); if ($this->lexer->token['type'] === CharacterTypeLexer::T_UPPER) { $upperCaseChars[] = $this->lexer->token['value']; } } return $upperCaseChars; } } $upperCaseCharacterExtractor = new UpperCaseCharacterExtracter(new CharacterTypeLexer()); $upperCaseCharacters = $upperCaseCharacterExtractor->getUpperCaseCharacters('1aBcdEfgHiJ12'); print_r($upperCaseCharacters); The variable ``$upperCaseCharacters`` contains all of the upper case characters: .. code-block:: php Array ( [0] => B [1] => E [2] => H [3] => J ) This is a simple example but it should demonstrate the low level API that can be used to build more complex parsers. lib/Doctrine/Common/Lexer/AbstractLexer.php000064400000020046144760110620014647 0ustar00. */ namespace Doctrine\Common\Lexer; /** * Base class for writing simple lexers, i.e. for creating small DSLs. * * @since 2.0 * @author Guilherme Blanco * @author Jonathan Wage * @author Roman Borschel */ abstract class AbstractLexer { /** * Lexer original input string. * * @var string */ private $input; /** * Array of scanned tokens. * * Each token is an associative array containing three items: * - 'value' : the string value of the token in the input string * - 'type' : the type of the token (identifier, numeric, string, input * parameter, none) * - 'position' : the position of the token in the input string * * @var array */ private $tokens = array(); /** * Current lexer position in input string. * * @var integer */ private $position = 0; /** * Current peek of current lexer position. * * @var integer */ private $peek = 0; /** * The next token in the input. * * @var array */ public $lookahead; /** * The last matched/seen token. * * @var array */ public $token; /** * Sets the input data to be tokenized. * * The Lexer is immediately reset and the new input tokenized. * Any unprocessed tokens from any previous input are lost. * * @param string $input The input to be tokenized. * * @return void */ public function setInput($input) { $this->input = $input; $this->tokens = array(); $this->reset(); $this->scan($input); } /** * Resets the lexer. * * @return void */ public function reset() { $this->lookahead = null; $this->token = null; $this->peek = 0; $this->position = 0; } /** * Resets the peek pointer to 0. * * @return void */ public function resetPeek() { $this->peek = 0; } /** * Resets the lexer position on the input to the given position. * * @param integer $position Position to place the lexical scanner. * * @return void */ public function resetPosition($position = 0) { $this->position = $position; } /** * Retrieve the original lexer's input until a given position. * * @param integer $position * * @return string */ public function getInputUntilPosition($position) { return substr($this->input, 0, $position); } /** * Checks whether a given token matches the current lookahead. * * @param integer|string $token * * @return boolean */ public function isNextToken($token) { return null !== $this->lookahead && $this->lookahead['type'] === $token; } /** * Checks whether any of the given tokens matches the current lookahead. * * @param array $tokens * * @return boolean */ public function isNextTokenAny(array $tokens) { return null !== $this->lookahead && in_array($this->lookahead['type'], $tokens, true); } /** * Moves to the next token in the input string. * * @return boolean */ public function moveNext() { $this->peek = 0; $this->token = $this->lookahead; $this->lookahead = (isset($this->tokens[$this->position])) ? $this->tokens[$this->position++] : null; return $this->lookahead !== null; } /** * Tells the lexer to skip input tokens until it sees a token with the given value. * * @param string $type The token type to skip until. * * @return void */ public function skipUntil($type) { while ($this->lookahead !== null && $this->lookahead['type'] !== $type) { $this->moveNext(); } } /** * Checks if given value is identical to the given token. * * @param mixed $value * @param integer $token * * @return boolean */ public function isA($value, $token) { return $this->getType($value) === $token; } /** * Moves the lookahead token forward. * * @return array|null The next token or NULL if there are no more tokens ahead. */ public function peek() { if (isset($this->tokens[$this->position + $this->peek])) { return $this->tokens[$this->position + $this->peek++]; } else { return null; } } /** * Peeks at the next token, returns it and immediately resets the peek. * * @return array|null The next token or NULL if there are no more tokens ahead. */ public function glimpse() { $peek = $this->peek(); $this->peek = 0; return $peek; } /** * Scans the input string for tokens. * * @param string $input A query string. * * @return void */ protected function scan($input) { static $regex; if ( ! isset($regex)) { $regex = sprintf( '/(%s)|%s/%s', implode(')|(', $this->getCatchablePatterns()), implode('|', $this->getNonCatchablePatterns()), $this->getModifiers() ); } $flags = PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE; $matches = preg_split($regex, $input, -1, $flags); if (false === $matches) { // Work around https://bugs.php.net/78122 $matches = array(array($input, 0)); } foreach ($matches as $match) { // Must remain before 'value' assignment since it can change content $type = $this->getType($match[0]); $this->tokens[] = array( 'value' => $match[0], 'type' => $type, 'position' => $match[1], ); } } /** * Gets the literal for a given token. * * @param integer $token * * @return string */ public function getLiteral($token) { $className = get_class($this); $reflClass = new \ReflectionClass($className); $constants = $reflClass->getConstants(); foreach ($constants as $name => $value) { if ($value === $token) { return $className . '::' . $name; } } return $token; } /** * Regex modifiers * * @return string */ protected function getModifiers() { return 'i'; } /** * Lexical catchable patterns. * * @return array */ abstract protected function getCatchablePatterns(); /** * Lexical non-catchable patterns. * * @return array */ abstract protected function getNonCatchablePatterns(); /** * Retrieve token type. Also processes the token value if necessary. * * @param string $value * * @return integer */ abstract protected function getType(&$value); } phpunit.xml.dist000064400000001375144760110620007726 0ustar00 tests lib/Doctrine tests/Doctrine/Common/Lexer/AbstractLexerTest.php000064400000017043144760110620016106 0ustar00concreteLexer = new ConcreteLexer(); } public function dataProvider() { return array( array( 'price=10', array( array( 'value' => 'price', 'type' => 'string', 'position' => 0, ), array( 'value' => '=', 'type' => 'operator', 'position' => 5, ), array( 'value' => 10, 'type' => 'int', 'position' => 6, ), ), ), ); } public function testResetPeek() { $expectedTokens = array( array( 'value' => 'price', 'type' => 'string', 'position' => 0, ), array( 'value' => '=', 'type' => 'operator', 'position' => 5, ), array( 'value' => 10, 'type' => 'int', 'position' => 6, ), ); $this->concreteLexer->setInput('price=10'); $this->assertEquals($expectedTokens[0], $this->concreteLexer->peek()); $this->assertEquals($expectedTokens[1], $this->concreteLexer->peek()); $this->concreteLexer->resetPeek(); $this->assertEquals($expectedTokens[0], $this->concreteLexer->peek()); } public function testResetPosition() { $expectedTokens = array( array( 'value' => 'price', 'type' => 'string', 'position' => 0, ), array( 'value' => '=', 'type' => 'operator', 'position' => 5, ), array( 'value' => 10, 'type' => 'int', 'position' => 6, ), ); $this->concreteLexer->setInput('price=10'); $this->assertNull($this->concreteLexer->lookahead); $this->assertTrue($this->concreteLexer->moveNext()); $this->assertEquals($expectedTokens[0], $this->concreteLexer->lookahead); $this->assertTrue($this->concreteLexer->moveNext()); $this->assertEquals($expectedTokens[1], $this->concreteLexer->lookahead); $this->concreteLexer->resetPosition(0); $this->assertTrue($this->concreteLexer->moveNext()); $this->assertEquals($expectedTokens[0], $this->concreteLexer->lookahead); } /** * @dataProvider dataProvider * * @param $input * @param $expectedTokens */ public function testMoveNext($input, $expectedTokens) { $this->concreteLexer->setInput($input); $this->assertNull($this->concreteLexer->lookahead); for ($i = 0; $i < count($expectedTokens); $i++) { $this->assertTrue($this->concreteLexer->moveNext()); $this->assertEquals($expectedTokens[$i], $this->concreteLexer->lookahead); } $this->assertFalse($this->concreteLexer->moveNext()); $this->assertNull($this->concreteLexer->lookahead); } public function testSkipUntil() { $this->concreteLexer->setInput('price=10'); $this->assertTrue($this->concreteLexer->moveNext()); $this->concreteLexer->skipUntil('operator'); $this->assertEquals( array( 'value' => '=', 'type' => 'operator', 'position' => 5, ), $this->concreteLexer->lookahead ); } public function testUtf8Mismatch() { $this->concreteLexer->setInput("\xE9=10"); $this->assertTrue($this->concreteLexer->moveNext()); $this->assertEquals( array( 'value' => "\xE9=10", 'type' => 'string', 'position' => 0, ), $this->concreteLexer->lookahead ); } /** * @dataProvider dataProvider * * @param $input * @param $expectedTokens */ public function testPeek($input, $expectedTokens) { $this->concreteLexer->setInput($input); foreach ($expectedTokens as $expectedToken) { $this->assertEquals($expectedToken, $this->concreteLexer->peek()); } $this->assertNull($this->concreteLexer->peek()); } /** * @dataProvider dataProvider * * @param $input * @param $expectedTokens */ public function testGlimpse($input, $expectedTokens) { $this->concreteLexer->setInput($input); foreach ($expectedTokens as $expectedToken) { $this->assertEquals($expectedToken, $this->concreteLexer->glimpse()); $this->concreteLexer->moveNext(); } $this->assertNull($this->concreteLexer->peek()); } public function inputUntilPositionDataProvider() { return array( array('price=10', 5, 'price'), ); } /** * @dataProvider inputUntilPositionDataProvider * * @param $input * @param $position * @param $expectedInput */ public function testGetInputUntilPosition($input, $position, $expectedInput) { $this->concreteLexer->setInput($input); $this->assertSame($expectedInput, $this->concreteLexer->getInputUntilPosition($position)); } /** * @dataProvider dataProvider * * @param $input * @param $expectedTokens */ public function testIsNextToken($input, $expectedTokens) { $this->concreteLexer->setInput($input); $this->concreteLexer->moveNext(); for ($i = 0; $i < count($expectedTokens); $i++) { $this->assertTrue($this->concreteLexer->isNextToken($expectedTokens[$i]['type'])); $this->concreteLexer->moveNext(); } } /** * @dataProvider dataProvider * * @param $input * @param $expectedTokens */ public function testIsNextTokenAny($input, $expectedTokens) { $allTokenTypes = array_map(function ($token) { return $token['type']; }, $expectedTokens); $this->concreteLexer->setInput($input); $this->concreteLexer->moveNext(); for ($i = 0; $i < count($expectedTokens); $i++) { $this->assertTrue($this->concreteLexer->isNextTokenAny(array($expectedTokens[$i]['type']))); $this->assertTrue($this->concreteLexer->isNextTokenAny($allTokenTypes)); $this->concreteLexer->moveNext(); } } public function testGetLiteral() { $this->assertSame('Doctrine\Tests\Common\Lexer\ConcreteLexer::INT', $this->concreteLexer->getLiteral('int')); $this->assertSame('fake_token', $this->concreteLexer->getLiteral('fake_token')); } public function testIsA() { $this->assertTrue($this->concreteLexer->isA(11, 'int')); $this->assertTrue($this->concreteLexer->isA(1.1, 'int')); $this->assertTrue($this->concreteLexer->isA('=', 'operator')); $this->assertTrue($this->concreteLexer->isA('>', 'operator')); $this->assertTrue($this->concreteLexer->isA('<', 'operator')); $this->assertTrue($this->concreteLexer->isA('fake_text', 'string')); } } tests/Doctrine/Common/Lexer/ConcreteLexer.php000064400000001561144760110620015243 0ustar00', '[a-z]+', '\d+', ); } protected function getNonCatchablePatterns() { return array( '\s+', '(.)', ); } protected function getType(&$value) { if (is_numeric($value)) { $value = (int)$value; return 'int'; } if (in_array($value, array('=', '<', '>'))) { return 'operator'; } if (is_string($value)) { return 'string'; } return; } protected function getModifiers() { return parent::getModifiers().'u'; } }