Skip to content
Draft
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
26 changes: 26 additions & 0 deletions parser/parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,32 @@ static void xx_ret_let_assignment(zval *ret, char *type, zval *operator, xx_pars
parser_add_int(ret, "char", state->active_char);
}

// New helper supporting nested property access assignments where the base is an expression (e.g. this->arr->arr = 1)
static void xx_ret_let_property_access_assignment(zval *ret, zval *operator, zval *left_expr, xx_parser_token *P, zval *expr, xx_scanner_state *state)
{
array_init(ret);

parser_add_str(ret, "assign-type", "property-access");
if (operator) {
parser_add_zval(ret, "operator", operator);
}
/* Store the left expression chain */
parser_add_zval(ret, "left", left_expr);

if (P) {
parser_add_str_free(ret, "property", P->token);
efree(P);
}

if (expr) {
parser_add_zval(ret, "expr", expr);
}

parser_add_str(ret, "file", state->active_file);
parser_add_int(ret, "line", state->active_line);
parser_add_int(ret, "char", state->active_char);
}

static void xx_ret_if_statement(zval *ret, zval *expr, zval *statements, zval *elseif_statements, zval *else_statements, xx_scanner_state *state)
{
array_init(ret);
Expand Down
23 changes: 12 additions & 11 deletions parser/zephir.lemon
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
%right BITWISE_NOT .
%right PARENTHESES_CLOSE .
%right SBRACKET_OPEN .
%right ARROW .
%left ARROW .

// The following text is included near the beginning of the C source
// code file that implements the parser.
Expand Down Expand Up @@ -464,10 +464,6 @@ xx_class_definition(R) ::= xx_class_properties_definition(C) xx_class_consts_def
xx_ret_class_definition(&R, &C, &M, &K, status->scanner_state);
}

xx_class_definition(R) ::= xx_class_consts_definition(K) xx_class_properties_definition(C) xx_class_methods_definition(M) . {
xx_ret_class_definition(&R, &C, &M, &K, status->scanner_state);
}

xx_interface_definition(R) ::= xx_class_consts_definition(C) . {
xx_ret_interface_definition(&R, NULL, &C, status->scanner_state);
}
Expand Down Expand Up @@ -1401,6 +1397,10 @@ xx_for_statement(R) ::= FOR IDENTIFIER(K) COMMA IDENTIFIER(V) IN REVERSE xx_comm
xx_ret_for_statement(&R, &E, K, V, 1, &L, status->scanner_state);
}

xx_for_statement(R) ::= FOR IDENTIFIER(K) COMMA IDENTIFIER(V) IN REVERSE xx_common_expr(E) BRACKET_OPEN BRACKET_CLOSE . {
xx_ret_for_statement(&R, &E, K, V, 1, NULL, status->scanner_state);
}

xx_for_statement(R) ::= FOR PARENTHESES_OPEN IDENTIFIER(V) IN xx_common_expr(E) PARENTHESES_CLOSE BRACKET_OPEN xx_statement_list(L) BRACKET_CLOSE . {
xx_ret_for_statement(&R, &E, NULL, V, 0, &L, status->scanner_state);
}
Expand Down Expand Up @@ -1523,14 +1523,15 @@ xx_let_assignment(R) ::= IDENTIFIER(D) ARROW IDENTIFIER(I) SBRACKET_OPEN SBRACKE
}

/* y->x[z][] = {expr} */
xx_let_assignment(R) ::= IDENTIFIER(D) ARROW IDENTIFIER(I) xx_array_offset_list(X) xx_assignment_operator(O) xx_assign_expr(E) . {
xx_ret_let_assignment(&R, "object-property-array-index", &O, D, I, &X, &E, status->scanner_state);
}

xx_let_assignment(R) ::= IDENTIFIER(D) ARROW IDENTIFIER(I) xx_array_offset_list(X) SBRACKET_OPEN SBRACKET_CLOSE xx_assignment_operator(O) xx_assign_expr(E) . {
xx_ret_let_assignment(&R, "object-property-array-index-append", &O, D, I, &X, &E, status->scanner_state);
}

/* {expr}->x = {expr} (nested property access) */
xx_let_assignment(R) ::= xx_common_expr(V) ARROW IDENTIFIER(I) xx_assignment_operator(O) xx_assign_expr(E) . {
xx_ret_let_property_access_assignment(&R, &O, &V, I, &E, status->scanner_state);
}

