diff --git a/src/ast.js b/src/ast.js index d11b556b5..5bed2679c 100644 --- a/src/ast.js +++ b/src/ast.js @@ -106,6 +106,7 @@ const Position = require("./ast/position"); * - [Namespace](#namespace) * - [PropertyStatement](#propertystatement) * - [Property](#property) + * - [PropertyHook](#propertyhook) * - [Declaration](#declaration) * - [Class](#class) * - [Interface](#interface) @@ -552,6 +553,7 @@ AST.prototype.checkNodes = function () { require("./ast/print"), require("./ast/program"), require("./ast/property"), + require("./ast/propertyhook"), require("./ast/propertylookup"), require("./ast/propertystatement"), require("./ast/reference"), diff --git a/src/ast/property.js b/src/ast/property.js index ad01986ce..8a03219a2 100644 --- a/src/ast/property.js +++ b/src/ast/property.js @@ -18,6 +18,7 @@ const KIND = "property"; * @property {boolean} readonly * @property {boolean} nullable * @property {Identifier|Array<Identifier>|null} type + * @propert {PropertyHook[]} hooks * @property {AttrGroup[]} attrGroups */ module.exports = Statement.extends( @@ -28,6 +29,7 @@ module.exports = Statement.extends( readonly, nullable, type, + hooks, attrGroups, docs, location, @@ -38,6 +40,7 @@ module.exports = Statement.extends( this.readonly = readonly; this.nullable = nullable; this.type = type; + this.hooks = hooks; this.attrGroups = attrGroups; }, ); diff --git a/src/ast/propertyhook.js b/src/ast/propertyhook.js new file mode 100644 index 000000000..8ff8a4ea2 --- /dev/null +++ b/src/ast/propertyhook.js @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2024 Glayzzle (BSD3 License) + * @authors https://github.com/glayzzle/php-parser/graphs/contributors + * @url http://glayzzle.com + */ +"use strict"; + +const Node = require("./node"); +const KIND = "propertyhook"; + +/** + * Defines a class property hook getter & setter + * + * @constructor PropertyHook + * @memberOf module:php-parser + * @extends {Node} + * @property {string} name + * @property {Boolean} isFinal + * @property {Boolean} byref + * @property {Parameter|null} parameter + * @property {Block|Statement} body + */ +module.exports = Node.extends( + KIND, + function PropertyHook(name, isFinal, byref, parameter, body, docs, location) { + Node.apply(this, [KIND, docs, location]); + this.name = name; + this.byref = byref; + this.parameter = parameter; + this.body = body; + this.isFinal = isFinal; + }, +); diff --git a/src/ast/propertystatement.js b/src/ast/propertystatement.js index 271c8e6a8..6cc46926d 100644 --- a/src/ast/propertystatement.js +++ b/src/ast/propertystatement.js @@ -52,6 +52,8 @@ PropertyStatement.prototype.parseFlags = function (flags) { } this.isStatic = flags[1] === 1; + this.isAbstract = flags[2] === 1; + this.isFinal = flags[2] === 2; }; module.exports = PropertyStatement; diff --git a/src/parser/class.js b/src/parser/class.js index 996f5acf3..4d086c1a5 100644 --- a/src/parser/class.js +++ b/src/parser/class.js @@ -152,8 +152,6 @@ module.exports = { // reads a variable const variables = this.read_variable_list(flags, attrs); attrs = []; - this.expect(";"); - this.next(); result = result.concat(variables); } else { // raise an error @@ -178,7 +176,7 @@ module.exports = { * ``` */ read_variable_list: function (flags, attrs) { - const result = this.node("propertystatement"); + let property_statement = this.node("propertystatement"); const properties = this.read_list( /* @@ -201,28 +199,119 @@ module.exports = { const name = this.text().substring(1); // ignore $ this.next(); propName = propName(name); - if (this.token === ";" || this.token === ",") { - return result(propName, null, readonly, nullable, type, attrs || []); - } else if (this.token === "=") { + + let value = null; + let property_hooks = []; + + this.expect([",", ";", "=", "{"]); + + // Property has a value + if (this.token === "=") { // https://github.com/php/php-src/blob/master/Zend/zend_language_parser.y#L815 - return result( - propName, - this.next().read_expr(), - readonly, - nullable, - type, - attrs || [], - ); + value = this.next().read_expr(); + } + + // Property is using a hook to define getter/setters + if (this.token === "{") { + property_hooks = this.read_property_hooks(); } else { - this.expect([",", ";", "="]); - return result(propName, null, nullable, type, attrs || []); + this.expect([";", ","]); } + + return result( + propName, + value, + readonly, + nullable, + type, + property_hooks, + attrs || [], + ); }, ",", ); - return result(null, properties, flags); + property_statement = property_statement(null, properties, flags); + + // semicolons are found only for regular properties definitions. + // Property hooks are terminated by a closing curly brace, }. + // property_statement is instanciated before this check to avoid including the semicolon in the AST end location of the property. + if (this.token === ";") { + this.next(); + } + return property_statement; }, + + /** + * Reads property hooks + * + * @returns {PropertyHook[]} + */ + read_property_hooks: function () { + if (this.version < 804) { + this.raiseError("Parse Error: Typed Class Constants requires PHP 8.4+"); + } + this.expect("{"); + this.next(); + + const hooks = []; + + while (this.token !== "}") { + hooks.push(this.read_property_hook()); + } + + if (this.token === "}") { + this.next(); + return hooks; + } + return []; + }, + + read_property_hook: function () { + const property_hooks = this.node("propertyhook"); + + const is_final = this.token === this.tok.T_FINAL; + if (is_final) this.next(); + + const is_reference = this.token === "&"; + if (is_reference) this.next(); + + const method_name = this.text(); + + if (method_name !== "get" && method_name !== "set") { + this.raiseError( + "Parse Error: Property hooks must be either 'get' or 'set'", + ); + } + this.next(); + + let parameter = null; + let body = null; + this.expect([this.tok.T_DOUBLE_ARROW, "{", "(", ";"]); + + // interface or abstract definition + if (this.token === ";") { + this.next(); + } + + if (this.token === "(") { + this.next(); + parameter = this.read_parameter(false); + this.expect(")"); + this.next(); + } + + if (this.token === this.tok.T_DOUBLE_ARROW) { + this.next(); + body = this.read_expr(); + this.next(); + } else if (this.token === "{") { + body = this.read_code_block(); + } + + return property_hooks(method_name, is_final, is_reference, parameter, body); + }, + /* * Reads constant list * ```ebnf @@ -460,9 +549,11 @@ module.exports = { this.next(); } attrs = []; + } else if (this.token === this.tok.T_STRING) { + result.push(this.read_variable_list(flags, attrs)); } else { // raise an error - this.error([this.tok.T_CONST, this.tok.T_FUNCTION]); + this.error([this.tok.T_CONST, this.tok.T_FUNCTION, this.tok.T_STRING]); this.next(); } } diff --git a/test/snapshot/__snapshots__/acid.test.js.snap b/test/snapshot/__snapshots__/acid.test.js.snap index 66955f4bb..6a5294025 100644 --- a/test/snapshot/__snapshots__/acid.test.js.snap +++ b/test/snapshot/__snapshots__/acid.test.js.snap @@ -831,6 +831,8 @@ Program { "visibility": "", }, PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "loc": Location { @@ -852,6 +854,7 @@ Program { "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "loc": Location { "end": Position { diff --git a/test/snapshot/__snapshots__/attributes.test.js.snap b/test/snapshot/__snapshots__/attributes.test.js.snap index 36fb1d4f0..8dd279b4a 100644 --- a/test/snapshot/__snapshots__/attributes.test.js.snap +++ b/test/snapshot/__snapshots__/attributes.test.js.snap @@ -365,6 +365,8 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ @@ -381,6 +383,7 @@ Program { "kind": "attrgroup", }, ], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -399,6 +402,8 @@ Program { "visibility": "public", }, PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ @@ -415,6 +420,7 @@ Program { "kind": "attrgroup", }, ], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -433,6 +439,8 @@ Program { "visibility": "private", }, PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ @@ -449,6 +457,7 @@ Program { "kind": "attrgroup", }, ], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -2566,6 +2575,8 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ @@ -2652,6 +2663,7 @@ Program { "kind": "attrgroup", }, ], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", diff --git a/test/snapshot/__snapshots__/class.test.js.snap b/test/snapshot/__snapshots__/class.test.js.snap index 6fd21a3aa..67bb7f6c0 100644 --- a/test/snapshot/__snapshots__/class.test.js.snap +++ b/test/snapshot/__snapshots__/class.test.js.snap @@ -40,6 +40,8 @@ Program { ], }, PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "leadingComments": [ @@ -66,6 +68,7 @@ Program { "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -461,11 +464,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -509,11 +515,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -535,11 +544,14 @@ Program { "visibility": "public", }, PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": true, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -594,11 +606,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": true, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -1158,163 +1173,7 @@ Program { } `; -exports[`Test classes Test that readonly method parameters are throwing errors 1`] = ` -Program { - "children": [ - Class { - "attrGroups": [], - "body": [ - Method { - "arguments": [ - Parameter { - "attrGroups": [], - "byref": false, - "flags": 1, - "kind": "parameter", - "name": null, - "nullable": false, - "readonly": false, - "type": null, - "value": null, - "variadic": false, - }, - ], - "attrGroups": [], - "body": null, - "byref": false, - "isAbstract": false, - "isFinal": false, - "isReadonly": false, - "isStatic": false, - "kind": "method", - "name": Identifier { - "kind": "identifier", - "name": "foo", - }, - "nullable": false, - "type": null, - "visibility": "public", - }, - PropertyStatement { - "isStatic": false, - "kind": "propertystatement", - "properties": [ - Property { - "attrGroups": null, - "kind": "property", - "name": Identifier { - "kind": "identifier", - "name": "id", - }, - "nullable": TypeReference { - "kind": "typereference", - "name": "int", - "raw": "int", - }, - "readonly": false, - "type": [], - "value": null, - }, - ], - "visibility": "", - }, - ], - "extends": null, - "implements": null, - "isAbstract": false, - "isAnonymous": false, - "isFinal": false, - "isReadonly": false, - "kind": "class", - "name": Identifier { - "kind": "identifier", - "name": "Bob", - }, - }, - ExpressionStatement { - "expression": undefined, - "kind": "expressionstatement", - }, - ], - "errors": [ - Error { - "expected": undefined, - "kind": "error", - "line": 3, - "message": "readonly properties can be used only on class constructor on line 3", - "token": undefined, - }, - Error { - "expected": 222, - "kind": "error", - "line": 3, - "message": "Parse Error : syntax error, unexpected 'readonly' (T_READ_ONLY), expecting T_VARIABLE on line 3", - "token": "'readonly' (T_READ_ONLY)", - }, - Error { - "expected": [ - ",", - ")", - ], - "kind": "error", - "line": 3, - "message": "Parse Error : syntax error, unexpected 'readonly' (T_READ_ONLY) on line 3", - "token": "'readonly' (T_READ_ONLY)", - }, - Error { - "expected": ")", - "kind": "error", - "line": 3, - "message": "Parse Error : syntax error, unexpected 'readonly' (T_READ_ONLY), expecting ')' on line 3", - "token": "'readonly' (T_READ_ONLY)", - }, - Error { - "expected": "{", - "kind": "error", - "line": 3, - "message": "Parse Error : syntax error, unexpected 'readonly' (T_READ_ONLY), expecting '{' on line 3", - "token": "'readonly' (T_READ_ONLY)", - }, - Error { - "expected": [ - ",", - ";", - "=", - ], - "kind": "error", - "line": 3, - "message": "Parse Error : syntax error, unexpected ')' on line 3", - "token": "')'", - }, - Error { - "expected": ";", - "kind": "error", - "line": 3, - "message": "Parse Error : syntax error, unexpected ')', expecting ';' on line 3", - "token": "')'", - }, - Error { - "expected": [ - 198, - 222, - 182, - ], - "kind": "error", - "line": 3, - "message": "Parse Error : syntax error, unexpected '{' on line 3", - "token": "'{'", - }, - Error { - "expected": "EXPR", - "kind": "error", - "line": 4, - "message": "Parse Error : syntax error, unexpected '}' on line 4", - "token": "'}'", - }, - ], - "kind": "program", -} -`; +exports[`Test classes Test that readonly method parameters are throwing errors 1`] = `"readonly properties can be used only on class constructor on line 3"`; exports[`Test classes Validate usual declarations 1`] = ` Program { @@ -1347,11 +1206,14 @@ Program { "visibility": "", }, PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": true, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -1522,11 +1384,14 @@ Program { "visibility": "public", }, PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -1541,11 +1406,14 @@ Program { "visibility": "protected", }, PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -1917,11 +1785,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", diff --git a/test/snapshot/__snapshots__/classpropertyhooks.test.js.snap b/test/snapshot/__snapshots__/classpropertyhooks.test.js.snap new file mode 100644 index 000000000..a28e33092 --- /dev/null +++ b/test/snapshot/__snapshots__/classpropertyhooks.test.js.snap @@ -0,0 +1,2106 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`classpropertyhooks abstract get + set 1`] = ` +Program { + "children": [ + Class { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": true, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": null, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "get", + "parameter": null, + }, + PropertyHook { + "body": null, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "set", + "parameter": null, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "email", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + "value": null, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "implements": null, + "isAbstract": true, + "isAnonymous": false, + "isFinal": false, + "isReadonly": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "User", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`classpropertyhooks abstract get 1`] = ` +Program { + "children": [ + Class { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": true, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": null, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "get", + "parameter": null, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "email", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + "value": null, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "implements": null, + "isAbstract": true, + "isAnonymous": false, + "isFinal": false, + "isReadonly": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "User", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`classpropertyhooks abstract set 1`] = ` +Program { + "children": [ + Class { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": true, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": null, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "set", + "parameter": null, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "email", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + "value": null, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "implements": null, + "isAbstract": true, + "isAnonymous": false, + "isFinal": false, + "isReadonly": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "User", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`classpropertyhooks can be access by reference 1`] = ` +Program { + "children": [ + Class { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "_baz", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + "value": null, + }, + ], + "visibility": "private", + }, + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "_baz", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "byref": true, + "isFinal": false, + "kind": "propertyhook", + "name": "get", + "parameter": null, + }, + PropertyHook { + "body": Block { + "children": [ + ExpressionStatement { + "expression": Assign { + "kind": "assign", + "left": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "_baz", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "operator": "=", + "right": Call { + "arguments": [ + Variable { + "curly": false, + "kind": "variable", + "name": "value", + }, + ], + "kind": "call", + "what": Name { + "kind": "name", + "name": "strtoupper", + "resolution": "uqn", + }, + }, + }, + "kind": "expressionstatement", + }, + ], + "kind": "block", + }, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "set", + "parameter": null, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "baz", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + "value": null, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "isReadonly": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "Foo", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`classpropertyhooks final on the hook itself 1`] = ` +Program { + "children": [ + Class { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": Block { + "children": [ + If { + "alternate": null, + "body": Block { + "children": [ + Throw { + "kind": "throw", + "what": New { + "arguments": [ + String { + "isDoubleQuote": false, + "kind": "string", + "raw": "'Invalid email'", + "unicode": false, + "value": "Invalid email", + }, + ], + "kind": "new", + "what": Name { + "kind": "name", + "name": "InvalidArgumentException", + "resolution": "uqn", + }, + }, + }, + ], + "kind": "block", + }, + "kind": "if", + "shortForm": false, + "test": Unary { + "kind": "unary", + "type": "!", + "what": Call { + "arguments": [ + Variable { + "curly": false, + "kind": "variable", + "name": "value", + }, + Name { + "kind": "name", + "name": "FILTER_VALIDATE_EMAIL", + "resolution": "uqn", + }, + Name { + "kind": "name", + "name": "FILTER_FLAG_EMAIL_UNICODE", + "resolution": "uqn", + }, + ], + "kind": "call", + "what": Name { + "kind": "name", + "name": "filter_var", + "resolution": "uqn", + }, + }, + }, + }, + ExpressionStatement { + "expression": Assign { + "kind": "assign", + "left": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "email", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "operator": "=", + "right": Variable { + "curly": false, + "kind": "variable", + "name": "value", + }, + }, + "kind": "expressionstatement", + }, + ], + "kind": "block", + }, + "byref": false, + "isFinal": true, + "kind": "propertyhook", + "name": "set", + "parameter": null, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "email", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + "value": null, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "isReadonly": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "StandardUser", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`classpropertyhooks final on the property 1`] = ` +Program { + "children": [ + Class { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": false, + "isFinal": true, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "name", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + "value": null, + }, + ], + "visibility": "public", + }, + PropertyStatement { + "isAbstract": false, + "isFinal": true, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": Call { + "arguments": [ + Variable { + "curly": false, + "kind": "variable", + "name": "value", + }, + ], + "kind": "call", + "what": Name { + "kind": "name", + "name": "strtolower", + "resolution": "uqn", + }, + }, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "set", + "parameter": null, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "username", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + "value": null, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "isReadonly": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "User", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`classpropertyhooks getter arrow function 1`] = ` +Program { + "children": [ + Class { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": Bin { + "kind": "bin", + "left": String { + "isDoubleQuote": false, + "kind": "string", + "raw": "'mailto:'", + "unicode": false, + "value": "mailto:", + }, + "right": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "email", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "type": ".", + }, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "get", + "parameter": null, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "credits", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + "value": null, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "isReadonly": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "BookViewModel", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`classpropertyhooks getter block 1`] = ` +Program { + "children": [ + Class { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": Block { + "children": [ + ExpressionStatement { + "expression": Bin { + "kind": "bin", + "left": String { + "isDoubleQuote": false, + "kind": "string", + "raw": "'mailto:'", + "unicode": false, + "value": "mailto:", + }, + "right": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "email", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "type": ".", + }, + "kind": "expressionstatement", + }, + ], + "kind": "block", + }, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "get", + "parameter": null, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "credits", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + "value": null, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "isReadonly": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "BookViewModel", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`classpropertyhooks not supported in php < 8.4 1`] = `"Parse Error: Typed Class Constants requires PHP 8.4+ on line 2"`; + +exports[`classpropertyhooks setter block form with explicit typed $value 1`] = ` +Program { + "children": [ + Class { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": Block { + "children": [ + ExpressionStatement { + "expression": Assign { + "kind": "assign", + "left": Variable { + "curly": false, + "kind": "variable", + "name": "tmp", + }, + "operator": "=", + "right": Bin { + "kind": "bin", + "left": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "credits", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "right": Number { + "kind": "number", + "value": "1", + }, + "type": "+", + }, + }, + "kind": "expressionstatement", + }, + ExpressionStatement { + "expression": Assign { + "kind": "assign", + "left": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "propertyName", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "operator": "=", + "right": Bin { + "kind": "bin", + "left": Variable { + "curly": false, + "kind": "variable", + "name": "value", + }, + "right": Variable { + "curly": false, + "kind": "variable", + "name": "tmp", + }, + "type": "+", + }, + }, + "kind": "expressionstatement", + }, + ], + "kind": "block", + }, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "set", + "parameter": Parameter { + "attrGroups": [], + "byref": false, + "flags": 0, + "kind": "parameter", + "name": Identifier { + "kind": "identifier", + "name": "value", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "int", + "raw": "int", + }, + "value": null, + "variadic": false, + }, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "credits", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "int", + "raw": "int", + }, + "value": null, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "isReadonly": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "BookViewModel", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`classpropertyhooks setter block form with explicit untyped $value 1`] = ` +Program { + "children": [ + Class { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": Block { + "children": [ + ExpressionStatement { + "expression": Assign { + "kind": "assign", + "left": Variable { + "curly": false, + "kind": "variable", + "name": "tmp", + }, + "operator": "=", + "right": Bin { + "kind": "bin", + "left": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "credits", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "right": Number { + "kind": "number", + "value": "1", + }, + "type": "+", + }, + }, + "kind": "expressionstatement", + }, + ExpressionStatement { + "expression": Assign { + "kind": "assign", + "left": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "propertyName", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "operator": "=", + "right": Bin { + "kind": "bin", + "left": Variable { + "curly": false, + "kind": "variable", + "name": "value", + }, + "right": Variable { + "curly": false, + "kind": "variable", + "name": "tmp", + }, + "type": "+", + }, + }, + "kind": "expressionstatement", + }, + ], + "kind": "block", + }, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "set", + "parameter": Parameter { + "attrGroups": [], + "byref": false, + "flags": 0, + "kind": "parameter", + "name": Identifier { + "kind": "identifier", + "name": "value", + }, + "nullable": false, + "readonly": false, + "type": null, + "value": null, + "variadic": false, + }, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "credits", + }, + "nullable": false, + "readonly": false, + "type": null, + "value": null, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "isReadonly": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "BookViewModel", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`classpropertyhooks setter block form with implicit $value 1`] = ` +Program { + "children": [ + Class { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": Block { + "children": [ + ExpressionStatement { + "expression": Assign { + "kind": "assign", + "left": Variable { + "curly": false, + "kind": "variable", + "name": "tmp", + }, + "operator": "=", + "right": Bin { + "kind": "bin", + "left": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "credits", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "right": Number { + "kind": "number", + "value": "1", + }, + "type": "+", + }, + }, + "kind": "expressionstatement", + }, + ExpressionStatement { + "expression": Assign { + "kind": "assign", + "left": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "propertyName", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "operator": "=", + "right": Bin { + "kind": "bin", + "left": Variable { + "curly": false, + "kind": "variable", + "name": "value", + }, + "right": Variable { + "curly": false, + "kind": "variable", + "name": "tmp", + }, + "type": "+", + }, + }, + "kind": "expressionstatement", + }, + ], + "kind": "block", + }, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "set", + "parameter": null, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "credits", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + "value": null, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "isReadonly": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "BookViewModel", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`classpropertyhooks setter expression form with explicit typed $value 1`] = ` +Program { + "children": [ + Class { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": Assign { + "kind": "assign", + "left": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "name", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "operator": "=", + "right": Call { + "arguments": [ + Variable { + "curly": false, + "kind": "variable", + "name": "new_name", + }, + ], + "kind": "call", + "what": Name { + "kind": "name", + "name": "strtolower", + "resolution": "uqn", + }, + }, + }, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "set", + "parameter": Parameter { + "attrGroups": [], + "byref": false, + "flags": 0, + "kind": "parameter", + "name": Identifier { + "kind": "identifier", + "name": "new_name", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + "value": null, + "variadic": false, + }, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "name", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + "value": null, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "isReadonly": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "Person", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`classpropertyhooks setter expression form with explicit untyped $value 1`] = ` +Program { + "children": [ + Class { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": Assign { + "kind": "assign", + "left": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "name", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "operator": "=", + "right": Call { + "arguments": [ + Variable { + "curly": false, + "kind": "variable", + "name": "new_name", + }, + ], + "kind": "call", + "what": Name { + "kind": "name", + "name": "strtolower", + "resolution": "uqn", + }, + }, + }, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "set", + "parameter": Parameter { + "attrGroups": [], + "byref": false, + "flags": 0, + "kind": "parameter", + "name": Identifier { + "kind": "identifier", + "name": "new_name", + }, + "nullable": false, + "readonly": false, + "type": null, + "value": null, + "variadic": false, + }, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "name", + }, + "nullable": false, + "readonly": false, + "type": null, + "value": null, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "isReadonly": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "Person", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`classpropertyhooks setter expression form with implicit $value 1`] = ` +Program { + "children": [ + Class { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": Assign { + "kind": "assign", + "left": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "credits", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "operator": "=", + "right": Variable { + "curly": false, + "kind": "variable", + "name": "value", + }, + }, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "set", + "parameter": null, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "credits", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + "value": null, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "isReadonly": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "BookViewModel", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`classpropertyhooks support default value 1`] = ` +Program { + "children": [ + Class { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "foo", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "get", + "parameter": null, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "foo", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + "value": String { + "isDoubleQuote": false, + "kind": "string", + "raw": "'default value'", + "unicode": false, + "value": "default value", + }, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "isReadonly": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "Example", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`classpropertyhooks support setter + getter 1`] = ` +Program { + "children": [ + Class { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "modified", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "bool", + "raw": "bool", + }, + "value": Boolean { + "kind": "boolean", + "raw": "false", + "value": false, + }, + }, + ], + "visibility": "private", + }, + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": Bin { + "kind": "bin", + "left": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "foo", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "right": RetIf { + "falseExpr": String { + "isDoubleQuote": false, + "kind": "string", + "raw": "''", + "unicode": false, + "value": "", + }, + "kind": "retif", + "parenthesizedExpression": true, + "test": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "modified", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "trueExpr": String { + "isDoubleQuote": false, + "kind": "string", + "raw": "' (modified)'", + "unicode": false, + "value": " (modified)", + }, + }, + "type": ".", + }, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "get", + "parameter": null, + }, + PropertyHook { + "body": Block { + "children": [ + ExpressionStatement { + "expression": Assign { + "kind": "assign", + "left": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "foo", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "operator": "=", + "right": Call { + "arguments": [ + Variable { + "curly": false, + "kind": "variable", + "name": "value", + }, + ], + "kind": "call", + "what": Name { + "kind": "name", + "name": "strtolower", + "resolution": "uqn", + }, + }, + }, + "kind": "expressionstatement", + }, + ExpressionStatement { + "expression": Assign { + "kind": "assign", + "left": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "modified", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "operator": "=", + "right": Boolean { + "kind": "boolean", + "raw": "true", + "value": true, + }, + }, + "kind": "expressionstatement", + }, + ], + "kind": "block", + }, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "set", + "parameter": Parameter { + "attrGroups": [], + "byref": false, + "flags": 0, + "kind": "parameter", + "name": Identifier { + "kind": "identifier", + "name": "value", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + "value": null, + "variadic": false, + }, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "foo", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + "value": String { + "isDoubleQuote": false, + "kind": "string", + "raw": "'default value'", + "unicode": false, + "value": "default value", + }, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "isReadonly": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "Example", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`classpropertyhooks support setter + getter 2`] = ` +Program { + "children": [ + Class { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "modified", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "bool", + "raw": "bool", + }, + "value": Boolean { + "kind": "boolean", + "raw": "false", + "value": false, + }, + }, + ], + "visibility": "private", + }, + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": Bin { + "kind": "bin", + "left": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "foo", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "right": RetIf { + "falseExpr": String { + "isDoubleQuote": false, + "kind": "string", + "raw": "''", + "unicode": false, + "value": "", + }, + "kind": "retif", + "parenthesizedExpression": true, + "test": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "modified", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "trueExpr": String { + "isDoubleQuote": false, + "kind": "string", + "raw": "' (modified)'", + "unicode": false, + "value": " (modified)", + }, + }, + "type": ".", + }, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "get", + "parameter": null, + }, + PropertyHook { + "body": Block { + "children": [ + ExpressionStatement { + "expression": Assign { + "kind": "assign", + "left": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "foo", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "operator": "=", + "right": Call { + "arguments": [ + Variable { + "curly": false, + "kind": "variable", + "name": "value", + }, + ], + "kind": "call", + "what": Name { + "kind": "name", + "name": "strtolower", + "resolution": "uqn", + }, + }, + }, + "kind": "expressionstatement", + }, + ExpressionStatement { + "expression": Assign { + "kind": "assign", + "left": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "modified", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "operator": "=", + "right": Boolean { + "kind": "boolean", + "raw": "true", + "value": true, + }, + }, + "kind": "expressionstatement", + }, + ], + "kind": "block", + }, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "set", + "parameter": null, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "foo", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + "value": String { + "isDoubleQuote": false, + "kind": "string", + "raw": "'default value'", + "unicode": false, + "value": "default value", + }, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "isReadonly": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "Example", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`classpropertyhooks support setter + getter 3`] = ` +Program { + "children": [ + Class { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": Bin { + "kind": "bin", + "left": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "foo", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "right": RetIf { + "falseExpr": String { + "isDoubleQuote": false, + "kind": "string", + "raw": "''", + "unicode": false, + "value": "", + }, + "kind": "retif", + "parenthesizedExpression": true, + "test": PropertyLookup { + "kind": "propertylookup", + "offset": Identifier { + "kind": "identifier", + "name": "modified", + }, + "what": Variable { + "curly": false, + "kind": "variable", + "name": "this", + }, + }, + "trueExpr": String { + "isDoubleQuote": false, + "kind": "string", + "raw": "' (modified)'", + "unicode": false, + "value": " (modified)", + }, + }, + "type": ".", + }, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "get", + "parameter": null, + }, + PropertyHook { + "body": Call { + "arguments": [ + Variable { + "curly": false, + "kind": "variable", + "name": "value", + }, + ], + "kind": "call", + "what": Name { + "kind": "name", + "name": "strtolower", + "resolution": "uqn", + }, + }, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "set", + "parameter": null, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "foo", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + "value": String { + "isDoubleQuote": false, + "kind": "string", + "raw": "'default value'", + "unicode": false, + "value": "default value", + }, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "implements": null, + "isAbstract": false, + "isAnonymous": false, + "isFinal": false, + "isReadonly": false, + "kind": "class", + "name": Identifier { + "kind": "identifier", + "name": "Example", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; diff --git a/test/snapshot/__snapshots__/comment.test.js.snap b/test/snapshot/__snapshots__/comment.test.js.snap index 0288dac0c..507ca0338 100644 --- a/test/snapshot/__snapshots__/comment.test.js.snap +++ b/test/snapshot/__snapshots__/comment.test.js.snap @@ -1846,6 +1846,8 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "leadingComments": [ @@ -1864,6 +1866,7 @@ Program { "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -1876,6 +1879,7 @@ Program { }, Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -1890,6 +1894,8 @@ Program { "visibility": "protected", }, PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": true, "kind": "propertystatement", "leadingComments": [ @@ -1908,6 +1914,7 @@ Program { "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", diff --git a/test/snapshot/__snapshots__/graceful.test.js.snap b/test/snapshot/__snapshots__/graceful.test.js.snap index cf45e3329..bdbda8496 100644 --- a/test/snapshot/__snapshots__/graceful.test.js.snap +++ b/test/snapshot/__snapshots__/graceful.test.js.snap @@ -5,7 +5,34 @@ Program { "children": [ Interface { "attrGroups": [], - "body": [], + "body": [ + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "", + }, + "nullable": false, + "readonly": false, + "type": Name { + "kind": "name", + "name": "baz", + "resolution": "uqn", + }, + "value": null, + }, + ], + "visibility": "", + }, + ], "extends": null, "kind": "interface", "name": Identifier { @@ -22,25 +49,34 @@ Program { "message": "Parse Error : syntax error, unexpected 'implement' (T_STRING), expecting '{' on line 1", "token": "'implement' (T_STRING)", }, + Error { + "expected": 222, + "kind": "error", + "line": 1, + "message": "Parse Error : syntax error, unexpected '{', expecting T_VARIABLE on line 1", + "token": "'{'", + }, Error { "expected": [ - 198, - 182, + ",", + ";", + "=", + "{", ], "kind": "error", "line": 1, - "message": "Parse Error : syntax error, unexpected 'baz' (T_STRING) on line 1", - "token": "'baz' (T_STRING)", + "message": "Parse Error : syntax error, unexpected '}' on line 1", + "token": "'}'", }, Error { "expected": [ - 198, - 182, + ";", + ",", ], "kind": "error", "line": 1, - "message": "Parse Error : syntax error, unexpected '{' on line 1", - "token": "'{'", + "message": "Parse Error : syntax error, unexpected '}' on line 1", + "token": "'}'", }, ], "kind": "program", @@ -296,23 +332,52 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { - "attrGroups": null, + "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", "name": "onst", }, - "nullable": Name { + "nullable": false, + "readonly": false, + "type": Name { "kind": "name", "name": "foo", "resolution": "uqn", }, + "value": null, + }, + ], + "visibility": "", + }, + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "", + }, + "nullable": false, "readonly": false, - "type": [], + "type": Name { + "kind": "name", + "name": "A", + "resolution": "uqn", + }, "value": null, }, ], @@ -345,6 +410,7 @@ Program { ",", ";", "=", + "{", ], "kind": "error", "line": 1, @@ -352,22 +418,43 @@ Program { "token": "'A' (T_STRING)", }, Error { - "expected": ";", + "expected": [ + ";", + ",", + ], "kind": "error", "line": 1, - "message": "Parse Error : syntax error, unexpected 'A' (T_STRING), expecting ';' on line 1", + "message": "Parse Error : syntax error, unexpected 'A' (T_STRING) on line 1", "token": "'A' (T_STRING)", }, + Error { + "expected": 222, + "kind": "error", + "line": 1, + "message": "Parse Error : syntax error, unexpected '=', expecting T_VARIABLE on line 1", + "token": "'='", + }, Error { "expected": [ - 198, - 222, - 182, + ",", + ";", + "=", + "{", ], "kind": "error", "line": 1, - "message": "Parse Error : syntax error, unexpected '=' on line 1", - "token": "'='", + "message": "Parse Error : syntax error, unexpected '1' (T_LNUMBER) on line 1", + "token": "'1' (T_LNUMBER)", + }, + Error { + "expected": [ + ";", + ",", + ], + "kind": "error", + "line": 1, + "message": "Parse Error : syntax error, unexpected '1' (T_LNUMBER) on line 1", + "token": "'1' (T_LNUMBER)", }, Error { "expected": [ @@ -839,23 +926,52 @@ Program { Trait { "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { - "attrGroups": null, + "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", "name": "mplement", }, - "nullable": Name { + "nullable": false, + "readonly": false, + "type": Name { "kind": "name", "name": "bar", "resolution": "uqn", }, + "value": null, + }, + ], + "visibility": "", + }, + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "", + }, + "nullable": false, "readonly": false, - "type": [], + "type": Name { + "kind": "name", + "name": "baz", + "resolution": "uqn", + }, "value": null, }, ], @@ -889,6 +1005,7 @@ Program { ",", ";", "=", + "{", ], "kind": "error", "line": 1, @@ -896,22 +1013,43 @@ Program { "token": "'baz' (T_STRING)", }, Error { - "expected": ";", + "expected": [ + ";", + ",", + ], "kind": "error", "line": 1, - "message": "Parse Error : syntax error, unexpected 'baz' (T_STRING), expecting ';' on line 1", + "message": "Parse Error : syntax error, unexpected 'baz' (T_STRING) on line 1", "token": "'baz' (T_STRING)", }, + Error { + "expected": 222, + "kind": "error", + "line": 1, + "message": "Parse Error : syntax error, unexpected '{', expecting T_VARIABLE on line 1", + "token": "'{'", + }, Error { "expected": [ - 198, - 222, - 182, + ",", + ";", + "=", + "{", ], "kind": "error", "line": 1, - "message": "Parse Error : syntax error, unexpected '{' on line 1", - "token": "'{'", + "message": "Parse Error : syntax error, unexpected '}' on line 1", + "token": "'}'", + }, + Error { + "expected": [ + ";", + ",", + ], + "kind": "error", + "line": 1, + "message": "Parse Error : syntax error, unexpected '}' on line 1", + "token": "'}'", }, ], "kind": "program", diff --git a/test/snapshot/__snapshots__/heredoc.test.js.snap b/test/snapshot/__snapshots__/heredoc.test.js.snap index cdf9be706..7a9b38fcd 100644 --- a/test/snapshot/__snapshots__/heredoc.test.js.snap +++ b/test/snapshot/__snapshots__/heredoc.test.js.snap @@ -1716,11 +1716,14 @@ FOOBAR", "visibility": "", }, PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", diff --git a/test/snapshot/__snapshots__/interface.test.js.snap b/test/snapshot/__snapshots__/interface.test.js.snap index 14dd6a85c..577a7789a 100644 --- a/test/snapshot/__snapshots__/interface.test.js.snap +++ b/test/snapshot/__snapshots__/interface.test.js.snap @@ -125,3 +125,176 @@ Program { "kind": "program", } `; + +exports[`interface property hooks get + set 1`] = ` +Program { + "children": [ + Interface { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": null, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "get", + "parameter": null, + }, + PropertyHook { + "body": null, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "set", + "parameter": null, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "readable", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "int", + "raw": "int", + }, + "value": null, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "kind": "interface", + "name": Identifier { + "kind": "identifier", + "name": "I", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`interface property hooks getter 1`] = ` +Program { + "children": [ + Interface { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": null, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "get", + "parameter": null, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "readable", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "int", + "raw": "int", + }, + "value": null, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "kind": "interface", + "name": Identifier { + "kind": "identifier", + "name": "I", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; + +exports[`interface property hooks setter 1`] = ` +Program { + "children": [ + Interface { + "attrGroups": [], + "body": [ + PropertyStatement { + "isAbstract": false, + "isFinal": false, + "isStatic": false, + "kind": "propertystatement", + "properties": [ + Property { + "attrGroups": [], + "hooks": [ + PropertyHook { + "body": null, + "byref": false, + "isFinal": false, + "kind": "propertyhook", + "name": "set", + "parameter": null, + }, + ], + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "readable", + }, + "nullable": false, + "readonly": false, + "type": TypeReference { + "kind": "typereference", + "name": "int", + "raw": "int", + }, + "value": null, + }, + ], + "visibility": "public", + }, + ], + "extends": null, + "kind": "interface", + "name": Identifier { + "kind": "identifier", + "name": "I", + }, + }, + ], + "errors": [], + "kind": "program", +} +`; diff --git a/test/snapshot/__snapshots__/namespace.test.js.snap b/test/snapshot/__snapshots__/namespace.test.js.snap index c59e4055c..ead6f0d16 100644 --- a/test/snapshot/__snapshots__/namespace.test.js.snap +++ b/test/snapshot/__snapshots__/namespace.test.js.snap @@ -167,6 +167,8 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "loc": Location { @@ -185,6 +187,7 @@ Program { "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "loc": Location { "end": Position { @@ -704,6 +707,8 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "loc": Location { @@ -722,6 +727,7 @@ Program { "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "loc": Location { "end": Position { diff --git a/test/snapshot/__snapshots__/nowdoc.test.js.snap b/test/snapshot/__snapshots__/nowdoc.test.js.snap index 73ee4fa13..51d7e5363 100644 --- a/test/snapshot/__snapshots__/nowdoc.test.js.snap +++ b/test/snapshot/__snapshots__/nowdoc.test.js.snap @@ -223,11 +223,14 @@ FOOBAR", "visibility": "", }, PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", diff --git a/test/snapshot/__snapshots__/property.test.js.snap b/test/snapshot/__snapshots__/property.test.js.snap index 59f1edd5f..398f41fa4 100644 --- a/test/snapshot/__snapshots__/property.test.js.snap +++ b/test/snapshot/__snapshots__/property.test.js.snap @@ -7,11 +7,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -51,11 +54,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -98,11 +104,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -142,11 +151,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -189,11 +201,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -233,11 +248,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": true, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -277,11 +295,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": true, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -324,11 +345,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -371,11 +395,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -415,11 +442,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -462,11 +492,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -533,11 +566,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -594,11 +630,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -649,11 +688,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -697,11 +739,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -745,11 +790,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -795,11 +843,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -861,11 +912,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -912,11 +966,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -983,11 +1040,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -1033,11 +1093,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -1080,11 +1143,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", diff --git a/test/snapshot/__snapshots__/propertystatement.test.js.snap b/test/snapshot/__snapshots__/propertystatement.test.js.snap index 3e3d14d9c..aa5dbba4d 100644 --- a/test/snapshot/__snapshots__/propertystatement.test.js.snap +++ b/test/snapshot/__snapshots__/propertystatement.test.js.snap @@ -7,11 +7,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -24,6 +27,7 @@ Program { }, Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -36,6 +40,7 @@ Program { }, Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -75,11 +80,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -92,6 +100,7 @@ Program { }, Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -104,6 +113,7 @@ Program { }, Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -143,11 +153,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", @@ -187,11 +200,14 @@ Program { "attrGroups": [], "body": [ PropertyStatement { + "isAbstract": false, + "isFinal": false, "isStatic": false, "kind": "propertystatement", "properties": [ Property { "attrGroups": [], + "hooks": [], "kind": "property", "name": Identifier { "kind": "identifier", diff --git a/test/snapshot/class.test.js b/test/snapshot/class.test.js index b93d642cb..ba597ba52 100644 --- a/test/snapshot/class.test.js +++ b/test/snapshot/class.test.js @@ -182,19 +182,19 @@ describe("Test classes", function () { }); it("Test that readonly method parameters are throwing errors", function () { - const ast = parser.parseEval( - ` + expect(() => { + parser.parseEval( + ` class Bob { public function foo(public readonly int $id) {} }`, - { - parser: { - version: "8.1", - suppressErrors: true, + { + parser: { + version: "8.1", + }, }, - }, - ); - expect(ast).toMatchSnapshot(); + ); + }).toThrowErrorMatchingSnapshot(); }); it("Test promoted nullable properties php 8", function () { diff --git a/test/snapshot/classpropertyhooks.test.js b/test/snapshot/classpropertyhooks.test.js new file mode 100644 index 000000000..36384874c --- /dev/null +++ b/test/snapshot/classpropertyhooks.test.js @@ -0,0 +1,254 @@ +const parser = require("../main"); +// +describe("classpropertyhooks", () => { + const test_parser = parser.create({ + parser: { + version: "8.4", + }, + }); + + it("not supported in php < 8.4", () => { + expect(() => { + parser.parseEval( + `class BookViewModel { + public string $credits { + get => 'mailto:' . $this->email; + } + }`, + { + parser: { + version: "8.3", + }, + }, + ); + }).toThrowErrorMatchingSnapshot(); + }); + + describe("getter", () => { + it("arrow function", () => { + expect( + test_parser.parseEval( + `class BookViewModel { + public string $credits { + get => 'mailto:' . $this->email; + } + }`, + ), + ).toMatchSnapshot(); + }); + + it("block", () => { + expect( + test_parser.parseEval( + `class BookViewModel { + public string $credits { + get { + 'mailto:' . $this->email; + } + } + }`, + ), + ).toMatchSnapshot(); + }); + }); + + describe("setter", () => { + describe("expression form", () => { + it("with implicit $value", () => { + expect( + test_parser.parseEval( + `class BookViewModel { + public string $credits { + set => $this->credits = $value; + } + }`, + ), + ).toMatchSnapshot(); + }); + + it("with explicit untyped $value", () => { + expect( + test_parser.parseEval( + `class Person { + public $name { + set($new_name) => $this->name = strtolower($new_name); + } + }`, + ), + ).toMatchSnapshot(); + }); + + it("with explicit typed $value", () => { + expect( + test_parser.parseEval( + `class Person { + public string $name { + set(string $new_name) => $this->name = strtolower($new_name); + } + }`, + ), + ).toMatchSnapshot(); + }); + }); + + describe("block form", () => { + it("with implicit $value", () => { + expect( + test_parser.parseEval( + `class BookViewModel { + public string $credits { + set { + $tmp = $this->credits + 1; + $this->propertyName = $value + $tmp; + } + } + }`, + ), + ).toMatchSnapshot(); + }); + + it("with explicit untyped $value", () => { + expect( + test_parser.parseEval( + `class BookViewModel { + public $credits { + set ($value) { + $tmp = $this->credits + 1; + $this->propertyName = $value + $tmp; + } + } + }`, + ), + ).toMatchSnapshot(); + }); + + it("with explicit typed $value", () => { + expect( + test_parser.parseEval( + `class BookViewModel { + public int $credits { + set (int $value) { + $tmp = $this->credits + 1; + $this->propertyName = $value + $tmp; + } + } + }`, + ), + ).toMatchSnapshot(); + }); + }); + }); + + it("support default value", () => { + expect( + test_parser.parseEval( + `class Example { + public string $foo = 'default value' { + get => $this->foo ; + } +}`, + ), + ).toMatchSnapshot(); + }); + + [ + `class Example { + private bool $modified = false; + public string $foo = 'default value' { + get => $this->foo . ($this->modified ? ' (modified)' : ''); + set(string $value) { + $this->foo = strtolower($value); + $this->modified = true; + } + } +}`, + `class Example +{ + private bool $modified = false; + + public string $foo = 'default value' { + get => $this->foo . ($this->modified ? ' (modified)' : ''); + + set { + $this->foo = strtolower($value); + $this->modified = true; + } + } +}`, + `class Example +{ + public string $foo = 'default value' { + get => $this->foo . ($this->modified ? ' (modified)' : ''); + set => strtolower($value); + } +}`, + ].forEach((code) => { + it("support setter + getter", () => { + expect(test_parser.parseEval(code)).toMatchSnapshot(); + }); + }); + + it("can be access by reference", () => { + expect( + test_parser.parseEval(`class Foo +{ + private string $_baz; + + public string $baz { + &get => $this->_baz; + set { + $this->_baz = strtoupper($value); + } + } +}`), + ).toMatchSnapshot(); + }); + + describe("final", () => { + it("on the hook itself", () => { + expect( + test_parser.parseEval(`class StandardUser +{ + public string $email { + final set { + if (! filter_var($value, FILTER_VALIDATE_EMAIL, FILTER_FLAG_EMAIL_UNICODE)) { + throw new InvalidArgumentException('Invalid email'); + } + $this->email = $value; + } + } +}`), + ).toMatchSnapshot(); + }); + + it("on the property", () => { + const code = `class User { + // Child classes may not add hooks of any kind to this property. + public final string $name; + + // Child classes may not add any hooks or override set, + // but this set will still apply. + public final string $username { + set => strtolower($value); + } +}`; + expect(test_parser.parseEval(code)).toMatchSnapshot(); + }); + }); + + describe("abstract", () => { + [ + ["get", `abstract class User { abstract public string $email { get; } }`], + ["set", `abstract class User { abstract public string $email { set; } }`], + [ + "get + set", + `abstract class User { abstract public string $email { get; set; } }`, + ], + ].forEach(([name, code]) => { + // eslint-disable-next-line jest/valid-title + it(name, () => { + expect(test_parser.parseEval(code)).toMatchSnapshot(); + }); + }); + }); +}); diff --git a/test/snapshot/interface.test.js b/test/snapshot/interface.test.js index 0c52595b4..2ebb2d240 100644 --- a/test/snapshot/interface.test.js +++ b/test/snapshot/interface.test.js @@ -17,4 +17,39 @@ describe("interface", function () { }), ).toMatchSnapshot(); }); + + describe("property hooks", function () { + const test_parser = parser.create({ + parser: { + version: "8.4", + }, + }); + + it("getter", () => { + const code = `interface I { + // An implementing class MUST have a publicly-readable property, + // but whether or not it's publicly settable is unrestricted. + public int $readable { get; } +}`; + expect(test_parser.parseEval(code)).toMatchSnapshot(); + }); + + it("setter", () => { + const code = `interface I { + // An implementing class MUST have a publicly-readable property, + // but whether or not it's publicly settable is unrestricted. + public int $readable { set; } +}`; + expect(test_parser.parseEval(code)).toMatchSnapshot(); + }); + + it("get + set", () => { + const code = `interface I { + // An implementing class MUST have a publicly-readable property, + // but whether or not it's publicly settable is unrestricted. + public int $readable { get; set;} +}`; + expect(test_parser.parseEval(code)).toMatchSnapshot(); + }); + }); }); diff --git a/types.d.ts b/types.d.ts index a5ad9c93f..bc91586d9 100644 --- a/types.d.ts +++ b/types.d.ts @@ -777,6 +777,16 @@ declare module "php-parser" { type: Identifier | Identifier[] | null; attrGroups: AttrGroup[]; } + /** + * Defines a class property hook getter & setter + */ + class PropertyHook extends Node { + name: string; + isFinal: boolean; + byref: boolean; + parameter: Parameter | null; + body: Block | Statement; + } /** * Lookup to an object property */