diff --git a/src/ast.js b/src/ast.js index 67f9644d5..0a3b05b43 100644 --- a/src/ast.js +++ b/src/ast.js @@ -555,6 +555,7 @@ AST.prototype.checkNodes = function () { require("./ast/try"), require("./ast/typereference"), require("./ast/unary"), + require("./ast/uniontype"), require("./ast/unset"), require("./ast/usegroup"), require("./ast/useitem"), diff --git a/src/ast/uniontype.js b/src/ast/uniontype.js new file mode 100644 index 000000000..32b6f5a91 --- /dev/null +++ b/src/ast/uniontype.js @@ -0,0 +1,24 @@ +/** + * Copyright (C) 2018 Glayzzle (BSD3 License) + * @authors https://github.com/glayzzle/php-parser/graphs/contributors + * @url http://glayzzle.com + */ +"use strict"; + +const Declaration = require("./declaration"); +const KIND = "uniontype"; + +/** + * A union of types + * @constructor UnionType + * @extends {Declaration} + * @property {TypeReference[]} types + */ +module.exports = Declaration.extends(KIND, function UnionType( + types, + docs, + location +) { + Declaration.apply(this, [KIND, null, docs, location]); + this.types = types; +}); diff --git a/src/parser/class.js b/src/parser/class.js index 6019433d1..57c5380e3 100644 --- a/src/parser/class.js +++ b/src/parser/class.js @@ -309,7 +309,7 @@ module.exports = { nullable = true; this.next(); } - let type = this.read_type(); + let type = this.read_types(); if (nullable && !type) { this.raiseError( "Expecting a type definition combined with nullable operator" diff --git a/src/parser/expr.js b/src/parser/expr.js index ec8e46c14..6312f5887 100644 --- a/src/parser/expr.js +++ b/src/parser/expr.js @@ -588,7 +588,7 @@ module.exports = { nullable = true; this.next(); } - returnType = this.read_type(); + returnType = this.read_types(); } if (this.expect(this.tok.T_DOUBLE_ARROW)) this.next(); const body = this.read_expr(); diff --git a/src/parser/function.js b/src/parser/function.js index a9f79b75e..f4ef967a4 100644 --- a/src/parser/function.js +++ b/src/parser/function.js @@ -128,7 +128,7 @@ module.exports = { nullable = true; this.next(); } - returnType = this.read_type(); + returnType = this.read_types(); } if (type === 1) { // closure @@ -202,14 +202,14 @@ module.exports = { const node = this.node("parameter"); let parameterName = null; let value = null; - let type = null; + let types = null; let nullable = false; if (this.token === "?") { this.next(); nullable = true; } - type = this.read_type(); - if (nullable && !type) { + types = this.read_types(); + if (nullable && !types) { this.raiseError( "Expecting a type definition combined with nullable operator" ); @@ -225,7 +225,24 @@ module.exports = { if (this.token == "=") { value = this.next().read_expr(); } - return node(parameterName, type, value, isRef, isVariadic, nullable); + return node(parameterName, types, value, isRef, isVariadic, nullable); + }, + read_types() { + const types = []; + const unionType = this.node("uniontype"); + let type = this.read_type(); + if (!type) return null; + types.push(type); + while (this.token === "|") { + this.next(); + type = this.read_type(); + types.push(type); + } + if (types.length === 1) { + return types[0]; + } else { + return unionType(types); + } }, /** * Reads a list of arguments diff --git a/test/snapshot/__snapshots__/class.test.js.snap b/test/snapshot/__snapshots__/class.test.js.snap index e64d302eb..e2c5657fa 100644 --- a/test/snapshot/__snapshots__/class.test.js.snap +++ b/test/snapshot/__snapshots__/class.test.js.snap @@ -469,18 +469,22 @@ Program { "name": "y", }, "nullable": false, - "type": Array [ - TypeReference { - "kind": "typereference", - "name": "float", - "raw": "float", - }, - TypeReference { - "kind": "typereference", - "name": "string", - "raw": "string", - }, - ], + "type": UnionType { + "kind": "uniontype", + "name": null, + "types": Array [ + TypeReference { + "kind": "typereference", + "name": "float", + "raw": "float", + }, + TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + ], + }, "value": null, }, ], @@ -504,6 +508,130 @@ Program { } `; +exports[`Test classes Test class union properties 1`] = ` +Program { + "children": Array [ + Class { + "body": Array [ + PropertyStatement { + "isStatic": true, + "kind": "propertystatement", + "properties": Array [ + Property { + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "foo", + }, + "nullable": false, + "type": UnionType { + "kind": "uniontype", + "name": null, + "types": Array [ + TypeReference { + "kind": "typereference", + "name": "int", + "raw": "int", + }, + TypeReference { + "kind": "typereference", + "name": "float", + "raw": "float", + }, + ], + }, + "value": null, + }, + ], + "visibility": "", + }, + PropertyStatement { + "isStatic": false, + "kind": "propertystatement", + "properties": Array [ + Property { + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "bar", + }, + "nullable": true, + "type": UnionType { + "kind": "uniontype", + "name": null, + "types": Array [ + Name { + "kind": "name", + "name": "Foo", + "resolution": "uqn", + }, + Name { + "kind": "name", + "name": "Bar", + "resolution": "uqn", + }, + ], + }, + "value": null, + }, + ], + "visibility": "private", + }, + PropertyStatement { + "isStatic": false, + "kind": "propertystatement", + "properties": Array [ + Property { + "kind": "property", + "name": Identifier { + "kind": "identifier", + "name": "a", + }, + "nullable": false, + "type": UnionType { + "kind": "uniontype", + "name": null, + "types": Array [ + Name { + "kind": "name", + "name": "Repo", + "resolution": "uqn", + }, + TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + Name { + "kind": "name", + "name": "null", + "resolution": "uqn", + }, + ], + }, + "value": null, + }, + ], + "visibility": "public", + }, + ], + "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 [ diff --git a/test/snapshot/__snapshots__/function.test.js.snap b/test/snapshot/__snapshots__/function.test.js.snap index 67e7f1119..360fced5c 100644 --- a/test/snapshot/__snapshots__/function.test.js.snap +++ b/test/snapshot/__snapshots__/function.test.js.snap @@ -445,6 +445,103 @@ Program { } `; +exports[`Function tests test function union types 1`] = ` +Program { + "children": Array [ + _Function { + "arguments": Array [ + Parameter { + "byref": false, + "kind": "parameter", + "name": Identifier { + "kind": "identifier", + "name": "a", + }, + "nullable": false, + "type": UnionType { + "kind": "uniontype", + "name": null, + "types": Array [ + TypeReference { + "kind": "typereference", + "name": "int", + "raw": "int", + }, + TypeReference { + "kind": "typereference", + "name": "float", + "raw": "float", + }, + ], + }, + "value": Number { + "kind": "number", + "value": "1", + }, + "variadic": false, + }, + Parameter { + "byref": false, + "kind": "parameter", + "name": Identifier { + "kind": "identifier", + "name": "b", + }, + "nullable": false, + "type": UnionType { + "kind": "uniontype", + "name": null, + "types": Array [ + Name { + "kind": "name", + "name": "Foo", + "resolution": "uqn", + }, + Name { + "kind": "name", + "name": "Bar", + "resolution": "uqn", + }, + ], + }, + "value": null, + "variadic": false, + }, + ], + "body": Block { + "children": Array [], + "kind": "block", + }, + "byref": false, + "kind": "function", + "name": Identifier { + "kind": "identifier", + "name": "foo", + }, + "nullable": false, + "type": UnionType { + "kind": "uniontype", + "name": null, + "types": Array [ + TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + TypeReference { + "kind": "typereference", + "name": "int", + "raw": "int", + }, + ], + }, + }, + ], + "errors": Array [], + "kind": "program", +} +`; + exports[`Function tests test reserved word for function name error 1`] = ` Program { "children": Array [ @@ -477,6 +574,106 @@ Program { } `; +exports[`Function tests test short function union types 1`] = ` +Program { + "children": Array [ + ExpressionStatement { + "expression": Closure { + "arguments": Array [ + Parameter { + "byref": false, + "kind": "parameter", + "name": Identifier { + "kind": "identifier", + "name": "a", + }, + "nullable": false, + "type": UnionType { + "kind": "uniontype", + "name": null, + "types": Array [ + TypeReference { + "kind": "typereference", + "name": "int", + "raw": "int", + }, + TypeReference { + "kind": "typereference", + "name": "float", + "raw": "float", + }, + ], + }, + "value": Number { + "kind": "number", + "value": "1", + }, + "variadic": false, + }, + Parameter { + "byref": false, + "kind": "parameter", + "name": Identifier { + "kind": "identifier", + "name": "b", + }, + "nullable": false, + "type": UnionType { + "kind": "uniontype", + "name": null, + "types": Array [ + Name { + "kind": "name", + "name": "Foo", + "resolution": "uqn", + }, + Name { + "kind": "name", + "name": "Bar", + "resolution": "uqn", + }, + ], + }, + "value": null, + "variadic": false, + }, + ], + "body": String { + "isDoubleQuote": true, + "kind": "string", + "raw": "\\"\\"", + "unicode": false, + "value": "", + }, + "byref": false, + "isStatic": false, + "kind": "arrowfunc", + "nullable": false, + "type": UnionType { + "kind": "uniontype", + "name": null, + "types": Array [ + TypeReference { + "kind": "typereference", + "name": "string", + "raw": "string", + }, + TypeReference { + "kind": "typereference", + "name": "int", + "raw": "int", + }, + ], + }, + }, + "kind": "expressionstatement", + }, + ], + "errors": Array [], + "kind": "program", +} +`; + exports[`Function tests test static closure 1`] = ` Program { "children": Array [ diff --git a/test/snapshot/class.test.js b/test/snapshot/class.test.js index 138a9e897..a26a1975d 100644 --- a/test/snapshot/class.test.js +++ b/test/snapshot/class.test.js @@ -129,6 +129,17 @@ describe("Test classes", function () { ).toMatchSnapshot(); }); + it("Test class union properties", function () { + expect( + parser.parseEval(` + class Test { + static int|float $foo; + private ?Foo|Bar $bar; + public Repo|string|null $a; + }`) + ).toMatchSnapshot(); + }); + it("empty", function () { expect(parser.parseEval("class Foo {}")).toMatchSnapshot(); }); diff --git a/test/snapshot/function.test.js b/test/snapshot/function.test.js index 2fa18a1a3..a2fc5ed94 100644 --- a/test/snapshot/function.test.js +++ b/test/snapshot/function.test.js @@ -14,6 +14,24 @@ describe("Function tests", function () { expect(ast).toMatchSnapshot(); }); + it("test function union types", function () { + const ast = parser.parseEval( + ` + function foo(int|float $a = 1, Foo|Bar $b) : string|int {} + ` + ); + expect(ast).toMatchSnapshot(); + }); + + it("test short function union types", function () { + const ast = parser.parseEval( + ` + fn (int|float $a = 1, Foo|Bar $b) : string|int => ""; + ` + ); + expect(ast).toMatchSnapshot(); + }); + it("implement #113 : typehint nodes", function () { expect( parser.parseEval(