From d14b2035c7a8231ae59ef2516227a50cca16bf8c Mon Sep 17 00:00:00 2001 From: Oliver Eisenhut Date: Thu, 13 Nov 2025 21:30:00 +0100 Subject: [PATCH 1/2] Add support for anonymous readonly classes --- src/parser/expr.js | 13 +- test/snapshot/__snapshots__/new.test.js.snap | 136 ++++++++++++++++++- test/snapshot/new.test.js | 14 ++ 3 files changed, 160 insertions(+), 3 deletions(-) diff --git a/src/parser/expr.js b/src/parser/expr.js index 3d73a15fb..fd7bf1e4c 100644 --- a/src/parser/expr.js +++ b/src/parser/expr.js @@ -763,7 +763,11 @@ module.exports = { return result(newExp, args); } const attrs = this.read_attr_list(); - if (this.token === this.tok.T_CLASS) { + const isReadonly = this.token === this.tok.T_READ_ONLY; + if ( + this.token === this.tok.T_CLASS || + (isReadonly && this.next().token === this.tok.T_CLASS) + ) { const what = this.node("class"); // Annonymous class declaration if (this.next().token === "(") { @@ -775,7 +779,12 @@ module.exports = { if (this.expect("{")) { body = this.next().read_class_body(true, false); } - const whatNode = what(null, propExtends, propImplements, body, [0, 0, 0]); + const whatNode = what(null, propExtends, propImplements, body, [ + 0, + 0, + 0, + isReadonly ? 1 : 0, + ]); whatNode.attrGroups = attrs; return result(whatNode, args); } diff --git a/test/snapshot/__snapshots__/new.test.js.snap b/test/snapshot/__snapshots__/new.test.js.snap index 58d7c35f8..e611538ce 100644 --- a/test/snapshot/__snapshots__/new.test.js.snap +++ b/test/snapshot/__snapshots__/new.test.js.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`new #348 - byref usage deprecated 1`] = ` Program { @@ -226,6 +226,140 @@ Program { } `; +exports[`new anonymous readonly 1`] = ` +Program { + "children": [ + ExpressionStatement { + "expression": New { + "arguments": [], + "kind": "new", + "what": Class { + "attrGroups": [], + "body": [], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": true, + "isFinal": false, + "isReadonly": true, + "kind": "class", + "name": null, + }, + }, + "kind": "expressionstatement", + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`new anonymous readonly no parens 1`] = ` +Program { + "children": [ + ExpressionStatement { + "expression": New { + "arguments": [], + "kind": "new", + "what": Class { + "attrGroups": [], + "body": [], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": true, + "isFinal": false, + "isReadonly": true, + "kind": "class", + "name": null, + }, + }, + "kind": "expressionstatement", + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`new anonymous readonly with argument 1`] = ` +Program { + "children": [ + ExpressionStatement { + "expression": New { + "arguments": [ + Variable { + "curly": false, + "kind": "variable", + "name": "var", + }, + ], + "kind": "new", + "what": Class { + "attrGroups": [], + "body": [], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": true, + "isFinal": false, + "isReadonly": true, + "kind": "class", + "name": null, + }, + }, + "kind": "expressionstatement", + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`new anonymous readonly with multiple argument 1`] = ` +Program { + "children": [ + ExpressionStatement { + "expression": New { + "arguments": [ + Variable { + "curly": false, + "kind": "variable", + "name": "one", + }, + Variable { + "curly": false, + "kind": "variable", + "name": "two", + }, + Variable { + "curly": false, + "kind": "variable", + "name": "three", + }, + ], + "kind": "new", + "what": Class { + "attrGroups": [], + "body": [], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": true, + "isFinal": false, + "isReadonly": true, + "kind": "class", + "name": null, + }, + }, + "kind": "expressionstatement", + }, + ], + "errors": [], + "kind": "program", +} +`; + exports[`new anonymous with argument 1`] = ` Program { "children": [ diff --git a/test/snapshot/new.test.js b/test/snapshot/new.test.js index db340d959..6839c6cbc 100644 --- a/test/snapshot/new.test.js +++ b/test/snapshot/new.test.js @@ -42,6 +42,20 @@ describe("new", function () { parser.parseEval("new class($one, $two, $three) {};"), ).toMatchSnapshot(); }); + it("anonymous readonly", function () { + expect(parser.parseEval("new readonly class() {};")).toMatchSnapshot(); + }); + it("anonymous readonly no parens", function () { + expect(parser.parseEval("new readonly class {};")).toMatchSnapshot(); + }); + it("anonymous readonly with argument", function () { + expect(parser.parseEval("new readonly class($var) {};")).toMatchSnapshot(); + }); + it("anonymous readonly with multiple argument", function () { + expect( + parser.parseEval("new readonly class($one, $two, $three) {};"), + ).toMatchSnapshot(); + }); it("static array", () => { expect( parser.parseEval("return new self::$mapping[$map]();"), From 9cf068077b335385c745b843e1c02c05bff35659 Mon Sep 17 00:00:00 2001 From: Oliver Eisenhut Date: Thu, 12 Feb 2026 09:07:33 +0100 Subject: [PATCH 2/2] Only allow anonymous readonly classes in php >= 8.3 --- src/parser/expr.js | 13 +++++++++---- test/snapshot/new.test.js | 9 +++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/parser/expr.js b/src/parser/expr.js index fd7bf1e4c..0000332a9 100644 --- a/src/parser/expr.js +++ b/src/parser/expr.js @@ -764,10 +764,15 @@ module.exports = { } const attrs = this.read_attr_list(); const isReadonly = this.token === this.tok.T_READ_ONLY; - if ( - this.token === this.tok.T_CLASS || - (isReadonly && this.next().token === this.tok.T_CLASS) - ) { + if (isReadonly) { + if (this.version < 803) { + this.raiseError( + "Anonymous readonly classes are not allowed before PHP 8.3", + ); + } + this.next(); + } + if (this.token === this.tok.T_CLASS) { const what = this.node("class"); // Annonymous class declaration if (this.next().token === "(") { diff --git a/test/snapshot/new.test.js b/test/snapshot/new.test.js index 6839c6cbc..bc4e0b9db 100644 --- a/test/snapshot/new.test.js +++ b/test/snapshot/new.test.js @@ -56,6 +56,15 @@ describe("new", function () { parser.parseEval("new readonly class($one, $two, $three) {};"), ).toMatchSnapshot(); }); + it("anonymous readonly class throws errors in PHP < 8.3", () => { + expect(() => + parser.parseEval("new readonly class() {};", { + parser: { + version: "8.2", + }, + }), + ).toThrow("Anonymous readonly classes are not allowed before PHP 8.3"); + }); it("static array", () => { expect( parser.parseEval("return new self::$mapping[$map]();"),