/* y::x = {expr} */
xx_let_assignment(R) ::= IDENTIFIER(D) DOUBLECOLON IDENTIFIER(I) xx_assignment_operator(O) xx_assign_expr(E) . {
xx_ret_let_assignment(&R, "static-property", &O, D, I, NULL, &E, status->scanner_state);
Expand Down Expand Up @@ -1994,11 +1995,11 @@ xx_common_expr(R) ::= xx_common_expr(O1) EXCLUSIVE_RANGE xx_common_expr(O2) . {
}

/* y = fetch x, z[k] */
xx_fetch_expr(R) ::= FETCH IDENTIFIER(O1) COMMA xx_common_expr(O2) . {
xx_fetch_expr(R) ::= FETCH IDENTIFIER(O1) COMMA xx_common_expr(E2) . {
{
zval identifier;
xx_ret_literal(&identifier, XX_T_IDENTIFIER, O1, status->scanner_state);
xx_ret_expr(&R, "fetch", &identifier, &O2, NULL, status->scanner_state);
xx_ret_expr(&R, "fetch", &identifier, &E2, NULL, status->scanner_state);
}
}

Expand Down
58 changes: 58 additions & 0 deletions tests/operators/property-access-nested-deep.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
--TEST--
Deep nested property access in let assignment (this->a->b->c = 42)
--SKIPIF--
<?php include(__DIR__ . '/../skipif.inc'); ?>
--FILE--
<?php
$code = <<<'ZEP'
namespace Debug;

class Chain
{
public a;
public b;
public c;

public function make()
{
let this->a = new Chain();
let this->a->b = new Chain();
let this->a->b->c = 42;
}
}
ZEP;

$ir = zephir_parse_file($code, '(eval code)');
$class = $ir[1];
$methods = $class['definition']['methods'];
$make = null;
foreach ($methods as $m) {
if ($m['name'] === 'make') { $make = $m; break; }
}
if (!$make) { echo "MISSING_METHOD\n"; return; }
$statements = $make['statements'];
$lets = [];
foreach ($statements as $st) { if ($st['type'] === 'let') { $lets[] = $st; } }
if (count($lets) !== 3) { echo "WRONG_LET_COUNT\n"; return; }
$a1 = $lets[0]['assignments'][0];
$a2 = $lets[1]['assignments'][0];
$a3 = $lets[2]['assignments'][0];
// Validate assign types
if ($a1['assign-type'] !== 'object-property' || $a1['property'] !== 'a') { echo "FIRST_FAIL\n"; return; }
if ($a2['assign-type'] !== 'property-access' || $a2['property'] !== 'b') { echo "SECOND_FAIL\n"; return; }
if ($a3['assign-type'] !== 'property-access' || $a3['property'] !== 'c') { echo "THIRD_FAIL\n"; return; }
// Check left chain forms
if ($a2['left']['type'] !== 'property-access') { echo "CHAIN2_FAIL\n"; return; }
if ($a3['left']['type'] !== 'property-access') { echo "CHAIN3_FAIL\n"; return; }
// Ensure deepest chain left-left structure ends with identifier 'a'
$left = $a3['left'];
// Walk back one level: left = ( (this->a->b) ) -> retrieve its left
$l2 = $left['left'];
$l3 = $l2['left']; // Should be identifier 'this'
if ($l2['right']['value'] !== 'b') { echo "B_PROP_MISSING\n"; return; }
if ($l3['right']['value'] !== 'a') { echo "A_PROP_MISSING\n"; return; }
echo "OK\n";
?>
--EXPECT--
OK

52 changes: 52 additions & 0 deletions tests/operators/property-access-nested.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
--TEST--
Nested property access in let assignment (this->arr->arr = 1)
--SKIPIF--
<?php include(__DIR__ . '/../skipif.inc'); ?>
--FILE--
<?php
$code = <<<'ZEP'
namespace Debug;

class ZephirDebug
{
public arr;

public static function test()
{
let this->arr = new ZephirDebug();
let this->arr->arr = 1;
}
}
ZEP;

try {
$ir = zephir_parse_file($code, '(eval code)');
// Find the class definition and its method to assert assignments exist
$class = $ir[1];
$methods = $class['definition']['methods'];
$testMethod = null;
foreach ($methods as $m) {
if ($m['name'] === 'test') { $testMethod = $m; break; }
}
if (!$testMethod) {
echo "MISSING_METHOD\n"; exit; }
$statements = $testMethod['statements'];
$lets = [];
foreach ($statements as $st) { if ($st['type'] === 'let') { $lets[] = $st; } }
if (count($lets) !== 2) { echo "WRONG_LET_COUNT\n"; exit; }
$a1 = $lets[0]['assignments'][0];
$a2 = $lets[1]['assignments'][0];
// First assignment should be a simple object property
if ($a1['assign-type'] !== 'object-property' || $a1['property'] !== 'arr') { echo "FIRST_ASSIGN_FAIL\n"; exit; }
// Second assignment should represent nested property access (current helper names it 'property-access')
if ($a2['assign-type'] !== 'property-access' || $a2['property'] !== 'arr') { echo "SECOND_ASSIGN_FAIL\n"; exit; }
// Ensure left side is a property-access expression chain
if ($a2['left']['type'] !== 'property-access') { echo "LEFT_EXPR_FAIL\n"; exit; }
echo "OK\n";
} catch (Throwable $e) {
echo 'EXCEPTION: ' . $e->getMessage() . "\n";
}
?>
--EXPECT--
OK

Loading