From b55446b48a4a588e8b62c7f9085ce0e7cb3b70fa Mon Sep 17 00:00:00 2001 From: Christian Zosel Date: Sat, 27 Feb 2021 18:01:24 +0100 Subject: [PATCH] feat(php8): named arguments --- src/ast.js | 1 + src/ast/namedargument.js | 28 +++ src/parser/function.js | 17 +- test/snapshot/__snapshots__/call.test.js.snap | 168 ++++++++++++++++++ test/snapshot/call.test.js | 40 +++++ 5 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 src/ast/namedargument.js diff --git a/src/ast.js b/src/ast.js index 67f9644d5..331b0ed64 100644 --- a/src/ast.js +++ b/src/ast.js @@ -517,6 +517,7 @@ AST.prototype.checkNodes = function () { require("./ast/method"), require("./ast/name"), require("./ast/namespace"), + require("./ast/namedargument"), require("./ast/new"), require("./ast/node"), require("./ast/noop"), diff --git a/src/ast/namedargument.js b/src/ast/namedargument.js new file mode 100644 index 000000000..c960c0f26 --- /dev/null +++ b/src/ast/namedargument.js @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2018 Glayzzle (BSD3 License) + * @authors https://github.com/glayzzle/php-parser/graphs/contributors + * @url http://glayzzle.com + */ +"use strict"; + +const Expression = require("./expression"); +const KIND = "namedargument"; + +/** + * Named arguments. + * @constructor namedargument + * @extends {Expression} + * @property {String} name + * @property {Expression} value + * @see https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments + */ +module.exports = Expression.extends(KIND, function namedargument( + name, + value, + docs, + location +) { + Expression.apply(this, [KIND, docs, location]); + this.name = name; + this.value = value; +}); diff --git a/src/parser/function.js b/src/parser/function.js index a9f79b75e..e29629a98 100644 --- a/src/parser/function.js +++ b/src/parser/function.js @@ -266,14 +266,27 @@ module.exports = { }, /** * ```ebnf - * argument_list ::= T_ELLIPSIS? expr + * argument_list ::= T_STRING ':' expr | T_ELLIPSIS? expr * ``` */ read_argument: function () { if (this.token === this.tok.T_ELLIPSIS) { return this.node("variadic")(this.next().read_expr()); } - return this.read_expr(); + const name = this.text(); + let res; + try { + res = this.read_expr(); + } catch (e) { + // happens if named argument name equals keyword name, e.g. + // foo(array: $a) - `array` is not a valid expr + this.expect(":") && this.next(); + return this.node("namedargument")(name, this.read_expr()); + } + if (this.token === ":") { + return this.node("namedargument")(name, this.next().read_expr()); + } + return res; }, /** * read type hinting diff --git a/test/snapshot/__snapshots__/call.test.js.snap b/test/snapshot/__snapshots__/call.test.js.snap index b3ffe948a..81669f7c1 100644 --- a/test/snapshot/__snapshots__/call.test.js.snap +++ b/test/snapshot/__snapshots__/call.test.js.snap @@ -97,6 +97,70 @@ Program { } `; +exports[`Test call comments 1`] = ` +Program { + "children": Array [ + ExpressionStatement { + "expression": Call { + "arguments": Array [ + Array { + "items": Array [ + Entry { + "byRef": false, + "key": null, + "kind": "entry", + "leadingComments": Array [ + CommentLine { + "kind": "commentline", + "offset": 10, + "value": "// comment +", + }, + ], + "unpack": false, + "value": Number { + "kind": "number", + "value": "100", + }, + }, + Entry { + "byRef": false, + "key": null, + "kind": "entry", + "unpack": false, + "value": Number { + "kind": "number", + "value": "0", + }, + }, + ], + "kind": "array", + "shortForm": false, + }, + ], + "kind": "call", + "what": Name { + "kind": "name", + "name": "foo", + "resolution": "uqn", + }, + }, + "kind": "expressionstatement", + }, + ], + "comments": Array [ + CommentLine { + "kind": "commentline", + "offset": 10, + "value": "// comment +", + }, + ], + "errors": Array [], + "kind": "program", +} +`; + exports[`Test call inside offsetlookup 1`] = ` Program { "children": Array [ @@ -636,6 +700,79 @@ Program { } `; +exports[`Test call keyword as named argument 1`] = ` +Program { + "children": Array [ + ExpressionStatement { + "expression": Call { + "arguments": Array [ + namedargument { + "kind": "namedargument", + "name": "array", + "value": Variable { + "curly": false, + "kind": "variable", + "name": "a", + }, + }, + ], + "kind": "call", + "what": Name { + "kind": "name", + "name": "foo", + "resolution": "uqn", + }, + }, + "kind": "expressionstatement", + }, + ], + "errors": Array [], + "kind": "program", +} +`; + +exports[`Test call mix of unnamed and named arguments 1`] = ` +Program { + "children": Array [ + ExpressionStatement { + "expression": Call { + "arguments": Array [ + Number { + "kind": "number", + "value": "50", + }, + namedargument { + "kind": "namedargument", + "name": "num", + "value": Number { + "kind": "number", + "value": "100", + }, + }, + namedargument { + "kind": "namedargument", + "name": "start_index", + "value": Number { + "kind": "number", + "value": "0", + }, + }, + ], + "kind": "call", + "what": Name { + "kind": "name", + "name": "foo", + "resolution": "uqn", + }, + }, + "kind": "expressionstatement", + }, + ], + "errors": Array [], + "kind": "program", +} +`; + exports[`Test call multiple (2) 1`] = ` Program { "children": Array [ @@ -752,6 +889,37 @@ Program { } `; +exports[`Test call named arguments in php 8.0 1`] = ` +Program { + "children": Array [ + ExpressionStatement { + "expression": Call { + "arguments": Array [ + namedargument { + "kind": "namedargument", + "name": "a", + "value": Variable { + "curly": false, + "kind": "variable", + "name": "a", + }, + }, + ], + "kind": "call", + "what": Name { + "kind": "name", + "name": "foo", + "resolution": "uqn", + }, + }, + "kind": "expressionstatement", + }, + ], + "errors": Array [], + "kind": "program", +} +`; + exports[`Test call nested 1`] = ` Program { "children": Array [ diff --git a/test/snapshot/call.test.js b/test/snapshot/call.test.js index cd35318d3..6df0e5e2f 100644 --- a/test/snapshot/call.test.js +++ b/test/snapshot/call.test.js @@ -244,4 +244,44 @@ describe("Test call", function () { }); expect(ast).toMatchSnapshot(); }); + it("named arguments in php 8.0", function () { + const astErr = parser.parseEval(`foo(a: $a);`, { + parser: { + version: "8.0", + debug: false, + }, + }); + expect(astErr).toMatchSnapshot(); + }); + it("keyword as named argument", function () { + const astErr = parser.parseEval(`foo(array: $a);`, { + parser: { + version: "8.0", + debug: false, + }, + }); + expect(astErr).toMatchSnapshot(); + }); + it("mix of unnamed and named arguments", function () { + const astErr = parser.parseEval(`foo(50, num: 100, start_index: 0);`, { + parser: { + version: "8.0", + debug: false, + }, + }); + expect(astErr).toMatchSnapshot(); + }); + it("comments", function () { + const astErr = parser.parseEval( + `foo(array // comment + (100, 0));`, + { + parser: { + extractDoc: true, + debug: false, + }, + } + ); + expect(astErr).toMatchSnapshot(); + }); });