diff --git a/lib/Doctrine/ORM/Query/Lexer.php b/lib/Doctrine/ORM/Query/Lexer.php index 3985a36b4..be71f4646 100644 --- a/lib/Doctrine/ORM/Query/Lexer.php +++ b/lib/Doctrine/ORM/Query/Lexer.php @@ -49,68 +49,70 @@ class Lexer extends \Doctrine\Common\Lexer const T_PLUS = 17; const T_OPEN_CURLY_BRACE = 18; const T_CLOSE_CURLY_BRACE = 19; - const T_ALIASED_NAME = 20; - const T_FULLY_QUALIFIED_NAME = 21; - // All tokens that are also identifiers should be >= 100 - const T_IDENTIFIER = 100; - const T_ALL = 101; - const T_AND = 102; - const T_ANY = 103; - const T_AS = 104; - const T_ASC = 105; - const T_AVG = 106; - const T_BETWEEN = 107; - const T_BOTH = 108; - const T_BY = 109; - const T_CASE = 110; - const T_COALESCE = 111; - const T_COUNT = 112; - const T_DELETE = 113; - const T_DESC = 114; - const T_DISTINCT = 115; - const T_ELSE = 116; - const T_EMPTY = 117; - const T_END = 118; - const T_ESCAPE = 119; - const T_EXISTS = 120; - const T_FALSE = 121; - const T_FROM = 122; - const T_GROUP = 123; - const T_HAVING = 124; - const T_HIDDEN = 125; - const T_IN = 126; - const T_INDEX = 127; - const T_INNER = 128; - const T_INSTANCE = 129; - const T_IS = 130; - const T_JOIN = 131; - const T_LEADING = 132; - const T_LEFT = 133; - const T_LIKE = 134; - const T_MAX = 135; - const T_MEMBER = 136; - const T_MIN = 137; - const T_NOT = 138; - const T_NULL = 139; - const T_NULLIF = 140; - const T_OF = 141; - const T_OR = 142; - const T_ORDER = 143; - const T_OUTER = 144; - const T_SELECT = 145; - const T_SET = 146; - const T_SOME = 147; - const T_SUM = 148; - const T_THEN = 149; - const T_TRAILING = 150; - const T_TRUE = 151; - const T_UPDATE = 152; - const T_WHEN = 153; - const T_WHERE = 154; - const T_WITH = 155; - const T_PARTIAL = 156; - const T_NEW = 157; + // All tokens that are identifiers or keywords that could be considered as identifiers should be >= 100 + const T_ALIASED_NAME = 100; + const T_FULLY_QUALIFIED_NAME = 101; + const T_IDENTIFIER = 102; + + // All keyword tokens should be >= 200 + const T_ALL = 200; + const T_AND = 201; + const T_ANY = 202; + const T_AS = 203; + const T_ASC = 204; + const T_AVG = 205; + const T_BETWEEN = 206; + const T_BOTH = 207; + const T_BY = 208; + const T_CASE = 209; + const T_COALESCE = 210; + const T_COUNT = 211; + const T_DELETE = 212; + const T_DESC = 213; + const T_DISTINCT = 214; + const T_ELSE = 215; + const T_EMPTY = 216; + const T_END = 217; + const T_ESCAPE = 218; + const T_EXISTS = 219; + const T_FALSE = 220; + const T_FROM = 221; + const T_GROUP = 222; + const T_HAVING = 223; + const T_HIDDEN = 224; + const T_IN = 225; + const T_INDEX = 226; + const T_INNER = 227; + const T_INSTANCE = 228; + const T_IS = 229; + const T_JOIN = 230; + const T_LEADING = 231; + const T_LEFT = 232; + const T_LIKE = 233; + const T_MAX = 234; + const T_MEMBER = 235; + const T_MIN = 236; + const T_NEW = 237; + const T_NOT = 238; + const T_NULL = 239; + const T_NULLIF = 240; + const T_OF = 241; + const T_OR = 242; + const T_ORDER = 243; + const T_OUTER = 244; + const T_PARTIAL = 245; + const T_SELECT = 246; + const T_SET = 247; + const T_SOME = 248; + const T_SUM = 249; + const T_THEN = 250; + const T_TRAILING = 251; + const T_TRUE = 252; + const T_UPDATE = 253; + const T_WHEN = 254; + const T_WHERE = 255; + const T_WITH = 256; /** * Creates a new query scanner object. @@ -129,7 +131,7 @@ class Lexer extends \Doctrine\Common\Lexer { return array( '[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 + '[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 @@ -167,7 +169,7 @@ class Lexer extends \Doctrine\Common\Lexer return self::T_STRING; // Recognize identifiers, aliased or qualified names - case (ctype_alpha($value[0]) || $value[0] === '_'): + case (ctype_alpha($value[0]) || $value[0] === '_' || $value[0] === '\\'): $name = 'Doctrine\ORM\Query\Lexer::T_' . strtoupper($value); if (defined($name)) { @@ -181,9 +183,11 @@ class Lexer extends \Doctrine\Common\Lexer 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 @@ -206,9 +210,6 @@ class Lexer extends \Doctrine\Common\Lexer case ($value === '{'): return self::T_OPEN_CURLY_BRACE; case ($value === '}'): return self::T_CLOSE_CURLY_BRACE; - case (strrpos($value, ':') !== false): - return self::T_ALIASED_NAME; - // Default default: // Do nothing diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index 6478b14b8..c5c31d6a5 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -311,14 +311,22 @@ class Parser { $lookaheadType = $this->lexer->lookahead['type']; - /* - * The following condition means: - * - If next token matches expectation -> ok. - * - If expectation is to get T_IDENTIFIER and we find one of the tokens that are reserved words *but* would work as identifiers as well (e. g. "FROM", DDC-505) -> ok. - * - Else fail. - */ - if ($lookaheadType !== $token && ($token !== Lexer::T_IDENTIFIER || $lookaheadType <= Lexer::T_IDENTIFIER)) { - $this->syntaxError($this->lexer->getLiteral($token)); + // Short-circuit on first condition, usually types match + if ($lookaheadType !== $token) { + // If parameter is not identifier (1-99) must be exact match + if ($token < Lexer::T_IDENTIFIER) { + $this->syntaxError($this->lexer->getLiteral($token)); + } + + // If parameter is keyword (200+) must be exact match + if ($token > Lexer::T_IDENTIFIER) { + $this->syntaxError($this->lexer->getLiteral($token)); + } + + // If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+) + if ($token === Lexer::T_IDENTIFIER && $lookaheadType < Lexer::T_IDENTIFIER) { + $this->syntaxError($this->lexer->getLiteral($token)); + } } $this->lexer->moveNext(); @@ -962,13 +970,17 @@ class Parser { if ($this->lexer->isNextToken(Lexer::T_FULLY_QUALIFIED_NAME)) { $this->match(Lexer::T_FULLY_QUALIFIED_NAME); + $schemaName = $this->lexer->token['value']; } else if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) { $this->match(Lexer::T_IDENTIFIER); + $schemaName = $this->lexer->token['value']; } else { $this->match(Lexer::T_ALIASED_NAME); + list($namespaceAlias, $simpleClassName) = explode(':', $this->lexer->token['value']); + $schemaName = $this->em->getConfiguration()->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName; } @@ -1216,8 +1228,10 @@ class Parser public function UpdateClause() { $this->match(Lexer::T_UPDATE); + $token = $this->lexer->lookahead; $abstractSchemaName = $this->AbstractSchemaName(); + $this->validateAbstractSchemaName($abstractSchemaName); if ($this->lexer->isNextToken(Lexer::T_AS)) { @@ -1272,7 +1286,9 @@ class Parser $token = $this->lexer->lookahead; $abstractSchemaName = $this->AbstractSchemaName(); + $this->validateAbstractSchemaName($abstractSchemaName); + $deleteClause = new AST\DeleteClause($abstractSchemaName); if ($this->lexer->isNextToken(Lexer::T_AS)) { @@ -1718,6 +1734,7 @@ class Parser public function RangeVariableDeclaration() { $abstractSchemaName = $this->AbstractSchemaName(); + $this->validateAbstractSchemaName($abstractSchemaName); if ($this->lexer->isNextToken(Lexer::T_AS)) { @@ -2242,9 +2259,12 @@ class Parser } // [["AS"] ["HIDDEN"] AliasResultVariable] + $mustHaveAliasResultVariable = false; if ($this->lexer->isNextToken(Lexer::T_AS)) { $this->match(Lexer::T_AS); + + $mustHaveAliasResultVariable = true; } $hiddenAliasResultVariable = false; @@ -2257,7 +2277,7 @@ class Parser $aliasResultVariable = null; - if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) { + if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(Lexer::T_IDENTIFIER)) { $token = $this->lexer->lookahead; $aliasResultVariable = $this->AliasResultVariable(); @@ -3150,6 +3170,7 @@ class Parser } $abstractSchemaName = $this->AbstractSchemaName(); + $this->validateAbstractSchemaName($abstractSchemaName); return $abstractSchemaName; diff --git a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php index 569200361..29f8fab85 100644 --- a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php @@ -129,7 +129,7 @@ class BasicFunctionalTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->clear(); - $user2 = $this->_em->createQuery('select u from Doctrine\Tests\Models\CMS\CmsUser u where u.id=?1') + $user2 = $this->_em->createQuery('select u from \Doctrine\Tests\Models\CMS\CmsUser u where u.id=?1') ->setParameter(1, $userId) ->getSingleResult(); diff --git a/tests/Doctrine/Tests/ORM/Functional/NewOperatorTest.php b/tests/Doctrine/Tests/ORM/Functional/NewOperatorTest.php index 7cde64b43..e8c0e300f 100644 --- a/tests/Doctrine/Tests/ORM/Functional/NewOperatorTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/NewOperatorTest.php @@ -1046,21 +1046,21 @@ class NewOperatorTest extends \Doctrine\Tests\OrmFunctionalTestCase /** * @expectedException Doctrine\ORM\Query\QueryException - * @expectedExceptionMessage [Semantical Error] line 0, col 11 near 'InvalidClass(u.name)': Error: Class "InvalidClass" is not defined. + * @expectedExceptionMessage [Semantical Error] line 0, col 11 near '\InvalidClass(u.name)': Error: Class "\InvalidClass" is not defined. */ public function testInvalidClassException() { - $dql = "SELECT new InvalidClass(u.name) FROM Doctrine\Tests\Models\CMS\CmsUser u"; + $dql = "SELECT new \InvalidClass(u.name) FROM Doctrine\Tests\Models\CMS\CmsUser u"; $this->_em->createQuery($dql)->getResult(); } /** * @expectedException Doctrine\ORM\Query\QueryException - * @expectedExceptionMessage [Semantical Error] line 0, col 11 near 'stdClass(u.name)': Error: Class "stdClass" has not a valid constructor. + * @expectedExceptionMessage [Semantical Error] line 0, col 11 near '\stdClass(u.name)': Error: Class "\stdClass" has not a valid constructor. */ public function testInvalidClassConstructorException() { - $dql = "SELECT new stdClass(u.name) FROM Doctrine\Tests\Models\CMS\CmsUser u"; + $dql = "SELECT new \stdClass(u.name) FROM Doctrine\Tests\Models\CMS\CmsUser u"; $this->_em->createQuery($dql)->getResult(); } diff --git a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php index a0888d94e..b90047e19 100644 --- a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php +++ b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php @@ -84,7 +84,11 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase { $this->setExpectedException('\Doctrine\ORM\Query\QueryException'); - $this->_em->getConfiguration()->setEntityNamespaces(array('Unknown' => 'Unknown', 'CMS' => 'Doctrine\Tests\Models\CMS')); + $this->_em->getConfiguration()->setEntityNamespaces(array( + 'Unknown' => 'Unknown', + 'CMS' => 'Doctrine\Tests\Models\CMS' + )); + $this->parseDql($dql); } @@ -92,6 +96,7 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase { return array( + array('SELECT \'foo\' AS foo\bar FROM Doctrine\Tests\Models\CMS\CmsUser u'), /* Checks for invalid IdentificationVariables and AliasIdentificationVariables */ array('SELECT \foo FROM Doctrine\Tests\Models\CMS\CmsUser \foo'), array('SELECT foo\ FROM Doctrine\Tests\Models\CMS\CmsUser foo\\'), @@ -118,7 +123,6 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase /* Checks for invalid AliasResultVariable */ array('SELECT \'foo\' AS \foo FROM Doctrine\Tests\Models\CMS\CmsUser u'), array('SELECT \'foo\' AS \foo\bar FROM Doctrine\Tests\Models\CMS\CmsUser u'), - array('SELECT \'foo\' AS foo\bar FROM Doctrine\Tests\Models\CMS\CmsUser u'), array('SELECT \'foo\' AS foo\ FROM Doctrine\Tests\Models\CMS\CmsUser u'), array('SELECT \'foo\' AS foo\\\\bar FROM Doctrine\Tests\Models\CMS\CmsUser u'), array('SELECT \'foo\' AS foo: FROM Doctrine\Tests\Models\CMS\CmsUser u'), diff --git a/tests/Doctrine/Tests/ORM/Query/ParserTest.php b/tests/Doctrine/Tests/ORM/Query/ParserTest.php index 6cfc449c6..1bc7f5dfb 100644 --- a/tests/Doctrine/Tests/ORM/Query/ParserTest.php +++ b/tests/Doctrine/Tests/ORM/Query/ParserTest.php @@ -24,12 +24,11 @@ class ParserTest extends \Doctrine\Tests\OrmTestCase * @covers Doctrine\ORM\Query\Parser::AbstractSchemaName * @group DDC-3715 */ - public function testAbstractSchemaNameFailsOnClassnamesWithLeadingBackslash() + public function testAbstractSchemaNameSupportsClassnamesWithLeadingBackslash() { - $this->setExpectedException('\Doctrine\ORM\Query\QueryException'); - $parser = $this->createParser('\Doctrine\Tests\Models\CMS\CmsUser'); - $parser->AbstractSchemaName(); + + $this->assertEquals('\Doctrine\Tests\Models\CMS\CmsUser', $parser->AbstractSchemaName()); } /** @@ -39,6 +38,7 @@ class ParserTest extends \Doctrine\Tests\OrmTestCase public function testAbstractSchemaNameSupportsIdentifier() { $parser = $this->createParser('stdClass'); + $this->assertEquals('stdClass', $parser->AbstractSchemaName()); } @@ -49,6 +49,7 @@ class ParserTest extends \Doctrine\Tests\OrmTestCase public function testAbstractSchemaNameSupportsNamespaceAlias() { $parser = $this->createParser('CMS:CmsUser'); + $parser->getEntityManager()->getConfiguration()->addEntityNamespace('CMS', 'Doctrine\Tests\Models\CMS'); $this->assertEquals('Doctrine\Tests\Models\CMS\CmsUser', $parser->AbstractSchemaName()); @@ -61,6 +62,7 @@ class ParserTest extends \Doctrine\Tests\OrmTestCase public function testAbstractSchemaNameSupportsNamespaceAliasWithRelativeClassname() { $parser = $this->createParser('Model:CMS\CmsUser'); + $parser->getEntityManager()->getConfiguration()->addEntityNamespace('Model', 'Doctrine\Tests\Models'); $this->assertEquals('Doctrine\Tests\Models\CMS\CmsUser', $parser->AbstractSchemaName()); @@ -74,7 +76,9 @@ class ParserTest extends \Doctrine\Tests\OrmTestCase public function testMatch($expectedToken, $inputString) { $parser = $this->createParser($inputString); + $parser->match($expectedToken); // throws exception if not matched + $this->addToAssertionCount(1); } @@ -88,6 +92,7 @@ class ParserTest extends \Doctrine\Tests\OrmTestCase $this->setExpectedException('\Doctrine\ORM\Query\QueryException'); $parser = $this->createParser($inputString); + $parser->match($expectedToken); } diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index bd5c4d598..6166a81ad 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -491,11 +491,22 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase { // This also uses FQCNs starting with or without a backslash in the INSTANCE OF parameter $this->assertSqlGeneration( - "SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF (Doctrine\Tests\Models\Company\CompanyEmployee, Doctrine\Tests\Models\Company\CompanyManager)", + "SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF (Doctrine\Tests\Models\Company\CompanyEmployee, \Doctrine\Tests\Models\Company\CompanyManager)", "SELECT c0_.id AS id_0, c0_.name AS name_1, c0_.discr AS discr_2 FROM company_persons c0_ WHERE c0_.discr IN ('employee', 'manager')" ); } + /** + * @group DDC-1194 + */ + public function testSupportsInstanceOfExpressionsInWherePartPrefixedSlash() + { + $this->assertSqlGeneration( + "SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF \Doctrine\Tests\Models\Company\CompanyEmployee", + "SELECT c0_.id AS id_0, c0_.name AS name_1, c0_.discr AS discr_2 FROM company_persons c0_ WHERE c0_.discr IN ('employee')" + ); + } + /** * @group DDC-1194 */