Repository: glayzzle/php-parser
Pull request #456 (2020-01-02 10:14:03)

Feat typed properties v2
related to https://github.com/glayzzle/php-parser/issues/346 - add https://wiki.php.net/rfc/typed_properties_v2


Patch:
diff --git a/src/ast/property.js b/src/ast/property.js
index eaee4c93..073ff436 100644
--- a/src/ast/property.js
+++ b/src/ast/property.js
@@ -14,14 +14,20 @@ const KIND = "property";
  * @extends {Statement}
  * @property {string} name
  * @property {Node|null} value
+ * @property {boolean} nullable
+ * @property {Identifier|Array<Identifier>|null} type
  */
 module.exports = Statement.extends(KIND, function Property(
   name,
   value,
+  nullable,
+  type,
   docs,
   location
 ) {
   Statement.apply(this, [KIND, docs, location]);
   this.name = name;
   this.value = value;
+  this.nullable = nullable;
+  this.type = type;
 });
diff --git a/src/parser/class.js b/src/parser/class.js
index d36ff70a..cf792d35 100644
--- a/src/parser/class.js
+++ b/src/parser/class.js
@@ -97,15 +97,25 @@ module.exports = {
         flags[1] = 0; // non static var
       }
 
-      if (this.token === this.tok.T_VARIABLE) {
+      if (this.token === this.tok.T_FUNCTION) {
+        // reads a function
+        result.push(this.read_function(false, flags));
+      } else if (
+        this.token === this.tok.T_VARIABLE ||
+        // support https://wiki.php.net/rfc/typed_properties_v2
+        (this.php74 &&
+          (this.token === "?" ||
+            this.token === this.tok.T_CALLABLE ||
+            this.token === this.tok.T_ARRAY ||
+            this.token === this.tok.T_NS_SEPARATOR ||
+            this.token === this.tok.T_STRING ||
+            this.token === this.tok.T_NAMESPACE))
+      ) {
         // reads a variable
         const variables = this.read_variable_list(flags);
         this.expect(";");
         this.next();
         result = result.concat(variables);
-      } else if (this.token === this.tok.T_FUNCTION) {
-        // reads a function
-        result.push(this.read_function(false, flags));
       } else {
         // raise an error
         this.error([
@@ -140,19 +150,20 @@ module.exports = {
        */
       function read_variable_declaration() {
         const result = this.node("property");
+        const [nullable, type] = this.read_optional_type();
         this.expect(this.tok.T_VARIABLE);
         let propName = this.node("identifier");
         const name = this.text().substring(1); // ignore $
         this.next();
         propName = propName(name);
         if (this.token === ";" || this.token === ",") {
-          return result(propName, null);
+          return result(propName, null, nullable, type);
         } else if (this.token === "=") {
           // https://github.com/php/php-src/blob/master/Zend/zend_language_parser.y#L815
-          return result(propName, this.next().read_expr());
+          return result(propName, this.next().read_expr(), nullable, type);
         } else {
           this.expect([",", ";", "="]);
-          return result(propName, null);
+          return result(propName, null, nullable, type);
         }
       },
       ","
@@ -268,6 +279,60 @@ module.exports = {
     if (result[2] == -1) result[2] = 0;
     return result;
   },
+
+  /**
+   * optional_type:
+   *	  /- empty -/	{ $$ = NULL; }
+   *   |	type_expr	{ $$ = $1; }
+   * ;
+   *
+   * type_expr:
+   *		type		{ $$ = $1; }
+   *	|	'?' type	{ $$ = $2; $$->attr |= ZEND_TYPE_NULLABLE; }
+   *	|	union_type	{ $$ = $1; }
+   * ;
+   *
+   * type:
+   * 		T_ARRAY		{ $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_ARRAY); }
+   * 	|	T_CALLABLE	{ $$ = zend_ast_create_ex(ZEND_AST_TYPE, IS_CALLABLE); }
+   * 	|	name		{ $$ = $1; }
+   * ;
+   *
+   * union_type:
+   * 		type '|' type       { $$ = zend_ast_create_list(2, ZEND_AST_TYPE_UNION, $1, $3); }
+   * 	|	union_type '|' type { $$ = zend_ast_list_add($1, $3); }
+   * ;
+   */
+  read_optional_type: function() {
+    let nullable = false;
+    if (this.token === "?") {
+      nullable = true;
+      this.next();
+    }
+    let type = this.read_type();
+    if (nullable && !type) {
+      this.raiseError(
+        "Expecting a type definition combined with nullable operator"
+      );
+    }
+    if (!nullable && !type) {
+      return [false, null];
+    }
+    if (this.token === "|") {
+      type = [type];
+      do {
+        this.next();
+        const variant = this.read_type();
+        if (!variant) {
+          this.raiseError("Expecting a type definition");
+          break;
+        }
+        type.push(variant);
+      } while (this.token === "|");
+    }
+    return [nullable, type];
+  },
+
   /**
    * reading an interface
    * ```ebnf


Test patch:
diff --git a/test/debug.js b/test/debug.js
index 898fdbad..775492f0 100644
--- a/test/debug.js
+++ b/test/debug.js
@@ -17,11 +17,15 @@
 const util = require('util');
 const parser = require("../src/index");
 const ast = parser.parseCode(`
-<?php$var = 1; ?>
+<?php
+class Test {
+  public ?int $prop = null;
+  public float|string $y;
+}
 `, { 
     parser: {
       debug: true,
-      php73: true
+      php74: true
     },
     ast: {
       withPositions: true,
diff --git a/test/snapshot/__snapshots__/acid.test.js.snap b/test/snapshot/__snapshots__/acid.test.js.snap
index 407970ab..88d61c55 100644
--- a/test/snapshot/__snapshots__/acid.test.js.snap
+++ b/test/snapshot/__snapshots__/acid.test.js.snap
@@ -880,6 +880,8 @@ Program {
                     },
                     "name": "dwarf",
                   },
+                  "nullable": false,
+                  "type": null,
                   "value": Array {
                     "items": Array [
                       Entry {
diff --git a/test/snapshot/__snapshots__/class.test.js.snap b/test/snapshot/__snapshots__/class.test.js.snap
index 48797f09..f84f93af 100644
--- a/test/snapshot/__snapshots__/class.test.js.snap
+++ b/test/snapshot/__snapshots__/class.test.js.snap
@@ -69,6 +69,8 @@ Program {
                 "kind": "identifier",
                 "name": "var",
               },
+              "nullable": false,
+              "type": null,
               "value": Boolean {
                 "kind": "boolean",
                 "raw": "true",
@@ -427,6 +429,81 @@ Program {
 }
 `;
 
+exports[`Test classes Implement typed_properties_v2 / php74 1`] = `
+Program {
+  "children": Array [
+    Class {
+      "body": Array [
+        PropertyStatement {
+          "isStatic": false,
+          "kind": "propertystatement",
+          "properties": Array [
+            Property {
+              "kind": "property",
+              "name": Identifier {
+                "kind": "identifier",
+                "name": "prop",
+              },
+              "nullable": true,
+              "type": TypeReference {
+                "kind": "typereference",
+                "name": "int",
+                "raw": "int",
+              },
+              "value": NullKeyword {
+                "kind": "nullkeyword",
+                "raw": "null",
+              },
+            },
+          ],
+          "visibility": "public",
+        },
+        PropertyStatement {
+          "isStatic": true,
+          "kind": "propertystatement",
+          "properties": Array [
+            Property {
+              "kind": "property",
+              "name": Identifier {
+                "kind": "identifier",
+                "name": "y",
+              },
+              "nullable": false,
+              "type": Array [
+                TypeReference {
+                  "kind": "typereference",
+                  "name": "float",
+                  "raw": "float",
+                },
+                TypeReference {
+                  "kind": "typereference",
+                  "name": "string",
+                  "raw": "string",
+                },
+              ],
+              "value": null,
+            },
+          ],
+          "visibility": "protected",
+        },
+      ],
+      "extends": null,
+      "implements": null,
+      "isAbstract": false,
+      "isAnonymous": false,
+      "isFinal": false,
+      "kind": "class",
+      "name": Identifier {
+        "kind": "identifier",
+        "name": "Test",
+      },
+    },
+  ],
+  "errors": Array [],
+  "kind": "program",
+}
+`;
+
 exports[`Test classes Test js properties 1`] = `
 Program {
   "children": Array [
@@ -442,6 +519,8 @@ Program {
                 "kind": "identifier",
                 "name": "constructor",
               },
+              "nullable": false,
+              "type": null,
               "value": null,
             },
           ],
@@ -694,6 +773,8 @@ Program {
                 "kind": "identifier",
                 "name": "var",
               },
+              "nullable": false,
+              "type": null,
               "value": null,
             },
           ],
@@ -851,6 +932,8 @@ Program {
                 "kind": "identifier",
                 "name": "foo",
               },
+              "nullable": false,
+              "type": null,
               "value": null,
             },
           ],
@@ -866,6 +949,8 @@ Program {
                 "kind": "identifier",
                 "name": "bar",
               },
+              "nullable": false,
+              "type": null,
               "value": null,
             },
           ],
diff --git a/test/snapshot/__snapshots__/comment.test.js.snap b/test/snapshot/__snapshots__/comment.test.js.snap
index f1bc08a2..d9855384 100644
--- a/test/snapshot/__snapshots__/comment.test.js.snap
+++ b/test/snapshot/__snapshots__/comment.test.js.snap
@@ -1695,6 +1695,8 @@ Program {
                 "kind": "identifier",
                 "name": "test",
               },
+              "nullable": false,
+              "type": null,
               "value": null,
             },
             Property {
@@ -1703,6 +1705,8 @@ Program {
                 "kind": "identifier",
                 "name": "toto",
               },
+              "nullable": false,
+              "type": null,
               "value": null,
             },
           ],
@@ -1731,6 +1735,8 @@ Program {
                 "kind": "identifier",
                 "name": "foo",
               },
+              "nullable": false,
+              "type": null,
               "value": Number {
                 "kind": "number",
                 "value": "123",
diff --git a/test/snapshot/__snapshots__/graceful.test.js.snap b/test/snapshot/__snapshots__/graceful.test.js.snap
index d6ae2966..0973aee0 100644
--- a/test/snapshot/__snapshots__/graceful.test.js.snap
+++ b/test/snapshot/__snapshots__/graceful.test.js.snap
@@ -292,21 +292,25 @@ Program {
   "children": Array [
     Class {
       "body": Array [
-        ClassConstant {
-          "constants": Array [
-            Constant {
-              "kind": "constant",
+        PropertyStatement {
+          "isStatic": false,
+          "kind": "propertystatement",
+          "properties": Array [
+            Property {
+              "kind": "property",
               "name": Identifier {
                 "kind": "identifier",
-                "name": "A",
+                "name": "onst",
               },
-              "value": Number {
-                "kind": "number",
-                "value": "1",
+              "nullable": false,
+              "type": ClassReference {
+                "kind": "classreference",
+                "name": "foo",
+                "resolution": "uqn",
               },
+              "value": null,
             },
           ],
-          "kind": "classconstant",
           "visibility": "",
         },
       ],
@@ -323,6 +327,31 @@ Program {
     },
   ],
   "errors": Array [
+    Error {
+      "expected": 222,
+      "kind": "error",
+      "line": 1,
+      "message": "Parse Error : syntax error, unexpected 'const' (T_CONST), expecting T_VARIABLE on line 1",
+      "token": "'const' (T_CONST)",
+    },
+    Error {
+      "expected": Array [
+        ",",
+        ";",
+        "=",
+      ],
+      "kind": "error",
+      "line": 1,
+      "message": "Parse Error : syntax error, unexpected 'A' (T_STRING) on line 1",
+      "token": "'A' (T_STRING)",
+    },
+    Error {
+      "expected": ";",
+      "kind": "error",
+      "line": 1,
+      "message": "Parse Error : syntax error, unexpected 'A' (T_STRING), expecting ';' on line 1",
+      "token": "'A' (T_STRING)",
+    },
     Error {
       "expected": Array [
         198,
@@ -331,15 +360,19 @@ Program {
       ],
       "kind": "error",
       "line": 1,
-      "message": "Parse Error : syntax error, unexpected 'foo' (T_STRING) on line 1",
-      "token": "'foo' (T_STRING)",
+      "message": "Parse Error : syntax error, unexpected '=' on line 1",
+      "token": "'='",
     },
     Error {
-      "expected": ";",
+      "expected": Array [
+        198,
+        222,
+        182,
+      ],
       "kind": "error",
       "line": 1,
-      "message": "Parse Error : syntax error, expecting ';' on line 1",
-      "token": "the end of file (EOF)",
+      "message": "Parse Error : syntax error, unexpected '1' (T_LNUMBER) on line 1",
+      "token": "'1' (T_LNUMBER)",
     },
     Error {
       "expected": "}",
@@ -789,7 +822,29 @@ exports[`Test graceful mode to suppress errors trait 1`] = `
 Program {
   "children": Array [
     Trait {
-      "body": Array [],
+      "body": Array [
+        PropertyStatement {
+          "isStatic": false,
+          "kind": "propertystatement",
+          "properties": Array [
+            Property {
+              "kind": "property",
+              "name": Identifier {
+                "kind": "identifier",
+                "name": "mplement",
+              },
+              "nullable": false,
+              "type": ClassReference {
+                "kind": "classreference",
+                "name": "bar",
+                "resolution": "uqn",
+              },
+              "value": null,
+            },
+          ],
+          "visibility": "",
+        },
+      ],
       "kind": "trait",
       "name": Identifier {
         "kind": "identifier",
@@ -806,36 +861,28 @@ Program {
       "token": "'extends' (T_EXTENDS)",
     },
     Error {
-      "expected": Array [
-        198,
-        222,
-        182,
-      ],
+      "expected": 222,
       "kind": "error",
       "line": 1,
-      "message": "Parse Error : syntax error, unexpected 'bar' (T_STRING) on line 1",
-      "token": "'bar' (T_STRING)",
+      "message": "Parse Error : syntax error, unexpected 'implement' (T_STRING), expecting T_VARIABLE on line 1",
+      "token": "'implement' (T_STRING)",
     },
     Error {
       "expected": Array [
-        198,
-        222,
-        182,
+        ",",
+        ";",
+        "=",
       ],
       "kind": "error",
       "line": 1,
-      "message": "Parse Error : syntax error, unexpected 'implement' (T_STRING) on line 1",
-      "token": "'implement' (T_STRING)",
+      "message": "Parse Error : syntax error, unexpected 'baz' (T_STRING) on line 1",
+      "token": "'baz' (T_STRING)",
     },
     Error {
-      "expected": Array [
-        198,
-        222,
-        182,
-      ],
+      "expected": ";",
       "kind": "error",
       "line": 1,
-      "message": "Parse Error : syntax error, unexpected 'baz' (T_STRING) on line 1",
+      "message": "Parse Error : syntax error, unexpected 'baz' (T_STRING), expecting ';' on line 1",
       "token": "'baz' (T_STRING)",
     },
     Error {
diff --git a/test/snapshot/__snapshots__/heredoc.test.js.snap b/test/snapshot/__snapshots__/heredoc.test.js.snap
index 615c5ca5..6cf24428 100644
--- a/test/snapshot/__snapshots__/heredoc.test.js.snap
+++ b/test/snapshot/__snapshots__/heredoc.test.js.snap
@@ -467,6 +467,8 @@ FOOBAR",
                 "kind": "identifier",
                 "name": "baz",
               },
+              "nullable": false,
+              "type": null,
               "value": Encapsed {
                 "kind": "encapsed",
                 "label": "FOOBAR",
diff --git a/test/snapshot/__snapshots__/namespace.test.js.snap b/test/snapshot/__snapshots__/namespace.test.js.snap
index 7b285893..95a51836 100644
--- a/test/snapshot/__snapshots__/namespace.test.js.snap
+++ b/test/snapshot/__snapshots__/namespace.test.js.snap
@@ -214,6 +214,8 @@ Program {
                     },
                     "name": "test",
                   },
+                  "nullable": false,
+                  "type": null,
                   "value": null,
                 },
               ],
@@ -741,6 +743,8 @@ Program {
                     },
                     "name": "test",
                   },
+                  "nullable": false,
+                  "type": null,
                   "value": null,
                 },
               ],
diff --git a/test/snapshot/__snapshots__/nowdoc.test.js.snap b/test/snapshot/__snapshots__/nowdoc.test.js.snap
index 5b5d6d2f..a9f89545 100644
--- a/test/snapshot/__snapshots__/nowdoc.test.js.snap
+++ b/test/snapshot/__snapshots__/nowdoc.test.js.snap
@@ -153,6 +153,8 @@ FOOBAR",
                 "kind": "identifier",
                 "name": "baz",
               },
+              "nullable": false,
+              "type": null,
               "value": Nowdoc {
                 "kind": "nowdoc",
                 "label": "FOOBAR",
diff --git a/test/snapshot/__snapshots__/property.test.js.snap b/test/snapshot/__snapshots__/property.test.js.snap
index b705de51..0bf2b517 100644
--- a/test/snapshot/__snapshots__/property.test.js.snap
+++ b/test/snapshot/__snapshots__/property.test.js.snap
@@ -15,6 +15,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": null,
             },
           ],
@@ -53,6 +55,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": Number {
                 "kind": "number",
                 "value": "10",
@@ -94,6 +98,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": null,
             },
           ],
@@ -132,6 +138,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": Number {
                 "kind": "number",
                 "value": "10",
@@ -173,6 +181,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": null,
             },
           ],
@@ -211,6 +221,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": null,
             },
           ],
@@ -249,6 +261,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": Number {
                 "kind": "number",
                 "value": "10",
@@ -290,6 +304,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": Number {
                 "kind": "number",
                 "value": "10",
@@ -331,6 +347,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": null,
             },
           ],
@@ -369,6 +387,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": Number {
                 "kind": "number",
                 "value": "10",
@@ -410,6 +430,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": Array {
                 "items": Array [
                   Boolean {
@@ -463,6 +485,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": Bin {
                 "kind": "bin",
                 "left": String {
@@ -518,6 +542,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": Bin {
                 "kind": "bin",
                 "left": Number {
@@ -567,6 +593,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": Boolean {
                 "kind": "boolean",
                 "raw": "true",
@@ -609,6 +637,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": ClassReference {
                 "kind": "classreference",
                 "name": "CONSTANT",
@@ -651,6 +681,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": String {
                 "isDoubleQuote": true,
                 "kind": "string",
@@ -695,6 +727,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": Encapsed {
                 "kind": "encapsed",
                 "label": "EOD",
@@ -756,6 +790,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": Nowdoc {
                 "kind": "nowdoc",
                 "label": "EOD",
@@ -801,6 +837,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": Array {
                 "items": Array [
                   Boolean {
@@ -854,6 +892,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": String {
                 "isDoubleQuote": false,
                 "kind": "string",
@@ -898,6 +938,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": Number {
                 "kind": "number",
                 "value": "10",
@@ -939,6 +981,8 @@ Program {
                 "kind": "identifier",
                 "name": "property",
               },
+              "nullable": false,
+              "type": null,
               "value": null,
             },
           ],
diff --git a/test/snapshot/__snapshots__/propertystatement.test.js.snap b/test/snapshot/__snapshots__/propertystatement.test.js.snap
index 81140b07..ef77f39e 100644
--- a/test/snapshot/__snapshots__/propertystatement.test.js.snap
+++ b/test/snapshot/__snapshots__/propertystatement.test.js.snap
@@ -15,6 +15,8 @@ Program {
                 "kind": "identifier",
                 "name": "dsn",
               },
+              "nullable": false,
+              "type": null,
               "value": null,
             },
             Property {
@@ -23,6 +25,8 @@ Program {
                 "kind": "identifier",
                 "name": "username",
               },
+              "nullable": false,
+              "type": null,
               "value": null,
             },
             Property {
@@ -31,6 +35,8 @@ Program {
                 "kind": "identifier",
                 "name": "password",
               },
+              "nullable": false,
+              "type": null,
               "value": null,
             },
           ],
@@ -69,6 +75,8 @@ Program {
                 "kind": "identifier",
                 "name": "dsn",
               },
+              "nullable": false,
+              "type": null,
               "value": null,
             },
             Property {
@@ -77,6 +85,8 @@ Program {
                 "kind": "identifier",
                 "name": "username",
               },
+              "nullable": false,
+              "type": null,
               "value": null,
             },
             Property {
@@ -85,6 +95,8 @@ Program {
                 "kind": "identifier",
                 "name": "password",
               },
+              "nullable": false,
+              "type": null,
               "value": null,
             },
           ],
@@ -123,6 +135,8 @@ Program {
                 "kind": "identifier",
                 "name": "dsn",
               },
+              "nullable": false,
+              "type": null,
               "value": null,
             },
           ],
@@ -161,6 +175,8 @@ Program {
                 "kind": "identifier",
                 "name": "dsn",
               },
+              "nullable": false,
+              "type": null,
               "value": null,
             },
           ],
diff --git a/test/snapshot/class.test.js b/test/snapshot/class.test.js
index 38e36788..867cc02d 100644
--- a/test/snapshot/class.test.js
+++ b/test/snapshot/class.test.js
@@ -17,6 +17,17 @@ describe("Test classes", function() {
     ).toMatchSnapshot();
   });
 
+  it("Implement typed_properties_v2 / php74", function() {
+    expect(
+      parser.parseEval(`
+    class Test {
+      public ?int $prop = null;
+      protected static float|string $y;
+    }
+    `)
+    ).toMatchSnapshot();    
+  });
+
   it("Validate usual declarations", function() {
     expect(
       parser.parseEval(`