From b2a501c22702c1a790ca2b61e6ae6fb4ceba7c14 Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Tue, 26 May 2026 19:59:41 +0200 Subject: [PATCH] fix: clone operator precedence and add comprehensive tests The clone operator was incorrectly consuming binary operators. For example: clone $obj + 1 was parsed as clone ($obj + 1) instead of the correct: (clone $obj) + 1 --- src/ast.js | 2 +- src/parser/expr.js | 7 +++-- test/snapshot/clone.test.js | 57 +++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/ast.js b/src/ast.js index db93a4744..43200650a 100644 --- a/src/ast.js +++ b/src/ast.js @@ -155,7 +155,7 @@ AST.precedence = {}; ["cast", "silent"], ["**"], // TODO: [ (array) - // TODO: clone, new + // TODO: new ].forEach(function (list, index) { list.forEach(function (operator) { AST.precedence[operator] = index + 1; diff --git a/src/parser/expr.js b/src/parser/expr.js index cbc07bdcc..23b066913 100644 --- a/src/parser/expr.js +++ b/src/parser/expr.js @@ -363,7 +363,8 @@ module.exports = { this.next(); if (this.version >= 805 && this.token === "(") { this.next(); - const what = this.read_expr(); + let what = this.read_variable(false, false); + what = this.handleDereferencable(what); let properties = null; if (this.token === ",") { properties = this.next().read_expr(); @@ -371,7 +372,9 @@ module.exports = { this.expect(")") && this.next(); return node(what, properties); } - return node(this.read_expr(), null); + let what = this.read_variable(false, false); + what = this.handleDereferencable(what); + return node(what, null); } switch (this.token) { diff --git a/test/snapshot/clone.test.js b/test/snapshot/clone.test.js index 157c53918..6a5049402 100644 --- a/test/snapshot/clone.test.js +++ b/test/snapshot/clone.test.js @@ -1,5 +1,32 @@ const parser = require("../main"); +function filterKey(fn, obj) { + if (Array.isArray(obj)) { + return obj.map((e) => filterKey(fn, e)); + } + + if (typeof obj === "object" && obj !== null) { + return Object.keys(obj) + .filter(fn) + .reduce( + (result, key) => ({ + ...result, + [key]: filterKey(fn, obj[key]), + }), + {}, + ); + } + + return obj; +} + +function shouldBeSame(a, b) { + const fn = (key) => key !== "parenthesizedExpression"; + expect(filterKey(fn, parser.parseEval(a))).toEqual( + filterKey(fn, parser.parseEval(b)), + ); +} + describe("clone", function () { it("simple", function () { expect(parser.parseEval("clone $obj;")).toMatchSnapshot(); @@ -20,3 +47,33 @@ describe("clone", function () { ).toMatchSnapshot(); }); }); + +describe("clone precedence comparison", function () { + it("clone $obj + 1 should be same as (clone $obj) + 1", function () { + shouldBeSame("clone $obj + 1", "(clone $obj) + 1"); + }); + it("clone $obj * 2 should be same as (clone $obj) * 2", function () { + shouldBeSame("clone $obj * 2", "(clone $obj) * 2"); + }); + it("clone $obj - 1 should be same as (clone $obj) - 1", function () { + shouldBeSame("clone $obj - 1", "(clone $obj) - 1"); + }); + it("clone $obj / 2 should be same as (clone $obj) / 2", function () { + shouldBeSame("clone $obj / 2", "(clone $obj) / 2"); + }); + it("clone $obj->prop + 1 should be same as (clone $obj->prop) + 1", function () { + shouldBeSame("clone $obj->prop + 1", "(clone $obj->prop) + 1"); + }); + it("clone $obj->method() * 2 should be same as (clone $obj->method()) * 2", function () { + shouldBeSame("clone $obj->method() * 2", "(clone $obj->method()) * 2"); + }); + it("clone $obj[0] + 1 should be same as (clone $obj[0]) + 1", function () { + shouldBeSame("clone $obj[0] + 1", "(clone $obj[0]) + 1"); + }); + it("-clone $obj should be same as -(clone $obj)", function () { + shouldBeSame("-clone $obj", "-(clone $obj)"); + }); + it("!clone $obj should be same as !(clone $obj)", function () { + shouldBeSame("!clone $obj", "!(clone $obj)"); + }); +});