Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG-5.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
33 changes: 32 additions & 1 deletion ext/phalcon/annotations/base.c
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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 {
Expand Down
33 changes: 32 additions & 1 deletion ext/phalcon/annotations/parser.c
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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 {
Expand Down
36 changes: 36 additions & 0 deletions tests/unit/Annotations/Reader/ParseDocBlockTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 <team@phalcon.io>
* @since 2026-06-05
* @issue https://github.com/phalcon/cphalcon/issues/16084
*/
public function testAnnotationsReaderParseDocBlockParenthesesInString(): void
{
$docBlock = <<<EOF
/**
* @SingleQuoteOpenParen(key='value(')
* @SingleQuoteCloseParen(key='value)')
* @DoubleQuoteParens(key="value()")
*/
EOF;

$reader = new Reader();
$parsed = $reader->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']);
}
}
Loading