diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 420af127..08321a7f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - node-version: [18.x, 20.x, 22.x] + node-version: [20.x, 22.x, 24.x] steps: # Check out the repository under $GITHUB_WORKSPACE, so this job can access it. diff --git a/.jshintrc b/.jshintrc index 70df3ab3..de8dca0f 100644 --- a/.jshintrc +++ b/.jshintrc @@ -2,7 +2,7 @@ "bitwise": true, "curly": true, "eqeqeq": true, - "esversion": 9, + "esversion": 11, "forin": true, "globals": { "requestIdleCallback": false diff --git a/package-lock.json b/package-lock.json index f15928ed..a225d784 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,12 +20,12 @@ "chai": "^4.3.10", "chai-as-promised": "^7.1.1", "jshint": "^2.13.6", - "mocha": "^10.4.0", + "mocha": "^11.7.5", "mocha-bootstrap": "^1.0.6", "nowdoc": "^1.0.1", - "phptest": "^0.0.10", - "phptoast": "^9.3.0", - "phptojs": "^10.2.0", + "phptest": "^0.0.12", + "phptoast": "^9.4.0", + "phptojs": "^10.3.0", "sinon": "^17.0.1", "sinon-chai": "^3.7.0" }, @@ -33,6 +33,120 @@ "node": ">=0.6" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -82,16 +196,6 @@ "dev": true, "license": "(Unlicense OR Apache-2.0)" }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -118,20 +222,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -221,19 +311,6 @@ "dev": true, "license": "MIT" }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -245,19 +322,6 @@ "concat-map": "0.0.1" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -404,28 +468,19 @@ } }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" } }, "node_modules/cli": { @@ -443,15 +498,18 @@ } }, "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/color-convert": { @@ -491,9 +549,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.41.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.41.0.tgz", - "integrity": "sha512-71Gzp96T9YPk63aUvE5Q5qP+DryB4ZloUZPSOebGM88VNw8VNfvdA7z6kGA8iGOTEzAomsRidp4jXSmUIJsL+Q==", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.49.0.tgz", + "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -508,6 +566,21 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/d": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", @@ -746,6 +819,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1029,19 +1109,6 @@ "type": "^2.7.2" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -1085,6 +1152,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1092,21 +1176,6 @@ "dev": true, "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1247,19 +1316,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/glob/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1515,19 +1571,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-boolean-object": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", @@ -1593,16 +1636,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-finalizationregistry": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", @@ -1648,19 +1681,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -1674,16 +1694,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-number-object": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", @@ -1701,6 +1711,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -1876,6 +1896,29 @@ "dev": true, "license": "MIT" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -1989,6 +2032,13 @@ "get-func-name": "^2.0.1" } }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2021,32 +2071,43 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mocha": { - "version": "10.8.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", - "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", "dev": true, "license": "MIT", "dependencies": { - "ansi-colors": "^4.1.3", "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", + "chokidar": "^4.0.1", "debug": "^4.3.5", - "diff": "^5.2.0", + "diff": "^7.0.0", "escape-string-regexp": "^4.0.0", "find-up": "^5.0.0", - "glob": "^8.1.0", + "glob": "^10.4.5", "he": "^1.2.0", + "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", + "minimatch": "^9.0.5", "ms": "^2.1.3", + "picocolors": "^1.1.1", "serialize-javascript": "^6.0.2", "strip-json-comments": "^3.1.1", "supports-color": "^8.1.1", - "workerpool": "^6.5.1", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", "yargs-unparser": "^2.0.0" }, "bin": { @@ -2054,13 +2115,13 @@ "mocha": "bin/mocha.js" }, "engines": { - "node": ">= 14.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/mocha-bootstrap": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/mocha-bootstrap/-/mocha-bootstrap-1.0.6.tgz", - "integrity": "sha512-IcN6mZ6mN8A27/Nd6llFJJ6xcXZTq+1dFp40HBoCaMn+gsFCB246l6wLOJXUXRk3QzAk8m4PUnE/uyRTc7j4kQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/mocha-bootstrap/-/mocha-bootstrap-1.1.1.tgz", + "integrity": "sha512-T2A6drsvlCQRL/OgDRDP8Mj1BmXeYph/eSitks5bnMBMMhkdZgQPpGdfXTlVOc4pS9R0OaSV6JJJo1qyIAFA4w==", "dev": true, "license": "MIT", "engines": { @@ -2069,53 +2130,67 @@ "peerDependencies": { "chai": "^4.3.10", "chai-as-promised": "^7.1.1", - "mocha": "^10.2.0", + "mocha": "^11.1.0", "sinon": "^17.0.1 || ^9.2.4", "sinon-chai": "^3.7.0" } }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, + "node_modules/mocha/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/mocha/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": ">=12" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mocha/node_modules/strip-json-comments": { @@ -2158,16 +2233,6 @@ "path-to-regexp": "^6.2.1" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/nowdoc": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/nowdoc/-/nowdoc-1.0.1.tgz", @@ -2283,13 +2348,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parsing": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/parsing/-/parsing-2.3.0.tgz", - "integrity": "sha512-9KFnyd1tFJy1ocwgcd7aYlYo+huI1FKuxvpO48yTvjyweB6qm4oQDP9oUOaJaybY6iS9UY0tds9tTJgRmeyyPQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/parsing/-/parsing-2.4.0.tgz", + "integrity": "sha512-+QA7h6P5i16rnLH8Esblz4A0unIlthUtTs/aNRsuIcBq6B/muCJuwlvzyTYBFi1E51HFFG1Zw21dyJGzbcf4tQ==", "dev": true, "license": "MIT", "dependencies": { + "core-js-pure": "^3.42.0", "microdash": "^1.4.2" }, "engines": { @@ -2316,6 +2389,33 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -2359,9 +2459,9 @@ } }, "node_modules/phptest": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/phptest/-/phptest-0.0.10.tgz", - "integrity": "sha512-RvGI9c6/HMrpAtbpk6Lmy51Lmpj4HTpUHB/OSc3oYOchchbBTc3d7iyNVm3hSd/Bp7RTwW4qSZwMPa5Pg3ehhw==", + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/phptest/-/phptest-0.0.12.tgz", + "integrity": "sha512-UHoQ60old/5ki2a2cxubKVzurQdkrLnZPExrO38xl15zApP8rRJ36B5t4pl3dAD/gHK0wMxe/Q2tz8CfbG1giQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2374,20 +2474,20 @@ "node": ">=12" }, "peerDependencies": { - "mocha": "^10.2.0", - "phptoast": "^9.1.0", - "phptojs": "^10.0.0" + "mocha": "^11.7.5", + "phptoast": "^9.4.0", + "phptojs": "^10.3.0" } }, "node_modules/phptoast": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/phptoast/-/phptoast-9.3.0.tgz", - "integrity": "sha512-QZbf5AJ7m+B0YrTyXlpJMqA97wjnYyoBD0JRntwB0dtPWg7xxIV8K/o08KMbk6DDN7Onuslz84eVj3pp1LWTlw==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/phptoast/-/phptoast-9.4.0.tgz", + "integrity": "sha512-6j3nW3HRApc5jWWYSTa5cirfJbo88BSiqZpckg7ZMq4CVmzEI3gwk2qza4BK5CJ0nSUrboXBGN/ME7niCy8uvA==", "dev": true, "license": "MIT", "dependencies": { "microdash": "^1.4.2", - "parsing": "^2.3.0", + "parsing": "^2.4.0", "phpcommon": "^2.0.2" }, "engines": { @@ -2395,9 +2495,9 @@ } }, "node_modules/phptojs": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/phptojs/-/phptojs-10.2.0.tgz", - "integrity": "sha512-Yj/V2ISzCsuCBkbuv8skTM8SPxGYuQfX5kRe+ijIaMuRq01POdy0YAdKFrDi924qw0tupqV2LRlEGlGJ4yA+Fw==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/phptojs/-/phptojs-10.3.0.tgz", + "integrity": "sha512-EJmZbPrjBTPSxH6Kg3eNVX+G4W+BV/qXxongW/cGrwgH6LuG4+34rSCZ+3Fc9PczuW/HVczyQRmNrhQ5lAFPDA==", "dev": true, "license": "MIT", "dependencies": { @@ -2412,18 +2512,12 @@ "node": ">=0.6" } }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } + "license": "ISC" }, "node_modules/possible-typed-array-names": { "version": "1.1.0", @@ -2469,16 +2563,17 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, "engines": { - "node": ">=8.10.0" + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/reflect.getprototypeof": { @@ -2702,6 +2797,29 @@ "node": ">= 0.4" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -2778,6 +2896,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/sinon": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", @@ -2866,6 +2997,22 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.10", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", @@ -2938,6 +3085,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", @@ -2976,19 +3137,6 @@ "node": ">=0.6" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/tr46": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", @@ -3145,6 +3293,22 @@ "webidl-conversions": "^4.0.2" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/which-boxed-primitive": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", @@ -3242,9 +3406,9 @@ } }, "node_modules/workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", "dev": true, "license": "Apache-2.0" }, @@ -3266,6 +3430,25 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3284,32 +3467,32 @@ } }, "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-unparser": { diff --git a/package.json b/package.json index 4fb2a8a5..ceb804d6 100644 --- a/package.json +++ b/package.json @@ -34,12 +34,12 @@ "chai": "^4.3.10", "chai-as-promised": "^7.1.1", "jshint": "^2.13.6", - "mocha": "^10.4.0", + "mocha": "^11.7.5", "mocha-bootstrap": "^1.0.6", "nowdoc": "^1.0.1", - "phptest": "^0.0.10", - "phptoast": "^9.3.0", - "phptojs": "^10.2.0", + "phptest": "^0.0.12", + "phptoast": "^9.4.0", + "phptojs": "^10.3.0", "sinon": "^17.0.1", "sinon-chai": "^3.7.0" }, diff --git a/src/CallFactory.js b/src/CallFactory.js index db8c2ac1..ee8f6150 100644 --- a/src/CallFactory.js +++ b/src/CallFactory.js @@ -63,14 +63,15 @@ _.extend(CallFactory.prototype, { }, /** - * Creates a new FFI Call + * Creates a new FFI Call. * - * @param {Value[]=} args + * @param {Reference[]|Value[]|Variable[]=} positionalArgs + * @param {Object.=} namedArgs */ - createFFICall: function (args) { + createFFICall: function (positionalArgs, namedArgs) { var factory = this; - return new factory.FFICall(args || []); + return new factory.FFICall(positionalArgs || [], namedArgs || null); }, /** diff --git a/src/Class.js b/src/Class.js index aea1cbf7..ab0e5a4b 100644 --- a/src/Class.js +++ b/src/Class.js @@ -12,13 +12,13 @@ module.exports = require('pauser')([ require('microdash'), require('phpcommon'), - require('./OOP/Class/IsolatedScope'), - require('es6-weak-map') + require('./Function/Callable'), + require('./OOP/Class/IsolatedScope') ], function ( _, phpCommon, - IsolatedScope, - WeakMap + Callable, + IsolatedScope ) { var IS_STATIC = 'isStatic', MAGIC_CALL = '__call', @@ -33,39 +33,10 @@ module.exports = require('pauser')([ VISIBILITY = 'visibility', hasOwn = {}.hasOwnProperty, Exception = phpCommon.Exception, - PHPError = phpCommon.PHPError, - methodLookupMap = new WeakMap(), - /** - * Fetches a method from the specified object - * - * TODO: Build method map when class is initialised rather than resolving at runtime like this - * - * @param {Object} object - * @param {string} methodName - * @returns {Function|null} - */ - getMethod = function (object, methodName) { - var methods; - - if (methodLookupMap.has(object)) { - methods = methodLookupMap.get(object); - } else { - methods = Object.create(null); - - _.forOwn(object, function (value, propertyName) { - if (_.isFunction(value)) { - methods[propertyName.toLowerCase()] = value; - } - }); - - methodLookupMap.set(object, methods); - } - - return methods[methodName.toLowerCase()] || null; - }; + PHPError = phpCommon.PHPError; /** - * Represents a class exposed to PHP-land + * Represents a class exposed to PHP-land. * * @param {ValueFactory} valueFactory * @param {ValueProvider} valueProvider @@ -193,15 +164,21 @@ module.exports = require('pauser')([ */ this.InternalClass = InternalClass; /** - * @type {Function|null} + * Method callables, indexed by actual name. + * + * @type {Object.} */ - this.methodCaller = methodCaller; + this.methodCallables = {}; /** - * Looked up method specs, indexed by lowercase name to handle case-insensitivity + * Method callables, indexed by lowercase name to handle case-insensitivity. * - * @type {Object.} + * @type {Object.} + */ + this.methodCallablesByLowerCase = {}; + /** + * @type {Function|null} */ - this.methodSpecCache = Object.create(null); + this.methodCaller = methodCaller; /** * @type {string} */ @@ -285,22 +262,21 @@ module.exports = require('pauser')([ * Omit both `objectValue` and `currentNativeObject` for a static call. * * @param {string} methodName The name of the method to call - * @param {Reference[]|Value[]} args The wrapped value objects or references to pass as arguments to the method + * @param {Reference[]|Value[]} positionalArgs The wrapped value objects or references to pass as arguments to the method + * @param {Object.|null} namedArgs The named arguments * @param {ObjectValue|null} objectValue The wrapped ObjectValue for this instance - * @param {object|null} currentNativeObject The current native JS object on the prototype chain to search for the method * @param {Class|null} currentClass The original called class (this function is called recursively for inherited methods) * @param {bool} isForwardingStaticCall eg. self::f() is forwarding, MyParentClass::f() is non-forwarding * @returns {ChainableInterface} Returns the result of the method if it is defined * @throws {PHPFatalError} Throws when the method is not defined */ - callMethod: function (methodName, args, objectValue, currentNativeObject, currentClass, isForwardingStaticCall) { + callMethod: function (methodName, positionalArgs, namedArgs, objectValue, currentClass, isForwardingStaticCall) { var classObject = this, - nativeObject = objectValue ? objectValue.getObject() : null, result, thisObject = classObject.callStack.getThisObject(); - function callMethod(currentObject, methodName, args) { - var method = getMethod(currentObject, methodName); + function callMethod(methodName, positionalArgs, namedArgs) { + var method = classObject.methodCallablesByLowerCase[methodName.toLowerCase()] || null; if (method !== null) { if (!objectValue && !method[IS_STATIC]) { @@ -324,48 +300,36 @@ module.exports = require('pauser')([ } } - // For a non-forwarding static call, pass the new static class through. - // (For a forwarding static call, we will pass `null` through as the "new static class" - // inside FunctionFactory, because we just want to use the one the caller has.) - if (!isForwardingStaticCall) { - classObject.functionFactory.setNewStaticClassIfWrapped(method, currentClass); - } - /* * Method may return a Value, Future-wrapped Reference or Variable etc. * If the method returns undefined (eg. userland and there was no return statement) * then the wrapper from FunctionFactory will ensure it is coerced to a NullValue for example. */ - return method.apply(objectValue, args); + return method.call(positionalArgs, namedArgs, objectValue, isForwardingStaticCall ? null : currentClass); } - if ( - currentObject === classObject.rootInternalPrototype && - classObject.superClass - ) { + if (classObject.superClass) { return classObject.superClass.callMethod( methodName, - args, + positionalArgs, + namedArgs, objectValue, - Object.getPrototypeOf(currentObject), currentClass, isForwardingStaticCall ); } - currentObject = Object.getPrototypeOf(currentObject); - - if (!currentObject) { - return null; - } - - return callMethod(currentObject, methodName, args); + return null; } if (classObject.methodCaller) { // A custom method caller/accelerator is installed, call it directly instead. // Used by e.g. JSObject to avoid additional magic method calls per native call. - return classObject.valueProvider.createFutureList(args) + if (namedArgs) { + throw new Exception('Cannot use named arguments with custom method caller'); + } + + return classObject.valueProvider.createFutureList(positionalArgs) .next(function (presentArgs) { return classObject.methodCaller.call( objectValue, @@ -382,29 +346,13 @@ module.exports = require('pauser')([ .asValue(); } - isForwardingStaticCall = !!isForwardingStaticCall; - - if (!currentNativeObject) { - // Walk up the prototype chain from the native object - currentNativeObject = nativeObject; - } + isForwardingStaticCall = Boolean(isForwardingStaticCall); if (!currentClass) { currentClass = classObject; } - if (nativeObject instanceof classObject.InternalClass) { - // Ignore own properties of the native object when searching for methods - if (currentNativeObject === nativeObject) { - currentNativeObject = Object.getPrototypeOf(currentNativeObject); - } - } else { - // For some special classes (eg. JSObject, Closure) the native object may not actually - // be an instance of the InternalClass, so fake inheritance of the native class - currentNativeObject = classObject.InternalClass.prototype; - } - - result = callMethod(currentNativeObject, methodName, args); + result = callMethod(methodName, positionalArgs, namedArgs); if (result !== null) { return result; @@ -416,14 +364,30 @@ module.exports = require('pauser')([ * Resolve all arguments (references or values) to present values and wrap them in an ArrayValue * so that we can pass it in as the second argument to the magic method. */ - return classObject.valueProvider.createFutureArray(args) - .next(function (argsArray) { + return classObject.valueProvider.createFutureArray(positionalArgs) + .next(function (positionalArgsArray) { + if (!namedArgs) { + return positionalArgsArray; + } + + return classObject.flow + .eachAsync(Object.keys(namedArgs), function (argName) { + return namedArgs[argName].getValue().next(function (presentArg) { + return positionalArgsArray.getElementByKey(classObject.valueFactory.createString(argName)) + .setValue(presentArg); + }); + }) + .next(function () { + return positionalArgsArray; + }); + }) + .next(function (positionalArgsArray) { if (!objectValue && thisObject) { // Magic __call(...) should override __callStatic(...) // when both are present for static call in object context. - result = callMethod(thisObject.getObject(), MAGIC_CALL, [ + result = callMethod(MAGIC_CALL, [ classObject.valueFactory.createString(methodName), - argsArray + positionalArgsArray ]); if (result !== null) { @@ -432,11 +396,10 @@ module.exports = require('pauser')([ } result = callMethod( - currentNativeObject, objectValue ? MAGIC_CALL : MAGIC_CALL_STATIC, [ classObject.valueFactory.createString(methodName), - argsArray + positionalArgsArray ] ); @@ -456,10 +419,11 @@ module.exports = require('pauser')([ * Calls the userland constructor for the provided object. * * @param {ObjectValue} objectValue - * @param {Value[]} args + * @param {Reference[]|Value[]} positionalArgs The wrapped value objects or references to pass as arguments to the constructor + * @param {Object.|null} namedArgs The named arguments * @returns {ChainableInterface} */ - construct: function (objectValue, args) { + construct: function (objectValue, positionalArgs, namedArgs) { var classObject = this; if (!classObject.constructorName) { @@ -467,7 +431,7 @@ module.exports = require('pauser')([ // if it has one, otherwise do nothing. if (classObject.superClass) { // Note that this may return a Future if the constructor paused. - return classObject.superClass.construct(objectValue, args); + return classObject.superClass.construct(objectValue, positionalArgs, namedArgs); } return objectValue; @@ -475,7 +439,7 @@ module.exports = require('pauser')([ // Call the constructor for the current class and not via the object value, // as the method may have been overridden by descendant classes. - return classObject.callMethod(classObject.constructorName, args, objectValue) + return classObject.callMethod(classObject.constructorName, positionalArgs, namedArgs, objectValue) /* * Discard the result value of the constructor method and return the new ObjectValue. * Note that if a pause occurs inside the constructor, a Future will @@ -486,6 +450,17 @@ module.exports = require('pauser')([ }); }, + /** + * Defines a method on this class. + * + * @param {string} methodName + * @param {Callable} callable + */ + defineMethod: function (methodName, callable) { + this.methodCallables[methodName] = callable; + this.methodCallablesByLowerCase[methodName.toLowerCase()] = callable; + }, + /** * Exports instances of this class with a defined unwrapper if one has been set, * otherwise wraps them in a native JS class that extends the PHP class' internal class @@ -587,7 +562,7 @@ module.exports = require('pauser')([ }, /** - * Fetches the internal native JS class for this class exposed to PHP + * Fetches the internal native JS class for this class exposed to PHP. * * @returns {Function} */ @@ -596,83 +571,22 @@ module.exports = require('pauser')([ }, /** - * Fetches the spec for an instance or static method - * - * TODO: Merge/replace MethodSpec with FunctionSpec/MethodContext etc.? + * Fetches the Callable for an instance or static method if defined, returning null otherwise. * - * @param {string} methodName The name of the method to fetch the spec for - * @param {ObjectValue=null} objectValue The wrapped ObjectValue for this instance - * @param {object=null} currentNativeObject The current native JS object on the prototype chain to search for the method - * @param {Class=null} originalClass The original class (this function is called recursively for inherited methods) - * @returns {MethodSpec|null} Returns the spec of the method if it exists, or null if it does not + * @param {string} methodName + * @returns {Callable|null} */ - getMethodSpec: function (methodName, objectValue, currentNativeObject, originalClass) { - var classObject = this, - lowercaseMethodName = methodName.toLowerCase(), - methodSpec, - nativeObject; - - function getMethodSpec(currentObject, methodName) { - var method = getMethod(currentObject, methodName); - - if (method !== null) { - return classObject.functionFactory.createMethodSpec(originalClass, classObject, methodName, method); - } - - if ( - currentObject === classObject.rootInternalPrototype && - classObject.superClass - ) { - return classObject.superClass.getMethodSpec( - methodName, - objectValue, - Object.getPrototypeOf(currentObject), - originalClass - ); - } - - currentObject = Object.getPrototypeOf(currentObject); - - if (!currentObject) { - return null; - } - - return getMethodSpec(currentObject, methodName); - } - - // Fetch spec from cache if possible - if (classObject.methodSpecCache[lowercaseMethodName]) { - return classObject.methodSpecCache[lowercaseMethodName]; - } - - nativeObject = objectValue ? objectValue.getObject() : null; - - if (!currentNativeObject) { - // Walk up the prototype chain from the native object - currentNativeObject = nativeObject; - } - - if (!originalClass) { - originalClass = classObject; - } - - if (nativeObject instanceof classObject.InternalClass) { - // Ignore own properties of the native object when searching for methods - if (currentNativeObject === nativeObject) { - currentNativeObject = Object.getPrototypeOf(currentNativeObject); - } - } else { - // For some special classes (eg. JSObject, Closure) the native object may not actually - // be an instance of the InternalClass, so fake inheritance of the native class - currentNativeObject = classObject.InternalClass.prototype; - } - - methodSpec = getMethodSpec(currentNativeObject, methodName); - - // Cache the spec for speed next time - classObject.methodSpecCache[lowercaseMethodName] = methodSpec; + getMethodCallable: function (methodName) { + return this.methodCallablesByLowerCase[methodName.toLowerCase()] || null; + }, - return methodSpec; + /** + * Fetches the map of Callables for both instance and static methods defined for the class. + * + * @returns {Object.} + */ + getMethodCallables: function () { + return this.methodCallables; }, /** @@ -955,16 +869,16 @@ module.exports = require('pauser')([ /** * Creates a new instance of this class. * - * @param {Value[]=} constructorArgs + * @param {Reference[]|Value[]} constructorPositionalArgs The wrapped value objects or references to pass as arguments to the constructor. + * @param {Object.=} constructorNamedArgs The named arguments. * @param {*[]=} shadowConstructorArgs * @returns {ChainableInterface} */ - instantiate: function (constructorArgs, shadowConstructorArgs) { + instantiate: function (constructorPositionalArgs, constructorNamedArgs, shadowConstructorArgs) { var classObject = this; - if (!constructorArgs) { - constructorArgs = []; - } + constructorPositionalArgs ??= []; // jshint ignore:line + constructorNamedArgs ??= null; // jshint ignore:line return classObject.initialiseConstants() .next(function () { @@ -978,7 +892,7 @@ module.exports = require('pauser')([ // Call the userland constructor. Note that the return value of .construct(...) // may in fact be a Future if there was a pause inside the userland __construct()or. - return classObject.construct(objectValue, constructorArgs); + return classObject.construct(objectValue, constructorPositionalArgs, constructorNamedArgs); }); }, @@ -994,7 +908,7 @@ module.exports = require('pauser')([ nativeObject = Object.create(classObject.InternalClass.prototype), objectValue = classObject.valueFactory.createObject(nativeObject, classObject); - classObject.internalConstruct(objectValue, shadowConstructorArgs); + classObject.internalConstruct(objectValue, shadowConstructorArgs || []); return objectValue; }, @@ -1002,14 +916,15 @@ module.exports = require('pauser')([ /** * Creates a new instance of this class and also sets the given internal properties (shorthand). * - * @param {Value[]} args + * @param {Reference[]|Value[]} positionalArgs The wrapped value objects or references to pass as arguments to the constructor + * @param {Object.|null} namedArgs The named arguments * @param {Object.} internals * @return {ChainableInterface} */ - instantiateWithInternals: function (args, internals) { + instantiateWithInternals: function (positionalArgs, namedArgs, internals) { var classObject = this; - return classObject.instantiate(args).next(function (objectValue) { + return classObject.instantiate(positionalArgs, namedArgs).next(function (objectValue) { _.forOwn(internals, function (value, name) { objectValue.setInternalProperty(name, value); }); diff --git a/src/ClassAutoloader.js b/src/ClassAutoloader.js index 04937ab4..ebee60b6 100644 --- a/src/ClassAutoloader.js +++ b/src/ClassAutoloader.js @@ -97,7 +97,7 @@ module.exports = require('pauser')([ magicAutoloadFunction = globalNamespace.getOwnFunction(MAGIC_AUTOLOAD_FUNCTION); if (magicAutoloadFunction) { - return magicAutoloadFunction(autoloader.valueFactory.createString(name)) + return magicAutoloadFunction.call([autoloader.valueFactory.createString(name)]) .asValue(); } diff --git a/src/Closure.js b/src/Closure.js index 51648d3d..40585493 100644 --- a/src/Closure.js +++ b/src/Closure.js @@ -18,7 +18,7 @@ module.exports = require('pauser')([ * @param {ClosureFactory} closureFactory * @param {ValueFactory} valueFactory * @param {NamespaceScope} namespaceScope - * @param {Function} wrappedFunction + * @param {Callable} callable * @param {Scope} enclosingScope * @param {ObjectValue|null} thisObject * @param {FunctionSpec} functionSpec @@ -29,7 +29,7 @@ module.exports = require('pauser')([ valueFactory, namespaceScope, enclosingScope, - wrappedFunction, + callable, thisObject, functionSpec ) { @@ -58,9 +58,9 @@ module.exports = require('pauser')([ */ this.valueFactory = valueFactory; /** - * @type {Function} + * @type {Callable} */ - this.wrappedFunction = wrappedFunction; + this.callable = callable; } _.extend(Closure.prototype, { @@ -87,15 +87,21 @@ module.exports = require('pauser')([ /** * Invokes this closure with the provided arguments, returning its result. * - * @param {Reference[]|Value[]} args + * @param {Reference[]|Value[]|Variable[]} positionalArguments + * @param {Object.|null} namedArguments * @param {ObjectValue|undefined} thisObject * @returns {ChainableInterface} */ - invoke: function (args, thisObject) { + invoke: function (positionalArguments, namedArguments, thisObject) { var closure = this; - // Note that the wrapped function could return a Future for async handling. - return closure.wrappedFunction.apply(thisObject || closure.thisObject, args); + // Note that the callable could return a Future for async handling. + return closure.callable.call( + positionalArguments, + namedArguments || null, + thisObject || closure.thisObject, + null // newStaticClass. + ); } }); diff --git a/src/ClosureFactory.js b/src/ClosureFactory.js index e9a68cbf..a697e102 100644 --- a/src/ClosureFactory.js +++ b/src/ClosureFactory.js @@ -75,7 +75,7 @@ module.exports = require('pauser')([ staticClass = null; } - wrappedFunction = factory.functionFactory.create( + wrappedFunction = factory.functionFactory.createCallable( namespaceScope, scopeClass, null, diff --git a/src/Core/Opcode/Handler/OpcodeHandlerFactory.js b/src/Core/Opcode/Handler/OpcodeHandlerFactory.js index 6a7cc3bf..e70eb6f3 100644 --- a/src/Core/Opcode/Handler/OpcodeHandlerFactory.js +++ b/src/Core/Opcode/Handler/OpcodeHandlerFactory.js @@ -9,8 +9,7 @@ 'use strict'; -var _ = require('microdash'), - slice = [].slice; +var _ = require('microdash'); /** * @param {ControlBridge} controlBridge @@ -68,9 +67,8 @@ _.extend(OpcodeHandlerFactory.prototype, { opcodeFetcher = wrapper.opcodeFetcherRepository.getFetcher(opcodeFetcherType), tracedOpcodeHandler; - tracedOpcodeHandler = function () { + tracedOpcodeHandler = function (...args) { var trace = wrapper.callStack.getCurrentTrace(), - args = slice.call(arguments), // Note that Opcode objects are pooled to minimise GC pressure. opcode = trace.fetchOpcode(opcodeFetcher, opcodeHandler, args), resumeValue = opcode.resume(), diff --git a/src/Core/Opcode/Handler/TypedOpcodeHandlerFactory.js b/src/Core/Opcode/Handler/TypedOpcodeHandlerFactory.js index 9fc77550..15004df5 100644 --- a/src/Core/Opcode/Handler/TypedOpcodeHandlerFactory.js +++ b/src/Core/Opcode/Handler/TypedOpcodeHandlerFactory.js @@ -11,7 +11,6 @@ var _ = require('microdash'), phpCommon = require('phpcommon'), - slice = [].slice, Exception = phpCommon.Exception; /** @@ -48,9 +47,8 @@ _.extend(TypedOpcodeHandlerFactory.prototype, { } typedHandler = variadicParameter ? - function withVariadicParameter() { + function withVariadicParameter(...args) { var argIndex = 0, - args = slice.call(arguments), argCount = args.length, coercedArgs = [], variadicArg = []; @@ -126,9 +124,8 @@ _.extend(TypedOpcodeHandlerFactory.prototype, { return coerceArgs(); } : - function withoutVariadicParameter() { + function withoutVariadicParameter(...args) { var argIndex = 0, - args = slice.call(arguments), argCount = args.length, coercedArgs = []; diff --git a/src/FFI/Call.js b/src/FFI/Call.js index 96e40f15..ae823f62 100644 --- a/src/FFI/Call.js +++ b/src/FFI/Call.js @@ -14,14 +14,19 @@ var _ = require('microdash'), Exception = phpCommon.Exception; /** - * @param {Value[]} args + @param {Reference[]|Value[]|Variable[]} positionalArgs + @param {Object.|null} namedArgs * @constructor */ -function Call(args) { +function Call(positionalArgs, namedArgs) { /** * @type {Reference[]|Value[]|Variable[]} */ - this.args = args; + this.args = positionalArgs; + /** + * @type {Object|null} + */ + this.namedArgs = namedArgs; } _.extend(Call.prototype, { @@ -95,6 +100,15 @@ _.extend(Call.prototype, { return null; }, + /** + * Fetches the named arguments passed to the called function, or null if none. + * + * @returns {Object.|null} + */ + getNamedArgs: function () { + return this.namedArgs; + }, + /** * Fetches the scope inside the called function * diff --git a/src/FFI/Call/Caller.js b/src/FFI/Call/Caller.js index 01865eb7..6d5b4a3b 100644 --- a/src/FFI/Call/Caller.js +++ b/src/FFI/Call/Caller.js @@ -69,10 +69,11 @@ module.exports = require('pauser')([ * * @param {ObjectValue} objectValue * @param {string} methodName - * @param {Value[]} args + * @param {Reference[]|Value[]|Variable[]} positionalArgs + * @param {Object.|null} namedArgs * @returns {Promise} */ - callMethodAsync: function (objectValue, methodName, args) { + callMethodAsync: function (objectValue, methodName, positionalArgs, namedArgs) { var caller = this; if (caller.mode !== 'async') { @@ -80,7 +81,7 @@ module.exports = require('pauser')([ } // Call the method - note that it may return a Future-wrapped Value for async operation. - return objectValue.callMethod(methodName, args) + return objectValue.callMethod(methodName, positionalArgs, namedArgs) // Pop the call off the stack _before_ returning, to mirror sync mode's behaviour. .finally(caller.popFFICall.bind(caller)) .catch(function (error) { @@ -102,11 +103,12 @@ module.exports = require('pauser')([ * * @param {ObjectValue} objectValue * @param {string} methodName - * @param {Value[]} args + * @param {Reference[]|Value[]|Variable[]} positionalArgs + * @param {Object.|null} namedArgs * @param {boolean=} useSyncApiAlthoughPsync * @returns {Promise|Value} */ - callMethodSyncLike: function (objectValue, methodName, args, useSyncApiAlthoughPsync) { + callMethodSyncLike: function (objectValue, methodName, positionalArgs, namedArgs, useSyncApiAlthoughPsync) { var caller = this; if (caller.mode === 'async') { @@ -115,7 +117,7 @@ module.exports = require('pauser')([ function invoke() { try { - return objectValue.callMethod(methodName, args).yieldSync(); + return objectValue.callMethod(methodName, positionalArgs, namedArgs).yieldSync(); } catch (error) { if (error instanceof Value && error.getType() === 'object') { // Method threw a PHP Exception, so throw a native JS error for it @@ -145,15 +147,16 @@ module.exports = require('pauser')([ }, /** - * Pushes an FFI call onto the call stack + * Pushes an FFI call onto the call stack. * - * @param {Value[]} args + * @param {Reference[]|Value[]|Variable[]} positionalArgs + * @param {Object.|null} namedArgs */ - pushFFICall: function (args) { + pushFFICall: function (positionalArgs, namedArgs) { var caller = this; - // Push an FFI call onto the stack, representing the call from JavaScript-land - caller.callStack.push(caller.callFactory.createFFICall(args)); + // Push an FFI call onto the stack, representing the call from JavaScript-land. + caller.callStack.push(caller.callFactory.createFFICall(positionalArgs, namedArgs)); }, /** diff --git a/src/FFI/Call/NativeCaller.js b/src/FFI/Call/NativeCaller.js index 49bc753f..8ffa5d18 100644 --- a/src/FFI/Call/NativeCaller.js +++ b/src/FFI/Call/NativeCaller.js @@ -33,30 +33,38 @@ module.exports = require('pauser')([ _.extend(NativeCaller.prototype, { /** * Encapsulates calling a PHP-land method from JS-land using the FFI API, - * unwrapping the result to a native value + * unwrapping the result to a native value. * * @param {ObjectValue} objectValue * @param {string} methodName - * @param {Value[]} args + * @param {Reference[]|Value[]|Variable[]} positionalArgs + * @param {Object.|null|boolean} namedArgs + * If a boolean is passed here it will be treated as useSyncApiAlthoughPsync. * @param {boolean=} useSyncApiAlthoughPsync * @returns {Promise<*>|*} */ - callMethod: function (objectValue, methodName, args, useSyncApiAlthoughPsync) { + callMethod: function (objectValue, methodName, positionalArgs, namedArgs, useSyncApiAlthoughPsync) { var nativeCaller = this, result; - // Push an FFI call onto the stack, representing the call from JavaScript-land - nativeCaller.caller.pushFFICall(args); + // Support calling with signature (objectValue, methodName, positionalArgs, useSyncApiAlthoughPsync) + if (typeof namedArgs === 'boolean') { + useSyncApiAlthoughPsync = namedArgs; + namedArgs = null; + } + + // Push an FFI call onto the stack, representing the call from JavaScript-land. + nativeCaller.caller.pushFFICall(positionalArgs, namedArgs); if (nativeCaller.mode === 'async') { - return nativeCaller.caller.callMethodAsync(objectValue, methodName, args) + return nativeCaller.caller.callMethodAsync(objectValue, methodName, positionalArgs, namedArgs) .then(function (resultValue) { return resultValue.getNative(); }); } - // Otherwise we're in sync or psync mode - result = nativeCaller.caller.callMethodSyncLike(objectValue, methodName, args, useSyncApiAlthoughPsync); + // Otherwise we're in sync or psync mode. + result = nativeCaller.caller.callMethodSyncLike(objectValue, methodName, positionalArgs, namedArgs, useSyncApiAlthoughPsync); return nativeCaller.mode === 'psync' && !useSyncApiAlthoughPsync ? result.then(function (resultValue) { diff --git a/src/FFI/Call/ValueCaller.js b/src/FFI/Call/ValueCaller.js index a97290cd..17871924 100644 --- a/src/FFI/Call/ValueCaller.js +++ b/src/FFI/Call/ValueCaller.js @@ -37,23 +37,24 @@ module.exports = require('pauser')([ * * @param {ObjectValue} objectValue * @param {string} methodName - * @param {Value[]} args + * @param {Reference[]|Value[]|Variable[]} positionalArgs + * @param {Object.|null} namedArgs * @param {boolean=} useSyncApiAlthoughPsync * @returns {Promise|Value} */ - callMethod: function (objectValue, methodName, args, useSyncApiAlthoughPsync) { + callMethod: function (objectValue, methodName, positionalArgs, namedArgs, useSyncApiAlthoughPsync) { var valueCaller = this; - // Push an FFI call onto the stack, representing the call from JavaScript-land - valueCaller.caller.pushFFICall(args); + // Push an FFI call onto the stack, representing the call from JavaScript-land. + valueCaller.caller.pushFFICall(positionalArgs, namedArgs); if (valueCaller.mode === 'async') { // Unlike NativeCaller, do not coerce to native here - return valueCaller.caller.callMethodAsync(objectValue, methodName, args); + return valueCaller.caller.callMethodAsync(objectValue, methodName, positionalArgs, namedArgs); } // Otherwise we're in sync or psync mode - return valueCaller.caller.callMethodSyncLike(objectValue, methodName, args, useSyncApiAlthoughPsync); + return valueCaller.caller.callMethodSyncLike(objectValue, methodName, positionalArgs, namedArgs, useSyncApiAlthoughPsync); } }); diff --git a/src/FFI/Internals/FunctionInternalsClassFactory.js b/src/FFI/Internals/FunctionInternalsClassFactory.js index 11193824..9aa68c17 100644 --- a/src/FFI/Internals/FunctionInternalsClassFactory.js +++ b/src/FFI/Internals/FunctionInternalsClassFactory.js @@ -117,9 +117,9 @@ _.extend(FunctionInternalsClassFactory.prototype, { namespace.defineFunction( name, - function () { + function (...args) { // Unwrap args from PHP-land to JS-land to native values if/as appropriate. - return valueCoercer.coerceArguments(arguments).next(function __uniterOutboundStackMarker__(effectiveArguments) { + return valueCoercer.coerceArguments(args).next(function __uniterOutboundStackMarker__(effectiveArguments) { return func.apply(internals, effectiveArguments); }); }, diff --git a/src/FFI/Internals/OverloadedFunctionInternalsClassFactory.js b/src/FFI/Internals/OverloadedFunctionInternalsClassFactory.js index 0e932c39..d35a9d0d 100644 --- a/src/FFI/Internals/OverloadedFunctionInternalsClassFactory.js +++ b/src/FFI/Internals/OverloadedFunctionInternalsClassFactory.js @@ -114,9 +114,9 @@ _.extend(OverloadedFunctionInternalsClassFactory.prototype, { return new OverloadedFunctionVariant( signature, - function () { + function (...args) { // Unwrap args from PHP-land to JS-land to native values if/as appropriate. - return valueCoercer.coerceArguments(arguments) + return valueCoercer.coerceArguments(args) .next(function __uniterOutboundStackMarker__(effectiveArguments) { return func.apply(internals, effectiveArguments); }); diff --git a/src/FFI/Value/AsyncObjectValue.js b/src/FFI/Value/AsyncObjectValue.js index 4242fcc8..d5546822 100644 --- a/src/FFI/Value/AsyncObjectValue.js +++ b/src/FFI/Value/AsyncObjectValue.js @@ -56,14 +56,15 @@ module.exports = require('pauser')([ * Calls the specified method of this object * * @param {string} methodName - * @param {Value[]?} args + * @param {Reference[]|Value[]|Variable[]} positionalArgs + * @param {Object.|null} namedArgs * @returns {Promise} Returns the result of the method * @throws {PHPFatalError} Throws when the method does not exist */ - callMethod: function (methodName, args) { + callMethod: function (methodName, positionalArgs, namedArgs) { var value = this; - return value.valueCaller.callMethod(value.wrappedObjectValue, methodName, args); + return value.valueCaller.callMethod(value.wrappedObjectValue, methodName, positionalArgs, namedArgs); } }); diff --git a/src/FFI/Value/PHPObject.js b/src/FFI/Value/PHPObject.js index c79f1e04..b6bfddde 100644 --- a/src/FFI/Value/PHPObject.js +++ b/src/FFI/Value/PHPObject.js @@ -52,16 +52,17 @@ module.exports = require('pauser')([ * where asynchronous (blocking) operation is possible. * * @param {string} methodName + * @param {*[]} argNatives * @returns {Promise<*>|*} */ - callMethod: function (methodName) { + callMethod: function (methodName, ...argNatives) { var phpObject = this, // Arguments will be from JS-land, so coerce any to internal PHP value objects - args = _.map([].slice.call(arguments, 1), function (arg) { + positionalArgs = _.map(argNatives, function (arg) { return phpObject.valueFactory.coerce(arg); }); - return phpObject.nativeCaller.callMethod(phpObject.objectValue, methodName, args); + return phpObject.nativeCaller.callMethod(phpObject.objectValue, methodName, positionalArgs); }, /** diff --git a/src/FFI/Value/Proxy/ProxyClassFactory.js b/src/FFI/Value/Proxy/ProxyClassFactory.js index ddb5b098..6830e604 100644 --- a/src/FFI/Value/Proxy/ProxyClassFactory.js +++ b/src/FFI/Value/Proxy/ProxyClassFactory.js @@ -38,7 +38,6 @@ _.extend(ProxyClassFactory.prototype, { */ create: function (classObject) { var currentClass, - currentPrototype, factory = this, methodNamesProxied = {}; @@ -70,37 +69,20 @@ _.extend(ProxyClassFactory.prototype, { currentClass = classObject; /* - * Iterate up the class hierarchy, proxying methods as we go. Note that - * in most cases the first class' prototype chain is probably all we need - * to process, however some classes in the hierarchy may have non-standard - * native objects (eg. JSObject) and so we need to process each one's - * prototype chain just in case. - * - * TODO: Remove the need for this duplication by handling the special JSObject case - * in that class alone. + * Iterate up the class hierarchy, proxying methods as we go. */ while (currentClass) { - currentPrototype = currentClass.getInternalClass().prototype; + /*jshint loopfunc: true */ + _.forOwn(currentClass.getMethodCallables(), function (callable, methodName) { + if (methodNamesProxied[methodName] === true) { + // Only proxy each method once. + return; + } - while (currentPrototype !== null && currentPrototype !== Object.prototype) { - /*jshint loopfunc: true */ - _.forOwn(currentPrototype, function (property, propertyName) { - if ( - // Only proxy methods - typeof property !== 'function' || - // Only proxy each method once - methodNamesProxied[propertyName] === true - ) { - return; - } + defineProxyMethod(methodName); - defineProxyMethod(propertyName); - - methodNamesProxied[propertyName] = true; - }); - - currentPrototype = Object.getPrototypeOf(currentPrototype); - } + methodNamesProxied[methodName] = true; + }); currentClass = currentClass.getSuperClass(); } diff --git a/src/FFI/Value/Proxy/ProxyMemberFactory.js b/src/FFI/Value/Proxy/ProxyMemberFactory.js index 8ea55dd6..aef77a65 100644 --- a/src/FFI/Value/Proxy/ProxyMemberFactory.js +++ b/src/FFI/Value/Proxy/ProxyMemberFactory.js @@ -46,7 +46,7 @@ function ProxyMemberFactory( _.extend(ProxyMemberFactory.prototype, { /** - * Creates a proxying method for the ProxyClass for a method of a PHP class + * Creates a proxying method for the ProxyClass for a method of a PHP class. * * @param {string} methodName * @returns {Function} @@ -54,9 +54,9 @@ _.extend(ProxyMemberFactory.prototype, { createProxyMethod: function (methodName) { var factory = this; - return function __uniterInboundStackMarker__() { + return function __uniterInboundStackMarker__(...argNatives) { // Arguments will be from JS-land, so coerce any to internal PHP value objects - var args = _.map(arguments, function (arg) { + var args = _.map(argNatives, function (arg) { return factory.valueFactory.coerce(arg); }), privates = factory.valueStorage.getPrivatesForNativeProxy(this), @@ -66,7 +66,7 @@ _.extend(ProxyMemberFactory.prototype, { // We are entering PHP-land from JS-land. factory.controlScope.enterCoroutine(); - return factory.nativeCaller.callMethod(objectValue, methodName, args, useSyncApiAlthoughPsync); + return factory.nativeCaller.callMethod(objectValue, methodName, args, null, useSyncApiAlthoughPsync); }; } }); diff --git a/src/Function/Callable.js b/src/Function/Callable.js new file mode 100644 index 00000000..5aab45d7 --- /dev/null +++ b/src/Function/Callable.js @@ -0,0 +1,262 @@ +/* + * PHPCore - PHP environment runtime components + * Copyright (c) Dan Phillimore (asmblah) + * https://github.com/uniter/phpcore/ + * + * Released under the MIT license + * https://github.com/uniter/phpcore/raw/master/MIT-LICENSE.txt + */ + +'use strict'; + +var _ = require('microdash'), + phpCommon = require('phpcommon'), + Exception = phpCommon.Exception, + FFIResult = require('../FFI/Result'), + Reference = require('../Reference/Reference'), + Variable = require('../Variable').sync(); + +/** + * Wraps a native JavaScript function, handling the PHP call stack and scoping. + * + * @param {ScopeFactory} scopeFactory + * @param {CallFactory} callFactory + * @param {ValueFactory} valueFactory + * @param {CallStack} callStack + * @param {Flow} flow + * @param {NamespaceScope} namespaceScope + * @param {Class|null} currentClass Used by e.g., `self::`. + * @param {ObjectValue|null} currentObject + * @param {Class|null} staticClass Used by e.g., `static::`. + * @param {FunctionSpec|OverloadedFunctionSpec} functionSpec + * @constructor + */ +function Callable( + scopeFactory, + callFactory, + valueFactory, + callStack, + flow, + namespaceScope, + currentClass, + currentObject, + staticClass, + functionSpec +) { + /** + * @type {CallFactory} + */ + this.callFactory = callFactory; + /** + * @type {CallStack} + */ + this.callStack = callStack; + /** + * @type {Class|null} + */ + this.currentClass = currentClass; + /** + * @type {ObjectValue|null} + */ + this.currentObject = currentObject; + /** + * @type {Flow} + */ + this.flow = flow; + /** + * @type {FunctionSpec|OverloadedFunctionSpec} + */ + this.functionSpec = functionSpec; + /** + * @type {boolean} + */ + this.isPHPCoreWrapped = true; + /** + * @type {NamespaceScope} + */ + this.namespaceScope = namespaceScope; + /** + * @type {ScopeFactory} + */ + this.scopeFactory = scopeFactory; + /** + * @type {Class|null} + */ + this.staticClass = staticClass; + /** + * @type {ValueFactory} + */ + this.valueFactory = valueFactory; +} + +_.extend(Callable.prototype, { + /** + * Calls the callable. + * + * @param {Reference[]|Value[]|Variable[]=} positionalArguments + * @param {Object.|null=} namedArguments + * @param {ObjectValue|null=} thisObject + * @param {Class|null=} newStaticClass Allow an explicit static class to be specified e.g., by a Closure. + * @returns {ChainableInterface} + */ + call: function (positionalArguments, namedArguments, thisObject, newStaticClass) { + var callable = this, + functionSpec, + func, + scope, + result; + + positionalArguments ??= []; // jshint ignore:line + namedArguments ??= null; // jshint ignore:line + functionSpec = callable.functionSpec.resolveFunctionSpec(positionalArguments.length); + func = functionSpec.getFunction(); + newStaticClass = newStaticClass ?? callable.staticClass; + thisObject = thisObject ?? callable.currentObject; + + /** + * Handles coercion and validates the result of the function call. + * + * @param {Reference|Value|Variable|*} result + * @returns {ChainableInterface} + */ + function finishCall(result) { + /** @var {Reference|Value|Variable} */ + var resultReference; + + if ((result instanceof Reference) || (result instanceof Variable)) { + // Result is a Reference, resolve to a value if needed + resultReference = functionSpec.isReturnByReference() ? + result : + result.getValue(); + } else if (!(result instanceof FFIResult)) { + // Result is either a Value or native value needing coercion + // (see below for note on FFIResults). + resultReference = callable.valueFactory.coerce(result); + } + + if (result instanceof FFIResult) { + // FFIResults must only be resolved after the call has been popped + resultReference = result.resolve(); + } + + return resultReference.next(function (presentResultReference) { + // Coerce return value or reference as required, capturing the value for later validation. + // Note that the coerced result for by-value functions will be written back to resultReference. + return functionSpec.coerceReturnReference(presentResultReference) + .next(function (resultValue) { + // Check the return value against the return type (if any). If the caller + // is in weak type-checking mode, the value will have been coerced if possible above. + return functionSpec.validateReturnReference(presentResultReference, resultValue); + }); + }); + } + + /** + * Performs the actual call, returning a Value or Future + * to be resolved on success or rejected on error. + * + * @returns {Future|Value} + */ + function doCall() { + return callable.flow + .maybeFuturise( + function () { + if (functionSpec.isUserland()) { + return func(); + } + + // Native functions expect arguments to be provided natively as normal. + return func.apply(scope, positionalArguments); + }, + function (pause, onResume) { + if (!functionSpec.isUserland()) { + throw new Exception( + 'Callable :: A built-in function enacted a Pause, did you mean to return a Future instead?' + ); + } + + onResume(doCall); + } + ); + } + + if (!callable.valueFactory.isValue(thisObject)) { + thisObject = null; + } + + /* + * Coerce parameter arguments as required, capturing all values for later validation. + * + * Coerced arguments for by-value parameters will be written back to `positionalArguments`. + * + * Any arguments that are references returning Futures will be resolved. + */ + result = functionSpec.coercePositionalArguments(positionalArguments) + .next(function (argValues) { + // Fill in any named arguments given after the positional ones. + return namedArguments ? + functionSpec.coerceNamedArguments(namedArguments, positionalArguments, argValues) : + argValues; + }) + .next(function (argValues) { + var call; + + scope = callable.scopeFactory.create(callable.currentClass, callable, thisObject); + call = callable.callFactory.create( + scope, + callable.namespaceScope, + // Note that the resolved argument values are stored against the call and not + // any references passed in, so we have the actual argument used at the time. + argValues, + newStaticClass + ); + + // Push the call onto the stack. + callable.callStack.push(call); + + // Now validate the arguments at this point (coercion was done earlier) + // - if any error is raised, then the call will still be popped off + // by the `finally` clause below. + return functionSpec.validateArguments(positionalArguments, argValues) + .next(function () { + /* + * Now populate any optional arguments that were omitted with their default values. + * + * Note that default args could be async and cause a pause, + * e.g., if a default value is a constant of an asynchronously autoloaded class. + */ + return functionSpec.populateDefaultArguments(positionalArguments); + }) + .next(function (populatedArguments) { + // Note that by this point all arguments will have been resolved to present values + // (i.e., any Futures will have been awaited and resolved). + positionalArguments = populatedArguments; + + if (functionSpec.isUserland()) { + // Userland functions' parameter arguments have variables declared + // in the function call's scope and then references or values loaded. + functionSpec.loadArguments(positionalArguments, scope); + } + + return doCall(); + }) + .next(finishCall) + .finally(function () { + // Once the call completes, whether with a result or a thrown error/exception, + // pop the call off of the stack. + + // TODO: This was previously not being done if an error occurred during arg defaults population, cover with unit test + callable.callStack.pop(); + }); + }); + + if (!functionSpec.isReturnByReference()) { + // Function is return-by-value, so ensure we have a value as the result. + result = result.asValue(); + } + + return result; + } +}); + +module.exports = Callable; diff --git a/src/Function/FunctionSpec.js b/src/Function/FunctionSpec.js index 8200604b..ba2442a7 100644 --- a/src/Function/FunctionSpec.js +++ b/src/Function/FunctionSpec.js @@ -10,11 +10,13 @@ 'use strict'; var _ = require('microdash'), + hasOwn = {}.hasOwnProperty, phpCommon = require('phpcommon'), AT_LEAST = 'core.at_least', EXACTLY = 'core.exactly', INVALID_RETURN_VALUE_TYPE = 'core.invalid_return_value_type', ONLY_REFERENCES_RETURNED_BY_REFERENCE = 'core.only_references_returned_by_reference', + UNKNOWN_NAMED_PARAMETER = 'core.unknown_named_parameter', WRONG_ARG_COUNT_USERLAND = 'core.wrong_arg_count_userland', WRONG_ARG_COUNT_BUILTIN = 'core.wrong_arg_count_builtin', WRONG_ARG_COUNT_BUILTIN_SINGLE = 'core.wrong_arg_count_builtin_single', @@ -61,19 +63,40 @@ function FunctionSpec( filePath, lineNumber ) { - var parameter, - variadicParameter = null; + var lastIndex, + parameter, + parameterNameToPositionMap = {}, + variadicParameter = null, + variadicParameterPosition = null; if (parameterList.length > 0) { - parameter = parameterList[parameterList.length - 1]; + // Handle cases where some parameter specs may be omitted for bundle-size reasons (null entries). + // Find the last non-null parameter to determine if it's variadic. + lastIndex = parameterList.length - 1; - if (parameter.isVariadic()) { - variadicParameter = parameter; + while (lastIndex >= 0 && parameterList[lastIndex] === null) { + lastIndex--; + } + + if (lastIndex >= 0) { + parameter = parameterList[lastIndex]; - // Remove the variadic parameter from the positional parameter list, - // as it will be handled specially. - parameterList.pop(); + if (parameter && typeof parameter.isVariadic === 'function' && parameter.isVariadic()) { + variadicParameter = parameter; + variadicParameterPosition = parameter.getPosition(); + + // Remove the variadic parameter entry from the positional list + // so the remaining entries represent only positional parameters. + parameterList.splice(lastIndex, 1); + } } + + // Build name -> position map, skipping null parameter entries. + _.each(parameterList, function (parameter, position) { + if (parameter) { + parameterNameToPositionMap[parameter.getName()] = position; + } + }); } /** @@ -116,6 +139,10 @@ function FunctionSpec( * @type {Parameter[]|null[]} */ this.parameterList = parameterList; + /** + * @type {Object.} + */ + this.parameterNameToPositionMap = parameterNameToPositionMap; /** * @type {ReferenceFactory} */ @@ -140,19 +167,89 @@ function FunctionSpec( * @type {Parameter|null} */ this.variadicParameter = variadicParameter; + /** + * @type {number|null} + */ + this.variadicParameterPosition = variadicParameterPosition; } _.extend(FunctionSpec.prototype, { + /** + * Coerces the given set of arguments for this function as needed. + * + * @param {Object.} namedArgumentMap + * @param {Reference[]|Value[]|Variable[]} positionalArgumentReferenceList + * @param {Value[]} positionalArgumentValueList + * @returns {FutureInterface} Returns all arguments resolved to values + */ + coerceNamedArguments: function ( + namedArgumentMap, + positionalArgumentReferenceList, + positionalArgumentValueList + ) { + var spec = this, + variadicParameterPosition = spec.variadicParameterPosition; + + return spec.flow + .forOwnAsync(namedArgumentMap, function (argumentReference, name) { + var nameValue, + position; + + if (!hasOwn.call(spec.parameterNameToPositionMap, name)) { + if (variadicParameterPosition === null) { + spec.callStack.raiseTranslatedError( + PHPError.E_ERROR, + UNKNOWN_NAMED_PARAMETER, + { + name: name + } + ); + } + + nameValue = spec.valueFactory.createString(name); + + return argumentReference.getValue() + .next(function (resolvedValue) { + return positionalArgumentReferenceList[variadicParameterPosition] + .getElementByKey(nameValue) + .setValue(resolvedValue); + }) + .next(function (resolvedValue) { + return positionalArgumentValueList[variadicParameterPosition] + .getElementByKey(nameValue) + .setValue(resolvedValue); + }); + } + + position = spec.parameterNameToPositionMap[name]; + + positionalArgumentReferenceList[position] = argumentReference; + + return argumentReference.getValue() + .next(function (resolvedValue) { + positionalArgumentValueList[position] = resolvedValue; + }); + }) + .next(function () { + return positionalArgumentValueList; + }); + }, + /** * Coerces the given set of arguments for this function as needed. * * @param {Reference[]|Value[]|Variable[]} argumentReferenceList * @returns {FutureInterface} Returns all arguments resolved to values */ - coerceArguments: function (argumentReferenceList) { + coercePositionalArguments: function (argumentReferenceList) { var coercedArguments = argumentReferenceList.slice(), spec = this, - parameterCount = spec.parameterList.length; + parameterCount = spec.parameterList.length, + isVariadic = spec.variadicParameter !== null, + isVariadicByReference = isVariadic && spec.variadicParameter.isPassedByReference(), + variadicArrayValue = isVariadic ? spec.valueFactory.createArray([]) : null, + variadicReferenceArrayValue = isVariadicByReference ? + spec.valueFactory.createArray([]) : null; return spec.flow.eachAsync(coercedArguments, function (argumentReference, index) { var parameter = spec.parameterList[index]; @@ -176,6 +273,30 @@ _.extend(FunctionSpec.prototype, { */ return parameter.coerceArgument(argumentReference) .next(function (coercedArgument) { + if (index >= parameterCount && spec.variadicParameter) { + // This is a variadic argument - add to the variadic array. + variadicArrayValue.getPushElement().setValue(coercedArgument); + + if (isVariadicByReference) { + if ( + argumentReference instanceof Reference && + !(argumentReference instanceof ReferenceSnapshot) + ) { + // Argument is a non-snapshot reference: wrap it in a snapshot + // to allow consistent synchronous access to the snapshotted value. + variadicReferenceArrayValue.getPushElement() + .setReference(spec.referenceFactory.createSnapshot( + argumentReference, + coercedArgument + )); + } else { + variadicReferenceArrayValue.getPushElement().setReference(argumentReference); + } + } + + return; + } + coercedArguments[index] = coercedArgument; if (parameter.isPassedByReference()) { @@ -183,8 +304,7 @@ _.extend(FunctionSpec.prototype, { argumentReference instanceof Reference && !(argumentReference instanceof ReferenceSnapshot) ) { - // Argument is a non-snapshot reference: wrap it in a snapshot - // to allow consistent synchronous access to the snapshotted value. + // Wrap in a snapshot for consistency as above. argumentReferenceList[index] = spec.referenceFactory.createSnapshot( argumentReference, coercedArgument @@ -197,6 +317,20 @@ _.extend(FunctionSpec.prototype, { } }); }).next(function () { + if (spec.variadicParameter) { + // Remove any variadic arguments from the result array. + coercedArguments.splice(parameterCount); + + // Add the variadic array to the coerced arguments. + coercedArguments[parameterCount] = variadicArrayValue; + + // Always push the reference array, even if empty. + argumentReferenceList.splice(parameterCount); + argumentReferenceList[parameterCount] = isVariadicByReference ? + variadicReferenceArrayValue : + variadicArrayValue; + } + return coercedArguments; }); }, @@ -251,17 +385,17 @@ _.extend(FunctionSpec.prototype, { }, /** - * Creates a new function (and its FunctionSpec) for an alias of the current FunctionSpec. + * Creates a new callable (and its FunctionSpec) for an alias of the current FunctionSpec. * * @param {string} aliasName * @param {FunctionFactory} functionFactory - * @return {Function} + * @return {Callable} */ createAliasFunction: function (aliasName, functionFactory) { var spec = this, aliasFunctionSpec = spec.createAliasFunctionSpec(aliasName); - return functionFactory.create( + return functionFactory.createCallable( spec.namespaceScope, // Class will always be null for 'normal' functions // as defining a function inside a class will define it @@ -402,8 +536,16 @@ _.extend(FunctionSpec.prototype, { var spec = this, count; + // Scan backwards for the last non-null parameter that is required. for (count = spec.parameterList.length; count > 0; count--) { - if (spec.parameterList[count - 1].isRequired()) { + var parameter = spec.parameterList[count - 1]; + + if (!parameter) { + // Missing/omitted parameter spec - skip it. + continue; + } + + if (parameter.isRequired()) { break; } } @@ -452,7 +594,18 @@ _.extend(FunctionSpec.prototype, { hasOptionalParameter: function () { var spec = this; - return spec.parameterList.length > 0 && !spec.parameterList[spec.parameterList.length - 1].isRequired(); + // Find the last non-null (non-omitted) parameter and determine whether it's optional. + for (var i = spec.parameterList.length - 1; i >= 0; i--) { + var parameter = spec.parameterList[i]; + + if (!parameter) { + continue; + } + + return !parameter.isRequired(); + } + + return false; }, /** @@ -489,28 +642,23 @@ _.extend(FunctionSpec.prototype, { * @param {Scope} scope */ loadArguments: function (argumentReferenceList, scope) { - var index, - spec = this, - variadicArgumentCount, - variadicArrayValue; + var spec = this; _.each(spec.parameterList, function (parameter, index) { - var localVariable = scope.getVariable(parameter.getName()); + var localVariable; + + if (!parameter) { + return; // Omitted parameter spec: nothing to load. + } + + localVariable = scope.getVariable(parameter.getName()); parameter.loadArgument(argumentReferenceList[index], localVariable); }); if (spec.variadicParameter) { - // Parameter is variadic: collect all remaining arguments, build an array containing them - // (with references if the parameter is by-reference) and store in the parameter's local variable. - variadicArgumentCount = argumentReferenceList.length; - variadicArrayValue = spec.valueFactory.createArray([]); - - for (index = spec.parameterList.length; index < variadicArgumentCount; index++) { - spec.variadicParameter.loadArgument(argumentReferenceList[index], variadicArrayValue.getPushElement()); - } - - scope.getVariable(spec.variadicParameter.getName()).setValue(variadicArrayValue); + scope.getVariable(spec.variadicParameter.getName()) + .setValue(argumentReferenceList[spec.variadicParameterPosition].getValue().yieldSync()); } }, @@ -636,8 +784,14 @@ _.extend(FunctionSpec.prototype, { if (argumentReferenceList.length > positionalParameterCount) { resultChainable = resultChainable.next(function () { - var variadicArgumentReferenceList = argumentReferenceList.slice(positionalParameterCount), - variadicArgumentValueList = argumentValueList.slice(positionalParameterCount); + var isVariadicByReference = spec.variadicParameter.isPassedByReference(), + variadicArrayValue = argumentValueList[positionalParameterCount], + variadicReferenceArrayValue = isVariadicByReference ? + argumentReferenceList[positionalParameterCount].getValue() : null, + variadicArgumentValueList = variadicArrayValue.getValues(), + variadicArgumentReferenceList = isVariadicByReference ? + variadicReferenceArrayValue.getValueReferences() : + variadicArgumentValueList; return spec.flow.eachAsync(variadicArgumentReferenceList, function (argumentReference, index) { return spec.variadicParameter.validateArgument( diff --git a/src/Function/Overloaded/InvalidOverloadedFunctionSpec.js b/src/Function/Overloaded/InvalidOverloadedFunctionSpec.js index 1b4ca245..c1ba3858 100644 --- a/src/Function/Overloaded/InvalidOverloadedFunctionSpec.js +++ b/src/Function/Overloaded/InvalidOverloadedFunctionSpec.js @@ -66,14 +66,11 @@ _.extend(InvalidOverloadedFunctionSpec.prototype, { * @param {Reference[]|Value[]|Variable[]} argumentReferenceList * @returns {FutureInterface} Returns all arguments resolved to values */ - coerceArguments: function (argumentReferenceList) { - var coercedArguments = [], - spec = this; + coercePositionalArguments: function (argumentReferenceList) { + var spec = this; - return spec.flow.eachAsync(argumentReferenceList, function (argumentReference, index) { - coercedArguments[index] = argumentReference.getValueOrNull(); - }).next(function () { - return coercedArguments; + return spec.flow.mapAsync(argumentReferenceList, function (argumentReference) { + return argumentReference.getValue(); }); }, @@ -275,47 +272,49 @@ _.extend(InvalidOverloadedFunctionSpec.prototype, { maximumParameterCount = spec.overloadedFunctionSpec.getMaximumParameterCount(), minimumParameterCount = spec.overloadedFunctionSpec.getMinimumParameterCount(); - if (spec.argumentCount < minimumParameterCount) { - spec.callStack.raiseTranslatedError( - PHPError.E_ERROR, - minimumParameterCount === 1 ? WRONG_ARG_COUNT_BUILTIN_SINGLE : WRONG_ARG_COUNT_BUILTIN, - { - func: spec.overloadedFunctionSpec.getName(), - bound: spec.translator.translate(AT_LEAST), - expectedCount: minimumParameterCount, - actualCount: spec.argumentCount, - callerFile: '(' + spec.translator.translate(UNKNOWN) + ')', - callerLine: '(' + spec.translator.translate(UNKNOWN) + ')' - }, - 'ArgumentCountError' - ); - } + return spec.flow.chainifyResultOf(function () { + if (spec.argumentCount < minimumParameterCount) { + spec.callStack.raiseTranslatedError( + PHPError.E_ERROR, + minimumParameterCount === 1 ? WRONG_ARG_COUNT_BUILTIN_SINGLE : WRONG_ARG_COUNT_BUILTIN, + { + func: spec.overloadedFunctionSpec.getName(), + bound: spec.translator.translate(AT_LEAST), + expectedCount: minimumParameterCount, + actualCount: spec.argumentCount, + callerFile: '(' + spec.translator.translate(UNKNOWN) + ')', + callerLine: '(' + spec.translator.translate(UNKNOWN) + ')' + }, + 'ArgumentCountError' + ); + } + + if (spec.argumentCount > maximumParameterCount) { + spec.callStack.raiseTranslatedError( + PHPError.E_ERROR, + maximumParameterCount === 1 ? WRONG_ARG_COUNT_BUILTIN_SINGLE : WRONG_ARG_COUNT_BUILTIN, + { + func: spec.overloadedFunctionSpec.getName(), + bound: spec.translator.translate(AT_MOST), + expectedCount: maximumParameterCount, + actualCount: spec.argumentCount, + callerFile: '(' + spec.translator.translate(UNKNOWN) + ')', + callerLine: '(' + spec.translator.translate(UNKNOWN) + ')' + }, + 'ArgumentCountError' + ); + } - if (spec.argumentCount > maximumParameterCount) { spec.callStack.raiseTranslatedError( PHPError.E_ERROR, - maximumParameterCount === 1 ? WRONG_ARG_COUNT_BUILTIN_SINGLE : WRONG_ARG_COUNT_BUILTIN, + NO_OVERLOAD_VARIANT_FOR_PARAMETER_COUNT, { func: spec.overloadedFunctionSpec.getName(), - bound: spec.translator.translate(AT_MOST), - expectedCount: maximumParameterCount, - actualCount: spec.argumentCount, - callerFile: '(' + spec.translator.translate(UNKNOWN) + ')', - callerLine: '(' + spec.translator.translate(UNKNOWN) + ')' + parameterCount: spec.argumentCount }, 'ArgumentCountError' ); - } - - spec.callStack.raiseTranslatedError( - PHPError.E_ERROR, - NO_OVERLOAD_VARIANT_FOR_PARAMETER_COUNT, - { - func: spec.overloadedFunctionSpec.getName(), - parameterCount: spec.argumentCount - }, - 'ArgumentCountError' - ); + }); }, /** diff --git a/src/Function/Overloaded/OverloadedFunctionDefiner.js b/src/Function/Overloaded/OverloadedFunctionDefiner.js index 21fecea5..135bb1d6 100644 --- a/src/Function/Overloaded/OverloadedFunctionDefiner.js +++ b/src/Function/Overloaded/OverloadedFunctionDefiner.js @@ -39,7 +39,7 @@ _.extend(OverloadedFunctionDefiner.prototype, { * @param {string} name * @param {OverloadedFunctionVariant[]} variants * @param {NamespaceScope} namespaceScope - * @returns {Function} + * @returns {Callable} */ defineFunction: function ( name, @@ -100,7 +100,7 @@ _.extend(OverloadedFunctionDefiner.prototype, { namespaceScope ); - return definer.functionFactory.create( + return definer.functionFactory.createCallable( namespaceScope, // Class will always be null for 'normal' functions // as defining a function inside a class will define it diff --git a/src/Function/Overloaded/OverloadedFunctionSpec.js b/src/Function/Overloaded/OverloadedFunctionSpec.js index c47c117d..1cc08dbc 100644 --- a/src/Function/Overloaded/OverloadedFunctionSpec.js +++ b/src/Function/Overloaded/OverloadedFunctionSpec.js @@ -60,11 +60,11 @@ function OverloadedFunctionSpec( _.extend(OverloadedFunctionSpec.prototype, { /** - * Creates a new function (and its FunctionSpec) for an alias of the current OverloadedFunctionSpec. + * Creates a new callable (and its FunctionSpec) for an alias of the current OverloadedFunctionSpec. * * @param {string} aliasName * @param {FunctionFactory} functionFactory - * @return {Function} + * @return {Callable} */ createAliasFunction: function (aliasName, functionFactory) { var spec = this, @@ -84,7 +84,7 @@ _.extend(OverloadedFunctionSpec.prototype, { spec.maximumParameterCount ); - return functionFactory.create( + return functionFactory.createCallable( spec.namespaceScope, // Class will always be null for 'normal' functions // as defining a function inside a class will define it diff --git a/src/Function/Parameter.js b/src/Function/Parameter.js index f84f3f1c..c0ccd5cd 100644 --- a/src/Function/Parameter.js +++ b/src/Function/Parameter.js @@ -254,6 +254,15 @@ _.extend(Parameter.prototype, { return this.name; }, + /** + * Fetches the zero-based position of this parameter in the signature. + * + * @returns {number} + */ + getPosition: function () { + return this.index; + }, + /** * Fetches this parameter's type. * diff --git a/src/FunctionFactory.js b/src/FunctionFactory.js index e8b5056d..f3375817 100644 --- a/src/FunctionFactory.js +++ b/src/FunctionFactory.js @@ -10,24 +10,13 @@ 'use strict'; module.exports = require('pauser')([ - require('microdash'), - require('phpcommon'), - require('./FFI/Result'), - require('./Control/Pause'), - require('./Reference/Reference'), - require('./Variable') + require('microdash') ], function ( - _, - phpCommon, - FFIResult, - Pause, - Reference, - Variable + _ ) { - var slice = [].slice, - Exception = phpCommon.Exception; /** + * @param {class} Callable * @param {class} MethodSpec * @param {ScopeFactory} scopeFactory * @param {CallFactory} callFactory @@ -39,6 +28,7 @@ module.exports = require('pauser')([ * @constructor */ function FunctionFactory( + Callable, MethodSpec, scopeFactory, callFactory, @@ -48,6 +38,10 @@ module.exports = require('pauser')([ controlBridge, controlScope ) { + /** + * @type {class} + */ + this.Callable = Callable; /** * @type {CallFactory} */ @@ -72,10 +66,6 @@ module.exports = require('pauser')([ * @type {class} */ this.MethodSpec = MethodSpec; - /** - * @type {Class|null} - */ - this.newStaticClassForNextCall = null; /** * @type {ScopeFactory} */ @@ -87,189 +77,38 @@ module.exports = require('pauser')([ } _.extend(FunctionFactory.prototype, { + /** - * Wraps the specified function in another that handles the PHP call stack and scoping + * Wraps the specified function in a Callable that handles the PHP call stack and scoping. * * @param {NamespaceScope} namespaceScope * @param {Class|null} currentClass Used by eg. self:: * @param {ObjectValue|null} currentObject * @param {Class|null} staticClass Used by eg. static:: - * @param {FunctionSpec|OverloadedFunctionSpec} spec - * @returns {Function} + * @param {FunctionSpec|OverloadedFunctionSpec} functionSpec + * @returns {Callable} */ - create: function ( + createCallable: function ( namespaceScope, currentClass, currentObject, staticClass, - spec + functionSpec ) { - var factory = this, - /** - * Wraps a function exposed to PHP-land. - * - * @returns {Future|Value} - */ - wrapperFunc = function () { - var argReferences = slice.call(arguments), - functionSpec = spec.resolveFunctionSpec(argReferences.length), - func = functionSpec.getFunction(), - thisObject = currentObject || this, - scope, - newStaticClass = null, - result; - - /** - * Handles coercion and validation of the result of the function call. - * - * @param {Reference|Value|Variable|*} result - * @returns {ChainableInterface} - */ - function finishCall(result) { - /** @var {Reference|Value|Variable} */ - var resultReference; - - if ((result instanceof Reference) || (result instanceof Variable)) { - // Result is a Reference, resolve to a value if needed - resultReference = functionSpec.isReturnByReference() ? - result : - result.getValue(); - } else if (!(result instanceof FFIResult)) { - // Result is either a Value or native value needing coercion - // (see below for note on FFIResults) - resultReference = factory.valueFactory.coerce(result); - } - - if (result instanceof FFIResult) { - // FFIResults must only be resolved after the call has been popped - resultReference = result.resolve(); - } - - return resultReference.next(function (presentResultReference) { - // Coerce return value or reference as required, capturing the value for later validation. - // Note that the coerced result for by-value functions will be written back to resultReference. - return functionSpec.coerceReturnReference(presentResultReference) - .next(function (resultValue) { - // Check the return value against the return type (if any). If the caller - // is in weak type-checking mode, the value will have been coerced if possible above. - return functionSpec.validateReturnReference(presentResultReference, resultValue); - }); - }); - } - - /** - * Performs the actual call, returning a Value or Future - * to be resolved on success or rejected on error. - * - * @returns {Future|Value} - */ - function doCall() { - return factory.flow - .maybeFuturise( - function () { - if (functionSpec.isUserland()) { - return func(); - } - - // Native functions expect arguments to be provided natively as normal. - return func.apply(scope, argReferences); - }, - function (pause, onResume) { - if (!functionSpec.isUserland()) { - throw new Exception( - 'FunctionFactory :: A built-in function enacted a Pause, did you mean to return a Future instead?' - ); - } - - onResume(doCall); - } - ); - } - - if (factory.newStaticClassForNextCall !== null) { - newStaticClass = factory.newStaticClassForNextCall; - factory.newStaticClassForNextCall = null; - } else if (staticClass) { - // Allow an explicit static class to be specified, eg. by a Closure - newStaticClass = staticClass; - } - - if (!factory.valueFactory.isValue(thisObject)) { - thisObject = null; - } - - /* - * Coerce parameter arguments as required, capturing all values for later validation. - * - * Coerced arguments for by-value parameters will be written back to argReferences. - * - * Any arguments that are references returning Futures will be resolved. - */ - result = functionSpec.coerceArguments(argReferences) - .next(function (argValues) { - var call; - - scope = factory.scopeFactory.create(currentClass, wrapperFunc, thisObject); - call = factory.callFactory.create( - scope, - namespaceScope, - // Note that the resolved argument values are stored against the call and not - // any references passed in, so we have the actual argument used at the time. - argValues, - newStaticClass - ); - - // Push the call onto the stack. - factory.callStack.push(call); - - // Now validate the arguments at this point (coercion was done earlier) - // - if any error is raised then the call will still be popped off - // by the finally clause below. - return functionSpec.validateArguments(argReferences, argValues); - }) - .next(function () { - /* - * Now populate any optional arguments that were omitted with their default values. - * - * Note that default args could be async and cause a pause, - * eg. if a default value is a constant of an asynchronously autoloaded class. - */ - return functionSpec.populateDefaultArguments(argReferences); - }) - .next(function (populatedArguments) { - // Note that by this point all arguments will have been resolved to present values - // (i.e. any Futures will have been awaited and resolved). - argReferences = populatedArguments; - - if (functionSpec.isUserland()) { - // Userland functions' parameter arguments have variables declared - // in the function call's scope and then references or values loaded. - functionSpec.loadArguments(argReferences, scope); - } - - return doCall(); - }) - .next(finishCall) - .finally(function () { - // Once the call completes, whether with a result or a thrown error/exception, - // pop the call off of the stack - - // TODO: This was previously not being done if an error occurred during arg defaults population, cover with unit test - factory.callStack.pop(); - }); - - if (!functionSpec.isReturnByReference()) { - // Function is return-by-value, so ensure we have a value as the result. - result = result.asValue(); - } - - return result; - }; - - wrapperFunc.functionSpec = spec; - wrapperFunc.isPHPCoreWrapped = true; - - return wrapperFunc; + var factory = this; + + return new this.Callable( + factory.scopeFactory, + factory.callFactory, + factory.valueFactory, + factory.callStack, + factory.flow, + namespaceScope, + currentClass, + currentObject, + staticClass, + functionSpec + ); }, /** @@ -284,21 +123,6 @@ module.exports = require('pauser')([ */ createMethodSpec: function (originalClass, classObject, methodName, method) { return new this.MethodSpec(originalClass, classObject, methodName, method); - }, - - /** - * Specifies the class to use as the static:: class for the next call - * to the specified wrapped function - * - * @param {Function} func - * @param {Class} newStaticClass - */ - setNewStaticClassIfWrapped: function (func, newStaticClass) { - if (!func.isPHPCoreWrapped) { - return; - } - - this.newStaticClassForNextCall = newStaticClass; } }); diff --git a/src/Namespace.js b/src/Namespace.js index 1ebd12cc..0a8a0a33 100644 --- a/src/Namespace.js +++ b/src/Namespace.js @@ -105,7 +105,7 @@ module.exports = require('pauser')([ */ this.functionSpecFactory = functionSpecFactory; /** - * @type {Object.} + * @type {Object.} */ this.functions = {}; /** @@ -311,7 +311,7 @@ module.exports = require('pauser')([ lineNumber || null ); - namespace.functions[lowerName] = namespace.functionFactory.create( + namespace.functions[lowerName] = namespace.functionFactory.createCallable( namespaceScope, // Class will always be null for 'normal' functions // as defining a function inside a class will define it @@ -435,7 +435,7 @@ module.exports = require('pauser')([ return namespace.flow.chainify(parsed.namespace.classes[lowerName]); } - // Otherwise the class must be successfully autoloaded or we fail. + // Otherwise the class must be successfully autoloaded, or we fail. return namespace.flow.chainifyCallbackFrom(function (resolve) { if (autoload !== false) { @@ -522,7 +522,7 @@ module.exports = require('pauser')([ * to the global namespace. Raises an error if the function is not defined at all * * @param {string|Function} name - * @returns {Function} + * @returns {Callable} */ getFunction: function (name) { var globalNamespace, @@ -532,6 +532,7 @@ module.exports = require('pauser')([ subNamespace; if (_.isFunction(name)) { + // TODO: Remove this behaviour. return name; } @@ -664,7 +665,7 @@ module.exports = require('pauser')([ return namespace.flow.chainify(parsed.namespace.traits[lowerName]); } - // Otherwise the trait must be successfully autoloaded or we fail. + // Otherwise the trait must be successfully autoloaded, or we fail. return namespace.flow.chainifyCallbackFrom(function (resolve) { if (autoload !== false) { diff --git a/src/NamespaceScope.js b/src/NamespaceScope.js index dcc0e99d..c4e9fb99 100644 --- a/src/NamespaceScope.js +++ b/src/NamespaceScope.js @@ -251,6 +251,12 @@ module.exports = require('pauser')([ return this.module.getFilePath(); }, + /** + * Fetches the Callable representing the specified global function in this namespace. + * + * @param {string} name + * @returns {Callable} + */ getFunction: function (name) { var match, scope = this, diff --git a/src/OOP/Class/ClassPromoter.js b/src/OOP/Class/ClassPromoter.js index 24014dee..5cc3f9f8 100644 --- a/src/OOP/Class/ClassPromoter.js +++ b/src/OOP/Class/ClassPromoter.js @@ -89,26 +89,26 @@ _.extend(ClassPromoter.prototype, { _.each(classDefinition.getTraits(), function (traitObject) { // Mix the trait's methods into the class. _.forOwn(traitObject.getMethods(), function (methodDefinition, methodName) { - InternalClass.prototype[methodName] = promoter.methodPromoter.promote( + classObject.defineMethod(methodName, promoter.methodPromoter.promote( methodName, methodDefinition, classObject, traitObject, namespaceScope, sharedMethodData - ); + )); }); }); _.forOwn(classDefinition.getMethods(), function (methodDefinition, methodName) { - InternalClass.prototype[methodName] = promoter.methodPromoter.promote( + classObject.defineMethod(methodName, promoter.methodPromoter.promote( methodName, methodDefinition, classObject, null, // No trait for this method. namespaceScope, sharedMethodData - ); + )); }); // Enable fetching the original class object (which may be different from the current class, diff --git a/src/OOP/Class/Definition/NativeDefinitionBuilder.js b/src/OOP/Class/Definition/NativeDefinitionBuilder.js index 94c69c9b..d3f83dd2 100644 --- a/src/OOP/Class/Definition/NativeDefinitionBuilder.js +++ b/src/OOP/Class/Definition/NativeDefinitionBuilder.js @@ -11,7 +11,6 @@ var _ = require('microdash'), phpCommon = require('phpcommon'), - slice = [].slice, MAGIC_CONSTRUCT = '__construct', ORIGINAL_MAGIC_CONSTRUCTOR = '__@_original_construct', @@ -74,6 +73,7 @@ _.extend(NativeDefinitionBuilder.prototype, { constructorName = null, hasMagicConstructor = false, methodData = {}, + methods = {}, proxyConstructor, rootInternalPrototype, InternalClass, @@ -89,9 +89,8 @@ _.extend(NativeDefinitionBuilder.prototype, { // the original if the derived class does not call parent::__construct(...) // - Unless the class defines the special `shadowConstructor` property, which // is always called regardless of whether the parent constructor is called explicitly. - InternalClass = function () { - var objectValue = this, - shadowConstructorArgs = slice.call(arguments); + InternalClass = function (...shadowConstructorArgs) { + var objectValue = this; if (definition.shadowConstructor) { definition.shadowConstructor.apply( @@ -104,27 +103,26 @@ _.extend(NativeDefinitionBuilder.prototype, { } }; InternalClass.prototype = Object.create(definition.prototype); - proxyConstructor = function () { - var args = arguments, - objectValue = this, + proxyConstructor = function (...positionalArgs) { + var objectValue = this.getThisObject(), // Will be the native object as the `this` object inside the (shadow) constructor // if auto-coercion is enabled, otherwise use the ObjectValue. unwrappedThisObject = autoCoercionEnabled ? objectValue.getObject() : objectValue; - return valueCoercer.coerceArguments(args) - .next(function (unwrappedArgs) { + return valueCoercer.coerceArguments(positionalArgs) + .next(function (unwrappedPositionalArgs) { // Call the original native constructor, returning its result // in case a Future is returned so that it may be awaited. - return definition.apply(unwrappedThisObject, unwrappedArgs); + return definition.apply(unwrappedThisObject, unwrappedPositionalArgs); }) .next(function () { // Call magic __construct method if defined for the original native class. if (hasMagicConstructor) { // Note that although constructors' return values are discarded, it may pause, in which case // a Future would be returned, which we then need to return in order to await. - return objectValue.callMethod(ORIGINAL_MAGIC_CONSTRUCTOR, args); + return objectValue.callMethod(ORIGINAL_MAGIC_CONSTRUCTOR, positionalArgs/*, namedArgs*/); } return builder.valueFactory.createNull(); @@ -132,7 +130,6 @@ _.extend(NativeDefinitionBuilder.prototype, { .asValue(); }; proxyConstructor.data = methodData; - InternalClass.prototype[MAGIC_CONSTRUCT] = proxyConstructor; constructorName = MAGIC_CONSTRUCT; // Record the prototype object that we should stop at when walking up the chain @@ -167,6 +164,15 @@ _.extend(NativeDefinitionBuilder.prototype, { methods[methodName] = method; } + methods[MAGIC_CONSTRUCT] = { + line: null, + args: null, + isStatic: false, + method: proxyConstructor, + ref: false, + ret: null + }; + return methods; } @@ -176,6 +182,8 @@ _.extend(NativeDefinitionBuilder.prototype, { }; }); + methods = buildMethods(); + return new ClassDefinition( name, namespace, @@ -187,7 +195,7 @@ _.extend(NativeDefinitionBuilder.prototype, { constructorName, InternalClass, methodData, - buildMethods(), + methods, rootInternalPrototype, {}, {}, diff --git a/src/OOP/Class/Definition/UserlandDefinitionBuilder.js b/src/OOP/Class/Definition/UserlandDefinitionBuilder.js index e30e5930..dbe7b739 100644 --- a/src/OOP/Class/Definition/UserlandDefinitionBuilder.js +++ b/src/OOP/Class/Definition/UserlandDefinitionBuilder.js @@ -85,17 +85,24 @@ _.extend(UserlandDefinitionBuilder.prototype, { valueCoercer = builder.ffiFactory.createValueCoercer(false); - // Ensure the class does not attempt to implement Throwable directly - _.each(interfaces, function (interfaceObject) { - if (interfaceObject.is('Throwable')) { - builder.callStack.raiseUncatchableFatalError( - CANNOT_IMPLEMENT_THROWABLE, - { - className: namespace.getPrefix() + name - } - ); - } - }); + /* + * Ensure classes do not attempt to implement Throwable directly. + * Note that interfaces _are_ allowed to extend Throwable, and classes _are_ allowed + * to implement a userland interface that extends Throwable, so only check for + * direct implementation of the Throwable interface itself (not subinterfaces). + */ + if (!definition.isInterface) { + _.each(interfaces, function (interfaceObject) { + if (interfaceObject.getName().toLowerCase() === 'throwable') { + builder.callStack.raiseUncatchableFatalError( + CANNOT_IMPLEMENT_THROWABLE, + { + className: namespace.getPrefix() + name + } + ); + } + }); + } InternalClass = function () {}; diff --git a/src/OOP/Class/MethodPromoter.js b/src/OOP/Class/MethodPromoter.js index 0a3bb527..1f6e6712 100644 --- a/src/OOP/Class/MethodPromoter.js +++ b/src/OOP/Class/MethodPromoter.js @@ -49,7 +49,7 @@ _.extend(MethodPromoter.prototype, { * @param {Trait|null} traitObject Trait object if this is a trait method, null otherwise. * @param {NamespaceScope} namespaceScope * @param {Object} sharedMethodData Method data object shared between all methods of a class - * @returns {Function} Returns the wrapped method function created + * @returns {Callable} Returns the wrapped method function created. */ promote: function ( methodName, @@ -82,7 +82,7 @@ _.extend(MethodPromoter.prototype, { lineNumber || null ); - method = promoter.functionFactory.create( + method = promoter.functionFactory.createCallable( namespaceScope, classObject, null, // Current object only applies to Closures, so nothing to set here. @@ -90,10 +90,8 @@ _.extend(MethodPromoter.prototype, { functionSpec ); - method[IS_STATIC] = methodIsStatic; - - // TODO: Remove this and just use method.functionSpec in Class instead? - method.data = sharedMethodData; + method[IS_STATIC] = methodIsStatic; // FIXME: Should not be adding expando properties in this way. + method.data = sharedMethodData; // FIXME: As above. return method; } diff --git a/src/OOP/NativeMethodDefinitionBuilder.js b/src/OOP/NativeMethodDefinitionBuilder.js index 0180f28f..a395375a 100644 --- a/src/OOP/NativeMethodDefinitionBuilder.js +++ b/src/OOP/NativeMethodDefinitionBuilder.js @@ -10,7 +10,6 @@ 'use strict'; var _ = require('microdash'), - slice = [].slice, IS_STATIC = 'isStatic', TypedFunction = require('../Function/TypedFunction'); @@ -67,9 +66,8 @@ _.extend(NativeMethodDefinitionBuilder.prototype, { line: null, args: parametersSpecData, isStatic: isStatic, - method: function () { - var args = slice.call(arguments), - scope = this, + method: function (...args) { + var scope = this, objectValue = scope.getThisObject(); return valueCoercer.coerceArguments(args).next(function (effectiveArguments) { diff --git a/src/Value/Array.js b/src/Value/Array.js index 6e7f0266..b5a790cb 100644 --- a/src/Value/Array.js +++ b/src/Value/Array.js @@ -658,7 +658,7 @@ module.exports = require('pauser')([ } return classObjectFuture.next(function (classObject) { - return classObject.getMethodSpec(methodNameValue.getNative()) !== null; + return classObject.getMethodCallable(methodNameValue.getNative()) !== null; }, function () { // TODO: Ensure that the error swallowed here cannot be something important @@ -871,6 +871,52 @@ module.exports = require('pauser')([ return elements[0].getValue(); }, + /** + * Prepends one or more elements to the beginning of the array, + * renumbering any numeric keys and resetting the internal pointer. + * + * @param {Value[]} newValues + * @returns {IntegerValue} + */ + unshift: function (newValues) { + var value = this, + newElements = [], + newKeysToElements = {}, + nextNumericKey = 0; + + // Add the new values to the front, each with a sequential numeric key. + _.each(newValues, function (newValue) { + var nativeKey = nextNumericKey++, + key = value.factory.createInteger(nativeKey), + element = value.elementProvider.createElement(value.factory, value.callStack, value, key, newValue); + + newKeysToElements[sanitiseKey(nativeKey)] = element; + newElements.push(element); + }); + + // Process existing elements, renumbering any numeric keys. + _.each(value.value, function (element) { + var key = element.getKey(), + nativeKey = key.getNative(); + + if (isFinite(nativeKey)) { + nativeKey = nextNumericKey++; + key = value.factory.createInteger(nativeKey); + element.setKey(key); + } + + newKeysToElements[sanitiseKey(nativeKey)] = element; + newElements.push(element); + }); + + // Internal array pointer needs to be reset to the start of the array + value.pointer = 0; + value.keysToElements = newKeysToElements; + value.value = newElements; + + return newElements.length; + }, + sort: function (callback) { this.value.sort(callback); } diff --git a/src/Value/BarewordString.js b/src/Value/BarewordString.js index 2ba1fcee..2e6543c7 100644 --- a/src/Value/BarewordString.js +++ b/src/Value/BarewordString.js @@ -76,29 +76,43 @@ module.exports = require('pauser')([ */ call: function (args) { var value = this, - func = value.namespaceScope.getFunction(value.value); + callable = value.namespaceScope.getFunction(value.value); - return func.apply(null, args); + return callable.apply(null, args); }, /** * Calls a static method of the class this string refers to. * * @param {StringValue} nameValue - * @param {Value[]} args + * @param {Reference[]|Value[]|Variable[]} positionalArgs + * @param {Object.|null} namedArgs * @param {bool} isForwarding eg. self::f() is forwarding, MyParentClass::f() is non-forwarding * @returns {ChainableInterface} */ - callStaticMethod: function (nameValue, args, isForwarding) { + callStaticMethod: function (nameValue, positionalArgs, namedArgs, isForwarding) { var value = this; // Note that this may pause due to autoloading. return value.namespaceScope.getClass(value.value) .next(function (classObject) { - return classObject.callMethod(nameValue.getNative(), args, null, null, null, !!isForwarding); + return classObject.callMethod(nameValue.getNative(), positionalArgs, namedArgs, null, null, Boolean(isForwarding)); }); }, + /** + * Fetches the Callable representing the global function this bareword references. + * + * Note that as functions cannot be autoloaded, the Callable can simply be returned synchronously. + * + * @returns {Callable} + */ + getCallable: function () { + var value = this; + + return value.namespaceScope.getFunction(value.value); + }, + /** * Fetches the fully-qualified version of this name (function or class) * @@ -163,15 +177,16 @@ module.exports = require('pauser')([ * Creates an instance of the class this string contains the name of, * relative to the current namespace. * - * @param {Value[]} args + * @param {Reference[]|Value[]} constructorPositionalArgs The wrapped value objects or references to pass as arguments to the constructor. + * @param {Object.=} constructorNamedArgs The named arguments. * @returns {ChainableInterface} */ - instantiate: function (args) { + instantiate: function (constructorPositionalArgs, constructorNamedArgs) { var value = this; return value.namespaceScope.getClass(value.value) .next(function (classObject) { - return classObject.instantiate(args); + return classObject.instantiate(constructorPositionalArgs, constructorNamedArgs); }); }, diff --git a/src/Value/Object.js b/src/Value/Object.js index 2ad417f2..79f81c07 100644 --- a/src/Value/Object.js +++ b/src/Value/Object.js @@ -214,29 +214,38 @@ module.exports = require('pauser')([ * Calls the specified method of this object * * @param {string} name - * @param {Value[]?} args + * @param {Reference[]|Value[]|Variable[]} positionalArgs + * @param {Object.|null} namedArgs * @returns {Value} Returns the result of the method if it exists * @throws {PHPFatalError} Throws when the method does not exist */ - callMethod: function (name, args) { + callMethod: function (name, positionalArgs, namedArgs) { var value = this; - return value.classObject.callMethod(name, args, value); + return value.classObject.callMethod(name, positionalArgs, namedArgs, value); }, /** - * Calls a static method of the class this object is an instance of + * Calls a static method of the class this object is an instance of. * * @param {StringValue} nameValue - * @param {Value[]} args + * @param {Reference[]|Value[]|Variable[]} positionalArgs + * @param {Object.|null} namedArgs * @param {bool} isForwarding eg. self::f() is forwarding, MyParentClass::f() is non-forwarding * @returns {Value} */ - callStaticMethod: function (nameValue, args, isForwarding) { + callStaticMethod: function (nameValue, positionalArgs, namedArgs, isForwarding) { // Could be a static call in object context, in which case we want to pass // the object value through. // This will be handled by a fetch of `callStack.getThisObject()` inside `.callMethod(...)` - return this.classObject.callMethod(nameValue.getNative(), args, null, null, null, isForwarding); + return this.classObject.callMethod( + nameValue.getNative(), + positionalArgs, + namedArgs, + null, + null, + isForwarding + ); }, /** @@ -1308,12 +1317,13 @@ module.exports = require('pauser')([ * Creates a new instance of the class of this object for a normal PHP object. * For a JSObject, if the wrapped object is a function then it will create * a new instance of the wrapped JS class instead, - * returning the resulting new JSObject instance + * returning the resulting new JSObject instance. * - * @param {Value[]} args + * @param {Reference[]|Value[]} constructorPositionalArgs The wrapped value objects or references to pass as arguments to the constructor. + * @param {Object.|null} constructorNamedArgs The named arguments. * @returns {ChainableInterface} */ - instantiate: function (args) { + instantiate: function (constructorPositionalArgs, constructorNamedArgs) { var value = this, nativeObject, unwrappedArgs; @@ -1321,17 +1331,21 @@ module.exports = require('pauser')([ if (value.getClassName() !== 'JSObject') { // A normal PHP object is being instantiated as a class - // we just need to create a new instance of this object's class - return value.classObject.instantiate(args); + return value.classObject.instantiate(constructorPositionalArgs, constructorNamedArgs); } - // A JS function is being instantiated as a class from PHP (bridge integration) + // A JS function is being instantiated as a class from PHP (bridge integration). if (!_.isFunction(value.value)) { throw new Error('Cannot create a new instance of a non-function JSObject'); } - // Unwrap the arguments to native values (as the constructor will be native JS) - unwrappedArgs = _.map(args, function (argValue) { + if (constructorNamedArgs) { + throw new Error('Cannot pass named arguments when instantiating a JSObject'); + } + + // Unwrap the arguments to native values (as the constructor will be native JS). + unwrappedArgs = _.map(constructorPositionalArgs, function (argValue) { return argValue.getNative(); }); @@ -1363,7 +1377,7 @@ module.exports = require('pauser')([ closure = value.getInternalProperty('closure'); - return closure.invoke(args); + return closure.invoke(args, null, undefined); }, /** @@ -1436,7 +1450,7 @@ module.exports = require('pauser')([ * @returns {boolean} */ isMethodDefined: function (methodName) { - return this.classObject.getMethodSpec(methodName) !== null; + return this.classObject.getMethodCallable(methodName) !== null; }, /** diff --git a/src/Value/String.js b/src/Value/String.js index 0ff5d9a2..3d1bf498 100644 --- a/src/Value/String.js +++ b/src/Value/String.js @@ -88,28 +88,29 @@ module.exports = require('pauser')([ methodNameValue = value.factory.createString(match[2]); // Note that this may return a Future-wrapped Value due to autoloading. - return classNameValue.callStaticMethod(methodNameValue, args); + return classNameValue.callStaticMethod(methodNameValue, args, null, false); } // Otherwise must just be the name of a function - return value.globalNamespace.getFunction(value.value).apply(null, args); + return value.globalNamespace.getFunction(value.value).call(args, null, null, null); }, /** * Calls a static method of the class this string refers to. * * @param {StringValue} nameValue - * @param {Value[]} args + * @param {Reference[]|Value[]|Variable[]} positionalArgs + * @param {Object.|null} namedArgs * @param {bool=} isForwarding eg. self::f() is forwarding, MyParentClass::f() is non-forwarding * @returns {ChainableInterface} */ - callStaticMethod: function (nameValue, args, isForwarding) { + callStaticMethod: function (nameValue, positionalArgs, namedArgs, isForwarding) { var value = this; // Note that this may pause due to autoloading. return value.globalNamespace.getClass(value.value) .next(function (classObject) { - return classObject.callMethod(nameValue.getNative(), args, null, null, null, !!isForwarding); + return classObject.callMethod(nameValue.getNative(), positionalArgs, namedArgs, null, null, Boolean(isForwarding)); }); }, @@ -503,18 +504,19 @@ module.exports = require('pauser')([ }, /** - * Creates an instance of the class this string contains the FQCN of + * Creates an instance of the class this string contains the FQCN of. * - * @param {Value[]} args + * @param {Reference[]|Value[]} constructorPositionalArgs The wrapped value objects or references to pass as arguments to the constructor. + * @param {Object.|null} constructorNamedArgs The named arguments. * @returns {ChainableInterface} */ - instantiate: function (args) { + instantiate: function (constructorPositionalArgs, constructorNamedArgs) { var value = this; - // Note that this may pause due to autoloading + // Note that this may pause due to autoloading. return value.globalNamespace.getClass(value.value) .next(function (classObject) { - return classObject.instantiate(args); + return classObject.instantiate(constructorPositionalArgs, constructorNamedArgs); }); }, @@ -551,7 +553,7 @@ module.exports = require('pauser')([ classObjectFuture = globalNamespace.getClass(className); return classObjectFuture.next(function (classObject) { - return classObject.getMethodSpec(methodName) !== null; + return classObject.getMethodCallable(methodName) !== null; }, function () { return false; // Class could not be found, so method must be uncallable. }); diff --git a/src/ValueFactory.js b/src/ValueFactory.js index 9c0cf429..51a6f5e8 100644 --- a/src/ValueFactory.js +++ b/src/ValueFactory.js @@ -542,7 +542,7 @@ module.exports = require('pauser')([ factory.closureClass = closureClass; } - return closureClass.instantiateWithInternals([], { + return closureClass.instantiateWithInternals([], null, { 'closure': closure }); }, @@ -580,7 +580,7 @@ module.exports = require('pauser')([ factory.createString(message || ''), factory.createInteger(code || 0), previousThrowable || factory.createNull() - ], [ + ], null, [ context, Boolean(skipCurrentStackFrame) ]); @@ -830,7 +830,7 @@ module.exports = require('pauser')([ factory.generatorClass = generatorClass; } - return generatorClass.instantiateWithInternals([], { + return generatorClass.instantiateWithInternals([], null, { 'iterator': iterator }); }, @@ -1121,18 +1121,20 @@ module.exports = require('pauser')([ * Creates an ObjectValue instance of the specified class. * * @param {string} className - * @param {Array} constructorArgNatives + * @param {Array} constructorPositionalArgNatives * @returns {ChainableInterface} */ - instantiateObject: function (className, constructorArgNatives) { + instantiateObject: function (className, constructorPositionalArgNatives) { var factory = this, - constructorArgValues = _.map(constructorArgNatives, function (argNative) { + constructorPositionalArgValues = _.map(constructorPositionalArgNatives, function (argNative) { return factory.coerce(argNative); }); + // TODO: Consider supporting named arguments here too? + return factory.globalNamespace.getClass(className) .next(function (classObject) { - return classObject.instantiate(constructorArgValues); + return classObject.instantiate(constructorPositionalArgValues); }); }, diff --git a/src/builtin/classes/Closure.js b/src/builtin/classes/Closure.js index dd35b1ef..51c1595f 100644 --- a/src/builtin/classes/Closure.js +++ b/src/builtin/classes/Closure.js @@ -111,8 +111,8 @@ module.exports = function (internals) { * * @returns {Value} */ - '__invoke': function () { - return this.invokeClosure([].slice.call(arguments)); + '__invoke': function (...args) { + return this.invokeClosure(args); } }); @@ -129,24 +129,24 @@ module.exports = function (internals) { // Unwrap PHP Closures to native JS functions that may be called // just like any other (with arguments coerced from JS->PHP // and the return value coerced from PHP->JS automatically) - return function __uniterInboundStackMarker__() { + return function __uniterInboundStackMarker__(...args) { var maybeFuture, // Wrap thisObj in *Value object thisObj = valueFactory.coerceObject(this), // Wrap all native JS values in *Value objects - args = valueFactory.coerceList(arguments); + positionalArgs = valueFactory.coerceList(args); // We are entering PHP-land from JS-land. controlScope.enterCoroutine(); // Push an FFI call onto the stack, representing the call from JavaScript-land - callStack.push(callFactory.createFFICall(args)); + callStack.push(callFactory.createFFICall(positionalArgs)); function popFFICall() { callStack.pop(); } - maybeFuture = closure.invoke.apply(closure, [args, thisObj]) + maybeFuture = closure.invoke(positionalArgs, null, thisObj) // Pop the call off the stack _before_ returning, to mirror sync mode's behaviour .finally(popFFICall) .catch(function (error) { diff --git a/src/builtin/classes/Error/ArgumentCountError.js b/src/builtin/classes/Error/ArgumentCountError.js index 0a566ca3..ab635d48 100644 --- a/src/builtin/classes/Error/ArgumentCountError.js +++ b/src/builtin/classes/Error/ArgumentCountError.js @@ -16,10 +16,10 @@ module.exports = function (internals) { * @see {@link https://secure.php.net/manual/en/class.argumentcounterror.php} * @constructor */ - function ArgumentCountError() { + function ArgumentCountError(...args) { // Synchronously await the superconstructor: should be fine as it should always be defined // and not require autoloading. - internals.callSuperConstructor(this, arguments).yieldSync(); + internals.callSuperConstructor(this, args).yieldSync(); } // Extend the base TypeError class diff --git a/src/builtin/classes/Error/CompileError.js b/src/builtin/classes/Error/CompileError.js index 2515a951..cb39f49a 100644 --- a/src/builtin/classes/Error/CompileError.js +++ b/src/builtin/classes/Error/CompileError.js @@ -16,10 +16,10 @@ module.exports = function (internals) { * @see {@link https://secure.php.net/manual/en/class.compileerror.php} * @constructor */ - function CompileError() { + function CompileError(...args) { // Synchronously await the superconstructor: should be fine as it should always be defined // and not require autoloading. - internals.callSuperConstructor(this, arguments).yieldSync(); + internals.callSuperConstructor(this, args).yieldSync(); } // Extend the base Error class diff --git a/src/builtin/classes/Error/ParseError.js b/src/builtin/classes/Error/ParseError.js index e94587a1..f09691f4 100644 --- a/src/builtin/classes/Error/ParseError.js +++ b/src/builtin/classes/Error/ParseError.js @@ -19,10 +19,10 @@ module.exports = function (internals) { * @see {@link https://secure.php.net/manual/en/class.parseerror.php} * @constructor */ - function ParseError() { + function ParseError(...args) { // Synchronously await the superconstructor: should be fine as it should always be defined // and not require autoloading. - internals.callSuperConstructor(this, arguments).yieldSync(); + internals.callSuperConstructor(this, args).yieldSync(); } internals.extendClass('CompileError'); diff --git a/src/builtin/classes/Error/TypeError.js b/src/builtin/classes/Error/TypeError.js index 6237d426..cb65b4c0 100644 --- a/src/builtin/classes/Error/TypeError.js +++ b/src/builtin/classes/Error/TypeError.js @@ -16,10 +16,10 @@ module.exports = function (internals) { * @see {@link https://secure.php.net/manual/en/class.typeerror.php} * @constructor */ - function TypeError() { + function TypeError(...args) { // Synchronously await the superconstructor: should be fine as it should always be defined // and not require autoloading. - internals.callSuperConstructor(this, arguments).yieldSync(); + internals.callSuperConstructor(this, args).yieldSync(); } // Extend the base Error class diff --git a/src/builtin/messages/error.en_GB.js b/src/builtin/messages/error.en_GB.js index 80b934fe..cfe95411 100644 --- a/src/builtin/messages/error.en_GB.js +++ b/src/builtin/messages/error.en_GB.js @@ -58,6 +58,7 @@ module.exports = { 'undefined_constant': 'Undefined constant \'${name}\'', 'undefined_method': 'Call to undefined method ${className}::${methodName}()', 'undefined_property': 'Undefined property: ${className}::$${propertyName}', + 'unknown_named_parameter': 'Unknown named parameter $${name}', 'unsupported_operand_types': 'Unsupported operand types: ${left} ${operator} ${right}', 'used_this_outside_object_context': 'Using $this when not in object context', 'value_not_callable': 'Value of type ${type} is not callable', diff --git a/src/builtin/opcodes/calculation.js b/src/builtin/opcodes/calculation.js index 5c23ea64..75ca0cd7 100644 --- a/src/builtin/opcodes/calculation.js +++ b/src/builtin/opcodes/calculation.js @@ -152,9 +152,27 @@ module.exports = function (internals) { 'string name, slot ...argReferences : slot', function (name, argReferences) { var namespaceScope = callStack.getEffectiveNamespaceScope(), - barewordString = valueFactory.createBarewordString(name, namespaceScope); + callable = namespaceScope.getFunction(name); - return barewordString.call(argReferences); + return callable.call(argReferences, null, null, null); + } + ), + + /** + * Calls a PHP function where the name is known statically, returning its result + * as a Value if it returns by-value or as a Reference if it returns by-reference. + * + * Used by "my_function(myParam: 21)" syntax. + */ + callFunctionNamed: internals.typeHandler( + // Note that arguments are snapshotted by FunctionSpec.coerceArguments(...) + // later on, as at that point we know whether a parameter is by-reference. + 'string name, any namedArgReferences, slot ...positionalArgReferences : slot', + function (name, namedArgReferences, positionalArgReferences) { + var namespaceScope = callStack.getEffectiveNamespaceScope(), + callable = namespaceScope.getFunction(name); + + return callable.call(positionalArgReferences, namedArgReferences, null, null); } ), @@ -171,6 +189,19 @@ module.exports = function (internals) { } ), + /** + * Calls a PHP instance method where the name is known statically with named arguments, + * returning its result as a Value if it returns by-value or as a Reference if it returns by-reference. + * + * Used by "$myObject->myMethod(myParam: 21)" syntax. + */ + callInstanceMethodNamed: internals.typeHandler( + 'val object, string method, any namedArgReferences, slot ...positionalArgReferences : slot', + function (objectValue, methodName, namedArgReferences, positionalArgReferences) { + return objectValue.callMethod(methodName, positionalArgReferences, namedArgReferences); + } + ), + /** * Calls a static method of a class. * @@ -182,7 +213,22 @@ module.exports = function (internals) { // TODO: Remove need for wrapping this as a *Value var methodValue = valueFactory.createString(methodName); - return classValue.callStaticMethod(methodValue, argReferences, isForwarding); + return classValue.callStaticMethod(methodValue, argReferences, null, isForwarding); + } + ), + + /** + * Calls a static method of a class with named arguments. + * + * Used by "MyClass::myMethod(myParam: 21)" syntax. + */ + callStaticMethodNamed: internals.typeHandler( + 'val class, string method, bool isForwarding, any namedArgReferences, slot ...positionalArgReferences : slot', + function (classValue, methodName, isForwarding, namedArgReferences, positionalArgReferences) { + // TODO: Remove need for wrapping this as a *Value. + var methodValue = valueFactory.createString(methodName); + + return classValue.callStaticMethod(methodValue, positionalArgReferences, namedArgReferences, isForwarding); } ), @@ -225,7 +271,7 @@ module.exports = function (internals) { callVariableStaticMethod: internals.typeHandler( 'val class, val methodName, bool isForwarding, slot ...argReferences : slot', function (classNameValue, methodNameValue, isForwarding, argReferences) { - return classNameValue.callStaticMethod(methodNameValue, argReferences, isForwarding); + return classNameValue.callStaticMethod(methodNameValue, argReferences, null, isForwarding); } ), @@ -429,6 +475,16 @@ module.exports = function (internals) { } ), + /** + * Used by transpiled PHP `new MyClass(myParam: 21)` expressions. + */ + createInstanceNamed: internals.typeHandler( + 'val className, any namedArgReferences, slot ...positionalArgReferences : val', + function (classNameValue, namedArgReferences, positionalArgReferences) { + return classNameValue.instantiate(positionalArgReferences, namedArgReferences); + } + ), + /** * Creates an IntegerValue. * diff --git a/src/builtin/opcodes/controlStructure.js b/src/builtin/opcodes/controlStructure.js index 9cbe2b08..5fb28c10 100644 --- a/src/builtin/opcodes/controlStructure.js +++ b/src/builtin/opcodes/controlStructure.js @@ -75,9 +75,10 @@ module.exports = function (internals) { function (name, definition) { var namespaceScope = callStack.getEffectiveNamespaceScope(); - // TODO: Note that we currently make no distinction between classes and interfaces, - // which is required by things like interface_exists(...). - return namespaceScope.defineClass(name, definition); + // Mark the definition as an interface so that downstream handling (e.g. the + // Throwable-implements check) can behave differently from a class definition. + // TODO: Model interfaces separately from classes. + return namespaceScope.defineClass(name, Object.assign({isInterface: true}, definition)); } ), diff --git a/src/builtin/services/base.js b/src/builtin/services/base.js index 24638e67..2b061fa8 100644 --- a/src/builtin/services/base.js +++ b/src/builtin/services/base.js @@ -15,6 +15,7 @@ var phpCommon = require('phpcommon'), CalculationOpcode = require('../../Core/Opcode/Opcode/CalculationOpcode'), CalculationOpcodeFetcher = require('../../Core/Opcode/Fetcher/CalculationOpcodeFetcher'), Call = require('../../Call'), + Callable = require('../../Function/Callable'), CallableTypeProvider = require('../../Type/Provider/Spec/CallableTypeProvider'), CallFactory = require('../../CallFactory'), CallInstrumentation = require('../../Instrumentation/CallInstrumentation'), @@ -299,6 +300,7 @@ module.exports = function (internals) { 'function_factory': function () { return new FunctionFactory( + Callable, MethodSpec, get(SCOPE_FACTORY), get(CALL_FACTORY), diff --git a/test/integration/builtin/classes/JSObjectTest.js b/test/integration/builtin/classes/JSObjectTest.js index 6fa80199..cb8b1f9f 100644 --- a/test/integration/builtin/classes/JSObjectTest.js +++ b/test/integration/builtin/classes/JSObjectTest.js @@ -358,4 +358,49 @@ EOS 'final array': [21, 101, 'my pushed value'] }); }); + + it('should create and wrap a native instance when using PHP `new` with a JS class function', async function () { + var php = nowdoc(function () {/*<<nameProp set by constructor'] = $object->nameProp; +$result['->numProp set by constructor'] = $object->numProp; +$result['->isMyClassInstance'] = $object->isMyClassInstance; + +return $result; + +EOS +*/;}), //jshint ignore:line + module = tools.asyncTranspile('/my/test/module.php', php, { + // Capture offsets of all nodes for line tracking. + phpToAST: {captureAllBounds: true}, + // Record line numbers for statements/expressions. + phpToJS: {lineNumbers: true} + }), + engine = module(); + + // Provide a JS "class" constructor function to PHP-land. + engine.defineGlobal('myJSClass', function Person(name, age) { + this.nameProp = name; + this.numProp = age; + // Verify that construction occurred as a real instance of Person. + this.isMyClassInstance = (this instanceof Person); + }); + + expect((await engine.execute()).getNative()).to.deep.equal({ + 'wrapped as JSObject': true, + 'instanceof $myJSClass': true, + '->nameProp set by constructor': 'Sally', + '->numProp set by constructor': 34, + '->isMyClassInstance': true // Ensure we have a native instance of Person (proper prototype). + }); + }); }); diff --git a/test/integration/builtin/interfaces/ThrowableTest.js b/test/integration/builtin/interfaces/ThrowableTest.js index 6d56ab03..16373889 100644 --- a/test/integration/builtin/interfaces/ThrowableTest.js +++ b/test/integration/builtin/interfaces/ThrowableTest.js @@ -67,6 +67,52 @@ EOS expect(engine.getStdout().readAll()).to.equal(''); }); + it('should allow a userland interface to extend Throwable', async function () { + var php = nowdoc(function () {/*<<getMessage(); +EOS +*/;}), //jshint ignore:line + module = tools.asyncTranspile('/path/to/my_module.php', php), + engine = module(); + + // Classes ARE allowed to indirectly implement Throwable - no error expected. + expect((await engine.execute()).getNative()).to.equal('My message'); + + expect(engine.getStderr().readAll()).to.equal(''); + expect(engine.getStdout().readAll()).to.equal(''); + }); + it('should correctly trap a userland class attempting to implement Throwable', async function () { var php = nowdoc(function () {/*<< $value) { + $othersText .= "($name=$value)"; + } + + return "myFunc('$first', '$second', '$third', ...: $othersText)"; +} + +$result = []; + +$result['defined named arguments only'] = myFunc(second: 42, first: 21, third: 100); // Note arguments in different order. +$result['both known and unknown named arguments'] = myFunc(21, third: 100, second: 42, another: 101, andAnother: 202); + +return $result; +EOS +*/;}), //jshint ignore:line + module = tools.asyncTranspile('/path/to/my_module.php', php), + engine = module(); + + expect((await engine.execute()).getNative()).to.deep.equal({ + 'defined named arguments only': 'myFunc(\'21\', \'42\', \'100\', ...: )', + 'both known and unknown named arguments': 'myFunc(\'21\', \'42\', \'100\', ...: (another=101)(andAnother=202))' + }); + }); +}); diff --git a/test/integration/ffi/api/builtin/classes/autoCoercing/syncModeAddonTest.js b/test/integration/ffi/api/builtin/classes/autoCoercing/syncModeAddonTest.js index 75270c5e..f0100cc9 100644 --- a/test/integration/ffi/api/builtin/classes/autoCoercing/syncModeAddonTest.js +++ b/test/integration/ffi/api/builtin/classes/autoCoercing/syncModeAddonTest.js @@ -47,8 +47,8 @@ EOS function () { return { 'SecondClass': function (internals) { - function SecondClass() { - internals.callSuperConstructor(this, arguments); + function SecondClass(...args) { + internals.callSuperConstructor(this, args); } internals.extendClass('FirstClass'); diff --git a/test/integration/ffi/api/builtin/functions/autoCoercing/namedArgumentTest.js b/test/integration/ffi/api/builtin/functions/autoCoercing/namedArgumentTest.js new file mode 100644 index 00000000..d937fa1f --- /dev/null +++ b/test/integration/ffi/api/builtin/functions/autoCoercing/namedArgumentTest.js @@ -0,0 +1,101 @@ +/* + * PHPCore - PHP environment runtime components + * Copyright (c) Dan Phillimore (asmblah) + * https://github.com/uniter/phpcore/ + * + * Released under the MIT license + * https://github.com/uniter/phpcore/raw/master/MIT-LICENSE.txt + */ + +'use strict'; + +var expect = require('chai').expect, + nowdoc = require('nowdoc'), + tools = require('../../../../../tools'), + PHPFatalError = require('phpcommon').PHPFatalError; + +describe('PHP builtin FFI function auto-coercion named argument integration', function () { + it('should support named arguments', async function () { + var php = nowdoc(function () {/*<<first = $first; + $this->second = $second; + $this->third = $third; + } + + public function getNumbers() { + return 'first: ' . $this->first . ', second: ' . $this->second . ', third: ' . $this->third; + } +} + +$result = []; + +$result['from bareword, named arguments only, in different order'] = new MyClass(second: 27, first: 21, third: 100)->getNumbers(); +$result['from bareword, named and positional arguments'] = new MyClass(121, third: 200, second: 127)->getNumbers(); + +$myClassName = 'MyClass'; +$result['from string, named arguments only, in different order'] = new $myClassName(second: 27, first: 21, third: 100)->getNumbers(); +$result['from string, named and positional arguments'] = new $myClassName(121, third: 200, second: 127)->getNumbers(); + +$myInstance = new MyClass(1, 2, 3); +$result['from existing instance, named arguments only, in different order'] = new $myInstance(second: 27, first: 21, third: 100)->getNumbers(); +$result['from existing instance, named and positional arguments'] = new $myInstance(121, third: 200, second: 127)->getNumbers(); + +return $result; +EOS +*/;}), //jshint ignore:line + module = tools.asyncTranspile('/path/to/my_module.php', php), + engine = module(); + + expect((await engine.execute()).getNative()).to.deep.equal({ + 'from bareword, named arguments only, in different order': 'first: 21, second: 27, third: 100', + 'from bareword, named and positional arguments': 'first: 121, second: 127, third: 200', + 'from string, named arguments only, in different order': 'first: 21, second: 27, third: 100', + 'from string, named and positional arguments': 'first: 121, second: 127, third: 200', + 'from existing instance, named arguments only, in different order': 'first: 21, second: 27, third: 100', + 'from existing instance, named and positional arguments': 'first: 121, second: 127, third: 200' + }); + }); + it('should raise a fatal error on attempting to instantiate an undefined class', async function () { var php = nowdoc(function () {/*<< 2; $i--): + $result[] = '[' . $i . ']'; +endfor; + +return $result; +EOS +*/;}),//jshint ignore:line + module = tools.asyncTranspile('/path/to/my_module.php', php), + engine = module(); + + expect((await engine.execute()).getNative()).to.deep.equal([ + '[5]', + '[4]', + '[3]' + ]); + }); + + it('should be able to loop in async mode with pauses', async function () { + var php = nowdoc(function () {/*<< get_async(2); $i = get_async($i) - 1): + $result[] = get_async('[' . get_async($i) . ']'); +endfor; + +return get_async($result); +EOS +*/;}),//jshint ignore:line + module = tools.asyncTranspile('/path/to/my_module.php', php), + engine = module(); + engine.defineFunction('get_async', function (internals) { + return function (value) { + return internals.createAsyncPresentValue(value); + }; + }); + + expect((await engine.execute()).getNative()).to.deep.equal([ + '[5]', + '[4]', + '[3]' + ]); + }); +}); diff --git a/test/integration/statements/alternativeControl/alternativeForeachLoopTest.js b/test/integration/statements/alternativeControl/alternativeForeachLoopTest.js new file mode 100644 index 00000000..dac6c5da --- /dev/null +++ b/test/integration/statements/alternativeControl/alternativeForeachLoopTest.js @@ -0,0 +1,241 @@ +/* + * PHPCore - PHP environment runtime components + * Copyright (c) Dan Phillimore (asmblah) + * https://github.com/uniter/phpcore/ + * + * Released under the MIT license + * https://github.com/uniter/phpcore/raw/master/MIT-LICENSE.txt + */ + +'use strict'; + +var expect = require('chai').expect, + nowdoc = require('nowdoc'), + tools = require('../../tools'); + +describe('PHP alternative control structure "foreach" loop statement integration', function () { + it('should be able to loop over a simple indexed array with pauses', async function () { + var php = nowdoc(function () {/*<< $value): + $result[] = get_async('value for ' . get_async($key) . ' is: ' . $value); +endforeach; + +return get_async($result); +EOS +*/;}),//jshint ignore:line + module = tools.asyncTranspile('/path/to/my_module.php', php), + engine = module(); + engine.defineFunction('get_async', function (internals) { + return function (value) { + return internals.createAsyncPresentValue(value); + }; + }); + + expect((await engine.execute()).getNative()).to.deep.equal([ + 'value for 0 is: first', + 'value for 1 is: second', + 'value for 2 is: third' + ]); + }); + + it('should be able to loop over a simple associative array with pauses', async function () { + var php = nowdoc(function () {/*<< 'first', get_async('two') => get_async('second'), 'three' => 'third'] as $key => $value): + $result[] = get_async('value for ' . $key . ' is: ' . get_async($value)); +endforeach; + +return get_async($result); +EOS +*/;}),//jshint ignore:line + module = tools.asyncTranspile('/path/to/my_module.php', php), + engine = module(); + engine.defineFunction('get_async', function (internals) { + return function (value) { + return internals.createAsyncPresentValue(value); + }; + }); + + expect((await engine.execute()).getNative()).to.deep.equal([ + 'value for one is: first', + 'value for two is: second', + 'value for three is: third' + ]); + }); + + it('should be able to loop over the visible properties of an object that does not implement Traversable with pauses', async function () { + var php = nowdoc(function () {/*<< $value): + $result[] = get_async('value for ' . $key . ' is: ' . get_async($value) . ' - from inside ParentClass'); + endforeach; + + return get_async($result); + } +} +class ChildClass extends ParentClass { + private $childProp = 'five'; + protected $childSharedProp = 'six'; + + public function getPropsVisibleToChild() { + $result = []; + + foreach ($this as $key => $value): + $result[] = get_async('value for ' . get_async($key) . ' is: ' . $value) . ' - from inside ChildClass'; + endforeach; + + return get_async($result); + } +} + +$result = []; +$myObject = get_async(new ChildClass); + +foreach (get_async($myObject) as $key => $value): + $result[] = 'value for ' . get_async($key) . ' is: ' . $value . ' - from outside the class'; +endforeach; + +$result[] = $myObject->getPropsVisibleToParent(); +$result[] = get_async($myObject->getPropsVisibleToChild()); + +return get_async($result); +EOS +*/;}),//jshint ignore:line + module = tools.asyncTranspile('/path/to/my_module.php', php), + engine = module(); + engine.defineFunction('get_async', function (internals) { + return function (value) { + return internals.createAsyncPresentValue(value); + }; + }); + + expect((await engine.execute()).getNative()).to.deep.equal([ + 'value for firstProp is: one - from outside the class', + 'value for secondProp is: two - from outside the class', + // Private property should not be accessible from outside the class. + [ + 'value for childSharedProp is: six - from inside ParentClass', + 'value for firstProp is: one - from inside ParentClass', + 'value for secondProp is: two - from inside ParentClass', + 'value for privateProp is: three - from inside ParentClass', + 'value for parentSharedProp is: four - from inside ParentClass' + ], + [ + 'value for childProp is: five - from inside ChildClass', + 'value for childSharedProp is: six - from inside ChildClass', + 'value for firstProp is: one - from inside ChildClass', + 'value for secondProp is: two - from inside ChildClass', + 'value for parentSharedProp is: four - from inside ChildClass' + ] + ]); + }); + + it('should be able to loop over an array fetched from instance property inside a closure passed as function arg with pauses', async function () { + var php = nowdoc(function () {/*<<myArray = get_async(['first', 'second', 'third']); + + foreach ($myObject->myArray as $key => $value): + $result[] = 'value for ' . get_async(get_async($key) . ' is: ' . $value); + endforeach; +})); + +return get_async($result); +EOS +*/;}),//jshint ignore:line + module = tools.asyncTranspile('/path/to/my_module.php', php), + engine = module(); + engine.defineFunction('get_async', function (internals) { + return function (value) { + return internals.createAsyncPresentValue(value); + }; + }); + + expect((await engine.execute()).getNative()).to.deep.equal([ + 'value for 0 is: first', + 'value for 1 is: second', + 'value for 2 is: third' + ]); + }); + + // Make sure that in order to resume iteration 21, we only resume-execute the loop body once. + // This is achieved by resetting the opIndex counter after a loop's condition opcode + // back to the value it had when the loop began. + it('should not iterate in order to resume the Nth iteration', async function () { + var php = nowdoc(function () {/*<< $value): + $result[] = get_async('value for ' . get_async($key) . ' is: ' . $value); +endforeach; + +return get_async($result); +EOS +*/;}), //jshint ignore:line + module = tools.asyncTranspile('/path/to/my_module.php', php), + loopConditionChecks = 0, + environment = tools.createAsyncEnvironment({}, [ + { + functionGroups: [ + function (internals) { + return { + 'get_async': function (value) { + return internals.createAsyncPresentValue(value); + } + }; + } + ], + opcodeGroups: function (internals) { + internals.setOpcodeFetcher('loopStructure'); + internals.allowOpcodeOverride(); + + return { + // Override the standard built-in isNotFinished() opcode's handler. This opcode + // is used every time a foreach loop iterates, as in our test. + isNotFinished: function (iterator) { + loopConditionChecks++; + + return internals.callPreviousHandler('isNotFinished', [iterator]); + } + }; + } + } + ]), + engine = module({}, environment); + + expect((await engine.execute()).getNative()).to.deep.equal([ + 'value for 0 is: first', + 'value for 1 is: second', + 'value for 2 is: third' + ]); + // One condition check should occur before each of the 3 iterations, + // with one extra check that returns false when the loop completes. + expect(loopConditionChecks).to.equal(4); + }); +}); diff --git a/test/integration/statements/alternativeControl/alternativeIfTest.js b/test/integration/statements/alternativeControl/alternativeIfTest.js new file mode 100644 index 00000000..d7aefcfe --- /dev/null +++ b/test/integration/statements/alternativeControl/alternativeIfTest.js @@ -0,0 +1,195 @@ +/* + * PHPCore - PHP environment runtime components + * Copyright (c) Dan Phillimore (asmblah) + * https://github.com/uniter/phpcore/ + * + * Released under the MIT license + * https://github.com/uniter/phpcore/raw/master/MIT-LICENSE.txt + */ + +'use strict'; + +var expect = require('chai').expect, + nowdoc = require('nowdoc'), + tools = require('../../tools'); + +describe('PHP alternative control structure "if" statement integration', function () { + it('should support conditions with logical and comparison operators', async function () { + var php = nowdoc(function () {/*<<myProp = true; + + if ($myObject->myProp): + $result[] = 'found'; + endif; +}); + +return $result; +EOS +*/;}),//jshint ignore:line + module = tools.asyncTranspile('/path/to/my_module.php', php), + engine = module(); + + expect((await engine.execute()).getNative()).to.deep.equal([ + 'found' + ]); + }); + + it('should support pause/resume', async function () { + var php = nowdoc(function () {/*<<Some inline HTML contentThis should not appearMore inline HTML 2): + $result[] = '[' . $i . ']'; + + $i--; +endwhile; + +return $result; +EOS +*/;}),//jshint ignore:line + module = tools.asyncTranspile('/path/to/my_module.php', php), + engine = module(); + + expect((await engine.execute()).getNative()).to.deep.equal([ + '[5]', + '[4]', + '[3]' + ]); + }); + + it('should be able to nest loops in sync mode', function () { + var php = nowdoc(function () {/*<< 2): + $result[] = '[' . $i . ']'; + + $j = 3; + + while ($j > 1): + $result[] = '[[' . $j . ']]'; + + $j--; + endwhile; + + $i--; +endwhile; + +return $result; +EOS +*/;}),//jshint ignore:line + module = tools.syncTranspile('/path/to/my_module.php', php), + engine = module(); + + expect(engine.execute().getNative()).to.deep.equal([ + '[5]', + '[[3]]', + '[[2]]', + '[4]', + '[[3]]', + '[[2]]', + '[3]', + '[[3]]', + '[[2]]' + ]); + }); + + it('should be able to loop in async mode with pauses', async function () { + var php = nowdoc(function () {/*<< get_async(2)): + $result[] = get_async('[' . get_async($i) . ']'); + + $i--; +endwhile; + +return get_async($result); +EOS +*/;}),//jshint ignore:line + module = tools.asyncTranspile('/path/to/my_module.php', php), + engine = module(); + engine.defineFunction('get_async', function (internals) { + return function (value) { + return internals.createAsyncPresentValue(value); + }; + }); + + expect((await engine.execute()).getNative()).to.deep.equal([ + '[5]', + '[4]', + '[3]' + ]); + }); + + it('should be able to nest loops in async mode with pauses', async function () { + var php = nowdoc(function () {/*<< get_async(2)): + $result[] = get_async('[' . get_async($i) . ']'); + + $j = 3; + + while (get_async($j) > get_async(1)): + $result[] = get_async('[[' . get_async($j) . ']]'); + + $j--; + endwhile; + + $i = get_async($i) - 1; +endwhile; + +return get_async($result); +EOS +*/;}),//jshint ignore:line + module = tools.asyncTranspile('/path/to/my_module.php', php), + engine = module(); + engine.defineFunction('get_async', function (internals) { + return function (value) { + return internals.createAsyncPresentValue(value); + }; + }); + + expect((await engine.execute()).getNative()).to.deep.equal([ + '[5]', + '[[3]]', + '[[2]]', + '[4]', + '[[3]]', + '[[2]]', + '[3]', + '[[3]]', + '[[2]]' + ]); + }); + + it('should support fetching the condition from accessor returning future in async mode', async function () { + var php = nowdoc(function () {/*<<myMethod('$first', '$second', '$third')"; + } +} + +$myObject = new MyClass('my value'); + +$result = []; +$result['named arguments only'] = $myObject->myMethod(second: 42, first: 21, third: 100); // Note arguments in different order. +$result['both named and positional arguments'] = $myObject->myMethod(21, third: 100, second: 42); + +return $result; +EOS +*/;}), //jshint ignore:line + module = tools.asyncTranspile('/path/to/my_module.php', php), + engine = module(); + + expect((await engine.execute()).getNative()).to.deep.equal({ + 'named arguments only': '->myMethod(\'21\', \'42\', \'100\')', + 'both named and positional arguments': '->myMethod(\'21\', \'42\', \'100\')' + }); + expect(engine.getStderr().readAll()).to.equal(''); + }); +}); diff --git a/test/integration/statements/class/method/static/namedArgumentTest.js b/test/integration/statements/class/method/static/namedArgumentTest.js new file mode 100644 index 00000000..b614f208 --- /dev/null +++ b/test/integration/statements/class/method/static/namedArgumentTest.js @@ -0,0 +1,44 @@ +/* + * PHPCore - PHP environment runtime components + * Copyright (c) Dan Phillimore (asmblah) + * https://github.com/uniter/phpcore/ + * + * Released under the MIT license + * https://github.com/uniter/phpcore/raw/master/MIT-LICENSE.txt + */ + +'use strict'; + +var expect = require('chai').expect, + nowdoc = require('nowdoc'), + tools = require('../../../../tools'); + +describe('PHP class static method named argument integration', function () { + it('should support named arguments', async function () { + var php = nowdoc(function () {/*<<myUndefinedMethod(21, secondArg: 101, thirdArg: 2); +EOS +*/;}),//jshint ignore:line + module = tools.asyncTranspile('/path/to/my_module.php', php), + engine = module(); + + expect((await engine.execute()).getNative()).to.equal('myUndefinedMethod :: 223'); + }); + it('magic __call(...) should override __callStatic(...) when both present for static call in object context', async function () { var php = nowdoc(function () {/*<<myMethod(); + + return $result; +} +EOS +*/;}),//jshint ignore:line + module = tools.asyncTranspile('/path/to/my_module.php', php), + engine = module(); + + expect((await engine.execute()).getNative()).to.deep.equal({ + 'myMethod()': 'hello from myMethod' + }); + expect(engine.getStderr().readAll()).to.equal(''); + }); }); diff --git a/test/integration/statements/trait/method/staticMethodTest.js b/test/integration/statements/trait/method/staticMethodTest.js index 0bb30706..e32458ca 100644 --- a/test/integration/statements/trait/method/staticMethodTest.js +++ b/test/integration/statements/trait/method/staticMethodTest.js @@ -56,4 +56,43 @@ EOS }); expect(engine.getStderr().readAll()).to.equal(''); }); + + it('should support traits with abstract static methods', async function () { + var php = nowdoc(function () {/*<< 1)', function () { + it('should raise an error when the argument count is below the minimum (<> 1)', async function () { overloadedFunctionSpec.getMinimumParameterCount.returns(4); overloadedFunctionSpec.getMaximumParameterCount.returns(6); - expect(function () { - functionSpec.validateArguments(); - }).to.throw( + await expect(functionSpec.validateArguments().toPromise()).to.eventually.be.rejectedWith( 'Fake PHP Fatal error (ArgumentCountError) for #core.wrong_arg_count_builtin ' + 'with {' + '"bound":"[Translated] core.at_least {}",' + @@ -92,14 +90,12 @@ describe('InvalidOverloadedFunctionSpec', function () { ); }); - it('should raise an error when the argument count is below the minimum (of 1)', function () { + it('should raise an error when the argument count is below the minimum (of 1)', async function () { createFunctionSpec(0); overloadedFunctionSpec.getMinimumParameterCount.returns(1); overloadedFunctionSpec.getMaximumParameterCount.returns(3); - expect(function () { - functionSpec.validateArguments(); - }).to.throw( + await expect(functionSpec.validateArguments().toPromise()).to.eventually.be.rejectedWith( 'Fake PHP Fatal error (ArgumentCountError) for #core.wrong_arg_count_builtin_single ' + 'with {' + '"bound":"[Translated] core.at_least {}",' + @@ -111,13 +107,11 @@ describe('InvalidOverloadedFunctionSpec', function () { ); }); - it('should raise an error when the argument count is above the maximum (<> 1)', function () { + it('should raise an error when the argument count is above the maximum (<> 1)', async function () { overloadedFunctionSpec.getMinimumParameterCount.returns(0); overloadedFunctionSpec.getMaximumParameterCount.returns(2); - expect(function () { - functionSpec.validateArguments(); - }).to.throw( + await expect(functionSpec.validateArguments().toPromise()).to.eventually.be.rejectedWith( 'Fake PHP Fatal error (ArgumentCountError) for #core.wrong_arg_count_builtin ' + 'with {' + '"bound":"[Translated] core.at_most {}",' + @@ -129,13 +123,11 @@ describe('InvalidOverloadedFunctionSpec', function () { ); }); - it('should raise an error when the argument count is above the maximum (of 1)', function () { + it('should raise an error when the argument count is above the maximum (of 1)', async function () { overloadedFunctionSpec.getMinimumParameterCount.returns(0); overloadedFunctionSpec.getMaximumParameterCount.returns(1); - expect(function () { - functionSpec.validateArguments(); - }).to.throw( + await expect(functionSpec.validateArguments().toPromise()).to.be.rejectedWith( 'Fake PHP Fatal error (ArgumentCountError) for #core.wrong_arg_count_builtin_single ' + 'with {' + '"bound":"[Translated] core.at_most {}",' + @@ -149,13 +141,11 @@ describe('InvalidOverloadedFunctionSpec', function () { // This can happen when there are overloads defined for argument counts 1 and 3, // but no variant defined for an argument count of 2, for example. - it('should raise an error when the argument count is between min and max', function () { + it('should raise an error when the argument count is between min and max', async function () { overloadedFunctionSpec.getMinimumParameterCount.returns(1); overloadedFunctionSpec.getMaximumParameterCount.returns(4); - expect(function () { - functionSpec.validateArguments(); - }).to.throw( + await expect(functionSpec.validateArguments().toPromise()).to.be.rejectedWith( 'Fake PHP Fatal error (ArgumentCountError) for #core.no_overload_variant_for_parameter_count ' + 'with {"parameterCount":3}' ); diff --git a/test/unit/Function/Overloaded/OverloadedFunctionDefinerTest.js b/test/unit/Function/Overloaded/OverloadedFunctionDefinerTest.js index a17c124a..9f8b6c00 100644 --- a/test/unit/Function/Overloaded/OverloadedFunctionDefinerTest.js +++ b/test/unit/Function/Overloaded/OverloadedFunctionDefinerTest.js @@ -112,7 +112,7 @@ describe('OverloadedFunctionDefiner', function () { ) .returns(overloadedFunctionSpec); - functionFactory.create + functionFactory.createCallable .withArgs( sinon.match.same(namespaceScope), null, diff --git a/test/unit/Function/Overloaded/OverloadedFunctionSpecTest.js b/test/unit/Function/Overloaded/OverloadedFunctionSpecTest.js index 3b279dd5..b4883975 100644 --- a/test/unit/Function/Overloaded/OverloadedFunctionSpecTest.js +++ b/test/unit/Function/Overloaded/OverloadedFunctionSpecTest.js @@ -69,7 +69,7 @@ describe('OverloadedFunctionSpec', function () { 5 ) .returns(aliasFunctionSpec); - functionFactory.create + functionFactory.createCallable .withArgs( sinon.match.same(namespaceScope), null, diff --git a/test/unit/FunctionFactoryTest.js b/test/unit/FunctionFactoryTest.js index cd24ff14..2c406783 100644 --- a/test/unit/FunctionFactoryTest.js +++ b/test/unit/FunctionFactoryTest.js @@ -10,24 +10,19 @@ 'use strict'; var expect = require('chai').expect, - phpCommon = require('phpcommon'), sinon = require('sinon'), tools = require('./tools'), Call = require('../../src/Call'), + Callable = require('../../src/Function/Callable'), CallFactory = require('../../src/CallFactory'), CallStack = require('../../src/CallStack'), Class = require('../../src/Class').sync(), ControlScope = require('../../src/Control/ControlScope'), - Exception = phpCommon.Exception, FunctionFactory = require('../../src/FunctionFactory').sync(), FunctionSpec = require('../../src/Function/FunctionSpec'), NamespaceScope = require('../../src/NamespaceScope').sync(), - OverloadedFunctionSpec = require('../../src/Function/Overloaded/OverloadedFunctionSpec'), - Reference = require('../../src/Reference/Reference'), Scope = require('../../src/Scope').sync(), - ScopeFactory = require('../../src/ScopeFactory'), - Value = require('../../src/Value').sync(), - Variable = require('../../src/Variable').sync(); + ScopeFactory = require('../../src/ScopeFactory'); describe('FunctionFactory', function () { var call, @@ -76,6 +71,7 @@ describe('FunctionFactory', function () { scopeFactory.create.returns(scope); factory = new FunctionFactory( + Callable, MethodSpec, scopeFactory, callFactory, @@ -87,391 +83,49 @@ describe('FunctionFactory', function () { ); }); - describe('create()', function () { - var callCreate, - overloadedFunctionSpec, - variantFunctionSpec; - - beforeEach(function () { - overloadedFunctionSpec = sinon.createStubInstance(OverloadedFunctionSpec); - variantFunctionSpec = sinon.createStubInstance(FunctionSpec); - - variantFunctionSpec.coerceArguments - .callsFake(function (argumentReferences) { - return futureFactory.createAsyncPresent( - argumentReferences.map(function (argumentReference) { - return valueFactory.coerce(argumentReference); - }) - ); - }); - variantFunctionSpec.coerceReturnReference.callsFake(function (returnReference) { - return futureFactory.createPresent(returnReference); - }); - variantFunctionSpec.populateDefaultArguments.returnsArg(0); - variantFunctionSpec.getFunction.returns(originalFunc); - variantFunctionSpec.getFunctionName.returns(name); - variantFunctionSpec.isReturnByReference.returns(false); - variantFunctionSpec.isUserland.returns(false); - overloadedFunctionSpec.resolveFunctionSpec.returns(variantFunctionSpec); - - variantFunctionSpec.validateArguments - .callsFake(function () { - return futureFactory.createPresent(); - }); - variantFunctionSpec.validateReturnReference - .callsFake(function (returnReference, returnValue) { - return futureFactory.createPresent(returnValue); - }); - - callCreate = function (currentObject, staticClass) { - return factory.create( + describe('createCallable()', function () { + it('should return a Callable instance', function () { + var functionSpec = sinon.createStubInstance(FunctionSpec), + result = factory.createCallable( namespaceScope, currentClass, - currentObject || null, - staticClass || null, - overloadedFunctionSpec + null, + null, + functionSpec ); - }; - }); - it('should return a wrapper function', function () { - expect(callCreate()).to.be.a('function'); + expect(result).to.be.an.instanceOf(Callable); }); - describe('the wrapper function returned', function () { - it('should return the eventual result from the wrapped function coerced to a Value when return-by-value', async function () { - var result, - resultValue; - originalFunc.returns(123); - - result = callCreate()(); - resultValue = await result.toPromise(); - - expect(resultValue).to.be.an.instanceOf(Value); - expect(resultValue.getType()).to.equal('int'); - expect(resultValue.getNative()).to.equal(123); - }); - - it('should throw an Exception if a built-in function enacts a Pause directly rather than returning a Future', async function () { - originalFunc.callsFake(function () { - var pause = pauseFactory.createPause(function () {}); - - pause.now(); - }); - - await expect(callCreate()().toPromise()).to.eventually.be.rejectedWith( - Exception, - 'FunctionFactory :: A built-in function enacted a Pause, did you mean to return a Future instead?' - ); - }); - - describe('when return-by-reference', function () { - var resultVariable; - - beforeEach(function () { - resultVariable = sinon.createStubInstance(Variable); - resultVariable.next.callsArgWith(0, resultVariable); - variantFunctionSpec.isReturnByReference.returns(true); - originalFunc.returns(resultVariable); - }); - - it('should return the eventual result from the wrapped function', async function () { - expect(await callCreate()().toPromise()).to.equal(resultVariable); - }); - - it('should validate the eventual result Variable/reference via .validateReturnReference()', async function () { - await callCreate()().toPromise(); - - expect(variantFunctionSpec.validateReturnReference).to.have.been.calledOnce; - expect(variantFunctionSpec.validateReturnReference).to.have.been.calledWith( - sinon.match.same(resultVariable) - ); - }); - - it('should validate the eventual result Value via .validateReturnReference()', async function () { - await callCreate()().toPromise(); - - expect(variantFunctionSpec.validateReturnReference).to.have.been.calledOnce; - expect(variantFunctionSpec.validateReturnReference).to.have.been.calledWith( - sinon.match.any, - sinon.match.same(resultVariable) - ); - }); - }); - - it('should pass the current Class to the ScopeFactory', async function () { - await callCreate()().toPromise(); - - expect(scopeFactory.create).to.have.been.calledOnce; - expect(scopeFactory.create).to.have.been.calledWith( - sinon.match.same(currentClass) - ); - }); - - it('should pass the wrapper function to the ScopeFactory', async function () { - var wrapperFunction = callCreate(); - - await wrapperFunction().toPromise(); - - expect(scopeFactory.create).to.have.been.calledOnce; - expect(scopeFactory.create).to.have.been.calledWith( - sinon.match.any, - sinon.match.same(wrapperFunction) - ); - }); - - it('should pass the `$this` object to the ScopeFactory when provided', async function () { - var currentObject = sinon.createStubInstance(Value); - - await callCreate(currentObject)().toPromise(); - - expect(scopeFactory.create).to.have.been.calledOnce; - expect(scopeFactory.create).to.have.been.calledWith( - sinon.match.any, - sinon.match.any, - sinon.match.same(currentObject) - ); - }); - - it('should pass the Scope to the CallFactory', async function () { - var currentObject = sinon.createStubInstance(Value); - - await callCreate(currentObject)().toPromise(); - - expect(callFactory.create).to.have.been.calledOnce; - expect(callFactory.create).to.have.been.calledWith( - sinon.match.same(scope) - ); - }); - - it('should pass the NamespaceScope to the CallFactory', async function () { - var currentObject = sinon.createStubInstance(Value); - - await callCreate(currentObject)().toPromise(); - - expect(callFactory.create).to.have.been.calledOnce; - expect(callFactory.create).to.have.been.calledWith( - sinon.match.any, - sinon.match.same(namespaceScope) - ); - }); - - it('should pass the arguments to the CallFactory', async function () { - var callArgs, - currentObject = sinon.createStubInstance(Value); - overloadedFunctionSpec.resolveFunctionSpec.resetBehavior(); - overloadedFunctionSpec.resolveFunctionSpec - .withArgs(2) - .returns(variantFunctionSpec); - - await callCreate(currentObject)(21, 27).toPromise(); - - expect(callFactory.create).to.have.been.calledOnce; - callArgs = callFactory.create.args[0][2]; - expect(callArgs).to.have.length(2); - expect(callArgs[0].getType()).to.equal('int'); - expect(callArgs[0].getNative()).to.equal(21); - expect(callArgs[1].getType()).to.equal('int'); - expect(callArgs[1].getNative()).to.equal(27); - }); - - it('should pass any "next" static class set', async function () { - var newStaticClass = sinon.createStubInstance(Class), - wrappedFunc = callCreate(); - factory.setNewStaticClassIfWrapped(wrappedFunc, newStaticClass); - - await wrappedFunc().toPromise(); - - expect(callFactory.create).to.have.been.calledOnce; - expect(callFactory.create).to.have.been.calledWith( - sinon.match.any, - sinon.match.any, - sinon.match.any, - sinon.match.same(newStaticClass) - ); - }); - - it('should pass any explicit static class set', async function () { - var explicitStaticClass = sinon.createStubInstance(Class); - - await callCreate(null, explicitStaticClass)().toPromise(); - - expect(callFactory.create).to.have.been.calledOnce; - expect(callFactory.create).to.have.been.calledWith( - sinon.match.any, - sinon.match.any, - sinon.match.any, - sinon.match.same(explicitStaticClass) - ); - }); - - it('should pass null as the new static class when no explicit or "next" one is set', async function () { - await callCreate()().toPromise(); - - expect(callFactory.create).to.have.been.calledOnce; - expect(callFactory.create).to.have.been.calledWith( - sinon.match.any, - sinon.match.any, - sinon.match.any, - null - ); - }); - - it('should pass the (JS) `this` object as the (PHP) `$this` object when not provided', async function () { - var jsThisObjectValue = sinon.createStubInstance(Value); - - await callCreate(null).call(jsThisObjectValue).toPromise(); - - expect(scopeFactory.create).to.have.been.calledOnce; - expect(scopeFactory.create).to.have.been.calledWith( - sinon.match.any, - sinon.match.any, - sinon.match.same(jsThisObjectValue) - ); - }); - - it('should pass null as the `$this` object when not provided and a non-Value (JS) `this` object was used', async function () { - var nonValueThisObject = {}; - - await callCreate(null).call(nonValueThisObject).toPromise(); - - expect(scopeFactory.create).to.have.been.calledOnce; - expect(scopeFactory.create).to.have.been.calledWith( - sinon.match.any, - sinon.match.any, - null - ); - }); - - it('should coerce parameter arguments as required', async function () { - var argValue1 = valueFactory.createInteger(21), - argValue2 = valueFactory.createInteger(101), - wrappedFunc = callCreate(); - - await wrappedFunc(argValue1, argValue2).toPromise(); - - expect(originalFunc).to.have.been.calledOnce; - expect(originalFunc.args[0][0].getType()).to.equal('int'); - expect(originalFunc.args[0][0].getNative()).to.equal(21); - expect(originalFunc.args[0][1].getType()).to.equal('int'); - expect(originalFunc.args[0][1].getNative()).to.equal(101); - }); - - it('should push the call onto the stack', async function () { - await callCreate()().toPromise(); - - expect(callStack.push).to.have.been.calledOnce; - expect(callStack.push).to.have.been.calledWith(sinon.match.same(call)); - }); - - it('should validate parameter arguments at the right point', async function () { - var argValue1 = valueFactory.createInteger(21), - argValue2 = valueFactory.createInteger(101), - wrappedFunc = callCreate(); - - await wrappedFunc(argValue1, argValue2).toPromise(); - - expect(variantFunctionSpec.validateArguments).to.have.been.calledOnce; - expect(variantFunctionSpec.validateArguments).to.have.been.calledWith([ - sinon.match.same(argValue1), - sinon.match.same(argValue2) - ]); - expect(variantFunctionSpec.validateArguments) - .to.have.been.calledAfter(variantFunctionSpec.coerceArguments); - }); - - it('should pop the call off the stack even when the argument validation throws', function () { - var error = new Error('argh'); - variantFunctionSpec.validateArguments.returns(futureFactory.createRejection(error)); - - return expect(callCreate()().toPromise()) - .to.eventually.be.rejectedWith(error) - .then(function () { - expect(callStack.pop).to.have.been.calledOnce; - }); - }); - - it('should populate default argument values at the right point', async function () { - var argValue1 = valueFactory.createInteger(21), - argValue2 = valueFactory.createInteger(101), - wrappedFunc = callCreate(); - - await wrappedFunc(argValue1, argValue2).toPromise(); - - expect(variantFunctionSpec.populateDefaultArguments).to.have.been.calledOnce; - expect(variantFunctionSpec.populateDefaultArguments).to.have.been.calledWith([ - sinon.match.same(argValue1), - sinon.match.same(argValue2) - ]); - expect(variantFunctionSpec.populateDefaultArguments) - .to.have.been.calledAfter(variantFunctionSpec.validateArguments); - expect(variantFunctionSpec.populateDefaultArguments) - .to.have.been.calledBefore(callStack.pop); - }); - - it('should pop the call off the stack when the wrapped function returns', async function () { - await callCreate()().toPromise(); - - expect(callStack.pop).to.have.been.calledOnce; - }); - - it('should pop the call off the stack even when the wrapped function throws', function () { - var error = new Error('argh'); - originalFunc.throws(error); - - return expect(callCreate()().toPromise()) - .to.eventually.be.rejectedWith(error) - .then(function () { - expect(callStack.pop).to.have.been.calledOnce; - }); - }); - - it('should pass the scope as the thisObject when calling the wrapped function', async function () { - await callCreate()().toPromise(); - - expect(originalFunc).to.have.been.calledOn(sinon.match.same(scope)); - }); - - it('should pass arguments through to a wrapped native function', async function () { - var argValue1 = valueFactory.createInteger(123), - argValue2 = valueFactory.createString('second'), - argValue3 = valueFactory.createString('another'); - - await callCreate()(argValue1, argValue2, argValue3).toPromise(); - - expect(originalFunc).to.have.been.calledOnce; - expect(originalFunc).to.have.been.calledWith( - sinon.match.same(argValue1), - sinon.match.same(argValue2), - sinon.match.same(argValue3) - ); - }); - - it('should load arguments via the FunctionSpec for a userland PHP function', async function () { - var argValue1 = valueFactory.createString('my first arg'), - argReference2 = sinon.createStubInstance(Reference); - variantFunctionSpec.isUserland.returns(true); - argReference2.getValue.returns(valueFactory.createString('my second arg')); - - await callCreate()(argValue1, argReference2).toPromise(); - - expect(originalFunc).to.have.been.calledOnce; - expect(originalFunc.args[0]).to.deep.equal([]); - expect(originalFunc).to.have.been.calledOn(undefined); - expect(variantFunctionSpec.loadArguments).to.have.been.calledOnce; - expect(variantFunctionSpec.loadArguments).to.have.been.calledWith( - [sinon.match.same(argValue1), sinon.match.same(argReference2)], - sinon.match.same(scope) - ); - }); - - it('should have the FunctionSpec stored against it', function () { - expect(callCreate().functionSpec).to.equal(overloadedFunctionSpec); - }); + it('should pass the correct arguments to the Callable constructor', function () { + var functionSpec = sinon.createStubInstance(FunctionSpec), + currentObject = valueFactory.createObject({}, currentClass), + staticClass = sinon.createStubInstance(Class), + // Wrap the real Callable as a spy so we can assert on constructor calls. + CallableSpy = sinon.spy(factory.Callable); + factory.Callable = CallableSpy; + + factory.createCallable( + namespaceScope, + currentClass, + currentObject, + staticClass, + functionSpec + ); - it('should have the isPHPCoreWrapped flag set against it', function () { - expect(callCreate().isPHPCoreWrapped).to.be.true; - }); + expect(CallableSpy).to.have.been.calledOnce; + expect(CallableSpy).to.have.been.calledWith( + sinon.match.same(scopeFactory), + sinon.match.same(callFactory), + sinon.match.same(valueFactory), + sinon.match.same(callStack), + sinon.match.same(flow), + sinon.match.same(namespaceScope), + sinon.match.same(currentClass), + sinon.match.same(currentObject), + sinon.match.same(staticClass), + sinon.match.same(functionSpec) + ); }); }); diff --git a/test/unit/NamespaceTest.js b/test/unit/NamespaceTest.js index d928a3e8..f095ead4 100644 --- a/test/unit/NamespaceTest.js +++ b/test/unit/NamespaceTest.js @@ -113,7 +113,7 @@ describe('Namespace', function () { return futureFactory.createPresent(traitObject); }); - functionFactory.create.callsFake(function (namespace, currentClass, currentObject, staticClass, functionSpec) { + functionFactory.createCallable.callsFake(function (namespace, currentClass, currentObject, staticClass, functionSpec) { var wrapperFunc = sinon.stub(); wrapperFunc.testArgs = { namespace: namespace, @@ -170,7 +170,7 @@ describe('Namespace', function () { 123 ) .returns(functionSpec); - functionFactory.create + functionFactory.createCallable .withArgs( sinon.match.same(namespaceScope), null, @@ -370,7 +370,7 @@ describe('Namespace', function () { 123 ) .returns(functionSpec); - functionFactory.create + functionFactory.createCallable .withArgs( sinon.match.same(namespaceScope), null, @@ -415,7 +415,7 @@ describe('Namespace', function () { 123 ) .returns(functionSpec); - functionFactory.create + functionFactory.createCallable .withArgs( sinon.match.same(namespaceScope), null, @@ -474,7 +474,7 @@ describe('Namespace', function () { 123 ) .returns(functionSpec); - functionFactory.create + functionFactory.createCallable .withArgs( sinon.match.same(namespaceScope), null, @@ -531,7 +531,7 @@ describe('Namespace', function () { 123 ) .returns(functionSpec); - functionFactory.create + functionFactory.createCallable .withArgs( sinon.match.same(namespaceScope), null, diff --git a/test/unit/OOP/Class/Definition/NativeDefinitionBuilderTest.js b/test/unit/OOP/Class/Definition/NativeDefinitionBuilderTest.js index ab2a10fe..278f4f4f 100644 --- a/test/unit/OOP/Class/Definition/NativeDefinitionBuilderTest.js +++ b/test/unit/OOP/Class/Definition/NativeDefinitionBuilderTest.js @@ -20,6 +20,7 @@ var expect = require('chai').expect, NativeDefinitionBuilder = require('../../../../../src/OOP/Class/Definition/NativeDefinitionBuilder'), NativeMethodDefinitionBuilder = require('../../../../../src/OOP/NativeMethodDefinitionBuilder'), ObjectValue = require('../../../../../src/Value/Object').sync(), + Scope = require('../../../../../src/Scope').sync(), Trait = require('../../../../../src/OOP/Trait/Trait'), ValueCoercer = require('../../../../../src/FFI/Value/ValueCoercer'); @@ -237,12 +238,16 @@ describe('NativeDefinitionBuilder', function () { beforeEach(function () { callProxyConstructor = function (name, autoCoercionEnabled) { + var scope; callInternalConstructor(name, autoCoercionEnabled); arg1 = valueFactory.createString('arg 1'); arg2 = valueFactory.createString('arg 2'); - return definition.getInternalClass().prototype.__construct.apply(objectValue, [arg1, arg2]); + scope = sinon.createStubInstance(Scope); + scope.getThisObject.returns(objectValue); + + return definition.getMethods().__construct.method.call(scope, arg1, arg2); }; }); diff --git a/test/unit/OOP/Class/Definition/UserlandDefinitionBuilderTest.js b/test/unit/OOP/Class/Definition/UserlandDefinitionBuilderTest.js index b6554d17..9258d07d 100644 --- a/test/unit/OOP/Class/Definition/UserlandDefinitionBuilderTest.js +++ b/test/unit/OOP/Class/Definition/UserlandDefinitionBuilderTest.js @@ -103,6 +103,8 @@ describe('UserlandDefinitionBuilder', function () { namespaceScope = sinon.createStubInstance(NamespaceScope); superClass = sinon.createStubInstance(Class); + firstInterface.getName.returns('FirstInterface'); + secondInterface.getName.returns('SecondInterface'); namespace.getPrefix.returns('My\\Stuff\\'); superClass.getInternalClass.returns(function () {}); @@ -255,9 +257,7 @@ describe('UserlandDefinitionBuilder', function () { }); it('should raise an uncatchable fatal error when a PHP-defined class attempts to implement Throwable', function () { - firstInterface.is - .withArgs('Throwable') - .returns(true); + firstInterface.getName.returns('Throwable'); expect(function () { callBuildDefinition('MyInvalidThrowable'); @@ -266,5 +266,22 @@ describe('UserlandDefinitionBuilder', function () { 'with {"className":"My\\\\Stuff\\\\MyInvalidThrowable"} in /path/to/my_module.php on line 1234' ); }); + + it('should not raise an error when a PHP-defined class implements a userland interface that extends Throwable', function () { + firstInterface.getName.returns('MyThrowable'); // Not Throwable itself. + + expect(function () { + callBuildDefinition('MyException'); + }).not.to.throw(); + }); + + it('should not raise an error when a PHP-defined interface extends Throwable', function () { + firstInterface.getName.returns('Throwable'); + definitionStructure.isInterface = true; + + expect(function () { + callBuildDefinition('MyThrowable'); + }).not.to.throw(); + }); }); }); diff --git a/test/unit/OOP/Class/MethodPromoterTest.js b/test/unit/OOP/Class/MethodPromoterTest.js index c7dce173..45bb73fd 100644 --- a/test/unit/OOP/Class/MethodPromoterTest.js +++ b/test/unit/OOP/Class/MethodPromoterTest.js @@ -77,7 +77,7 @@ describe('MethodPromoter', function () { ) .returns(functionSpec); - functionFactory.create + functionFactory.createCallable .withArgs( sinon.match.same(namespaceScope), sinon.match.same(classObject), diff --git a/test/unit/PHPStateTest.js b/test/unit/PHPStateTest.js index 2882c8ce..168f16b5 100644 --- a/test/unit/PHPStateTest.js +++ b/test/unit/PHPStateTest.js @@ -136,8 +136,8 @@ describe('PHPState', function () { { classes: { 'Some\\Stuff\\AClass': function (internals) { - function AClass() { - internals.callSuperConstructor(this, arguments).yieldSync(); + function AClass(...args) { + internals.callSuperConstructor(this, args).yieldSync(); } return AClass; @@ -630,11 +630,10 @@ describe('PHPState', function () { expect( ( await state.getFunction('My\\Stuff\\myAliasFunc') - .call( - null, + .call([ valueFactory.createInteger(21), valueFactory.createInteger(4) - ) + ]) .toPromise() ) .getNative() @@ -660,9 +659,9 @@ describe('PHPState', function () { return numberToDouble * 2; }); - resultValue = await state.getFunction('double_it')( + resultValue = await state.getFunction('double_it').call([ valueFactory.createInteger(21) - ).toPromise(); + ]).toPromise(); expect(resultValue.getType()).to.equal('int'); expect(resultValue.getNative()).to.equal(42); @@ -677,9 +676,9 @@ describe('PHPState', function () { // Explicitly fetch via the namespace, to ensure we aren't just erroneously // allowing function names to contain backslashes - resultValue = await namespace.getFunction('double_it')( + resultValue = await namespace.getFunction('double_it').call([ valueFactory.createInteger(21) - ).toPromise(); + ]).toPromise(); expect(resultValue.getType()).to.equal('int'); expect(resultValue.getNative()).to.equal(42); @@ -730,7 +729,10 @@ describe('PHPState', function () { }); func = state.getFunction('My\\Stuff\\my_multiplier'); - resultValue = await func(valueFactory.createInteger(4), valueFactory.createInteger(3)).toPromise(); + resultValue = await func.call([ + valueFactory.createInteger(4), + valueFactory.createInteger(3) + ]).toPromise(); expect(resultValue.getType()).to.equal('int'); expect(resultValue.getNative()).to.equal(12); @@ -748,7 +750,10 @@ describe('PHPState', function () { }); func = state.getFunction('My\\Stuff\\my_multiplier'); - resultValue = await func(valueFactory.createInteger(4), valueFactory.createInteger(3)).toPromise(); + resultValue = await func.call([ + valueFactory.createInteger(4), + valueFactory.createInteger(3) + ]).toPromise(); expect(resultValue.getType()).to.equal('int'); expect(resultValue.getNative()).to.equal(12); @@ -833,9 +838,9 @@ describe('PHPState', function () { return numberToDoubleReference.getValue().getNative() * 2; }); - resultValue = await state.getFunction('double_it')( + resultValue = await state.getFunction('double_it').call([ valueFactory.createInteger(21) - ).toPromise(); + ]).toPromise(); expect(resultValue.getType()).to.equal('int'); expect(resultValue.getNative()).to.equal(42); @@ -850,9 +855,9 @@ describe('PHPState', function () { // Explicitly fetch via the namespace, to ensure we aren't just erroneously // allowing function names to contain backslashes - resultValue = await namespace.getFunction('double_it')( + resultValue = await namespace.getFunction('double_it').call([ valueFactory.createInteger(21) - ).toPromise(); + ]).toPromise(); expect(resultValue.getType()).to.equal('int'); expect(resultValue.getNative()).to.equal(42); @@ -907,8 +912,13 @@ describe('PHPState', function () { }); }); func = state.getFunction('My\\Stuff\\my_overloaded_func'); - resultValue1 = await func(valueFactory.createInteger(4)).toPromise(); - resultValue2 = await func(valueFactory.createString('this'), valueFactory.createString('that')).toPromise(); + resultValue1 = await func.call([ + valueFactory.createInteger(4) + ]).toPromise(); + resultValue2 = await func.call([ + valueFactory.createString('this'), + valueFactory.createString('that') + ]).toPromise(); expect(resultValue1.getType()).to.equal('string'); expect(resultValue1.getNative()).to.equal('My number was: 4'); @@ -935,8 +945,13 @@ describe('PHPState', function () { }); }); func = state.getFunction('My\\Stuff\\my_overloaded_func'); - resultValue1 = await func(valueFactory.createInteger(4)).toPromise(); - resultValue2 = await func(valueFactory.createString('this'), valueFactory.createString('that')).toPromise(); + resultValue1 = await func.call([ + valueFactory.createInteger(4) + ]).toPromise(); + resultValue2 = await func.call([ + valueFactory.createString('this'), + valueFactory.createString('that') + ]).toPromise(); expect(resultValue1.getType()).to.equal('string'); expect(resultValue1.getNative()).to.equal('My number was: 4'); @@ -1089,11 +1104,10 @@ describe('PHPState', function () { expect( ( await state.getFunction('My\\Stuff\\myFunc') - .call( - null, + .call([ valueFactory.createInteger(10), valueFactory.createInteger(7) - ) + ]) .toPromise() ) .getNative() diff --git a/test/unit/Value/ArrayTest.js b/test/unit/Value/ArrayTest.js index 92794dad..5c896b99 100644 --- a/test/unit/Value/ArrayTest.js +++ b/test/unit/Value/ArrayTest.js @@ -15,6 +15,7 @@ var expect = require('chai').expect, tools = require('../tools'), ArrayIterator = require('../../../src/Iterator/ArrayIterator'), ArrayValue = require('../../../src/Value/Array').sync(), + Callable = require('../../../src/Function/Callable'), CallStack = require('../../../src/CallStack'), Class = require('../../../src/Class').sync(), ElementReference = require('../../../src/Reference/Element'), @@ -22,7 +23,6 @@ var expect = require('chai').expect, IntegerValue = require('../../../src/Value/Integer').sync(), KeyReferencePair = require('../../../src/KeyReferencePair'), KeyValuePair = require('../../../src/KeyValuePair'), - MethodSpec = require('../../../src/MethodSpec'), Namespace = require('../../../src/Namespace').sync(), NamespaceScope = require('../../../src/NamespaceScope').sync(), ObjectValue = require('../../../src/Value/Object').sync(), @@ -1139,13 +1139,13 @@ describe('ArrayValue', function () { describe('isCallable()', function () { it('should return true for a valid instance method name of a given object', async function () { var classObject = sinon.createStubInstance(Class), - methodSpec = sinon.createStubInstance(MethodSpec), + methodCallable = sinon.createStubInstance(Callable), objectValue = sinon.createStubInstance(ObjectValue); objectValue.getClass.returns(classObject); objectValue.getType.returns('object'); - classObject.getMethodSpec + classObject.getMethodCallable .withArgs('myStaticMethod') - .returns(methodSpec); + .returns(methodCallable); globalNamespace.hasClass .withArgs('My\\Fqcn') .returns(true); @@ -1167,10 +1167,10 @@ describe('ArrayValue', function () { it('should return true for a valid static method name of a given class', async function () { var classObject = sinon.createStubInstance(Class), - methodSpec = sinon.createStubInstance(MethodSpec); - classObject.getMethodSpec + methodCallable = sinon.createStubInstance(Callable); + classObject.getMethodCallable .withArgs('myStaticMethod') - .returns(methodSpec); + .returns(methodCallable); globalNamespace.hasClass .withArgs('My\\Fqcn') .returns(true); @@ -1236,7 +1236,7 @@ describe('ArrayValue', function () { objectValue = sinon.createStubInstance(ObjectValue); objectValue.getClass.returns(classObject); objectValue.getType.returns('object'); - classObject.getMethodSpec + classObject.getMethodCallable .withArgs('myNonExistentStaticMethod') .returns(null); globalNamespace.hasClass @@ -1260,7 +1260,7 @@ describe('ArrayValue', function () { it('should return true for a non-existent static method', async function () { var classObject = sinon.createStubInstance(Class); - classObject.getMethodSpec + classObject.getMethodCallable .withArgs('myNonExistentStaticMethod') .returns(null); globalNamespace.hasClass @@ -1726,4 +1726,81 @@ describe('ArrayValue', function () { ); }); }); + + describe('unshift()', function () { + it('should return the new count of elements in the array', function () { + expect(value.unshift([factory.createString('new element')])).to.equal(3); + }); + + it('should return the new count when unshifting multiple values', function () { + expect(value.unshift([ + factory.createString('first new'), + factory.createString('second new') + ])).to.equal(4); + }); + + it('should increase the length of the array by the number of unshifted values', function () { + value.unshift([factory.createString('new element')]); + + expect(value.getLength()).to.equal(3); + }); + + it('should prepend multiple values, increasing the length accordingly', function () { + value.unshift([ + factory.createString('first new'), + factory.createString('second new') + ]); + + expect(value.getLength()).to.equal(4); + }); + + it('should renumber existing numeric keys to follow the new elements', function () { + elements.length = 0; + elements.push(createKeyValuePair(factory.createInteger(5), factory.createString('numeric el'))); + elements.push(createKeyValuePair(factory.createString('strKey'), factory.createString('string el'))); + createValue(); + + value.unshift([factory.createString('new element')]); + + // Existing numeric key (5) should be renumbered to 1 (after the new element at 0). + expect(value.getKeyByIndex(0).getNative()).to.equal(0); + expect(value.getElementByIndex(0).getValue().getNative()).to.equal('new element'); + expect(value.getKeyByIndex(1).getNative()).to.equal(1); + expect(value.getElementByIndex(1).getValue().getNative()).to.equal('numeric el'); + expect(value.getKeyByIndex(2).getNative()).to.equal('strKey'); + expect(value.getElementByIndex(2).getValue().getNative()).to.equal('string el'); + }); + + it('should preserve string keys unchanged', function () { + value.unshift([factory.createString('new element')]); + + expect(value.getKeyByIndex(1).getNative()).to.equal('firstEl'); + expect(value.getKeyByIndex(2).getNative()).to.equal('secondEl'); + }); + + it('should place the new element at index 0', function () { + var newValue = factory.createString('new element'); + + value.unshift([newValue]); + + expect(value.getElementByIndex(0).getValue()).to.equal(newValue); + }); + + it('should reset the internal pointer to the start of the array', function () { + value.setPointer(1); + + value.unshift([factory.createString('new element')]); + + expect(value.getPointer()).to.equal(0); + }); + + it('should increase the length by 1 when the array was empty', function () { + elements.length = 0; + createValue(); + + value.unshift([factory.createString('new element')]); + + expect(value.getLength()).to.equal(1); + }); + }); }); diff --git a/test/unit/Value/BarewordStringTest.js b/test/unit/Value/BarewordStringTest.js index 31284765..43e0f237 100644 --- a/test/unit/Value/BarewordStringTest.js +++ b/test/unit/Value/BarewordStringTest.js @@ -149,7 +149,7 @@ describe('BarewordStringValue', function () { namespaceScope.getClass.withArgs('My\\Space\\MyClass').returns(futureFactory.createPresent(classObject)); createValue('My\\Space\\MyClass'); - result = await value.callStaticMethod(methodNameValue, [argValue], false).toPromise(); + result = await value.callStaticMethod(methodNameValue, [argValue], null, false).toPromise(); expect(result).to.equal(resultValue); expect(classObject.callMethod).to.have.been.calledOnce; @@ -173,7 +173,7 @@ describe('BarewordStringValue', function () { namespaceScope.getClass.withArgs('My\\Space\\MyClass').returns(futureFactory.createPresent(classObject)); createValue('My\\Space\\MyClass'); - result = await value.callStaticMethod(methodNameValue, [argValue], true).toPromise(); + result = await value.callStaticMethod(methodNameValue, [argValue], null, true).toPromise(); expect(result).to.equal(resultValue); expect(classObject.callMethod).to.have.been.calledOnce; diff --git a/test/unit/Value/ObjectTest.js b/test/unit/Value/ObjectTest.js index cf720e8a..18e2ab28 100644 --- a/test/unit/Value/ObjectTest.js +++ b/test/unit/Value/ObjectTest.js @@ -16,6 +16,7 @@ var expect = require('chai').expect, ArrayIterator = require('../../../src/Iterator/ArrayIterator'), ArrayValue = require('../../../src/Value/Array').sync(), BooleanValue = require('../../../src/Value/Boolean').sync(), + Callable = require('../../../src/Function/Callable'), CallStack = require('../../../src/CallStack'), Class = require('../../../src/Class').sync(), Closure = require('../../../src/Closure').sync(), @@ -24,7 +25,6 @@ var expect = require('chai').expect, GeneratorIterator = require('../../../src/Iterator/GeneratorIterator'), IntegerValue = require('../../../src/Value/Integer').sync(), KeyValuePair = require('../../../src/KeyValuePair'), - MethodSpec = require('../../../src/MethodSpec'), Namespace = require('../../../src/Namespace').sync(), NamespaceScope = require('../../../src/NamespaceScope').sync(), ObjectElement = require('../../../src/Reference/ObjectElement'), @@ -67,7 +67,7 @@ describe('ObjectValue', function () { globalNamespace = sinon.createStubInstance(Namespace); referenceFactory = state.getReferenceFactory(); classObject = sinon.createStubInstance(Class); - classObject.getMethodSpec.returns(null); + classObject.getMethodCallable.returns(null); classObject.getName.returns('My\\Space\\AwesomeClass'); classObject.getSuperClass.returns(null); classObject.isAutoCoercionEnabled.returns(false); @@ -403,6 +403,7 @@ describe('ObjectValue', function () { expect(classObject.callMethod).to.have.been.calledWith( '__invoke', [sinon.match.same(argValue)], + sinon.match.any, sinon.match.same(value) ); }); @@ -420,6 +421,7 @@ describe('ObjectValue', function () { expect(classObject.callMethod).to.have.been.calledWith( 'myMethod', [sinon.match.same(argValue)], + sinon.match.any, sinon.match.same(value) ); }); @@ -432,7 +434,7 @@ describe('ObjectValue', function () { resultValue = sinon.createStubInstance(Value); classObject.callMethod.returns(resultValue); - expect(value.callStaticMethod(methodNameValue, [argValue], false)).to.equal(resultValue); + expect(value.callStaticMethod(methodNameValue, [argValue], null, false)).to.equal(resultValue); expect(classObject.callMethod).to.have.been.calledOnce; expect(classObject.callMethod).to.have.been.calledWith( 'myMethod', @@ -450,7 +452,7 @@ describe('ObjectValue', function () { resultValue = sinon.createStubInstance(Value); classObject.callMethod.returns(resultValue); - expect(value.callStaticMethod(methodNameValue, [argValue], true)).to.equal(resultValue); + expect(value.callStaticMethod(methodNameValue, [argValue], null, true)).to.equal(resultValue); expect(classObject.callMethod).to.have.been.calledOnce; expect(classObject.callMethod).to.have.been.calledWith( 'myMethod', @@ -896,9 +898,9 @@ describe('ObjectValue', function () { classObject.callMethod .withArgs('__toString') .returns(factory.createString('hello ')); - classObject.getMethodSpec + classObject.getMethodCallable .withArgs('__toString') - .returns(sinon.createStubInstance(MethodSpec)); + .returns(sinon.createStubInstance(Callable)); result = await value.concat(factory.createFloat(7.2)).toPromise(); @@ -911,9 +913,9 @@ describe('ObjectValue', function () { classObject.callMethod .withArgs('__toString') .returns(factory.createPresent(factory.createString('hello '))); - classObject.getMethodSpec + classObject.getMethodCallable .withArgs('__toString') - .returns(sinon.createStubInstance(MethodSpec)); + .returns(sinon.createStubInstance(Callable)); result = await value.concat(factory.createFloat(123.4)).toPromise(); @@ -946,9 +948,9 @@ describe('ObjectValue', function () { classObject.callMethod .withArgs('__toString') .returns(factory.createString('hello from my object')); - classObject.getMethodSpec + classObject.getMethodCallable .withArgs('__toString') - .returns(sinon.createStubInstance(MethodSpec)); + .returns(sinon.createStubInstance(Callable)); result = await value.convertForStringType().toPromise(); @@ -1241,7 +1243,7 @@ describe('ObjectValue', function () { elementValue = factory.createString('my value'), keyValue = factory.createString('my key'); classObject.callMethod - .withArgs('offsetGet', [keyValue], sinon.match.same(value)) + .withArgs('offsetGet', [keyValue], sinon.match.any, sinon.match.same(value)) .returns(elementValue); classObject.is .withArgs('ArrayAccess') @@ -2136,7 +2138,7 @@ describe('ObjectValue', function () { describe('isCallable()', function () { beforeEach(function () { - classObject.getMethodSpec + classObject.getMethodCallable .returns(null); classObject.is .withArgs('Closure') @@ -2152,10 +2154,9 @@ describe('ObjectValue', function () { }); it('should return true for an instance of a non-Closure class implementing ->__invoke()', async function () { - var methodSpec = sinon.createStubInstance(MethodSpec); - classObject.getMethodSpec + classObject.getMethodCallable .withArgs('__invoke') - .returns(methodSpec); + .returns(sinon.createStubInstance(Callable)); expect(await value.isCallable().toPromise()).to.be.true; }); @@ -2191,13 +2192,13 @@ describe('ObjectValue', function () { describe('isMethodDefined()', function () { it('should return true when the method is defined', function () { - classObject.getMethodSpec.withArgs('myMethod').returns(sinon.createStubInstance(MethodSpec)); + classObject.getMethodCallable.withArgs('myMethod').returns(sinon.createStubInstance(Callable)); expect(value.isMethodDefined('myMethod')).to.be.true; }); it('should return false when the method is not defined', function () { - classObject.getMethodSpec.withArgs('myMethod').returns(null); + classObject.getMethodCallable.withArgs('myMethod').returns(null); expect(value.isMethodDefined('myMethod')).to.be.false; }); diff --git a/test/unit/Value/StringTest.js b/test/unit/Value/StringTest.js index 5b0793da..17d0d823 100644 --- a/test/unit/Value/StringTest.js +++ b/test/unit/Value/StringTest.js @@ -15,12 +15,12 @@ var expect = require('chai').expect, tools = require('../tools'), ArrayValue = require('../../../src/Value/Array').sync(), BooleanValue = require('../../../src/Value/Boolean').sync(), + Callable = require('../../../src/Function/Callable'), CallStack = require('../../../src/CallStack'), Class = require('../../../src/Class').sync(), Exception = phpCommon.Exception, IntegerValue = require('../../../src/Value/Integer').sync(), KeyValuePair = require('../../../src/KeyValuePair'), - MethodSpec = require('../../../src/MethodSpec'), Namespace = require('../../../src/Namespace').sync(), NamespaceScope = require('../../../src/NamespaceScope').sync(), NumericParse = require('../../../src/Semantics/NumericParse'), @@ -363,18 +363,18 @@ describe('StringValue', function () { describe('call()', function () { it('should call the function and return its result when string only contains a function name', async function () { var argValue = sinon.createStubInstance(Value), + callable = sinon.createStubInstance(Callable), result, - resultValue = factory.createString('my result'), - func = sinon.stub().returns(resultValue); - globalNamespace.getFunction.withArgs('My\\Space\\my_function').returns(func); + resultValue = factory.createString('my result'); + callable.call.returns(resultValue); + globalNamespace.getFunction.withArgs('My\\Space\\my_function').returns(callable); createValue('My\\Space\\my_function'); result = await value.call([argValue], namespaceScope).toPromise(); expect(result).to.equal(resultValue); - expect(func).to.have.been.calledOnce; - expect(func).to.have.been.calledOn(null); - expect(func).to.have.been.calledWith(sinon.match.same(argValue)); + expect(callable.call).to.have.been.calledOnce; + expect(callable.call).to.have.been.calledWith([sinon.match.same(argValue)], null, null, null); }); it('should call the static method and return its result when string contains [class]::[method]', async function () { @@ -427,7 +427,7 @@ describe('StringValue', function () { .returns(futureFactory.createPresent(classObject)); createValue('\\My\\Space\\MyClass'); - result = await value.callStaticMethod(methodNameValue, [argValue], false).toPromise(); + result = await value.callStaticMethod(methodNameValue, [argValue], null, false).toPromise(); expect(result).to.equal(resultValue); expect(classObject.callMethod).to.have.been.calledOnce; @@ -452,7 +452,7 @@ describe('StringValue', function () { .returns(futureFactory.createPresent(classObject)); createValue('\\My\\Space\\MyClass'); - result = await value.callStaticMethod(methodNameValue, [argValue], true).toPromise(); + result = await value.callStaticMethod(methodNameValue, [argValue], null, true).toPromise(); expect(result).to.equal(resultValue); expect(classObject.callMethod).to.have.been.calledOnce; @@ -1529,16 +1529,16 @@ describe('StringValue', function () { it('should return true for a static method name that exists', async function () { var classObject = sinon.createStubInstance(Class), - methodSpec = sinon.createStubInstance(MethodSpec); + methodCallable = sinon.createStubInstance(Callable); globalNamespace.getClass .withArgs('My\\Fqcn') .returns(futureFactory.createPresent(classObject)); globalNamespace.hasClass .withArgs('My\\Fqcn') .returns(true); - classObject.getMethodSpec + classObject.getMethodCallable .withArgs('myMethod') - .returns(methodSpec); + .returns(methodCallable); createValue('My\\Fqcn::myMethod'); expect(await value.isCallable(globalNamespace).toPromise()).to.be.true; @@ -1561,7 +1561,7 @@ describe('StringValue', function () { globalNamespace.hasClass .withArgs('My\\Fqcn') .returns(true); - classObject.getMethodSpec + classObject.getMethodCallable .withArgs('myMethod') .returns(null); createValue('My\\Fqcn::myMethod'); diff --git a/test/unit/ValueFactoryTest.js b/test/unit/ValueFactoryTest.js index 97433011..c947937d 100644 --- a/test/unit/ValueFactoryTest.js +++ b/test/unit/ValueFactoryTest.js @@ -72,16 +72,16 @@ describe('ValueFactory', function () { 'future_factory': function (set, get) { var futureFactory; - function TrackedFuture() { - Future.apply(this, arguments); + function TrackedFuture(...args) { + Future.apply(this, args); futuresCreated++; } util.inherits(TrackedFuture, Future); - function TrackedPresent() { - Present.apply(this, arguments); + function TrackedPresent(...args) { + Present.apply(this, args); presentsCreated++; } @@ -467,7 +467,7 @@ describe('ValueFactory', function () { .withArgs('Closure') .returns(futureFactory.createPresent(closureClassObject)); closureClassObject.instantiateWithInternals - .withArgs(sinon.match.any, { + .withArgs(sinon.match.any, null, { closure: sinon.match.same(closure) }) .returns(objectValue); @@ -483,7 +483,7 @@ describe('ValueFactory', function () { .withArgs('Closure') .returns(futureFactory.createPresent(closureClassObject)); closureClassObject.instantiateWithInternals - .withArgs(sinon.match.any, { + .withArgs(sinon.match.any, null, { closure: sinon.match.same(closure) }) .returns(objectValue); @@ -575,6 +575,7 @@ describe('ValueFactory', function () { expect(myClassObject.instantiate).to.have.been.calledWith( sinon.match.any, + null, [ 'my context', sinon.match.any @@ -599,6 +600,7 @@ describe('ValueFactory', function () { expect(myClassObject.instantiate).to.have.been.calledWith( sinon.match.any, + null, [ sinon.match.any, true @@ -779,7 +781,6 @@ describe('ValueFactory', function () { .callsFake(function (instance) { return instance.getObject(); }); - JSObjectClass.getMethodSpec.withArgs('__get').returns({}); JSObjectClass.getName.returns('JSObject'); JSObjectClass.getSuperClass.returns(null); JSObjectClass.is.withArgs('JSObject').returns(true); @@ -972,7 +973,7 @@ describe('ValueFactory', function () { .returns(objectValue); expect(factory.createGeneratorObject(call, innerFunction)).to.equal(objectValue); - iterator = generatorClassObject.instantiateWithInternals.args[0][1].iterator; + iterator = generatorClassObject.instantiateWithInternals.args[0][2].iterator; expect(iterator).to.be.an.instanceOf(GeneratorIterator); expect(iterator.getInnerFunction()).to.equal(innerFunction); expect(iterator.getFunctionCall()).to.equal(call); diff --git a/test/unit/builtin/classes/ClosureTest.js b/test/unit/builtin/classes/ClosureTest.js index 3549e132..cd14655d 100644 --- a/test/unit/builtin/classes/ClosureTest.js +++ b/test/unit/builtin/classes/ClosureTest.js @@ -81,7 +81,7 @@ describe('PHP builtin Closure class', function () { InternalClosureClass = closureClassFactory(internals); closureClass = sinon.createStubInstance(Class); closureClass.getName.returns('Closure'); - closureClass.instantiateWithInternals.callsFake(function (args, internals) { + closureClass.instantiateWithInternals.callsFake(function (positionalArgs, namedArgs, internals) { var objectValue = valueFactory.createObject({}, closureClass); _.forOwn(internals, function (value, name) { @@ -370,6 +370,7 @@ describe('PHP builtin Closure class', function () { expect(closure.invoke).to.have.been.calledOnce; expect(closure.invoke).to.have.been.calledWith( sinon.match.any, + null, sinon.match.same(coercedThisObject) ); }); @@ -458,6 +459,7 @@ describe('PHP builtin Closure class', function () { expect(closure.invoke).to.have.been.calledOnce; expect(closure.invoke).to.have.been.calledWith( sinon.match.any, + null, sinon.match.same(coercedThisObject) ); }); @@ -536,6 +538,7 @@ describe('PHP builtin Closure class', function () { expect(closure.invoke).to.have.been.calledOnce; expect(closure.invoke).to.have.been.calledWith( sinon.match.any, + null, sinon.match.same(coercedThisObject) ); }); diff --git a/test/when.js b/test/when.js index 1377fa6f..b5293c7b 100644 --- a/test/when.js +++ b/test/when.js @@ -4,9 +4,9 @@ * @deprecated Remove this. */ module.exports = function (done, callback) { - return function () { + return function (...args) { try { - callback.apply(this, arguments); + callback.apply(this, args); done(); } catch (error) { done(error);