From ce9c3561c39bc192f82fdd9b36e4f41c340323fc Mon Sep 17 00:00:00 2001 From: mao <118530756+maoxor@users.noreply.github.com> Date: Thu, 29 Oct 2026 17:36:26 +0100 Subject: [PATCH 1/2] fix: don't carry class constant attributes over to the next member read_class_body reset the attribute list after enum cases, methods and properties, but not after constants, so the member following an attributed constant got the same attributes. The location snapshot had recorded the leaked attribute on `$bar`. --- src/parser/class.js | 1 + .../__snapshots__/location.test.js.snap | 39 +------------------ test/snapshot/attributes.test.js | 17 ++++++++ 3 files changed, 19 insertions(+), 38 deletions(-) diff --git a/src/parser/class.js b/src/parser/class.js index fad28269a..8676ce25d 100644 --- a/src/parser/class.js +++ b/src/parser/class.js @@ -119,6 +119,7 @@ module.exports = { this.raiseError("Cannot use asymmetric visibility on constants"); } const constants = this.read_constant_list(flags, attrs, locStart); + attrs = []; if (this.expect(";")) { this.next(); } diff --git a/test/snapshot/__snapshots__/location.test.js.snap b/test/snapshot/__snapshots__/location.test.js.snap index c76790e11..891454f18 100644 --- a/test/snapshot/__snapshots__/location.test.js.snap +++ b/test/snapshot/__snapshots__/location.test.js.snap @@ -4180,44 +4180,7 @@ Program { }, "properties": [ Property { - "attrGroups": [ - AttrGroup { - "attrs": [ - Attribute { - "args": [], - "kind": "attribute", - "loc": Location { - "end": Position { - "column": 22, - "line": 1, - "offset": 22, - }, - "source": "Deprecated", - "start": Position { - "column": 12, - "line": 1, - "offset": 12, - }, - }, - "name": "Deprecated", - }, - ], - "kind": "attrgroup", - "loc": Location { - "end": Position { - "column": 23, - "line": 1, - "offset": 23, - }, - "source": "#[Deprecated]", - "start": Position { - "column": 10, - "line": 1, - "offset": 10, - }, - }, - }, - ], + "attrGroups": [], "hooks": [], "kind": "property", "loc": Location { diff --git a/test/snapshot/attributes.test.js b/test/snapshot/attributes.test.js index 6f71e9e66..9119fa55a 100644 --- a/test/snapshot/attributes.test.js +++ b/test/snapshot/attributes.test.js @@ -266,6 +266,23 @@ describe("Parse Attributes", () => { ).toMatchSnapshot(); }); + it("doesnt repeat attributes from a previous class constant", () => { + const ast = parser.parseEval(` + class A { + #[B] + const C = 1; + const D = 2; + public $e; + function f() {} + } + `); + const [c, d, e, f] = ast.children[0].body; + expect(c.attrGroups).toHaveLength(1); + expect(d.attrGroups).toEqual([]); + expect(e.properties[0].attrGroups).toEqual([]); + expect(f.attrGroups).toEqual([]); + }); + it("parses attributes as comments for PHP < 8", () => { expect( parser.parseEval( From 2cebde15fb182295dd3dcc405b33d96c1970c0f3 Mon Sep 17 00:00:00 2001 From: mao <118530756+maoxor@users.noreply.github.com> Date: Thu, 29 Oct 2026 17:36:26 +0100 Subject: [PATCH 2/2] fix: keep attributes on classes and interfaces declared in a block read_inner_statement read the attributes but did not pass them on to class and interface declarations, and did not accept readonly classes at all. --- src/parser/statement.js | 5 +++-- test/snapshot/attributes.test.js | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/parser/statement.js b/src/parser/statement.js index 4bdb56177..0d15d8652 100644 --- a/src/parser/statement.js +++ b/src/parser/statement.js @@ -171,10 +171,11 @@ module.exports = { // optional flags case this.tok.T_ABSTRACT: case this.tok.T_FINAL: + case this.tok.T_READ_ONLY: case this.tok.T_CLASS: - return this.read_class_declaration_statement(); + return this.read_class_declaration_statement(attrs); case this.tok.T_INTERFACE: - return this.read_interface_declaration_statement(); + return this.read_interface_declaration_statement(attrs); case this.tok.T_TRAIT: return this.read_trait_declaration_statement(attrs); case this.tok.T_ENUM: diff --git a/test/snapshot/attributes.test.js b/test/snapshot/attributes.test.js index 9119fa55a..b52b1cef7 100644 --- a/test/snapshot/attributes.test.js +++ b/test/snapshot/attributes.test.js @@ -283,6 +283,32 @@ describe("Parse Attributes", () => { expect(f.attrGroups).toEqual([]); }); + it("can parse attributes on declarations in inner statements", () => { + const ast = parser.parseEval(` + if (true) { + #[A] + class B {} + #[C] + final class D {} + #[E] + interface F {} + #[G] + readonly class H {} + } + `); + expect( + ast.children[0].body.children.map((node) => [ + node.kind, + node.attrGroups.map((group) => group.attrs[0].name), + ]), + ).toEqual([ + ["class", ["A"]], + ["class", ["C"]], + ["interface", ["E"]], + ["class", ["G"]], + ]); + }); + it("parses attributes as comments for PHP < 8", () => { expect( parser.parseEval(