diff --git a/CHANGELOG-5.0.md b/CHANGELOG-5.0.md index eae819aa0e..338f7da3a8 100644 --- a/CHANGELOG-5.0.md +++ b/CHANGELOG-5.0.md @@ -20,6 +20,7 @@ - Fixed `Phalcon\Mvc\Model\Query\Builder::orderBy()` when the array syntax is used with complex PHQL expressions. Previously any array item containing a space was split as a simple `column direction` pair, corrupting expressions such as `CASE WHEN inv_status_flag = 1 THEN 0 ELSE 1 END ASC`. The builder now only treats a trailing `ASC`/`DESC` as the direction (autoescaping a simple column) and preserves complex expressions verbatim. [#17077](https://github.com/phalcon/cphalcon/issues/17077) - Fixed `Phalcon\Mvc\Model\Query` (PHQL) parsing of identifiers whose name begins with the `NOT` keyword. Columns, tables, and aliases such as `notice_id` were truncated to `ice_id` (the leading `not` was dropped), causing the database to report the column as unknown - most visibly in `Phalcon\Mvc\Model\Query\Builder` join conditions built via `createBuilder()`. The scanner's re2c backtracking marker shared the token-start pointer, so the `NOT BETWEEN` rule advanced it past `not`; escaped identifiers containing internal escapes (e.g. `[col\[0\]]`) were corrupted by the same root cause. [#16831](https://github.com/phalcon/cphalcon/issues/16831) - Fixed PHQL parser cache to use string-keyed lookups (`zend_hash_str_find`/`zend_hash_str_update`) instead of integer keys derived from `zend_inline_hash_func`, eliminating hash collisions that caused different PHQL queries to return identical cached ASTs [#14791](https://github.com/phalcon/cphalcon/issues/14791) +- Fixed `Phalcon\Annotations\Reader` failing to parse a docblock when an annotation argument is a string literal containing a parenthesis (e.g. `@SomeAnnotation(key='value(')`). The docblock pre-scan that locates each `@Annotation(...)` span counted every `(`/`)`, including those inside quoted values, so an unbalanced parenthesis in a string consumed the rest of the comment and produced a "Scanning error". [#16084](https://github.com/phalcon/cphalcon/issues/16084) ### Removed diff --git a/ext/phalcon/annotations/base.c b/ext/phalcon/annotations/base.c index a485147eb5..e69040d643 100644 --- a/ext/phalcon/annotations/base.c +++ b/ext/phalcon/annotations/base.c @@ -131,7 +131,7 @@ int phannot_parse_annotations(zval *result, zval *comment, zval *file_path, zval */ static void phannot_remove_comment_separators(char **ret, int *ret_len, const char *comment, int length, int *start_lines) { - char ch; + char ch, quote; int start_mode = 1, j, i, open_parentheses; smart_str processed_str = {0}; @@ -182,6 +182,37 @@ static void phannot_remove_comment_separators(char **ret, int *ret_len, const ch smart_str_appendc(&processed_str, ch); + if (ch == '"' || ch == '\'') { + quote = ch; + + /** + * Consume the whole string literal so that any + * parentheses inside it are not counted as structural + */ + for (j++; j < length; j++) { + ch = comment[j]; + smart_str_appendc(&processed_str, ch); + + if (ch == '\\') { + j++; + if (j < length) { + smart_str_appendc(&processed_str, comment[j]); + } + continue; + } + + if (ch == quote) { + break; + } + + if (ch == '\n') { + (*start_lines)++; + } + } + + continue; + } + if (ch == '(') { open_parentheses++; } else { diff --git a/ext/phalcon/annotations/parser.c b/ext/phalcon/annotations/parser.c index 2a20b2d2ed..ca8d977ac9 100644 --- a/ext/phalcon/annotations/parser.c +++ b/ext/phalcon/annotations/parser.c @@ -1177,7 +1177,7 @@ int phannot_parse_annotations(zval *result, zval *comment, zval *file_path, zval */ static void phannot_remove_comment_separators(char **ret, int *ret_len, const char *comment, int length, int *start_lines) { - char ch; + char ch, quote; int start_mode = 1, j, i, open_parentheses; smart_str processed_str = {0}; @@ -1228,6 +1228,37 @@ static void phannot_remove_comment_separators(char **ret, int *ret_len, const ch smart_str_appendc(&processed_str, ch); + if (ch == '"' || ch == '\'') { + quote = ch; + + /** + * Consume the whole string literal so that any + * parentheses inside it are not counted as structural + */ + for (j++; j < length; j++) { + ch = comment[j]; + smart_str_appendc(&processed_str, ch); + + if (ch == '\\') { + j++; + if (j < length) { + smart_str_appendc(&processed_str, comment[j]); + } + continue; + } + + if (ch == quote) { + break; + } + + if (ch == '\n') { + (*start_lines)++; + } + } + + continue; + } + if (ch == '(') { open_parentheses++; } else { diff --git a/tests/unit/Annotations/Reader/ParseDocBlockTest.php b/tests/unit/Annotations/Reader/ParseDocBlockTest.php index 258ecb01ed..9387f15e9e 100644 --- a/tests/unit/Annotations/Reader/ParseDocBlockTest.php +++ b/tests/unit/Annotations/Reader/ParseDocBlockTest.php @@ -95,4 +95,40 @@ public function testAnnotationsReaderParseDocBlock(): void $parsed[3] ); } + + /** + * An annotation argument that is a string containing a parenthesis must be + * parsed correctly - the parenthesis inside the string is not a structural + * one and must not break the docblock scanning. + * + * @author Phalcon Team + * @since 2026-06-05 + * @issue https://github.com/phalcon/cphalcon/issues/16084 + */ + public function testAnnotationsReaderParseDocBlockParenthesesInString(): void + { + $docBlock = <<parseDocBlock($docBlock); + + $this->assertIsArray($parsed); + $this->assertCount(3, $parsed); + + $this->assertSame('SingleQuoteOpenParen', $parsed[0]['name']); + $this->assertSame('key', $parsed[0]['arguments'][0]['name']); + $this->assertSame('value(', $parsed[0]['arguments'][0]['expr']['value']); + + $this->assertSame('SingleQuoteCloseParen', $parsed[1]['name']); + $this->assertSame('value)', $parsed[1]['arguments'][0]['expr']['value']); + + $this->assertSame('DoubleQuoteParens', $parsed[2]['name']); + $this->assertSame('value()', $parsed[2]['arguments'][0]['expr']['value']); + } }