From 2e6bac6c4c3b7af14b424450a59c741a30adc573 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 5 Jun 2026 21:07:04 -0500 Subject: [PATCH 1/3] [#16084] - better parentheses handling #16084 Assisted-by: Claude Code --- ext/phalcon/annotations/base.c | 33 +++++++++++++++++++++++++++++++- ext/phalcon/annotations/parser.c | 33 +++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/ext/phalcon/annotations/base.c b/ext/phalcon/annotations/base.c index a485147eb53..e69040d643d 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 2a20b2d2ed8..ca8d977ac9c 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 { From 85e84a250e5408ece6636d1c189c09092bfeb6f3 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 5 Jun 2026 21:09:08 -0500 Subject: [PATCH 2/3] [#16084] - adding test #16084 Assisted-by: Claude Code --- .../Annotations/Reader/ParseDocBlockTest.php | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/unit/Annotations/Reader/ParseDocBlockTest.php b/tests/unit/Annotations/Reader/ParseDocBlockTest.php index 258ecb01ed1..9387f15e9e7 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']); + } } From 8d076855129723da1f101b5b878ce0764cfb57b3 Mon Sep 17 00:00:00 2001 From: Nikolaos Dimopoulos Date: Fri, 5 Jun 2026 21:12:06 -0500 Subject: [PATCH 3/3] [#16084] - updating changelog Assisted-by: Claude Code --- CHANGELOG-5.0.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG-5.0.md b/CHANGELOG-5.0.md index eae819aa0ec..338f7da3a80 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