diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c8014962..abe5cf9c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 1.35.0 + +- **[Breaking]** Upgraded to TypeScript 6.0 + Thanks @RealColdFry for the following fixes: +- Fixed many bugs with try/catch/finally + - Also fixed some bugs with try/catch/finally in promises +- Fixed a bug where collection iterators would not correctly update when the collection updated +- No longer generate dead code after `break` statements +- Fixed a bug where math.atan2 was incorrectly used instead of math.atan for Lua 5.4 +- Fixed some inconsistencies with number constants (Number.MAX_SAFE_INTEGER, Number.MIN_VALUE, etc) +- Fixed a bug with the `>>>` operator for Lua 5.3 +- Fixed incorrect side effects for array destructors +- Fixed broken code when generating requires for files with `.` in the file name (e.g. `foo.tests.ts`), now the periods will be translated to `_` +- Fixed some incorrect handling of synthetic nodes +- Fixed a bug where object spread could lead to incorrect code being generated +- Fixed a bug with Object.defineProperty sharing values accross object instances + ## 1.34.0 - Added support for the Lua 5.5 target (it mostly does the same as the 5.4 target for now) diff --git a/benchmark/tsconfig.json b/benchmark/tsconfig.json index e1c9c2941..5422f1770 100644 --- a/benchmark/tsconfig.json +++ b/benchmark/tsconfig.json @@ -4,7 +4,8 @@ "lib": ["esnext"], // Dev types are JIT "types": ["lua-types/jit", "@typescript-to-lua/language-extensions"], - "moduleResolution": "node", + "module": "nodenext", + "moduleResolution": "nodenext", "outDir": "dist", "rootDir": "src", "strict": true, diff --git a/package-lock.json b/package-lock.json index 1b482b466..28a32847e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "typescript-to-lua", - "version": "1.34.0", + "version": "1.36.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "typescript-to-lua", - "version": "1.34.0", + "version": "1.36.0", "license": "MIT", "dependencies": { "@typescript-to-lua/language-extensions": "1.19.0", @@ -25,25 +25,25 @@ "@types/node": "^22.10.0", "@types/picomatch": "^2.3.0", "@types/resolve": "1.14.0", - "eslint": "^9.22.0", - "eslint-plugin-jest": "^28.8.3", + "eslint": "^9.39.4", + "eslint-plugin-jest": "^28.14.0", "fs-extra": "^8.1.0", "javascript-stringify": "^2.0.1", - "jest": "^29.5.0", + "jest": "^29.7.0", "jest-circus": "^29.7.0", "lua-types": "^2.14.1", "lua-wasm-bindings": "^0.5.3", "prettier": "^2.8.8", - "ts-jest": "^29.2.5", + "ts-jest": "^29.4.9", "ts-node": "^10.9.2", - "typescript": "5.9.3", - "typescript-eslint": "^8.46.3" + "typescript": "6.0.2", + "typescript-eslint": "^8.58.0" }, "engines": { "node": ">=16.10.0" }, "peerDependencies": { - "typescript": "5.9.3" + "typescript": "6.0.2" } }, "node_modules/@ampproject/remapping": { @@ -616,9 +616,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -648,24 +648,25 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -698,20 +699,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -722,9 +723,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -1317,44 +1318,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1602,22 +1565,21 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz", - "integrity": "sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", + "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.3", - "@typescript-eslint/type-utils": "8.46.3", - "@typescript-eslint/utils": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/type-utils": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1627,9 +1589,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.46.3", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "@typescript-eslint/parser": "^8.58.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -1643,18 +1605,18 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.3.tgz", - "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz", + "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.46.3", - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1664,20 +1626,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.3.tgz", - "integrity": "sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz", + "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.3", - "@typescript-eslint/types": "^8.46.3", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.58.0", + "@typescript-eslint/types": "^8.58.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1687,18 +1649,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.3.tgz", - "integrity": "sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz", + "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3" + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1709,9 +1671,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.3.tgz", - "integrity": "sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz", + "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==", "dev": true, "license": "MIT", "engines": { @@ -1722,21 +1684,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.3.tgz", - "integrity": "sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz", + "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3", - "@typescript-eslint/utils": "8.46.3", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1746,14 +1708,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.3.tgz", - "integrity": "sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", + "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", "dev": true, "license": "MIT", "engines": { @@ -1765,22 +1727,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.3.tgz", - "integrity": "sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz", + "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.3", - "@typescript-eslint/tsconfig-utils": "8.46.3", - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/project-service": "8.58.0", + "@typescript-eslint/tsconfig-utils": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1790,46 +1751,59 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.3.tgz", - "integrity": "sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz", + "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.3", - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1839,19 +1813,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.3.tgz", - "integrity": "sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz", + "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.3", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.58.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1861,6 +1835,19 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@typescript-to-lua/language-extensions": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/@typescript-to-lua/language-extensions/-/language-extensions-1.19.0.tgz", @@ -1905,9 +1892,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -1991,13 +1978,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -2132,9 +2112,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -2421,9 +2401,9 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -2500,22 +2480,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/electron-to-chromium": { "version": "1.5.27", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.27.tgz", @@ -2590,26 +2554,26 @@ } }, "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -2628,7 +2592,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -2651,9 +2615,9 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "28.8.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.8.3.tgz", - "integrity": "sha512-HIQ3t9hASLKm2IhIOqnu+ifw7uLZkIlR7RYNv7fMcEi/p0CIiJmfriStQS2LDkgtY4nyLbIZAD+JL347Yc2ETQ==", + "version": "28.14.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.14.0.tgz", + "integrity": "sha512-P9s/qXSMTpRTerE2FQ0qJet2gKbcGyFTPAJipoKxmWqR6uuFqIqk8FuEfg5yBieOezVrEfAMZrEwJ6yEp+1MFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2918,36 +2882,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/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/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2962,16 +2896,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -2995,39 +2919,6 @@ "node": ">=16.0.0" } }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3073,9 +2964,9 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -3222,12 +3113,37 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } }, "node_modules/has-flag": { "version": "4.0.0", @@ -3520,25 +3436,6 @@ "node": ">=8" } }, - "node_modules/jake": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/javascript-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz", @@ -3594,6 +3491,7 @@ "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -4711,16 +4609,6 @@ "dev": true, "license": "MIT" }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -4758,6 +4646,16 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4772,6 +4670,13 @@ "dev": true, "license": "MIT" }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4971,9 +4876,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -5156,27 +5061,6 @@ ], "license": "MIT" }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -5254,45 +5138,10 @@ "node": ">=10" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -5533,6 +5382,55 @@ "node": ">=8" } }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -5554,9 +5452,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -5567,19 +5465,20 @@ } }, "node_modules/ts-jest": { - "version": "29.2.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", - "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "version": "29.4.9", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz", + "integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==", "dev": true, + "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", - "ejs": "^3.1.10", "fast-json-stable-stringify": "^2.1.0", - "jest-util": "^29.0.0", + "handlebars": "^4.7.9", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.6.3", + "semver": "^7.7.4", + "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "bin": { @@ -5590,11 +5489,12 @@ }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0", - "@jest/types": "^29.0.0", - "babel-jest": "^29.0.0", - "jest": "^29.0.0", - "typescript": ">=4.3 <6" + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" }, "peerDependenciesMeta": { "@babel/core": { @@ -5611,9 +5511,25 @@ }, "esbuild": { "optional": true + }, + "jest-util": { + "optional": true } } }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -5695,9 +5611,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -5710,16 +5626,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.3.tgz", - "integrity": "sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.0.tgz", + "integrity": "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.46.3", - "@typescript-eslint/parser": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3", - "@typescript-eslint/utils": "8.46.3" + "@typescript-eslint/eslint-plugin": "8.58.0", + "@typescript-eslint/parser": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5729,8 +5645,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" } }, "node_modules/undici-types": { @@ -5848,6 +5778,13 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index fe6771138..aff9ba384 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "typescript-to-lua", - "version": "1.34.0", + "version": "1.36.0", "description": "A generic TypeScript to Lua transpiler. Write your code in TypeScript and publish Lua!", "repository": "https://github.com/TypeScriptToLua/TypeScriptToLua", "homepage": "https://typescripttolua.github.io/", @@ -31,7 +31,7 @@ "lint:prettier": "prettier --check . || (echo 'Run `npm run fix:prettier` to fix it.' && exit 1)", "lint:eslint": "eslint .", "fix:prettier": "prettier --write .", - "check:language-extensions": "tsc --strict language-extensions/index.d.ts", + "check:language-extensions": "tsc --strict --ignoreConfig language-extensions/index.d.ts", "preversion": "npm run build && npm test", "postversion": "git push && git push --tags" }, @@ -42,7 +42,7 @@ "node": ">=16.10.0" }, "peerDependencies": { - "typescript": "5.9.3" + "typescript": "6.0.2" }, "dependencies": { "@typescript-to-lua/language-extensions": "1.19.0", @@ -58,18 +58,18 @@ "@types/node": "^22.10.0", "@types/picomatch": "^2.3.0", "@types/resolve": "1.14.0", - "eslint": "^9.22.0", - "eslint-plugin-jest": "^28.8.3", + "eslint": "^9.39.4", + "eslint-plugin-jest": "^28.14.0", "fs-extra": "^8.1.0", "javascript-stringify": "^2.0.1", - "jest": "^29.5.0", + "jest": "^29.7.0", "jest-circus": "^29.7.0", "lua-types": "^2.14.1", "lua-wasm-bindings": "^0.5.3", "prettier": "^2.8.8", - "ts-jest": "^29.2.5", + "ts-jest": "^29.4.9", "ts-node": "^10.9.2", - "typescript": "5.9.3", - "typescript-eslint": "^8.46.3" + "typescript": "6.0.2", + "typescript-eslint": "^8.58.0" } } diff --git a/src/CompilerOptions.ts b/src/CompilerOptions.ts index 47ada916e..1a167e7c3 100644 --- a/src/CompilerOptions.ts +++ b/src/CompilerOptions.ts @@ -99,9 +99,5 @@ export function validateOptions(options: CompilerOptions): ts.Diagnostic[] { diagnostics.push(diagnosticFactories.unsupportedJsxEmit()); } - if (options.paths && !options.baseUrl) { - diagnostics.push(diagnosticFactories.pathsWithoutBaseUrl()); - } - return diagnostics; } diff --git a/src/LuaPrinter.ts b/src/LuaPrinter.ts index b0a02dfaf..26b55421b 100644 --- a/src/LuaPrinter.ts +++ b/src/LuaPrinter.ts @@ -328,7 +328,7 @@ export class LuaPrinter { statementNodes.push(node); - if (lua.isReturnStatement(statement)) break; + if (lua.isReturnStatement(statement) || lua.isBreakStatement(statement)) break; } return statementNodes.length > 0 ? [...intersperse(statementNodes, "\n"), "\n"] : []; diff --git a/src/lualib/Map.ts b/src/lualib/Map.ts index 16fa7e453..c2bcb43a4 100644 --- a/src/lualib/Map.ts +++ b/src/lualib/Map.ts @@ -113,46 +113,64 @@ export class Map { } public entries(): IterableIterator<[K, V]> { + const getFirstKey = () => this.firstKey; const { items, nextKey } = this; - let key = this.firstKey; + let key: K | undefined; + let started = false; return { [Symbol.iterator](): IterableIterator<[K, V]> { return this; }, next(): IteratorResult<[K, V]> { - const result = { done: !key, value: [key, items.get(key!)] as [K, V] }; - key = nextKey.get(key!); - return result; + if (!started) { + started = true; + key = getFirstKey(); + } else { + key = nextKey.get(key!); + } + return { done: !key, value: [key!, items.get(key!)] as [K, V] }; }, }; } public keys(): IterableIterator { + const getFirstKey = () => this.firstKey; const nextKey = this.nextKey; - let key = this.firstKey; + let key: K | undefined; + let started = false; return { [Symbol.iterator](): IterableIterator { return this; }, next(): IteratorResult { - const result = { done: !key, value: key }; - key = nextKey.get(key!); - return result as IteratorResult; + if (!started) { + started = true; + key = getFirstKey(); + } else { + key = nextKey.get(key!); + } + return { done: !key, value: key! }; }, }; } public values(): IterableIterator { + const getFirstKey = () => this.firstKey; const { items, nextKey } = this; - let key = this.firstKey; + let key: K | undefined; + let started = false; return { [Symbol.iterator](): IterableIterator { return this; }, next(): IteratorResult { - const result = { done: !key, value: items.get(key!) }; - key = nextKey.get(key!); - return result; + if (!started) { + started = true; + key = getFirstKey(); + } else { + key = nextKey.get(key!); + } + return { done: !key, value: items.get(key!) }; }, }; } diff --git a/src/lualib/ObjectAssign.ts b/src/lualib/ObjectAssign.ts index a7f691433..7d0a3a9b1 100644 --- a/src/lualib/ObjectAssign.ts +++ b/src/lualib/ObjectAssign.ts @@ -2,8 +2,10 @@ export function __TS__ObjectAssign(this: void, target: T, ...sources: T[]): T { for (const i of $range(1, sources.length)) { const source = sources[i - 1]; - for (const key in source) { - target[key] = source[key]; + if (type(source) === "table") { + for (const key in source) { + target[key] = source[key]; + } } } diff --git a/src/lualib/Promise.ts b/src/lualib/Promise.ts index c5b6aa2a5..187e5380d 100644 --- a/src/lualib/Promise.ts +++ b/src/lualib/Promise.ts @@ -45,7 +45,6 @@ export class __TS__Promise implements Promise { private fulfilledCallbacks: Array> = []; private rejectedCallbacks: PromiseReject[] = []; - private finallyCallbacks: Array<() => void> = []; // @ts-ignore public [Symbol.toStringTag]: string; // Required to implement interface, no output Lua @@ -124,16 +123,23 @@ export class __TS__Promise implements Promise { } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally + // Delegates to .then() so that a new Promise is returned (per ES spec §27.2.5.3) + // and the original fulfillment value / rejection reason is preserved. public finally(onFinally?: () => void): Promise { - if (onFinally) { - this.finallyCallbacks.push(onFinally); - - if (this.state !== PromiseState.Pending) { - // If promise already resolved or rejected, immediately fire finally callback - onFinally(); - } - } - return this; + return this.then( + onFinally + ? (value: T): T => { + onFinally(); + return value; + } + : undefined, + onFinally + ? (reason: any): never => { + onFinally(); + throw reason; + } + : undefined + ); } private resolve(value: T | PromiseLike): void { @@ -168,25 +174,13 @@ export class __TS__Promise implements Promise { private invokeCallbacks(callbacks: ReadonlyArray<(value: T) => void>, value: T): void { const callbacksLength = callbacks.length; - const finallyCallbacks = this.finallyCallbacks; - const finallyCallbacksLength = finallyCallbacks.length; if (callbacksLength !== 0) { for (const i of $range(1, callbacksLength - 1)) { callbacks[i - 1](value); } // Tail call optimization for a common case. - if (finallyCallbacksLength === 0) { - return callbacks[callbacksLength - 1](value); - } - callbacks[callbacksLength - 1](value); - } - - if (finallyCallbacksLength !== 0) { - for (const i of $range(1, finallyCallbacksLength - 1)) { - finallyCallbacks[i - 1](); - } - return finallyCallbacks[finallyCallbacksLength - 1](); + return callbacks[callbacksLength - 1](value); } } diff --git a/src/lualib/Set.ts b/src/lualib/Set.ts index b46555edf..e302e4094 100644 --- a/src/lualib/Set.ts +++ b/src/lualib/Set.ts @@ -102,46 +102,64 @@ export class Set { } public entries(): IterableIterator<[T, T]> { + const getFirstKey = () => this.firstKey; const nextKey = this.nextKey; - let key: T = this.firstKey!; + let key: T | undefined; + let started = false; return { [Symbol.iterator](): IterableIterator<[T, T]> { return this; }, next(): IteratorResult<[T, T]> { - const result = { done: !key, value: [key, key] as [T, T] }; - key = nextKey.get(key); - return result; + if (!started) { + started = true; + key = getFirstKey(); + } else { + key = nextKey.get(key!); + } + return { done: !key, value: [key!, key!] as [T, T] }; }, }; } public keys(): IterableIterator { + const getFirstKey = () => this.firstKey; const nextKey = this.nextKey; - let key: T = this.firstKey!; + let key: T | undefined; + let started = false; return { [Symbol.iterator](): IterableIterator { return this; }, next(): IteratorResult { - const result = { done: !key, value: key }; - key = nextKey.get(key); - return result; + if (!started) { + started = true; + key = getFirstKey(); + } else { + key = nextKey.get(key!); + } + return { done: !key, value: key! }; }, }; } public values(): IterableIterator { + const getFirstKey = () => this.firstKey; const nextKey = this.nextKey; - let key: T = this.firstKey!; + let key: T | undefined; + let started = false; return { [Symbol.iterator](): IterableIterator { return this; }, next(): IteratorResult { - const result = { done: !key, value: key }; - key = nextKey.get(key); - return result; + if (!started) { + started = true; + key = getFirstKey(); + } else { + key = nextKey.get(key!); + } + return { done: !key, value: key! }; }, }; } diff --git a/src/lualib/SetDescriptor.ts b/src/lualib/SetDescriptor.ts index 4339c8cc1..ccb4bf3f0 100644 --- a/src/lualib/SetDescriptor.ts +++ b/src/lualib/SetDescriptor.ts @@ -26,6 +26,17 @@ export function __TS__SetDescriptor( setmetatable(target, metatable); } + // When setting a descriptor on an instance (not a prototype), ensure it has + // its own metatable so descriptors are not shared across instances. + // See: https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1625 + if (!isPrototype && !rawget(metatable, "_isOwnDescriptorMetatable")) { + const instanceMetatable: any = {}; + instanceMetatable._isOwnDescriptorMetatable = true; + setmetatable(instanceMetatable, metatable); + setmetatable(target, instanceMetatable); + metatable = instanceMetatable; + } + const value = rawget(target, key); if (value !== undefined) rawset(target, key, undefined); diff --git a/src/transformation/builtins/math.ts b/src/transformation/builtins/math.ts index ab831d52f..82a118ec1 100644 --- a/src/transformation/builtins/math.ts +++ b/src/transformation/builtins/math.ts @@ -42,14 +42,20 @@ export function transformMathCall( const expressionName = calledMethod.name.text; switch (expressionName) { - // Lua 5.3: math.atan(y, x) + // Lua 5.3+: math.atan(y, x) // Otherwise: math.atan2(y, x) case "atan2": { if (context.luaTarget === LuaTarget.Universal) { return transformLuaLibFunction(context, LuaLibFeature.MathAtan2, node, ...params); } - const method = lua.createStringLiteral(context.luaTarget === LuaTarget.Lua53 ? "atan" : "atan2"); + const useAtan2 = + context.luaTarget === LuaTarget.Lua50 || + context.luaTarget === LuaTarget.Lua51 || + context.luaTarget === LuaTarget.Lua52 || + context.luaTarget === LuaTarget.LuaJIT || + context.luaTarget === LuaTarget.Luau; + const method = lua.createStringLiteral(useAtan2 ? "atan2" : "atan"); return lua.createCallExpression(lua.createTableIndexExpression(math, method), params, node); } diff --git a/src/transformation/builtins/number.ts b/src/transformation/builtins/number.ts index 17c44b6fd..ecb98da64 100644 --- a/src/transformation/builtins/number.ts +++ b/src/transformation/builtins/number.ts @@ -77,31 +77,62 @@ export function transformNumberProperty( node ); case "MIN_VALUE": + // 2 ^ -1074 = 5e-324 (smallest positive double) return lua.createBinaryExpression( - lua.createNumericLiteral(-2), - lua.createNumericLiteral(1074), + lua.createNumericLiteral(2), + lua.createNumericLiteral(-1074), lua.SyntaxKind.PowerOperator, node ); case "MIN_SAFE_INTEGER": - return lua.createBinaryExpression( - lua.createNumericLiteral(-2), - lua.createNumericLiteral(1074), - lua.SyntaxKind.PowerOperator, + // -(2 ^ 53 - 1) = -9007199254740991 + return lua.createUnaryExpression( + lua.createParenthesizedExpression( + lua.createBinaryExpression( + lua.createBinaryExpression( + lua.createNumericLiteral(2), + lua.createNumericLiteral(53), + lua.SyntaxKind.PowerOperator + ), + lua.createNumericLiteral(1), + lua.SyntaxKind.SubtractionOperator + ) + ), + lua.SyntaxKind.NegationOperator, node ); case "MAX_SAFE_INTEGER": + // 2 ^ 53 - 1 = 9007199254740991 return lua.createBinaryExpression( - lua.createNumericLiteral(2), - lua.createNumericLiteral(1024), - lua.SyntaxKind.PowerOperator, + lua.createBinaryExpression( + lua.createNumericLiteral(2), + lua.createNumericLiteral(53), + lua.SyntaxKind.PowerOperator + ), + lua.createNumericLiteral(1), + lua.SyntaxKind.SubtractionOperator, node ); case "MAX_VALUE": + // (2 - 2 ^ -52) * 2 ^ 1023 = 1.7976931348623157e+308 return lua.createBinaryExpression( - lua.createNumericLiteral(2), - lua.createNumericLiteral(1024), - lua.SyntaxKind.PowerOperator, + lua.createParenthesizedExpression( + lua.createBinaryExpression( + lua.createNumericLiteral(2), + lua.createBinaryExpression( + lua.createNumericLiteral(2), + lua.createNumericLiteral(-52), + lua.SyntaxKind.PowerOperator + ), + lua.SyntaxKind.SubtractionOperator + ) + ), + lua.createBinaryExpression( + lua.createNumericLiteral(2), + lua.createNumericLiteral(1023), + lua.SyntaxKind.PowerOperator + ), + lua.SyntaxKind.MultiplicationOperator, node ); diff --git a/src/transformation/utils/function-context.ts b/src/transformation/utils/function-context.ts index d61ffb5bf..e28950e2e 100644 --- a/src/transformation/utils/function-context.ts +++ b/src/transformation/utils/function-context.ts @@ -108,9 +108,7 @@ function computeDeclarationContextType(context: TransformationContext, signature const thisParameter = getExplicitThisParameter(signatureDeclaration); if (thisParameter) { // Explicit 'this' - return thisParameter.type && thisParameter.type.kind === ts.SyntaxKind.VoidKeyword - ? ContextType.Void - : ContextType.NonVoid; + return thisParameter.type?.kind === ts.SyntaxKind.VoidKeyword ? ContextType.Void : ContextType.NonVoid; } // noSelf declaration on function signature diff --git a/src/transformation/utils/scope.ts b/src/transformation/utils/scope.ts index fa66ee3e2..314f1d795 100644 --- a/src/transformation/utils/scope.ts +++ b/src/transformation/utils/scope.ts @@ -38,6 +38,9 @@ export interface Scope { importStatements?: lua.Statement[]; loopContinued?: LoopContinued; functionReturned?: boolean; + asyncTryHasReturn?: boolean; + asyncTryHasBreak?: boolean; + asyncTryHasContinue?: LoopContinued; } export interface HoistingResult { @@ -84,6 +87,23 @@ export function findScope(context: TransformationContext, scopeTypes: ScopeType) } } +export function findAsyncTryScopeInStack(context: TransformationContext): Scope | undefined { + for (const scope of walkScopesUp(context)) { + if (scope.type === ScopeType.Function) return undefined; + if (scope.type === ScopeType.Try || scope.type === ScopeType.Catch) return scope; + } + return undefined; +} + +/** Like findAsyncTryScopeInStack, but also stops at Loop boundaries. */ +export function findAsyncTryScopeBeforeLoop(context: TransformationContext): Scope | undefined { + for (const scope of walkScopesUp(context)) { + if (scope.type === ScopeType.Function || scope.type === ScopeType.Loop) return undefined; + if (scope.type === ScopeType.Try || scope.type === ScopeType.Catch) return scope; + } + return undefined; +} + export function addScopeVariableDeclaration(scope: Scope, declaration: lua.VariableDeclarationStatement) { scope.variableDeclarations ??= []; diff --git a/src/transformation/utils/typescript/index.ts b/src/transformation/utils/typescript/index.ts index e5984b08f..f12bf4721 100644 --- a/src/transformation/utils/typescript/index.ts +++ b/src/transformation/utils/typescript/index.ts @@ -14,12 +14,14 @@ export function hasExportEquals(sourceFile: ts.SourceFile): boolean { * Search up until finding a node satisfying the callback */ export function findFirstNodeAbove(node: ts.Node, callback: (n: ts.Node) => n is T): T | undefined { - let current = node; + // Synthetic nodes (created by pre-transformers like usingTransformer) may have an unset .parent. + // Fall back to ts.getOriginalNode so we can still walk the source-parsed parent chain. + let current = ts.getOriginalNode(node); while (current.parent) { if (callback(current.parent)) { return current.parent; } else { - current = current.parent; + current = ts.getOriginalNode(current.parent); } } } diff --git a/src/transformation/visitors/binary-expression/bit.ts b/src/transformation/visitors/binary-expression/bit.ts index 79e9c1f3c..5ae876630 100644 --- a/src/transformation/visitors/binary-expression/bit.ts +++ b/src/transformation/visitors/binary-expression/bit.ts @@ -75,6 +75,22 @@ export function transformBinaryBitOperation( case LuaTarget.Lua52: return transformBinaryBitLibOperation(node, left, right, operator, "bit32"); default: + // Lua 5.3+ `>>` is arithmetic (sign-extending), but TS `>>>` is logical (zero-fill). + // Emit `(left & 0xFFFFFFFF) >> right` to convert to unsigned 32-bit first. + if (operator === ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken) { + const mask = lua.createBinaryExpression( + left, + lua.createNumericLiteral(0xffffffff, node), + lua.SyntaxKind.BitwiseAndOperator, + node + ); + return lua.createBinaryExpression( + lua.createParenthesizedExpression(mask, node), + right, + lua.SyntaxKind.BitwiseRightShiftOperator, + node + ); + } const luaOperator = transformBitOperatorToLuaOperator(context, node, operator); return lua.createBinaryExpression(left, right, luaOperator, node); } diff --git a/src/transformation/visitors/break-continue.ts b/src/transformation/visitors/break-continue.ts index 2e64a38eb..e41a7934b 100644 --- a/src/transformation/visitors/break-continue.ts +++ b/src/transformation/visitors/break-continue.ts @@ -2,10 +2,22 @@ import * as ts from "typescript"; import { LuaTarget } from "../../CompilerOptions"; import * as lua from "../../LuaAST"; import { FunctionVisitor } from "../context"; -import { findScope, LoopContinued, ScopeType } from "../utils/scope"; +import { findAsyncTryScopeBeforeLoop, findScope, LoopContinued, ScopeType } from "../utils/scope"; +import { isInAsyncFunction } from "../utils/typescript"; export const transformBreakStatement: FunctionVisitor = (breakStatement, context) => { - void context; + const tryScope = isInAsyncFunction(breakStatement) ? findAsyncTryScopeBeforeLoop(context) : undefined; + if (tryScope) { + tryScope.asyncTryHasBreak = true; + return [ + lua.createAssignmentStatement( + lua.createIdentifier("____hasBroken"), + lua.createBooleanLiteral(true), + breakStatement + ), + lua.createReturnStatement([], breakStatement), + ]; + } return lua.createBreakStatement(breakStatement); }; @@ -28,6 +40,19 @@ export const transformContinueStatement: FunctionVisitor = scope.loopContinued = continuedWith; } + const tryScope = isInAsyncFunction(statement) ? findAsyncTryScopeBeforeLoop(context) : undefined; + if (tryScope) { + tryScope.asyncTryHasContinue = continuedWith; + return [ + lua.createAssignmentStatement( + lua.createIdentifier("____hasContinued"), + lua.createBooleanLiteral(true), + statement + ), + lua.createReturnStatement([], statement), + ]; + } + const label = `__continue${scope?.id ?? ""}`; switch (continuedWith) { diff --git a/src/transformation/visitors/errors.ts b/src/transformation/visitors/errors.ts index f81519e4c..77067dda8 100644 --- a/src/transformation/visitors/errors.ts +++ b/src/transformation/visitors/errors.ts @@ -5,7 +5,7 @@ import { FunctionVisitor, TransformationContext } from "../context"; import { unsupportedForTarget, unsupportedForTargetButOverrideAvailable } from "../utils/diagnostics"; import { createUnpackCall } from "../utils/lua-ast"; import { transformLuaLibFunction } from "../utils/lualib"; -import { Scope, ScopeType } from "../utils/scope"; +import { findScope, LoopContinued, Scope, ScopeType } from "../utils/scope"; import { isInAsyncFunction, isInGeneratorFunction } from "../utils/typescript"; import { wrapInAsyncAwaiter } from "./async-await"; import { transformScopeBlock } from "./block"; @@ -14,7 +14,7 @@ import { isInMultiReturnFunction } from "./language-extensions/multi"; import { createReturnStatement } from "./return"; const transformAsyncTry: FunctionVisitor = (statement, context) => { - const [tryBlock] = transformScopeBlock(context, statement.tryBlock, ScopeType.Try); + const [tryBlock, tryScope] = transformScopeBlock(context, statement.tryBlock, ScopeType.Try); if ( (context.options.luaTarget === LuaTarget.Lua50 || context.options.luaTarget === LuaTarget.Lua51) && @@ -31,45 +31,116 @@ const transformAsyncTry: FunctionVisitor = (statement, context) return tryBlock.statements; } - // __TS__AsyncAwaiter() + // __TS__AsyncAwaiter() const awaiter = wrapInAsyncAwaiter(context, tryBlock.statements, false); const awaiterIdentifier = lua.createIdentifier("____try"); const awaiterDefinition = lua.createVariableDeclarationStatement(awaiterIdentifier, awaiter); - // local ____try = __TS__AsyncAwaiter() - const result: lua.Statement[] = [awaiterDefinition]; + // Transform catch/finally and collect scope info before building the result + let catchScope: Scope | undefined; + const chainCalls: lua.Statement[] = []; + + if (statement.catchClause) { + // ____try = ____try.catch() + const [catchFunction, cScope] = transformCatchClause(context, statement.catchClause); + catchScope = cScope; + if (catchFunction.params) { + catchFunction.params.unshift(lua.createAnonymousIdentifier()); + } + + const catchBodyStatements = catchFunction.body ? catchFunction.body.statements : []; + const asyncWrappedCatch = wrapInAsyncAwaiter(context, [...catchBodyStatements], false); + catchFunction.body = lua.createBlock([lua.createReturnStatement([asyncWrappedCatch])]); + + const awaiterCatch = lua.createTableIndexExpression(awaiterIdentifier, lua.createStringLiteral("catch")); + const catchCall = lua.createCallExpression(awaiterCatch, [awaiterIdentifier, catchFunction]); + chainCalls.push(lua.createAssignmentStatement(lua.cloneIdentifier(awaiterIdentifier), catchCall)); + } if (statement.finallyBlock) { - const awaiterFinally = lua.createTableIndexExpression(awaiterIdentifier, lua.createStringLiteral("finally")); + // ____try = ____try.finally() + const finallyStatements = context.transformStatements(statement.finallyBlock.statements); + const asyncWrappedFinally = wrapInAsyncAwaiter(context, finallyStatements, false); const finallyFunction = lua.createFunctionExpression( - lua.createBlock(context.transformStatements(statement.finallyBlock.statements)) + lua.createBlock([lua.createReturnStatement([asyncWrappedFinally])]) ); + + const awaiterFinally = lua.createTableIndexExpression(awaiterIdentifier, lua.createStringLiteral("finally")); const finallyCall = lua.createCallExpression( awaiterFinally, [awaiterIdentifier, finallyFunction], statement.finallyBlock ); - // ____try.finally() - result.push(lua.createExpressionStatement(finallyCall)); + chainCalls.push(lua.createAssignmentStatement(lua.cloneIdentifier(awaiterIdentifier), finallyCall)); } - if (statement.catchClause) { - // ____try.catch() - const [catchFunction] = transformCatchClause(context, statement.catchClause); - if (catchFunction.params) { - catchFunction.params.unshift(lua.createAnonymousIdentifier()); + // __TS__Await(____try) + const promiseAwait = transformLuaLibFunction(context, LuaLibFeature.Await, statement, awaiterIdentifier); + chainCalls.push(lua.createExpressionStatement(promiseAwait, statement)); + + const hasReturn = tryScope.asyncTryHasReturn ?? catchScope?.asyncTryHasReturn; + const hasBreak = tryScope.asyncTryHasBreak ?? catchScope?.asyncTryHasBreak; + const hasContinue = tryScope.asyncTryHasContinue ?? catchScope?.asyncTryHasContinue; + + // Build result in output order: flag declarations, awaiter, chain calls, post-checks + const result: lua.Statement[] = []; + + if (hasReturn || hasBreak || hasContinue !== undefined) { + const flagDecls: lua.Identifier[] = []; + if (hasReturn) { + flagDecls.push(lua.createIdentifier("____hasReturned")); + flagDecls.push(lua.createIdentifier("____returnValue")); } + if (hasBreak) { + flagDecls.push(lua.createIdentifier("____hasBroken")); + } + if (hasContinue !== undefined) { + flagDecls.push(lua.createIdentifier("____hasContinued")); + } + result.push(lua.createVariableDeclarationStatement(flagDecls)); + } - const awaiterCatch = lua.createTableIndexExpression(awaiterIdentifier, lua.createStringLiteral("catch")); - const catchCall = lua.createCallExpression(awaiterCatch, [awaiterIdentifier, catchFunction]); + result.push(awaiterDefinition); + result.push(...chainCalls); - // await ____try.catch() - const promiseAwait = transformLuaLibFunction(context, LuaLibFeature.Await, statement, catchCall); - result.push(lua.createExpressionStatement(promiseAwait, statement)); - } else { - // await ____try - const promiseAwait = transformLuaLibFunction(context, LuaLibFeature.Await, statement, awaiterIdentifier); - result.push(lua.createExpressionStatement(promiseAwait, statement)); + if (hasReturn) { + result.push( + lua.createIfStatement( + lua.createIdentifier("____hasReturned"), + lua.createBlock([createReturnStatement(context, [lua.createIdentifier("____returnValue")], statement)]) + ) + ); + } + + if (hasBreak) { + result.push( + lua.createIfStatement(lua.createIdentifier("____hasBroken"), lua.createBlock([lua.createBreakStatement()])) + ); + } + + if (hasContinue !== undefined) { + const loopScope = findScope(context, ScopeType.Loop); + const label = `__continue${loopScope?.id ?? ""}`; + + const continueStatements: lua.Statement[] = []; + switch (hasContinue) { + case LoopContinued.WithGoto: + continueStatements.push(lua.createGotoStatement(label)); + break; + case LoopContinued.WithContinue: + continueStatements.push(lua.createContinueStatement()); + break; + case LoopContinued.WithRepeatBreak: + continueStatements.push( + lua.createAssignmentStatement(lua.createIdentifier(label), lua.createBooleanLiteral(true)) + ); + continueStatements.push(lua.createBreakStatement()); + break; + } + + result.push( + lua.createIfStatement(lua.createIdentifier("____hasContinued"), lua.createBlock(continueStatements)) + ); } return result; @@ -146,6 +217,10 @@ export const transformTryStatement: FunctionVisitor = (statemen returnedIdentifier, lua.SyntaxKind.AndOperator ); + } else if (statement.finallyBlock) { + // try without catch, but with finally — need to capture error for re-throw + const errorIdentifier = lua.createIdentifier("____error"); + result.push(lua.createVariableDeclarationStatement([tryResultIdentifier, errorIdentifier], tryCall)); } else { // try without return or catch result.push(lua.createExpressionStatement(tryCall)); @@ -155,6 +230,23 @@ export const transformTryStatement: FunctionVisitor = (statemen result.push(...context.transformStatements(statement.finallyBlock)); } + // Re-throw error if try had no catch but had a finally. + // On pcall failure the error is the second return value, which lands in + // ____hasReturned (when functionReturned) or ____error (otherwise). + if (!statement.catchClause && statement.finallyBlock) { + const notTryCondition = lua.createUnaryExpression( + lua.cloneIdentifier(tryResultIdentifier), + lua.SyntaxKind.NotOperator + ); + const errorIdentifier = tryScope.functionReturned + ? lua.cloneIdentifier(returnedIdentifier) + : lua.createIdentifier("____error"); + const rethrow = lua.createExpressionStatement( + lua.createCallExpression(lua.createIdentifier("error"), [errorIdentifier, lua.createNumericLiteral(0)]) + ); + result.push(lua.createIfStatement(notTryCondition, lua.createBlock([rethrow]))); + } + if (returnCondition && returnedIdentifier) { const returnValues: lua.Expression[] = []; diff --git a/src/transformation/visitors/return.ts b/src/transformation/visitors/return.ts index 14d785166..3a61314ff 100644 --- a/src/transformation/visitors/return.ts +++ b/src/transformation/visitors/return.ts @@ -3,7 +3,7 @@ import * as lua from "../../LuaAST"; import { FunctionVisitor, TransformationContext } from "../context"; import { validateAssignment } from "../utils/assignment-validation"; import { createUnpackCall, wrapInTable } from "../utils/lua-ast"; -import { ScopeType, walkScopesUp } from "../utils/scope"; +import { findAsyncTryScopeInStack, ScopeType, walkScopesUp } from "../utils/scope"; import { transformArguments } from "./call"; import { returnsMultiType, @@ -16,11 +16,7 @@ import { import { invalidMultiFunctionReturnType } from "../utils/diagnostics"; import { isInAsyncFunction } from "../utils/typescript"; -function transformExpressionsInReturn( - context: TransformationContext, - node: ts.Expression, - insideTryCatch: boolean -): lua.Expression[] { +function transformExpressionsInReturn(context: TransformationContext, node: ts.Expression): lua.Expression[] { const expressionType = context.checker.getTypeAtLocation(node); // skip type assertions @@ -36,20 +32,7 @@ function transformExpressionsInReturn( context.diagnostics.push(invalidMultiFunctionReturnType(innerNode)); } - let returnValues = transformArguments(context, innerNode.arguments); - if (insideTryCatch) { - returnValues = [wrapInTable(...returnValues)]; // Wrap results when returning inside try/catch - } - return returnValues; - } - - // Force-wrap LuaMultiReturn when returning inside try/catch - if ( - insideTryCatch && - returnsMultiType(context, innerNode) && - !shouldMultiReturnCallBeWrapped(context, innerNode) - ) { - return [wrapInTable(context.transformExpression(node))]; + return transformArguments(context, innerNode.arguments); } } else if (isInMultiReturnFunction(context, innerNode) && isMultiReturnType(expressionType)) { // Unpack objects typed as LuaMultiReturn @@ -63,12 +46,32 @@ export function transformExpressionBodyToReturnStatement( context: TransformationContext, node: ts.Expression ): lua.Statement { - const expressions = transformExpressionsInReturn(context, node, false); + const expressions = transformExpressionsInReturn(context, node); return createReturnStatement(context, expressions, node); } +function transformReturnExpressionForTryCatch(context: TransformationContext, node: ts.Expression): lua.Expression { + const innerNode = ts.skipOuterExpressions(node, ts.OuterExpressionKinds.Assertions); + + if (ts.isCallExpression(innerNode)) { + if (isMultiFunctionCall(context, innerNode)) { + const type = context.checker.getContextualType(node); + if (type && !canBeMultiReturnType(type)) { + context.diagnostics.push(invalidMultiFunctionReturnType(innerNode)); + } + return wrapInTable(...transformArguments(context, innerNode.arguments)); + } + + if (returnsMultiType(context, innerNode) && !shouldMultiReturnCallBeWrapped(context, innerNode)) { + return wrapInTable(context.transformExpression(node)); + } + } + + return context.transformExpression(node); +} + export const transformReturnStatement: FunctionVisitor = (statement, context) => { - let results: lua.Expression[]; + const asyncTryScope = isInAsyncFunction(statement) ? findAsyncTryScopeInStack(context) : undefined; if (statement.expression) { const expressionType = context.checker.getTypeAtLocation(statement.expression); @@ -76,11 +79,32 @@ export const transformReturnStatement: FunctionVisitor = (st if (returnType) { validateAssignment(context, statement, expressionType, returnType); } + } - results = transformExpressionsInReturn(context, statement.expression, isInTryCatch(context)); - } else { - // Empty return + if (asyncTryScope) { + asyncTryScope.asyncTryHasReturn = true; + const stmts: lua.Statement[] = [ + lua.createAssignmentStatement( + lua.createIdentifier("____hasReturned"), + lua.createBooleanLiteral(true), + statement + ), + ]; + if (statement.expression) { + const returnValue = transformReturnExpressionForTryCatch(context, statement.expression); + stmts.push(lua.createAssignmentStatement(lua.createIdentifier("____returnValue"), returnValue, statement)); + } + stmts.push(lua.createReturnStatement([], statement)); + return stmts; + } + + let results: lua.Expression[]; + if (!statement.expression) { results = []; + } else if (isInTryCatch(context)) { + results = [transformReturnExpressionForTryCatch(context, statement.expression)]; + } else { + results = transformExpressionsInReturn(context, statement.expression); } return createReturnStatement(context, results, statement); diff --git a/src/transformation/visitors/variable-declaration.ts b/src/transformation/visitors/variable-declaration.ts index f15ed0115..9ed5ec553 100644 --- a/src/transformation/visitors/variable-declaration.ts +++ b/src/transformation/visitors/variable-declaration.ts @@ -12,7 +12,7 @@ import { createCallableTable, isFunctionTypeWithProperties } from "./function"; import { transformIdentifier } from "./identifier"; import { isMultiReturnCall } from "./language-extensions/multi"; import { transformPropertyName } from "./literal"; -import { moveToPrecedingTemp } from "./expression-list"; +import { moveToPrecedingTemp, transformExpressionList } from "./expression-list"; export function transformArrayBindingElement( context: TransformationContext, @@ -62,7 +62,7 @@ export function transformBindingPattern( } // Build the path to the table - const tableExpression = propertyAccessStack.reduce( + const tableExpression = propertyAccessStack.reduce( (path, property) => lua.createTableIndexExpression(path, transformPropertyName(context, property)), table ); @@ -205,7 +205,7 @@ export function transformBindingVariableDeclaration( // Don't unpack array literals const values = initializer.elements.length > 0 - ? initializer.elements.map(e => context.transformExpression(e)) + ? transformExpressionList(context, initializer.elements) : lua.createNilLiteral(); statements.push(...createLocalOrExportedOrGlobalDeclaration(context, vars, values, initializer)); } else { diff --git a/src/transpilation/diagnostics.ts b/src/transpilation/diagnostics.ts index 348542ac5..1ee589708 100644 --- a/src/transpilation/diagnostics.ts +++ b/src/transpilation/diagnostics.ts @@ -56,6 +56,8 @@ export const cannotBundleLibrary = createDiagnosticFactory( export const unsupportedJsxEmit = createDiagnosticFactory(() => 'JSX is only supported with "react" jsx option.'); -export const pathsWithoutBaseUrl = createDiagnosticFactory( - () => "When configuring 'paths' in tsconfig.json, the option 'baseUrl' must also be provided." +export const emitPathCollision = createDiagnosticFactory( + (outputPath: string, file1: string, file2: string) => + `Output path '${outputPath}' is used by both '${file1}' and '${file2}'. ` + + `Dots in file/directory names are replaced with underscores for Lua module resolution.` ); diff --git a/src/transpilation/resolve.ts b/src/transpilation/resolve.ts index bd16773e9..e011f4350 100644 --- a/src/transpilation/resolve.ts +++ b/src/transpilation/resolve.ts @@ -27,7 +27,7 @@ interface ResolutionResult { } class ResolutionContext { - private noResolvePaths: picomatch.Matcher[]; + private noResolvePaths: picomatch.Glob[]; public diagnostics: ts.Diagnostic[] = []; public resolvedFiles = new Map(); @@ -38,9 +38,7 @@ class ResolutionContext { private readonly emitHost: EmitHost, private readonly plugins: Plugin[] ) { - const unique = [...new Set(options.noResolvePaths)]; - const matchers = unique.map(x => picomatch(x)); - this.noResolvePaths = matchers; + this.noResolvePaths = [...new Set(options.noResolvePaths)]; } public addAndResolveDependencies(file: ProcessedFile): void { @@ -73,7 +71,7 @@ class ResolutionContext { return; } - if (this.noResolvePaths.find(isMatch => isMatch(required.requirePath))) { + if (this.noResolvePaths.find(glob => picomatch.isMatch(required.requirePath, glob))) { if (this.options.tstlVerbose) { console.log( `Skipping module resolution of ${required.requirePath} as it is in the tsconfig noResolvePaths.` @@ -215,14 +213,16 @@ class ResolutionContext { const fileFromPath = this.getFileFromPath(resolvedPath); if (fileFromPath) return fileFromPath; - if (this.options.paths && this.options.baseUrl) { + if (this.options.paths) { // If no file found yet and paths are present, try to find project file via paths mappings - const fileFromPaths = this.tryGetModuleNameFromPaths( - dependencyPath, - this.options.paths, - this.options.baseUrl - ); - if (fileFromPaths) return fileFromPaths; + // When baseUrl is not set, resolve paths relative to the tsconfig directory (TS 6.0+ behavior) + const pathsBase = + this.options.baseUrl ?? + (this.options.configFilePath ? path.dirname(this.options.configFilePath) : undefined); + if (pathsBase) { + const fileFromPaths = this.tryGetModuleNameFromPaths(dependencyPath, this.options.paths, pathsBase); + if (fileFromPaths) return fileFromPaths; + } } // Not a TS file in our project sources, use resolver to check if we can find dependency diff --git a/src/transpilation/transpiler.ts b/src/transpilation/transpiler.ts index 9bac1f5bf..7f79af35d 100644 --- a/src/transpilation/transpiler.ts +++ b/src/transpilation/transpiler.ts @@ -4,6 +4,7 @@ import { CompilerOptions, isBundleEnabled, LuaLibImportKind, LuaTarget } from ". import { buildMinimalLualibBundle, findUsedLualibFeatures, getLuaLibBundle } from "../LuaLib"; import { normalizeSlashes, trimExtension } from "../utils"; import { getBundleResult } from "./bundle"; +import { emitPathCollision } from "./diagnostics"; import { getPlugins, Plugin } from "./plugins"; import { resolveDependencies } from "./resolve"; import { getProgramTranspileResult, TranspileOptions } from "./transpile"; @@ -143,10 +144,17 @@ export class Transpiler { diagnostics.push(...bundleDiagnostics); emitPlan = [bundleFile]; } else { - emitPlan = resolutionResult.resolvedFiles.map(file => ({ - ...file, - outputPath: getEmitPath(file.fileName, program), - })); + const outputPathMap = new Map(); + emitPlan = resolutionResult.resolvedFiles.map(file => { + const outputPath = getEmitPath(file.fileName, program); + const existing = outputPathMap.get(outputPath); + if (existing) { + diagnostics.push(emitPathCollision(outputPath, existing, file.fileName)); + } else { + outputPathMap.set(outputPath, file.fileName); + } + return { ...file, outputPath }; + }); } performance.endSection("getEmitPlan"); @@ -189,11 +197,17 @@ export function getEmitPathRelativeToOutDir(fileName: string, program: ts.Progra emitPathSplits[0] = "lua_modules"; } + // Replace dots with underscores in path segments so that Lua's require() + // resolves correctly. Dots are path separators in Lua's module system, so + // "Foo.Bar/index.lua" would be unreachable via require("Foo.Bar.index") + // since Lua interprets it as "Foo/Bar/index.lua". + emitPathSplits[emitPathSplits.length - 1] = trimExtension(emitPathSplits[emitPathSplits.length - 1]); + emitPathSplits = emitPathSplits.map(segment => segment.replace(/\./g, "_")); + // Set extension const extension = ((program.getCompilerOptions() as CompilerOptions).extension ?? "lua").trim(); const trimmedExtension = extension.startsWith(".") ? extension.substring(1) : extension; - emitPathSplits[emitPathSplits.length - 1] = - trimExtension(emitPathSplits[emitPathSplits.length - 1]) + "." + trimmedExtension; + emitPathSplits[emitPathSplits.length - 1] += "." + trimmedExtension; return path.join(...emitPathSplits); } diff --git a/src/utils.ts b/src/utils.ts index b1877e368..70cdb2940 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -77,7 +77,7 @@ export function cast( } export function assert(value: any, message?: string | Error): asserts value { - nativeAssert(value, message); + nativeAssert.ok(value, message); } export function assertNever(_value: never): never { diff --git a/test/setup.ts b/test/setup.ts index e431d8519..704c59bed 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -13,7 +13,7 @@ declare global { expect.extend({ toHaveDiagnostics(diagnostics: ts.Diagnostic[], expected?: number[]): jest.CustomMatcherResult { - assert(Array.isArray(diagnostics)); + assert.ok(Array.isArray(diagnostics)); // @ts-ignore const matcherHint = this.utils.matcherHint("toHaveDiagnostics", undefined, "", this); diff --git a/test/transpile/__snapshots__/module-resolution.spec.ts.snap b/test/transpile/__snapshots__/module-resolution.spec.ts.snap index c9f0be298..e87569ded 100644 --- a/test/transpile/__snapshots__/module-resolution.spec.ts.snap +++ b/test/transpile/__snapshots__/module-resolution.spec.ts.snap @@ -10,8 +10,8 @@ exports[`supports complicated paths configuration 1`] = ` exports[`supports paths configuration 1`] = ` [ - "/paths-simple/myprogram/dist/main.lua", "/paths-simple/myprogram/dist/mypackage/bar.lua", "/paths-simple/myprogram/dist/mypackage/index.lua", + "/paths-simple/myprogram/dist/myprogram/main.lua", ] `; diff --git a/test/transpile/module-resolution.spec.ts b/test/transpile/module-resolution.spec.ts index ff29ac9b5..e6543df69 100644 --- a/test/transpile/module-resolution.spec.ts +++ b/test/transpile/module-resolution.spec.ts @@ -4,7 +4,6 @@ import * as util from "../util"; import * as ts from "typescript"; import { BuildMode } from "../../src"; import { normalizeSlashes } from "../../src/utils"; -import { pathsWithoutBaseUrl } from "../../src/transpilation/diagnostics"; describe("basic module resolution", () => { const projectPath = path.resolve(__dirname, "module-resolution", "project-with-node-modules"); @@ -457,6 +456,7 @@ describe("module resolution should not try to resolve modules in noResolvePaths" export const result = b.foo(); ` + .addExtraFile("preload.d.ts", `declare module "preload" {}`) .addExtraFile("preload.lua", 'package.preload["ignoreme"] = function() return nil end') .addExtraFile( "actualfile.ts", @@ -598,7 +598,7 @@ test("module resolution uses baseURL to resolve imported files", () => { return { baz = function() return "baz" end } ` ) - .setOptions({ baseUrl: "./myproject/mydeps" }) + .setOptions({ baseUrl: "./myproject/mydeps", ignoreDeprecations: "6.0" }) .expectToEqual({ fooResult: "foo", barResult: "bar", @@ -707,10 +707,6 @@ test("supports complicated paths configuration", () => { .expectToEqual({ foo: 314, bar: 271 }); }); -test("paths without baseUrl is error", () => { - util.testFunction``.setOptions({ paths: {} }).expectToHaveDiagnostics([pathsWithoutBaseUrl.code]); -}); - test("module resolution using plugin", () => { const baseProjectPath = path.resolve(__dirname, "module-resolution", "project-with-module-resolution-plugin"); const projectTsConfig = path.join(baseProjectPath, "tsconfig.json"); diff --git a/test/transpile/module-resolution/paths-base-tsconfig/tsconfig.base.json b/test/transpile/module-resolution/paths-base-tsconfig/tsconfig.base.json index 3f961c78a..4d3934b0d 100644 --- a/test/transpile/module-resolution/paths-base-tsconfig/tsconfig.base.json +++ b/test/transpile/module-resolution/paths-base-tsconfig/tsconfig.base.json @@ -2,6 +2,7 @@ "compilerOptions": { "rootDir": ".", "baseUrl": ".", + "ignoreDeprecations": "6.0", "paths": { "mypackage": ["packages/mypackage/src/index.ts"], "mypackage/*": ["packages/mypackage/src/*"] diff --git a/test/transpile/module-resolution/paths-simple/myprogram/tsconfig.json b/test/transpile/module-resolution/paths-simple/myprogram/tsconfig.json index f01271ac9..a21584fac 100644 --- a/test/transpile/module-resolution/paths-simple/myprogram/tsconfig.json +++ b/test/transpile/module-resolution/paths-simple/myprogram/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "..", "outDir": "dist", "paths": { "myOtherPackage": ["../mypackage"], diff --git a/test/transpile/module-resolution/project-with-node-modules/tsconfig.json b/test/transpile/module-resolution/project-with-node-modules/tsconfig.json index 935b64af6..faa93f0e3 100644 --- a/test/transpile/module-resolution/project-with-node-modules/tsconfig.json +++ b/test/transpile/module-resolution/project-with-node-modules/tsconfig.json @@ -7,6 +7,7 @@ "target": "esnext", "lib": ["esnext"], "types": [], - "rootDir": "." + "rootDir": ".", + "noUncheckedSideEffectImports": false } } diff --git a/test/transpile/transformers/fixtures.ts b/test/transpile/transformers/fixtures.ts index dff7667be..4bb890135 100644 --- a/test/transpile/transformers/fixtures.ts +++ b/test/transpile/transformers/fixtures.ts @@ -36,7 +36,7 @@ export const compilerOptions = (options: tstl.CompilerOptions): ts.TransformerFactory => context => file => { - assert(options.plugins?.length === 1); + assert.ok(options.plugins?.length === 1); return visitAndReplace(context, file, node => { if (!ts.isReturnStatement(node)) return; return ts.factory.updateReturnStatement(node, ts.factory.createTrue()); diff --git a/test/unit/__snapshots__/expressions.spec.ts.snap b/test/unit/__snapshots__/expressions.spec.ts.snap index e2adff370..720f7014e 100644 --- a/test/unit/__snapshots__/expressions.spec.ts.snap +++ b/test/unit/__snapshots__/expressions.spec.ts.snap @@ -374,14 +374,14 @@ return ____exports" exports[`Bitop [5.3] ("a>>>=b") 1`] = ` "local ____exports = {} -a = a >> b +a = (a & 4294967295) >> b ____exports.__result = a return ____exports" `; exports[`Bitop [5.3] ("a>>>b") 1`] = ` "local ____exports = {} -____exports.__result = a >> b +____exports.__result = (a & 4294967295) >> b return ____exports" `; @@ -445,14 +445,14 @@ return ____exports" exports[`Bitop [5.4] ("a>>>=b") 1`] = ` "local ____exports = {} -a = a >> b +a = (a & 4294967295) >> b ____exports.__result = a return ____exports" `; exports[`Bitop [5.4] ("a>>>b") 1`] = ` "local ____exports = {} -____exports.__result = a >> b +____exports.__result = (a & 4294967295) >> b return ____exports" `; @@ -516,14 +516,14 @@ return ____exports" exports[`Bitop [5.5] ("a>>>=b") 1`] = ` "local ____exports = {} -a = a >> b +a = (a & 4294967295) >> b ____exports.__result = a return ____exports" `; exports[`Bitop [5.5] ("a>>>b") 1`] = ` "local ____exports = {} -____exports.__result = a >> b +____exports.__result = (a & 4294967295) >> b return ____exports" `; diff --git a/test/unit/__snapshots__/identifiers.spec.ts.snap b/test/unit/__snapshots__/identifiers.spec.ts.snap index b90b10398..cfd11a893 100644 --- a/test/unit/__snapshots__/identifiers.spec.ts.snap +++ b/test/unit/__snapshots__/identifiers.spec.ts.snap @@ -52,18 +52,14 @@ exports[`ambient identifier must be a valid lua identifier ("enum $$ {}"): code exports[`ambient identifier must be a valid lua identifier ("enum $$ {}"): diagnostics 1`] = `"main.ts(3,9): error TSTL: Invalid ambient identifier name '$$$'. Ambient identifiers must be valid lua identifiers."`; -exports[`ambient identifier must be a valid lua identifier ("function $$();"): code 1`] = `"local ____ = _____24_24_24"`; +exports[`ambient identifier must be a valid lua identifier ("function $$(): void;"): code 1`] = `"local ____ = _____24_24_24"`; -exports[`ambient identifier must be a valid lua identifier ("function $$();"): diagnostics 1`] = `"main.ts(3,9): error TSTL: Invalid ambient identifier name '$$$'. Ambient identifiers must be valid lua identifiers."`; +exports[`ambient identifier must be a valid lua identifier ("function $$(): void;"): diagnostics 1`] = `"main.ts(3,9): error TSTL: Invalid ambient identifier name '$$$'. Ambient identifiers must be valid lua identifiers."`; exports[`ambient identifier must be a valid lua identifier ("let $$: any;"): code 1`] = `"local ____ = _____24_24_24"`; exports[`ambient identifier must be a valid lua identifier ("let $$: any;"): diagnostics 1`] = `"main.ts(3,9): error TSTL: Invalid ambient identifier name '$$$'. Ambient identifiers must be valid lua identifiers."`; -exports[`ambient identifier must be a valid lua identifier ("module $$ { export const bar: any; }"): code 1`] = `"local ____ = _____24_24_24"`; - -exports[`ambient identifier must be a valid lua identifier ("module $$ { export const bar: any; }"): diagnostics 1`] = `"main.ts(3,9): error TSTL: Invalid ambient identifier name '$$$'. Ambient identifiers must be valid lua identifiers."`; - exports[`ambient identifier must be a valid lua identifier ("namespace $$ { export const bar: any; }"): code 1`] = `"local ____ = _____24_24_24"`; exports[`ambient identifier must be a valid lua identifier ("namespace $$ { export const bar: any; }"): diagnostics 1`] = `"main.ts(3,9): error TSTL: Invalid ambient identifier name '$$$'. Ambient identifiers must be valid lua identifiers."`; diff --git a/test/unit/__snapshots__/loops.spec.ts.snap b/test/unit/__snapshots__/loops.spec.ts.snap index 29b609730..886964ebc 100644 --- a/test/unit/__snapshots__/loops.spec.ts.snap +++ b/test/unit/__snapshots__/loops.spec.ts.snap @@ -3,7 +3,7 @@ exports[`forin[Array]: code 1`] = ` "local ____exports = {} function ____exports.__main(self) - local array = {} + local array = {1, 2, 3} for key in pairs(array) do end end diff --git a/test/unit/annotations/customConstructor.spec.ts b/test/unit/annotations/customConstructor.spec.ts index 86858f700..c9ddcf739 100644 --- a/test/unit/annotations/customConstructor.spec.ts +++ b/test/unit/annotations/customConstructor.spec.ts @@ -11,8 +11,8 @@ test("CustomCreate", () => { const tsHeader = ` /** @customConstructor Point2DCreate */ class Point2D { - public x: number; - public y: number; + public x!: number; + public y!: number; constructor(x: number, y: number) { // No values assigned } diff --git a/test/unit/assignments.spec.ts b/test/unit/assignments.spec.ts index 53291f50f..3ad7da149 100644 --- a/test/unit/assignments.spec.ts +++ b/test/unit/assignments.spec.ts @@ -342,7 +342,7 @@ test.each([ test("local variable declaration referencing self indirectly", () => { util.testFunction` - let cb: () => void; + let cb: () => void = () => { throw "Expecting this to be overwritten"; }; function foo(newCb: () => void) { cb = newCb; @@ -360,7 +360,7 @@ test("local variable declaration referencing self indirectly", () => { test("local multiple variable declaration referencing self indirectly", () => { util.testFunction` - let cb: () => void; + let cb: () => void = () => { throw "Expecting this to be overwritten"; }; function foo(newCb: () => void) { cb = newCb; @@ -395,7 +395,7 @@ describe.each(["x &&= y", "x ||= y"])("boolean compound assignment (%p)", assign test.each([undefined, 3])("nullish coalescing compound assignment", initialValue => { util.testFunction` - let x: number = ${util.formatCode(initialValue)}; + let x: number | undefined = ${util.formatCode(initialValue)}; x ??= 5; return x; `.expectToMatchJsResult(); diff --git a/test/unit/builtins/array.spec.ts b/test/unit/builtins/array.spec.ts index dfb6807ec..5e3cd8887 100644 --- a/test/unit/builtins/array.spec.ts +++ b/test/unit/builtins/array.spec.ts @@ -325,7 +325,7 @@ test.each([ { array: [0, 2, 4, 8], predicate: "false" }, ])("array.find (%p)", ({ array, predicate }) => { util.testFunction` - const array = ${util.formatCode(array)}; + const array: number[] = ${util.formatCode(array)}; return array.find((elem, index, arr) => ${predicate} && arr[index] === elem); `.expectToMatchJsResult(); }); @@ -337,7 +337,7 @@ test.each([ { array: [0, 2, 4, 8], searchElement: 8 }, ])("array.findIndex (%p)", ({ array, searchElement }) => { util.testFunction` - const array = ${util.formatCode(array)}; + const array: number[] = ${util.formatCode(array)}; return array.findIndex((elem, index, arr) => elem === ${searchElement} && arr[index] === elem); `.expectToMatchJsResult(); }); @@ -421,9 +421,16 @@ test.each([ { array: [0, 1, 2, 3], start: 1, deleteCount: undefined }, { array: [0, 1, 2, 3], start: 1, deleteCount: null }, ])("array.splice (%p)", ({ array, start, deleteCount, newElements = [] }) => { + const deleteCountCode = + deleteCount === undefined + ? "undefined as any" + : deleteCount === null + ? "null as any" + : util.formatCode(deleteCount); + const newElementsCode = newElements.length > 0 ? ", " + util.formatCode(...newElements) : ""; util.testFunction` - const array = ${util.formatCode(array)}; - array.splice(${util.formatCode(start, deleteCount, ...newElements)}); + const array: number[] = ${util.formatCode(array)}; + array.splice(${util.formatCode(start)}, ${deleteCountCode}${newElementsCode}); return array; `.expectToMatchJsResult(); }); @@ -510,8 +517,11 @@ test("array.join without separator argument", () => { util.testExpression`["test1", "test2"].join()`.expectToMatchJsResult(); }); +test("array.indexOf empty array", () => { + util.testExpression`([] as string[]).indexOf("test1")`.expectToMatchJsResult(); +}); + test.each([ - { array: [], args: ["test1"] }, { array: ["test1"], args: ["test1"] }, { array: ["test1", "test2"], args: ["test2"] }, { array: ["test1", "test2", "test3"], args: ["test3", 1] }, @@ -571,7 +581,7 @@ test.each([{ array: [1, 2, 3] }, { array: [1, 2, 3, 4] }, { array: [1] }, { arra "array.reverse (%p)", ({ array }) => { util.testFunction` - const array = ${util.formatCode(array)}; + const array: number[] = ${util.formatCode(array)}; array.reverse(); return array; `.expectToMatchJsResult(); @@ -580,7 +590,7 @@ test.each([{ array: [1, 2, 3] }, { array: [1, 2, 3, 4] }, { array: [1] }, { arra test.each([{ array: [1, 2, 3] }, { array: [1] }, { array: [] }])("array.shift (%p)", ({ array }) => { util.testFunction` - const array = ${util.formatCode(array)}; + const array: number[] = ${util.formatCode(array)}; const value = array.shift(); return { array, value }; `.expectToMatchJsResult(); @@ -593,7 +603,7 @@ test.each([ { array: [], args: [1] }, ])("array.unshift (%p)", ({ array, args }) => { util.testFunction` - const array = ${util.formatCode(array)}; + const array: number[] = ${util.formatCode(array)}; const value = array.unshift(${util.formatCode(...args)}); return { array, value }; `.expectToMatchJsResult(); @@ -601,7 +611,7 @@ test.each([ test.each([{ array: [4, 5, 3, 2, 1] }, { array: [1] }, { array: [] }])("array.sort (%p)", ({ array }) => { util.testFunctionTemplate` - const array = ${array}; + const array: number[] = ${array}; array.sort(); return array; `.expectToMatchJsResult(); @@ -659,7 +669,7 @@ describe.each(["reduce", "reduceRight"])("array.%s", reduce => { }); test("empty no initial", () => { - util.testExpression`[].${reduce}(() => {})`.expectToMatchJsResult(true); + util.testExpression`([] as any[]).${reduce}(() => {})`.expectToMatchJsResult(true); }); test("undefined returning callback", () => { @@ -675,7 +685,7 @@ test.each([{ array: [] }, { array: ["a", "b", "c"] }, { array: [{ foo: "foo" }, "array.entries (%p)", ({ array }) => { util.testFunction` - const array = ${util.formatCode(array)}; + const array: any[] = ${util.formatCode(array)}; const result = []; for (const [i, v] of array.entries()) { result.push([i, v]); @@ -800,9 +810,12 @@ test.each([ }); describe("array.fill", () => { - test.each(["[]", "[1]", "[1,2,3,4]"])("Fills full length of array without other parameters (%p)", arr => { - util.testExpression`${arr}.fill(5)`.expectToMatchJsResult(); - }); + test.each(["([] as number[])", "[1]", "[1,2,3,4]"])( + "Fills full length of array without other parameters (%p)", + arr => { + util.testExpression`${arr}.fill(5)`.expectToMatchJsResult(); + } + ); test.each(["[1,2,3]", "[1,2,3,4,5,6]"])("Fills starting from start parameter (%p)", arr => { util.testExpression`${arr}.fill(5, 3)`.expectToMatchJsResult(); diff --git a/test/unit/builtins/async-await.spec.ts b/test/unit/builtins/async-await.spec.ts index 45adb4518..ad64f031d 100644 --- a/test/unit/builtins/async-await.spec.ts +++ b/test/unit/builtins/async-await.spec.ts @@ -38,7 +38,7 @@ test("high amount of chained awaits doesn't cause stack overflow", () => { await delay(); } result = "success"; - } catch (e) { + } catch (e: any) { result = e; } } @@ -51,7 +51,7 @@ test("high amount of chained awaits doesn't cause stack overflow", () => { test("can await already resolved promise", () => { util.testFunction` - const result = []; + const result: unknown[] = []; async function abc() { return await Promise.resolve(30); } @@ -63,7 +63,7 @@ test("can await already resolved promise", () => { test("can await already rejected promise", () => { util.testFunction` - const result = []; + const result: unknown[] = []; async function abc() { return await Promise.reject("test rejection"); } @@ -604,7 +604,7 @@ describe("try/catch in async function", () => { () => util.testModule` export let reason = ""; - let reject: (reason: string) => void; + let reject: (reason: string) => void = () => { throw "should be overridden!"; } async function foo(): Promise { try { @@ -675,6 +675,7 @@ describe("try/catch in async function", () => { } catch { + return "catch"; } } @@ -815,4 +816,310 @@ describe("try/catch in async function", () => { }, }); }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1659 + test("await inside catch handler resolves correctly (#1659)", () => { + util.testFunction` + let reject: (reason: string) => void = () => {}; + + async function failing() { + return new Promise((_, rej) => { reject = rej; }); + } + + async function run() { + try { + await failing(); + } catch (e) { + log("catch"); + const a = await Promise.resolve(true); + log("a", a); + } + } + + run(); + reject("error"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["catch", "a", true]); + }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1659 + test("await inside finally handler resolves correctly (#1659)", () => { + util.testFunction` + let reject: (reason: string) => void = () => {}; + + async function failing() { + return new Promise((_, rej) => { reject = rej; }); + } + + async function run() { + try { + await failing(); + } finally { + log("finally"); + const a = await Promise.resolve(true); + log("a", a); + } + } + + run().catch(() => {}); + reject("error"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["finally", "a", true]); + }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1659 + test("await inside both catch and finally handlers (#1659)", () => { + util.testFunction` + let reject: (reason: string) => void = () => {}; + + async function failing() { + return new Promise((_, rej) => { reject = rej; }); + } + + async function run() { + try { + await failing(); + } catch (e) { + log("catch"); + const a = await Promise.resolve("caught"); + log("a", a); + } finally { + log("finally"); + const b = await Promise.resolve("done"); + log("b", b); + } + } + + run(); + reject("error"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["catch", "a", "caught", "finally", "b", "done"]); + }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1659 + test("awaited value in catch is returned from async function (#1659)", () => { + util.testFunction` + const failing = defer(); + const recovery = defer(); + + async function run() { + try { + await failing.promise; + return "succeeded"; + } catch (e) { + return await recovery.promise; + } + } + + run().then(value => log("result", value)); + + failing.reject("error"); + recovery.resolve("recovered"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["result", "recovered"]); + }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1706 + test("return inside try with deferred promise (#1706)", () => { + util.testFunction` + let resolveLater!: (value: string) => void; + + function deferredPromise(): Promise { + return new Promise(resolve => { + resolveLater = (v) => resolve(v); + }); + } + + async function fn(): Promise { + try { + return await deferredPromise(); + } catch { + return 'caught'; + } + log('unreachable!'); + } + + const promise = fn(); + resolveLater('ok'); + promise.then(v => log(v)); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["ok"]); + }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1706 + test("return inside try in loop with deferred promise (#1706)", () => { + util.testFunction` + let resolveLater!: (value: string) => void; + + function deferredPromise(): Promise { + return new Promise(resolve => { + resolveLater = (v) => resolve(v); + }); + } + + async function fn(): Promise { + while (true) { + try { + return await deferredPromise(); + } catch { + return 'caught'; + } + log('unreachable!'); + } + } + + const promise = fn(); + resolveLater('ok'); + promise.then(v => log(v)); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["ok"]); + }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1706 + test("return from catch with deferred promise (#1706)", () => { + util.testFunction` + let rejectLater!: (reason: string) => void; + + function deferredPromise(): Promise { + return new Promise((_, reject) => { + rejectLater = (r) => reject(r); + }); + } + + async function fn(): Promise { + try { + return await deferredPromise(); + } catch (e) { + return 'caught: ' + e; + } + log('unreachable!'); + } + + const promise = fn(); + rejectLater('oops'); + promise.then(v => log(v)); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["caught: oops"]); + }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1706 + util.testEachVersion( + "break inside try in async loop (#1706)", + () => util.testModule` + export let result = "not set"; + async function fn(): Promise { + while (true) { + try { + await Promise.resolve(); + break; + } catch {} + } + result = "done"; + } + fn(); + `, + { + ...util.expectEachVersionExceptJit(builder => builder.expectToEqual({ result: "done" })), + [LuaTarget.Lua50]: builder => + builder.expectToHaveDiagnostics([unsupportedForTargetButOverrideAvailable.code]), + [LuaTarget.Lua51]: builder => + builder.expectToHaveDiagnostics([unsupportedForTargetButOverrideAvailable.code]), + } + ); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1706 + util.testEachVersion( + "continue inside try in async loop (#1706)", + () => util.testModule` + export const results: number[] = []; + async function fn(): Promise { + for (let i = 0; i < 3; i++) { + try { + await Promise.resolve(); + if (i === 1) continue; + } catch {} + results.push(i); + } + } + fn(); + `, + { + ...util.expectEachVersionExceptJit(builder => builder.expectToEqual({ results: [0, 2] })), + [LuaTarget.Lua50]: builder => + builder.expectToHaveDiagnostics([unsupportedForTargetButOverrideAvailable.code]), + [LuaTarget.Lua51]: builder => + builder.expectToHaveDiagnostics([unsupportedForTargetButOverrideAvailable.code]), + } + ); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1706 + test("multi return from try in async function (#1706)", () => { + util.testFunction` + async function fn(): Promise> { + try { + await Promise.resolve(); + return $multi("foo", "bar"); + } catch { + return $multi("err", "err"); + } + } + + let result: string[] = []; + fn().then(v => { const [a, b] = v; result = [a, b]; }); + + return result; + ` + .withLanguageExtensions() + .expectToEqual(["foo", "bar"]); + }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1706 + test("return inside try with finally (#1706)", () => { + util.testFunction` + let resolveLater!: (value: string) => void; + + function deferredPromise(): Promise { + return new Promise(resolve => { + resolveLater = (v) => resolve(v); + }); + } + + async function fn(): Promise { + try { + return await deferredPromise(); + } finally { + log('finally'); + } + } + + const promise = fn(); + resolveLater('ok'); + promise.then(v => log(v)); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["finally", "ok"]); + }); }); diff --git a/test/unit/builtins/map.spec.ts b/test/unit/builtins/map.spec.ts index f2e1cbfbe..c9f6c1a2b 100644 --- a/test/unit/builtins/map.spec.ts +++ b/test/unit/builtins/map.spec.ts @@ -210,10 +210,48 @@ describe.each(iterationMethods)("map.%s() preserves insertion order", iterationM }); }); +describe.each(iterationMethods)("map.%s() handles mutation", iterationMethod => { + test("iterator persists after delete", () => { + util.testFunction` + const map = new Map([[1, "a"], [2, "b"]]); + const iter = map.${iterationMethod}(); + map.delete(1); + return iter.next().value; + `.expectToMatchJsResult(); + }); + + test("iterator with delete and add between iterations", () => { + util.testFunction` + const map = new Map([[1, "a"], [2, "b"], [3, "c"]]); + const iter = map.${iterationMethod}(); + iter.next(); // 1 + map.delete(2); + map.set(4, "d"); + const results: IteratorResult[] = []; + let r = iter.next(); + while (!r.done) { results.push({ done: r.done, value: r.value }); r = iter.next(); } + return results; + `.expectToMatchJsResult(); + }); + + test("iterator does not restart after exhaustion", () => { + util.testFunction` + const map = new Map([[1, "a"], [2, "b"]]); + const iter = map.${iterationMethod}(); + const results: boolean[] = []; + results.push(iter.next().done!); + results.push(iter.next().done!); + results.push(iter.next().done!); // should be done + results.push(iter.next().done!); // should still be done, not restart + return results; + `.expectToMatchJsResult(); + }); +}); + describe("Map.groupBy", () => { test("empty", () => { util.testFunction` - const array = []; + const array: number[] = []; const map = Map.groupBy(array, (num, index) => { return num % 2 === 0 ? "even": "odd"; diff --git a/test/unit/builtins/math.spec.ts b/test/unit/builtins/math.spec.ts index f3ff1f306..d09b331cd 100644 --- a/test/unit/builtins/math.spec.ts +++ b/test/unit/builtins/math.spec.ts @@ -95,8 +95,8 @@ util.testEachVersion("Math.atan2", () => util.testExpression`Math.atan2(4, 5)`, [tstl.LuaTarget.Lua51]: builder => builder.tap(expectMathAtan2), [tstl.LuaTarget.Lua52]: builder => builder.tap(expectMathAtan2), [tstl.LuaTarget.Lua53]: builder => builder.tap(expectMathAtan), - [tstl.LuaTarget.Lua54]: builder => builder.tap(expectMathAtan2), - [tstl.LuaTarget.Lua55]: builder => builder.tap(expectMathAtan2), + [tstl.LuaTarget.Lua54]: builder => builder.tap(expectMathAtan), + [tstl.LuaTarget.Lua55]: builder => builder.tap(expectMathAtan), [tstl.LuaTarget.Luau]: builder => builder.tap(expectMathAtan2), }); diff --git a/test/unit/builtins/numbers.spec.ts b/test/unit/builtins/numbers.spec.ts index aa3a8927d..9570e4af2 100644 --- a/test/unit/builtins/numbers.spec.ts +++ b/test/unit/builtins/numbers.spec.ts @@ -212,17 +212,23 @@ test.each(["42", "undefined"])("prototype call on nullable number (%p)", value = }); test.each([ - "Number.NEGATIVE_INFINITY <= Number.MIN_VALUE", - "Number.MIN_VALUE <= Number.MIN_SAFE_INTEGER", - - "Number.MAX_SAFE_INTEGER <= Number.MAX_VALUE", - "Number.MAX_VALUE <= Number.POSITIVE_INFINITY", + // Full ordering: NEG_INF < MIN_SAFE < 0 < MIN_VALUE < EPSILON < MAX_SAFE < MAX_VALUE < POS_INF + "Number.NEGATIVE_INFINITY < Number.MIN_SAFE_INTEGER", "Number.MIN_SAFE_INTEGER < 0", - - "0 < Number.EPSILON", + "0 < Number.MIN_VALUE", + "Number.MIN_VALUE < Number.EPSILON", "Number.EPSILON < Number.MAX_SAFE_INTEGER", -])("Numer constants have correct relative sizes (%p)", comparison => { - util.testExpression(comparison).expectToEqual(true); + "Number.MAX_SAFE_INTEGER < Number.MAX_VALUE", + "Number.MAX_VALUE < Number.POSITIVE_INFINITY", + + // Verify specific values + "Number.MIN_VALUE > 0", + "Number.MIN_SAFE_INTEGER === -(2**53 - 1)", + "Number.MAX_SAFE_INTEGER === 2**53 - 1", + "Number.MAX_SAFE_INTEGER + 1 !== Number.MAX_SAFE_INTEGER", + "Number.MIN_SAFE_INTEGER - 1 !== Number.MIN_SAFE_INTEGER", +])("Number constants have correct relative sizes (%p)", comparison => { + util.testExpression(comparison).expectToMatchJsResult(); }); // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1629 diff --git a/test/unit/builtins/object.spec.ts b/test/unit/builtins/object.spec.ts index 1edc8a9d6..411da903b 100644 --- a/test/unit/builtins/object.spec.ts +++ b/test/unit/builtins/object.spec.ts @@ -10,6 +10,16 @@ test.each([ util.testExpression`Object.assign(${util.formatCode(initial)}, ${argsString})`.expectToMatchJsResult(); }); +test.each([ + "Object.assign({}, false)", + "Object.assign({}, null)", + "Object.assign({}, undefined)", + "Object.assign({}, null, undefined)", + "Object.assign({ a: 1 }, false, { b: 2 })", +])("Object.assign skips non-object sources (%p)", expression => { + util.testExpression(expression).expectToMatchJsResult(); +}); + test.each([{}, { abc: 3 }, { abc: 3, def: "xyz" }])("Object.entries (%p)", obj => { const testBuilder = util.testExpressionTemplate`Object.entries(${obj})`; // Need custom matcher because order is not guaranteed in neither JS nor Lua @@ -147,7 +157,7 @@ describe("Object.defineProperty", () => { test.each(trueFalseTests)("configurable (%p)", value => { util.testFunction` - const foo = { bar: true }; + const foo: { bar?: boolean } = { bar: true }; Object.defineProperty(foo, "bar", { configurable: ${value} }); try { delete foo.bar } catch {}; return foo.bar; @@ -196,6 +206,81 @@ describe("Object.defineProperty", () => { return { prop: foo.bar, err }; `.expectToMatchJsResult(); }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1625 + test("instance isolation", () => { + util.testFunction` + class Test { + declare obj: object; + constructor() { + Object.defineProperty(this, "obj", { value: {}, writable: true, configurable: true }); + } + } + const t1 = new Test(); + const t2 = new Test(); + return t1.obj === t2.obj; + `.expectToMatchJsResult(); + }); + + test("instance isolation with three instances", () => { + util.testFunction` + class Test { + declare obj: object; + constructor() { + Object.defineProperty(this, "obj", { value: {}, writable: true, configurable: true }); + } + } + const t1 = new Test(); + const t2 = new Test(); + const t3 = new Test(); + return [t1.obj === t2.obj, t1.obj === t3.obj, t2.obj === t3.obj]; + `.expectToMatchJsResult(); + }); + + test("instance isolation with mutation", () => { + util.testFunction` + class Test { + declare value: number; + constructor(v: number) { + Object.defineProperty(this, "value", { value: v, writable: true, configurable: true }); + } + } + const t1 = new Test(1); + const t2 = new Test(2); + return [t1.value, t2.value]; + `.expectToMatchJsResult(); + }); + + test("instance isolation with multiple properties", () => { + util.testFunction` + class Test { + declare a: string; + declare b: string; + constructor(a: string, b: string) { + Object.defineProperty(this, "a", { value: a, writable: true, configurable: true }); + Object.defineProperty(this, "b", { value: b, writable: true, configurable: true }); + } + } + const t1 = new Test("x", "y"); + const t2 = new Test("p", "q"); + return [t1.a, t1.b, t2.a, t2.b]; + `.expectToMatchJsResult(); + }); + + test("instance isolation preserves prototype methods", () => { + util.testFunction` + class Test { + declare val: number; + constructor(v: number) { + Object.defineProperty(this, "val", { value: v, writable: true, configurable: true }); + } + getVal() { return this.val; } + } + const t1 = new Test(10); + const t2 = new Test(20); + return [t1.getVal(), t2.getVal()]; + `.expectToMatchJsResult(); + }); }); describe("Object.getOwnPropertyDescriptor", () => { @@ -221,7 +306,7 @@ describe("Object.getOwnPropertyDescriptors", () => { describe("delete from object", () => { test("delete from object", () => { util.testFunction` - const obj = { foo: "bar", bar: "baz" }; + const obj: { foo?: string, bar: string } = { foo: "bar", bar: "baz" }; return [delete obj["foo"], obj]; `.expectToMatchJsResult(); }); @@ -236,7 +321,7 @@ describe("delete from object", () => { // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/993 test("delete from object with metatable", () => { util.testFunction` - const obj = { foo: "bar", bar: "baz" }; + const obj: { foo?: string, bar: string } = { foo: "bar", bar: "baz" }; setmetatable(obj, {}); return [delete obj["foo"], obj]; ` @@ -268,7 +353,7 @@ describe("delete from object", () => { describe("Object.groupBy", () => { test("empty", () => { util.testFunction` - const array = []; + const array: number[] = []; return Object.groupBy(array, (num, index) => { return num % 2 === 0 ? "even": "odd"; diff --git a/test/unit/builtins/promise.spec.ts b/test/unit/builtins/promise.spec.ts index cb18db994..f323b030f 100644 --- a/test/unit/builtins/promise.spec.ts +++ b/test/unit/builtins/promise.spec.ts @@ -1323,3 +1323,51 @@ describe("Promise.race", () => { }); }); }); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1660 +describe("Promise.finally", () => { + test("returns a different promise instance", () => { + util.testFunction` + const p1 = new Promise(() => {}); + const p2 = p1.finally(); + return p1 === p2; + `.expectToMatchJsResult(); + }); + + test("preserves fulfillment value", () => { + util.testFunction` + const result = Promise.resolve(42).finally(() => {}) as any; + return result.value; + `.expectToEqual(42); + }); + + test("preserves rejection reason", () => { + util.testFunction` + const result = Promise.reject("err").finally(() => {}) as any; + return result.rejectionReason; + `.expectToEqual("err"); + }); + + test("callback executes on fulfillment", () => { + util.testFunction` + let called = false; + Promise.resolve(1).finally(() => { called = true; }); + return called; + `.expectToEqual(true); + }); + + test("callback executes on rejection", () => { + util.testFunction` + let called = false; + Promise.reject("err").finally(() => { called = true; }); + return called; + `.expectToEqual(true); + }); + + test("finally with undefined callback", () => { + util.testFunction` + const result = Promise.resolve(99).finally(undefined) as any; + return result.value; + `.expectToEqual(99); + }); +}); diff --git a/test/unit/builtins/set.spec.ts b/test/unit/builtins/set.spec.ts index a03f27ab2..234be91a7 100644 --- a/test/unit/builtins/set.spec.ts +++ b/test/unit/builtins/set.spec.ts @@ -100,7 +100,7 @@ test("set has false", () => { test("set has null", () => { util.testFunction` - let myset = new Set(["a", "c"]); + let myset = new Set(["a", "c"]); return myset.has(null); `.expectToMatchJsResult(); }); @@ -195,6 +195,48 @@ describe.each(iterationMethods)("set.%s() preserves insertion order", iterationM }); }); +describe.each(iterationMethods)("set.%s() handles mutation", iterationMethod => { + test("iterator persists after delete", () => { + util.testFunction` + const set1 = new Set(); + set1.add(42); + set1.add("forty two"); + + const iterator1 = set1.${iterationMethod}(); + set1.delete(42); + + return iterator1.next().value; + `.expectToMatchJsResult(); + }); + + test("iterator with delete and add between iterations", () => { + util.testFunction` + const set = new Set([1, 2, 3]); + const iter = set.${iterationMethod}(); + iter.next(); // 1 + set.delete(2); + set.add(4); + const results: IteratorResult[] = []; + let r = iter.next(); + while (!r.done) { results.push({ done: r.done, value: r.value }); r = iter.next(); } + return results; + `.expectToMatchJsResult(); + }); + + test("iterator does not restart after exhaustion", () => { + util.testFunction` + const set = new Set([1, 2]); + const iter = set.${iterationMethod}(); + const results: boolean[] = []; + results.push(iter.next().done!); + results.push(iter.next().done!); + results.push(iter.next().done!); // should be done + results.push(iter.next().done!); // should still be done, not restart + return results; + `.expectToMatchJsResult(); + }); +}); + test("instanceof Set without creating set", () => { util.testFunction` const myset = 3 as any; diff --git a/test/unit/builtins/string.spec.ts b/test/unit/builtins/string.spec.ts index 977944e55..7f2ad312d 100644 --- a/test/unit/builtins/string.spec.ts +++ b/test/unit/builtins/string.spec.ts @@ -84,7 +84,7 @@ describe.each(["replace", "replaceAll"])("string.%s", method => { test.each(testCases)("function replacer %p", ({ inp, searchValue, replaceValue }) => { util.testFunction` const result = { - args: [], + args: [] as string[], string: "" } function replacer(...args: any[]): string { diff --git a/test/unit/builtins/weakMap.spec.ts b/test/unit/builtins/weakMap.spec.ts index 812e43622..8ff5d4c2e 100644 --- a/test/unit/builtins/weakMap.spec.ts +++ b/test/unit/builtins/weakMap.spec.ts @@ -74,7 +74,7 @@ test("weakMap has null", () => { util.testFunction` ${initRefsTs} let mymap = new WeakMap([[{}, true]]); - return mymap.has(null); + return mymap.has(null as any); `.expectToMatchJsResult(); }); diff --git a/test/unit/classes/classes.spec.ts b/test/unit/classes/classes.spec.ts index f28c3b43d..e7b8cc304 100644 --- a/test/unit/classes/classes.spec.ts +++ b/test/unit/classes/classes.spec.ts @@ -894,7 +894,7 @@ test("get inherted __index member from super (DotA 2 inheritance) (#1537)", () = interface Connected extends I {} class Connected {} - declare function setmetatable(this: void, t: any, mt: any); + declare function setmetatable(this: void, t: any, mt: any): void; const A = { foo() { diff --git a/test/unit/classes/decorators.spec.ts b/test/unit/classes/decorators.spec.ts index 4782746be..095bd8e3a 100644 --- a/test/unit/classes/decorators.spec.ts +++ b/test/unit/classes/decorators.spec.ts @@ -22,8 +22,8 @@ test("Class decorator with no parameters", () => { } return { decoratedClass: new TestClass(), context: { - kind: classDecoratorContext.kind, - name: classDecoratorContext.name, + kind: classDecoratorContext!.kind, + name: classDecoratorContext!.name, } }; `.expectToMatchJsResult(); }); @@ -40,7 +40,7 @@ test("Class decorator with parameters", () => { @setNum(420) class TestClass { - public decoratorNum; + public decoratorNum?: number; } return new TestClass(); @@ -64,8 +64,8 @@ test("Multiple class decorators", () => { @setTen @setNum class TestClass { - public decoratorTen; - public decoratorNum; + public decoratorTen?: number; + public decoratorNum?: number; } return new TestClass(); @@ -95,7 +95,7 @@ test("Class decorator with inheritance", () => { test("Class decorators are applied in order and executed in reverse order", () => { util.testFunction` - const order = []; + const order: string[] = []; function pushOrder(index: number) { order.push("eval " + index); @@ -232,10 +232,10 @@ test("class method decorator", () => { } return { result: new TestClass().myMethod(4), context: { - kind: methodDecoratorContext.kind, - name: methodDecoratorContext.name, - private: methodDecoratorContext.private, - static: methodDecoratorContext.static + kind: methodDecoratorContext!.kind, + name: methodDecoratorContext!.name, + private: methodDecoratorContext!.private, + static: methodDecoratorContext!.static } }; `.expectToMatchJsResult(); }); @@ -243,7 +243,7 @@ test("class method decorator", () => { test("this in decorator points to class being decorated", () => { util.testFunction` function methodDecorator(method: (v: number) => number, context: ClassMethodDecoratorContext) { - return function() { + return function(this: TestClass) { const thisCallTime = this.myInstanceVariable; return thisCallTime; }; @@ -280,10 +280,10 @@ test("class getter decorator", () => { } return { result: new TestClass().getterValue, context: { - kind: getterDecoratorContext.kind, - name: getterDecoratorContext.name, - private: getterDecoratorContext.private, - static: getterDecoratorContext.static + kind: getterDecoratorContext!.kind, + name: getterDecoratorContext!.name, + private: getterDecoratorContext!.private, + static: getterDecoratorContext!.static } }; `.expectToMatchJsResult(); }); @@ -295,13 +295,13 @@ test("class setter decorator", () => { function setterDecorator(setter: (v: number) => void, context: ClassSetterDecoratorContext) { setterDecoratorContext = context; - return function(v: number) { + return function(this: TestClass, v: number) { setter.call(this, v + 15); }; } class TestClass { - public value: number; + public value?: number; @setterDecorator set valueSetter(v: number) { this.value = v; } @@ -310,10 +310,10 @@ test("class setter decorator", () => { const instance = new TestClass(); instance.valueSetter = 23; return { result: instance.value, context: { - kind: setterDecoratorContext.kind, - name: setterDecoratorContext.name, - private: setterDecoratorContext.private, - static: setterDecoratorContext.static + kind: setterDecoratorContext!.kind, + name: setterDecoratorContext!.name, + private: setterDecoratorContext!.private, + static: setterDecoratorContext!.static } }; `.expectToMatchJsResult(); }); @@ -332,10 +332,10 @@ test("class field decorator", () => { } return { result: new TestClass(), context: { - kind: fieldDecoratorContext.kind, - name: fieldDecoratorContext.name, - private: fieldDecoratorContext.private, - static: fieldDecoratorContext.static, + kind: fieldDecoratorContext!.kind, + name: fieldDecoratorContext!.name, + private: fieldDecoratorContext!.private, + static: fieldDecoratorContext!.static, } }; `.expectToEqual({ result: { @@ -399,7 +399,7 @@ describe("legacy experimentalDecorators", () => { @setNum(420) class TestClass { - public decoratorNum; + public decoratorNum?: number; } return new TestClass(); @@ -425,8 +425,8 @@ describe("legacy experimentalDecorators", () => { @setTen @setNum class TestClass { - public decoratorTen; - public decoratorNum; + public decoratorTen?: number; + public decoratorNum?: number; } return new TestClass(); @@ -460,7 +460,7 @@ describe("legacy experimentalDecorators", () => { test("Class decorators are applied in order and executed in reverse order", () => { util.testFunction` - const order = []; + const order: string[] = []; function pushOrder(index: number) { order.push("eval " + index); @@ -509,25 +509,25 @@ describe("legacy experimentalDecorators", () => { test.each([ ["@decorator method() {}"], - ["@decorator property;"], + ["@decorator property: any;"], ["@decorator propertyWithInitializer = () => {};"], - ["@decorator ['evaluated property'];"], + ["@decorator ['evaluated property']: any;"], ["@decorator get getter() { return 5 }"], - ["@decorator set setter(value) {}"], + ["@decorator set setter(value: any) {}"], ["@decorator static method() {}"], - ["@decorator static property;"], + ["@decorator static property: any;"], ["@decorator static propertyWithInitializer = () => {}"], ["@decorator static get getter() { return 5 }"], - ["@decorator static set setter(value) {}"], - ["@decorator static ['evaluated property'];"], - ["method(@decorator a) {}"], - ["static method(@decorator a) {}"], - ["constructor(@decorator a) {}"], + ["@decorator static set setter(value: any) {}"], + ["@decorator static ['evaluated property']: any;"], + ["method(@decorator a: any) {}"], + ["static method(@decorator a: any) {}"], + ["constructor(@decorator a: any) {}"], ])("Decorate class member (%p)", classMember => { util.testFunction` let decoratorParameters: any; - const decorator = (target, key, index?) => { + const decorator = (target: any, key: any, index?: any) => { const targetKind = target === Foo ? "Foo" : target === Foo.prototype ? "Foo.prototype" : "unknown"; decoratorParameters = { targetKind, key, index: typeof index }; }; @@ -548,10 +548,10 @@ describe("legacy experimentalDecorators", () => { ["desc.writable = true", "return { configurable: true }"], ])("Combine decorators (%p + %p)", (decorateA, decorateB) => { util.testFunction` - const A = (target, key, desc): any => { ${decorateA} }; - const B = (target, key, desc): any => { ${decorateB} }; + const A = (target: any, key: any, desc: any): any => { ${decorateA} }; + const B = (target: any, key: any, desc: any): any => { ${decorateB} }; class Foo { @A @B static method() {} } - const { value, ...rest } = Object.getOwnPropertyDescriptor(Foo, "method"); + const { value, ...rest } = Object.getOwnPropertyDescriptor(Foo, "method")!; return rest; ` .setOptions({ experimentalDecorators: true }) @@ -562,7 +562,7 @@ describe("legacy experimentalDecorators", () => { "Use decorator to override method value %s", overrideStatement => { util.testFunction` - const decorator = (target, key, desc): any => { ${overrideStatement} }; + const decorator = (target: any, key: any, desc: any): any => { ${overrideStatement} }; class Foo { @decorator static method() {} } return Foo.method; ` diff --git a/test/unit/conditionals.spec.ts b/test/unit/conditionals.spec.ts index f862ca8a3..59ca587bc 100644 --- a/test/unit/conditionals.spec.ts +++ b/test/unit/conditionals.spec.ts @@ -75,7 +75,7 @@ test.each([ ])("Ternary operator (%p)", ({ input, options }) => { util.testFunction` const literalValue = "literal"; - let variableValue: string; + let variableValue: string = "variable"; let maybeBooleanValue: string | boolean = false; let maybeUndefinedValue: string | undefined; return ${input}; diff --git a/test/unit/destructuring.spec.ts b/test/unit/destructuring.spec.ts index 65ecf95a3..36b73daaf 100644 --- a/test/unit/destructuring.spec.ts +++ b/test/unit/destructuring.spec.ts @@ -3,40 +3,50 @@ import * as util from "../util"; const allBindings = "x, y, z, rest"; const testCases = [ - { binding: "{ x }", value: { x: true } }, - { binding: "{ x, y }", value: { x: false, y: true } }, - { binding: "{ x: z, y }", value: { x: true, y: false } }, - { binding: "{ x: { x, y }, z }", value: { x: { x: true, y: false }, z: false } }, - { binding: "{ x, y = true }", value: { x: false, y: false } }, - { binding: "{ x = true }", value: {} }, - { binding: "{ x, y = true }", value: { x: false } }, - { binding: "{ ...rest }", value: {} }, - { binding: "{ x, ...rest }", value: { x: "x" } }, - { binding: "{ x, ...rest }", value: { x: "x", y: "y", z: "z" } }, - { binding: "{ x, ...y }", value: { x: "x", y: "y", z: "z" } }, - { binding: "{ x: y, ...z }", value: { x: "x", y: "y", z: "z" } }, - - { binding: "[]", value: [] }, - { binding: "[x, y]", value: ["x", "y"] }, - { binding: "[x, , y]", value: ["x", "", "y"] }, - { binding: "[x = true]", value: [false] }, - { binding: "[[x, y]]", value: [["x", "y"]] }, - { binding: "[x, ...rest]", value: ["x"] }, - { binding: "[x, ...rest]", value: ["x", "y", "z"] }, - - { binding: "{ y: [z = true] }", value: { y: [false] } }, - { binding: "{ x: [x, y] }", value: { x: ["x", "y"] } }, - { binding: "{ x: [{ y }] }", value: { x: [{ y: "y" }] } }, -].map(({ binding, value }) => ({ binding, value: util.formatCode(value) })); + { binding: "{ x }", type: "{ x: boolean }", value: { x: true } }, + { binding: "{ x, y }", type: "{ x: boolean, y: boolean }", value: { x: false, y: true } }, + { binding: "{ x: z, y }", type: "{ x: boolean, y?: boolean, z?: boolean }", value: { x: true, y: false } }, + { + binding: "{ x: { x, y }, z }", + type: "{ x: { x: boolean, y: boolean }, z: boolean }", + value: { x: { x: true, y: false }, z: false }, + }, + { binding: "{ x, y = true }", type: "{ x: boolean, y?: boolean }", value: { x: false, y: false } }, + { binding: "{ x = true }", type: "{ x?: boolean }", value: {} }, + { binding: "{ x, y = true }", type: "{ x: boolean, y?: boolean }", value: { x: false } }, + { binding: "{ ...rest }", type: "any", value: {} }, + { binding: "{ x, ...rest }", type: "any", value: { x: "x" } }, + { binding: "{ x, ...rest }", type: "any", value: { x: "x", y: "y", z: "z" } }, + { binding: "{ x, ...y }", type: "any", value: { x: "x", y: "y", z: "z" } }, + { binding: "{ x: y, ...z }", type: "any", value: { x: "x", y: "y", z: "z" } }, + + { binding: "[]", type: "boolean[]", value: [] }, + { binding: "[x, y]", type: "[string, string]", value: ["x", "y"] }, + { binding: "[x, , y]", type: "string[]", value: ["x", "", "y"] }, + { binding: "[x = true]", type: "boolean[]", value: [false] }, + { binding: "[x = true]", type: "boolean[]", value: [] }, + { binding: "[[x, y]]", type: "Array", value: [["x", "y"]] }, + { binding: "[x, ...rest]", type: "string[]", value: ["x"] }, + { binding: "[x, ...rest]", type: "string[]", value: ["x", "y", "z"] }, + + { binding: "{ y: [z = true] }", type: "{ y: boolean[] }", value: { y: [false] } }, + { binding: "{ y: [z = true] }", type: "{ y: boolean[] }", value: { y: [] } }, + { binding: "{ x: [x, y] }", type: "{ x: [string, string] }", value: { x: ["x", "y"] } }, + { binding: "{ x: [{ y }] }", type: "{ x: [{ y: string }] }", value: { x: [{ y: "y" }] } }, +].map(({ binding, type, value }) => ({ binding, type, value: util.formatCode(value) })); test.each([ ...testCases, - { binding: "{ x, y }, z", value: "{ x: false, y: false }, true" }, - { binding: "{ x, y }, { z }", value: "{ x: false, y: false }, { z: true }" }, -])("in function parameter (%p)", ({ binding, value }) => { + { binding: "{ x, y }", type: "{ x: boolean, y: boolean }, z: boolean", value: "{ x: false, y: false }, true" }, + { + binding: "{ x, y }", + type: "{ x: boolean, y: boolean }, { z }: { z: boolean }", + value: "{ x: false, y: false }, { z: true }", + }, +])("in function parameter (%p)", ({ binding, type, value }) => { util.testFunction` let ${allBindings}; - function test(${binding}) { + function test(${binding}: ${type}) { return { ${allBindings} }; } @@ -226,6 +236,33 @@ describe("array destructuring optimization", () => { .expectToMatchJsResult(); }); + test("array literal with side effects in elements", () => { + util.testFunction` + const arr = [1, 2]; + let i = 0; + let [v1, v2] = [arr[i], arr[++i]]; + return { v1, v2 }; + `.expectToMatchJsResult(); + }); + + test("array literal with many side effects in elements", () => { + util.testFunction` + const arr = [10, 20, 30, 40]; + let i = 0; + let [v1, v2, v3, v4] = [arr[i++], arr[i++], arr[i++], arr[i++]]; + return { v1, v2, v3, v4 }; + `.expectToMatchJsResult(); + }); + + test("array literal with mixed pure and impure elements", () => { + util.testFunction` + const arr = [10, 20, 30]; + let i = 0; + let [v1, v2, v3] = [1, arr[++i], 2]; + return { v1, v2, v3, i }; + `.expectToMatchJsResult(); + }); + test("array union", () => { util.testFunction` const array: [string] | [] = ["bar"]; diff --git a/test/unit/enum.spec.ts b/test/unit/enum.spec.ts index 3ff4ce3fe..ae552b9c1 100644 --- a/test/unit/enum.spec.ts +++ b/test/unit/enum.spec.ts @@ -4,7 +4,7 @@ import * as util from "../util"; const serializeEnum = (identifier: string) => `(() => { const mappedTestEnum: any = {}; for (const key in ${identifier}) { - mappedTestEnum[(key as any).toString()] = ${identifier}[key]; + mappedTestEnum[(key as any).toString()] = (${identifier} as any)[key]; } return mappedTestEnum; })()`; diff --git a/test/unit/error.spec.ts b/test/unit/error.spec.ts index af69d7e1d..8204cc36a 100644 --- a/test/unit/error.spec.ts +++ b/test/unit/error.spec.ts @@ -88,7 +88,7 @@ test("multi return from try", () => { } catch { } } - const [foo, bar] = foobar(); + const [foo, bar] = foobar()!; return foo + bar; `.withLanguageExtensions(); expect(testBuilder.getMainLuaCodeChunk()).not.toMatch("unpack(foobar"); @@ -129,7 +129,7 @@ test("multi return from catch", () => { function foobar(): LuaMultiReturn<[string, string]> { try { throw "foobar"; - } catch (e) { + } catch (e: any) { return $multi(e.toString(), " catch"); } } @@ -243,7 +243,7 @@ test("multi return from catch->finally", () => { function foobar() { try { throw "foo"; - } catch (e) { + } catch (e: any) { return $multi(evaluate(e), "bar"); } finally { return $multi("final", "ly"); @@ -256,6 +256,28 @@ test("multi return from catch->finally", () => { .expectToMatchJsResult(); }); +test("throw propagates through finally to outer catch", () => { + util.testFunction` + function thrower() { + try { + throw "Error"; + } finally { + } + } + + function caller() { + try { + thrower(); + return "NoCatch"; + } catch (e) { + return e; + } + } + + return caller(); + `.expectToMatchJsResult(); +}); + test("return from nested finally", () => { util.testFunction` let x = ""; @@ -395,6 +417,82 @@ test("sourceMapTraceback maps anonymous function locations in .lua files (#1665) expect(result).toContain(""); }); +test("try/finally rethrow preserves error value", () => { + util.testFunction` + function foo() { + try { + throw "oops"; + } finally { + } + } + try { foo(); return "no error"; } catch(e) { return e; } + `.expectToMatchJsResult(); +}); + +test("try/finally with return and throw paths", () => { + util.testFunction` + function foo(shouldReturn: boolean) { + try { + if (shouldReturn) return "returned"; + throw "thrown"; + } finally { + } + } + const results: any[] = []; + results.push(foo(true)); + try { foo(false); } catch(e) { results.push(e); } + return results; + `.expectToMatchJsResult(); +}); + +test("try/finally runs finally side effect before rethrow", () => { + util.testFunction` + let sideEffect = false; + function foo() { + try { + throw "err"; + } finally { + sideEffect = true; + } + } + try { foo(); } catch(e) {} + return sideEffect; + `.expectToMatchJsResult(); +}); + +test("try/finally with return and throw paths and non-empty finally body", () => { + util.testFunction` + let sideEffect = false; + function foo(shouldReturn: boolean) { + try { + if (shouldReturn) return "ok"; + throw "err"; + } finally { + sideEffect = true; + } + } + const results: any[] = []; + results.push(foo(true)); + results.push(sideEffect); + sideEffect = false; + try { foo(false); } catch(e) { results.push(e); } + results.push(sideEffect); + return results; + `.expectToMatchJsResult(); +}); + +test("try/finally rethrow with non-string error", () => { + util.testFunction` + function foo() { + try { + throw 42; + } finally { + } + } + try { foo(); return "no error"; } catch(e) { return e; } + `.expectToMatchJsResult(); +}); + util.testEachVersion( "error stacktrace omits constructor and __TS_New", () => util.testFunction` diff --git a/test/unit/expressions.spec.ts b/test/unit/expressions.spec.ts index ee0eab5f4..12fd85be1 100644 --- a/test/unit/expressions.spec.ts +++ b/test/unit/expressions.spec.ts @@ -117,6 +117,35 @@ test.each(unsupportedIn53And54)("Unsupported bitop 5.4 (%p)", input => { .expectDiagnosticsToMatchSnapshot([unsupportedRightShiftOperator.code]); }); +// Execution tests: verify >>> produces correct results matching JS semantics +for (const expression of ["-5 >>> 0", "-1 >>> 0", "1 >>> 0", "-1 >>> 16", "255 >>> 4"]) { + util.testEachVersion(`Unsigned right shift execution (${expression})`, () => util.testExpression(expression), { + [tstl.LuaTarget.Universal]: false, + [tstl.LuaTarget.Lua50]: false, // No bit library in WASM runtime + [tstl.LuaTarget.Lua51]: false, // No bit library in WASM runtime + [tstl.LuaTarget.Lua52]: builder => builder.expectToMatchJsResult(), + [tstl.LuaTarget.Lua53]: builder => builder.expectToMatchJsResult(), + [tstl.LuaTarget.Lua54]: builder => builder.expectToMatchJsResult(), + [tstl.LuaTarget.Lua55]: builder => builder.expectToMatchJsResult(), + [tstl.LuaTarget.LuaJIT]: false, // Can't execute LuaJIT in tests + [tstl.LuaTarget.Luau]: false, + }); +} + +for (const code of ["let a = -5; a >>>= 0; return a;", "let a = -1; a >>>= 16; return a;"]) { + util.testEachVersion(`Unsigned right shift assignment execution (${code})`, () => util.testFunction(code), { + [tstl.LuaTarget.Universal]: false, + [tstl.LuaTarget.Lua50]: false, // No bit library in WASM runtime + [tstl.LuaTarget.Lua51]: false, // No bit library in WASM runtime + [tstl.LuaTarget.Lua52]: builder => builder.expectToMatchJsResult(), + [tstl.LuaTarget.Lua53]: builder => builder.expectToMatchJsResult(), + [tstl.LuaTarget.Lua54]: builder => builder.expectToMatchJsResult(), + [tstl.LuaTarget.Lua55]: builder => builder.expectToMatchJsResult(), + [tstl.LuaTarget.LuaJIT]: false, // Can't execute LuaJIT in tests + [tstl.LuaTarget.Luau]: false, + }); +} + test.each(["1+1", "-1+1", "1*30+4", "1*(3+4)", "1*(3+4*2)", "10-(4+5)"])( "Binary expressions ordering parentheses (%p)", input => { diff --git a/test/unit/functions/functionProperties.spec.ts b/test/unit/functions/functionProperties.spec.ts index 16b61ab46..85b3ec155 100644 --- a/test/unit/functions/functionProperties.spec.ts +++ b/test/unit/functions/functionProperties.spec.ts @@ -160,7 +160,7 @@ test("async arrow function with property assigned to variable", () => { test("call function with property using call method", () => { util.testFunction` - function foo(s: string) { return this + s; } + function foo(this: string, s: string) { return this + s; } foo.baz = "baz"; return foo.call("foo", "bar") + foo.baz; `.expectToMatchJsResult(); @@ -168,7 +168,7 @@ test("call function with property using call method", () => { test("call function with property using apply method", () => { util.testFunction` - function foo(s: string) { return this + s; } + function foo(this: string, s: string) { return this + s; } foo.baz = "baz"; return foo.apply("foo", ["bar"]) + foo.baz; `.expectToMatchJsResult(); @@ -176,7 +176,7 @@ test("call function with property using apply method", () => { test("call function with property using bind method", () => { util.testFunction` - function foo(s: string) { return this + s; } + function foo(this: string, s: string) { return this + s; } foo.baz = "baz"; return foo.bind("foo", "bar")() + foo.baz; `.expectToMatchJsResult(); diff --git a/test/unit/functions/functions.spec.ts b/test/unit/functions/functions.spec.ts index 7bdf6b040..27b48a1ee 100644 --- a/test/unit/functions/functions.spec.ts +++ b/test/unit/functions/functions.spec.ts @@ -5,7 +5,7 @@ import { unsupportedForTarget } from "../../../src/transformation/utils/diagnost test("Arrow Function Expression", () => { util.testFunction` - const add = (a, b) => a + b; + const add = (a: number, b: number) => a + b; return add(1, 2); `.expectToMatchJsResult(); }); @@ -25,17 +25,22 @@ test.each(["i++", "i--", "++i", "--i"])("Arrow function unary expression (%p)", `.expectToMatchJsResult(); }); -test.each(["b => a = b", "b => a += b", "b => a -= b", "b => a *= b", "b => a /= b", "b => a **= b", "b => a %= b"])( - "Arrow function assignment (%p)", - lambda => { - util.testFunction` +test.each([ + "(b: number) => a = b", + "(b: number) => a += b", + "(b: number) => a -= b", + "(b: number) => a *= b", + "(b: number) => a /= b", + "(b: number) => a **= b", + "(b: number) => a %= b", +])("Arrow function assignment (%p)", lambda => { + util.testFunction` let a = 10; let lambda = ${lambda}; lambda(5); return a; `.expectToMatchJsResult(); - } -); +}); test.each([{ args: [] }, { args: [1] }, { args: [1, 2] }])("Arrow default values (%p)", ({ args }) => { util.testFunction` @@ -46,7 +51,7 @@ test.each([{ args: [] }, { args: [1] }, { args: [1, 2] }])("Arrow default values test("Function Expression", () => { util.testFunction` - let add = function(a, b) {return a+b}; + let add = function(a: number, b: number) {return a+b}; return add(1,2); `.expectToMatchJsResult(); }); @@ -251,13 +256,13 @@ test.each(functionTypeDeclarations)("Function call (%s)", (_, type) => { }); test.each([ - "function fn() {}", - "function fn(x, y, z) {}", - "function fn(x, y, z, ...args) {}", - "function fn(...args) {}", - "function fn(this: void) {}", - "function fn(this: void, x, y, z) {}", - "function fnReference(x, y, z) {} const fn = fnReference;", + "function fn(): void {}", + "function fn(x: any, y: any, z: any): void {}", + "function fn(x: any, y: any, z: any, ...args: any[]): void {}", + "function fn(...args: any[]): void {}", + "function fn(this: void): void {}", + "function fn(this: void, x: any, y: any, z: any): void {}", + "function fnReference(x: any, y: any, z: any): void {} const fn = fnReference;", "const wrap = (fn: (...args: any[]) => any) => (...args: any[]) => fn(...args); const fn = wrap((x, y, z) => {});", ])("function.length (%p)", declaration => { util.testFunction` @@ -432,7 +437,7 @@ test("Complex element access call no args", () => { test("Complex element access call statement", () => { util.testFunction` - let foo: string; + let foo: string | undefined; class C { prop = "bar"; method(s: string) { foo = s + this.prop; } diff --git a/test/unit/functions/generators.spec.ts b/test/unit/functions/generators.spec.ts index aa62d719d..6cf935528 100644 --- a/test/unit/functions/generators.spec.ts +++ b/test/unit/functions/generators.spec.ts @@ -27,7 +27,7 @@ test(".next()", () => { test(".next() with parameters", () => { util.testFunction` - function* generator() { + function* generator(): Generator { return yield 0; } diff --git a/test/unit/functions/validation/__snapshots__/invalidFunctionAssignments.spec.ts.snap b/test/unit/functions/validation/__snapshots__/invalidFunctionAssignments.spec.ts.snap index b7175df75..87cb07b9d 100644 --- a/test/unit/functions/validation/__snapshots__/invalidFunctionAssignments.spec.ts.snap +++ b/test/unit/functions/validation/__snapshots__/invalidFunctionAssignments.spec.ts.snap @@ -340,17 +340,17 @@ exports[`Invalid function argument ({"definition": "namespace NoSelfFuncNs { exports[`Invalid function argument ({"definition": "namespace SelfAnonFuncNSMergedNoSelfNS { export function nsFuncSelf(s: string): string { return s; } } /** @noSelf */ namespace SelfAnonFuncNSMergedNoSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "SelfAnonFuncNSMergedNoSelfNS.nsFuncSelf"}): diagnostics 1`] = `"main.ts(5,27): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid function argument ({"value": "(function(this: any, s) { return s; })"}): diagnostics 1`] = `"main.ts(4,27): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; +exports[`Invalid function argument ({"value": "(function(this: any, s: string) { return s; })"}): diagnostics 1`] = `"main.ts(4,27): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid function argument ({"value": "(function(this: void, s) { return s; })"}): diagnostics 1`] = `"main.ts(4,27): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid function argument ({"value": "(function(this: void, s: string) { return s; })"}): diagnostics 1`] = `"main.ts(4,27): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid function argument ({"value": "(function(this: void, s) { return s; })"}): diagnostics 2`] = `"main.ts(4,27): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid function argument ({"value": "(function(this: void, s: string) { return s; })"}): diagnostics 2`] = `"main.ts(4,27): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid function argument ({"value": "function(this: any, s) { return s; }"}): diagnostics 1`] = `"main.ts(4,27): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; +exports[`Invalid function argument ({"value": "function(this: any, s: string) { return s; }"}): diagnostics 1`] = `"main.ts(4,27): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid function argument ({"value": "function(this: void, s) { return s; }"}): diagnostics 1`] = `"main.ts(4,27): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid function argument ({"value": "function(this: void, s: string) { return s; }"}): diagnostics 1`] = `"main.ts(4,27): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid function argument ({"value": "function(this: void, s) { return s; }"}): diagnostics 2`] = `"main.ts(4,27): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid function argument ({"value": "function(this: void, s: string) { return s; }"}): diagnostics 2`] = `"main.ts(4,27): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; exports[`Invalid function argument with cast ({"definition": "/** @noSelfInFile */ let noSelfInFileFunc: {(s: string): string} = function(s) { return s; };", "value": "noSelfInFileFunc"}): diagnostics 1`] = ` "main.ts(4,23): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'. @@ -732,17 +732,17 @@ exports[`Invalid function assignment ({"definition": "namespace NoSelfFuncNs { exports[`Invalid function assignment ({"definition": "namespace SelfAnonFuncNSMergedNoSelfNS { export function nsFuncSelf(s: string): string { return s; } } /** @noSelf */ namespace SelfAnonFuncNSMergedNoSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "SelfAnonFuncNSMergedNoSelfNS.nsFuncSelf"}): diagnostics 1`] = `"main.ts(5,18): error TSTL: Unable to convert function with a 'this' parameter to function with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid function assignment ({"value": "(function(this: any, s) { return s; })"}): diagnostics 1`] = `"main.ts(4,18): error TSTL: Unable to convert function with a 'this' parameter to function with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; +exports[`Invalid function assignment ({"value": "(function(this: any, s: string) { return s; })"}): diagnostics 1`] = `"main.ts(4,18): error TSTL: Unable to convert function with a 'this' parameter to function with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid function assignment ({"value": "(function(this: void, s) { return s; })"}): diagnostics 1`] = `"main.ts(4,18): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid function assignment ({"value": "(function(this: void, s: string) { return s; })"}): diagnostics 1`] = `"main.ts(4,18): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid function assignment ({"value": "(function(this: void, s) { return s; })"}): diagnostics 2`] = `"main.ts(4,18): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid function assignment ({"value": "(function(this: void, s: string) { return s; })"}): diagnostics 2`] = `"main.ts(4,18): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid function assignment ({"value": "function(this: any, s) { return s; }"}): diagnostics 1`] = `"main.ts(4,18): error TSTL: Unable to convert function with a 'this' parameter to function with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; +exports[`Invalid function assignment ({"value": "function(this: any, s: string) { return s; }"}): diagnostics 1`] = `"main.ts(4,18): error TSTL: Unable to convert function with a 'this' parameter to function with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid function assignment ({"value": "function(this: void, s) { return s; }"}): diagnostics 1`] = `"main.ts(4,18): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid function assignment ({"value": "function(this: void, s: string) { return s; }"}): diagnostics 1`] = `"main.ts(4,18): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid function assignment ({"value": "function(this: void, s) { return s; }"}): diagnostics 2`] = `"main.ts(4,18): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid function assignment ({"value": "function(this: void, s: string) { return s; }"}): diagnostics 2`] = `"main.ts(4,18): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; exports[`Invalid function assignment with cast ({"definition": "/** @noSelfInFile */ let noSelfInFileFunc: {(s: string): string} = function(s) { return s; };", "value": "noSelfInFileFunc"}): diagnostics 1`] = ` "main.ts(4,14): error TSTL: Unable to convert function with a 'this' parameter to function with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'. @@ -1124,17 +1124,17 @@ exports[`Invalid function generic argument ({"definition": "namespace NoSelfFunc exports[`Invalid function generic argument ({"definition": "namespace SelfAnonFuncNSMergedNoSelfNS { export function nsFuncSelf(s: string): string { return s; } } /** @noSelf */ namespace SelfAnonFuncNSMergedNoSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "SelfAnonFuncNSMergedNoSelfNS.nsFuncSelf"}): diagnostics 1`] = `"main.ts(5,27): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid function generic argument ({"value": "(function(this: any, s) { return s; })"}): diagnostics 1`] = `"main.ts(4,27): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; +exports[`Invalid function generic argument ({"value": "(function(this: any, s: string) { return s; })"}): diagnostics 1`] = `"main.ts(4,27): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid function generic argument ({"value": "(function(this: void, s) { return s; })"}): diagnostics 1`] = `"main.ts(4,27): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid function generic argument ({"value": "(function(this: void, s: string) { return s; })"}): diagnostics 1`] = `"main.ts(4,27): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid function generic argument ({"value": "(function(this: void, s) { return s; })"}): diagnostics 2`] = `"main.ts(4,27): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid function generic argument ({"value": "(function(this: void, s: string) { return s; })"}): diagnostics 2`] = `"main.ts(4,27): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid function generic argument ({"value": "function(this: any, s) { return s; }"}): diagnostics 1`] = `"main.ts(4,27): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; +exports[`Invalid function generic argument ({"value": "function(this: any, s: string) { return s; }"}): diagnostics 1`] = `"main.ts(4,27): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid function generic argument ({"value": "function(this: void, s) { return s; }"}): diagnostics 1`] = `"main.ts(4,27): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid function generic argument ({"value": "function(this: void, s: string) { return s; }"}): diagnostics 1`] = `"main.ts(4,27): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid function generic argument ({"value": "function(this: void, s) { return s; }"}): diagnostics 2`] = `"main.ts(4,27): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid function generic argument ({"value": "function(this: void, s: string) { return s; }"}): diagnostics 2`] = `"main.ts(4,27): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; exports[`Invalid function overload assignment ("(this: void, s: string) => string"): diagnostics 1`] = `"main.ts(7,52): error TSTL: Unsupported assignment of function with different overloaded types for 'this'. Overloads should all have the same type for 'this'."`; @@ -1484,17 +1484,17 @@ exports[`Invalid function return ({"definition": "namespace NoSelfFuncNs { exports[`Invalid function return ({"definition": "namespace SelfAnonFuncNSMergedNoSelfNS { export function nsFuncSelf(s: string): string { return s; } } /** @noSelf */ namespace SelfAnonFuncNSMergedNoSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "SelfAnonFuncNSMergedNoSelfNS.nsFuncSelf"}): diagnostics 1`] = `"main.ts(5,17): error TSTL: Unable to convert function with a 'this' parameter to function with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid function return ({"value": "(function(this: any, s) { return s; })"}): diagnostics 1`] = `"main.ts(4,17): error TSTL: Unable to convert function with a 'this' parameter to function with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; +exports[`Invalid function return ({"value": "(function(this: any, s: string) { return s; })"}): diagnostics 1`] = `"main.ts(4,17): error TSTL: Unable to convert function with a 'this' parameter to function with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid function return ({"value": "(function(this: void, s) { return s; })"}): diagnostics 1`] = `"main.ts(4,17): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid function return ({"value": "(function(this: void, s: string) { return s; })"}): diagnostics 1`] = `"main.ts(4,17): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid function return ({"value": "(function(this: void, s) { return s; })"}): diagnostics 2`] = `"main.ts(4,17): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid function return ({"value": "(function(this: void, s: string) { return s; })"}): diagnostics 2`] = `"main.ts(4,17): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid function return ({"value": "function(this: any, s) { return s; }"}): diagnostics 1`] = `"main.ts(4,17): error TSTL: Unable to convert function with a 'this' parameter to function with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; +exports[`Invalid function return ({"value": "function(this: any, s: string) { return s; }"}): diagnostics 1`] = `"main.ts(4,17): error TSTL: Unable to convert function with a 'this' parameter to function with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid function return ({"value": "function(this: void, s) { return s; }"}): diagnostics 1`] = `"main.ts(4,17): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid function return ({"value": "function(this: void, s: string) { return s; }"}): diagnostics 1`] = `"main.ts(4,17): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid function return ({"value": "function(this: void, s) { return s; }"}): diagnostics 2`] = `"main.ts(4,17): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid function return ({"value": "function(this: void, s: string) { return s; }"}): diagnostics 2`] = `"main.ts(4,17): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; exports[`Invalid function return with cast ({"definition": "/** @noSelfInFile */ let noSelfInFileFunc: {(s: string): string} = function(s) { return s; };", "value": "noSelfInFileFunc"}): diagnostics 1`] = ` "main.ts(4,17): error TSTL: Unable to convert function with a 'this' parameter to function with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'. @@ -1885,17 +1885,17 @@ exports[`Invalid function variable declaration ({"definition": "namespace NoSelf exports[`Invalid function variable declaration ({"definition": "namespace SelfAnonFuncNSMergedNoSelfNS { export function nsFuncSelf(s: string): string { return s; } } /** @noSelf */ namespace SelfAnonFuncNSMergedNoSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "SelfAnonFuncNSMergedNoSelfNS.nsFuncSelf"}): diagnostics 1`] = `"main.ts(4,59): error TSTL: Unable to convert function with a 'this' parameter to function with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid function variable declaration ({"value": "(function(this: any, s) { return s; })"}): diagnostics 1`] = `"main.ts(3,59): error TSTL: Unable to convert function with a 'this' parameter to function with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; +exports[`Invalid function variable declaration ({"value": "(function(this: any, s: string) { return s; })"}): diagnostics 1`] = `"main.ts(3,59): error TSTL: Unable to convert function with a 'this' parameter to function with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid function variable declaration ({"value": "(function(this: void, s) { return s; })"}): diagnostics 1`] = `"main.ts(3,47): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid function variable declaration ({"value": "(function(this: void, s: string) { return s; })"}): diagnostics 1`] = `"main.ts(3,47): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid function variable declaration ({"value": "(function(this: void, s) { return s; })"}): diagnostics 2`] = `"main.ts(3,58): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid function variable declaration ({"value": "(function(this: void, s: string) { return s; })"}): diagnostics 2`] = `"main.ts(3,58): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid function variable declaration ({"value": "function(this: any, s) { return s; }"}): diagnostics 1`] = `"main.ts(3,59): error TSTL: Unable to convert function with a 'this' parameter to function with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; +exports[`Invalid function variable declaration ({"value": "function(this: any, s: string) { return s; }"}): diagnostics 1`] = `"main.ts(3,59): error TSTL: Unable to convert function with a 'this' parameter to function with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid function variable declaration ({"value": "function(this: void, s) { return s; }"}): diagnostics 1`] = `"main.ts(3,47): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid function variable declaration ({"value": "function(this: void, s: string) { return s; }"}): diagnostics 1`] = `"main.ts(3,47): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid function variable declaration ({"value": "function(this: void, s) { return s; }"}): diagnostics 2`] = `"main.ts(3,58): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid function variable declaration ({"value": "function(this: void, s: string) { return s; }"}): diagnostics 2`] = `"main.ts(3,58): error TSTL: Unable to convert function with no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; exports[`Invalid interface method assignment: diagnostics 1`] = `"main.ts(5,22): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; @@ -2260,17 +2260,17 @@ exports[`Invalid object with method argument ({"definition": "namespace NoSelfFu exports[`Invalid object with method argument ({"definition": "namespace SelfAnonFuncNSMergedNoSelfNS { export function nsFuncSelf(s: string): string { return s; } } /** @noSelf */ namespace SelfAnonFuncNSMergedNoSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "SelfAnonFuncNSMergedNoSelfNS.nsFuncSelf"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(5,35): error TSTL: Unable to convert function with a 'this' parameter to function 'obj.fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid object with method argument ({"value": "(function(this: any, s) { return s; })"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(4,35): error TSTL: Unable to convert function with a 'this' parameter to function 'obj.fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; +exports[`Invalid object with method argument ({"value": "(function(this: any, s: string) { return s; })"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(4,35): error TSTL: Unable to convert function with a 'this' parameter to function 'obj.fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid object with method argument ({"value": "(function(this: void, s) { return s; })"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(4,35): error TSTL: Unable to convert function with no 'this' parameter to function 'obj.fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid object with method argument ({"value": "(function(this: void, s: string) { return s; })"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(4,35): error TSTL: Unable to convert function with no 'this' parameter to function 'obj.fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid object with method argument ({"value": "(function(this: void, s) { return s; })"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(4,35): error TSTL: Unable to convert function with no 'this' parameter to function 'obj.fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid object with method argument ({"value": "(function(this: void, s: string) { return s; })"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(4,35): error TSTL: Unable to convert function with no 'this' parameter to function 'obj.fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid object with method argument ({"value": "function(this: any, s) { return s; }"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(4,35): error TSTL: Unable to convert function with a 'this' parameter to function 'obj.fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; +exports[`Invalid object with method argument ({"value": "function(this: any, s: string) { return s; }"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(4,35): error TSTL: Unable to convert function with a 'this' parameter to function 'obj.fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid object with method argument ({"value": "function(this: void, s) { return s; }"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(4,35): error TSTL: Unable to convert function with no 'this' parameter to function 'obj.fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid object with method argument ({"value": "function(this: void, s: string) { return s; }"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(4,35): error TSTL: Unable to convert function with no 'this' parameter to function 'obj.fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid object with method argument ({"value": "function(this: void, s) { return s; }"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(4,35): error TSTL: Unable to convert function with no 'this' parameter to function 'obj.fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid object with method argument ({"value": "function(this: void, s: string) { return s; }"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(4,35): error TSTL: Unable to convert function with no 'this' parameter to function 'obj.fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; exports[`Invalid object with method assignment ({"definition": "/** @noSelf */ class AnonFuncNSMergedNoSelfClass { method(s: string): string { return s; } } namespace AnonFuncNSMergedNoSelfClass { export function nsFunc(s: string) { return s; } }", "value": "AnonFuncNSMergedNoSelfClass.nsFunc"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(5,19): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; @@ -2629,17 +2629,17 @@ exports[`Invalid object with method assignment ({"definition": "namespace NoSelf exports[`Invalid object with method assignment ({"definition": "namespace SelfAnonFuncNSMergedNoSelfNS { export function nsFuncSelf(s: string): string { return s; } } /** @noSelf */ namespace SelfAnonFuncNSMergedNoSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "SelfAnonFuncNSMergedNoSelfNS.nsFuncSelf"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(5,19): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid object with method assignment ({"value": "(function(this: any, s) { return s; })"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(4,19): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; +exports[`Invalid object with method assignment ({"value": "(function(this: any, s: string) { return s; })"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(4,19): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid object with method assignment ({"value": "(function(this: void, s) { return s; })"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(4,19): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid object with method assignment ({"value": "(function(this: void, s: string) { return s; })"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(4,19): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid object with method assignment ({"value": "(function(this: void, s) { return s; })"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(4,19): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid object with method assignment ({"value": "(function(this: void, s: string) { return s; })"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(4,19): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid object with method assignment ({"value": "function(this: any, s) { return s; }"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(4,19): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; +exports[`Invalid object with method assignment ({"value": "function(this: any, s: string) { return s; }"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(4,19): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid object with method assignment ({"value": "function(this: void, s) { return s; }"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(4,19): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid object with method assignment ({"value": "function(this: void, s: string) { return s; }"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(4,19): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid object with method assignment ({"value": "function(this: void, s) { return s; }"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(4,19): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid object with method assignment ({"value": "function(this: void, s: string) { return s; }"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(4,19): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; exports[`Invalid object with method assignment with cast ({"definition": "/** @noSelfInFile */ let noSelfInFileFunc: {(s: string): string} = function(s) { return s; };", "value": "noSelfInFileFunc"}, "(noSelfInFileFunc) as ((this: any, s: string) => string)", false): diagnostics 1`] = ` "main.ts(4,19): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'. @@ -3038,14 +3038,14 @@ exports[`Invalid object with method variable declaration ({"definition": "namesp exports[`Invalid object with method variable declaration ({"definition": "namespace SelfAnonFuncNSMergedNoSelfNS { export function nsFuncSelf(s: string): string { return s; } } /** @noSelf */ namespace SelfAnonFuncNSMergedNoSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "SelfAnonFuncNSMergedNoSelfNS.nsFuncSelf"}, "(this: void, s: string) => string"): diagnostics 1`] = `"main.ts(4,68): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid object with method variable declaration ({"value": "(function(this: any, s) { return s; })"}, "(this: void, s: string) => string"): diagnostics 1`] = `"main.ts(3,68): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; +exports[`Invalid object with method variable declaration ({"value": "(function(this: any, s: string) { return s; })"}, "(this: void, s: string) => string"): diagnostics 1`] = `"main.ts(3,68): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid object with method variable declaration ({"value": "(function(this: void, s) { return s; })"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(3,56): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid object with method variable declaration ({"value": "(function(this: void, s: string) { return s; })"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(3,56): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid object with method variable declaration ({"value": "(function(this: void, s) { return s; })"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(3,67): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid object with method variable declaration ({"value": "(function(this: void, s: string) { return s; })"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(3,67): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid object with method variable declaration ({"value": "function(this: any, s) { return s; }"}, "(this: void, s: string) => string"): diagnostics 1`] = `"main.ts(3,68): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; +exports[`Invalid object with method variable declaration ({"value": "function(this: any, s: string) { return s; }"}, "(this: void, s: string) => string"): diagnostics 1`] = `"main.ts(3,68): error TSTL: Unable to convert function with a 'this' parameter to function 'fn' with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'."`; -exports[`Invalid object with method variable declaration ({"value": "function(this: void, s) { return s; }"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(3,56): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid object with method variable declaration ({"value": "function(this: void, s: string) { return s; }"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(3,56): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; -exports[`Invalid object with method variable declaration ({"value": "function(this: void, s) { return s; }"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(3,67): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; +exports[`Invalid object with method variable declaration ({"value": "function(this: void, s: string) { return s; }"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(3,67): error TSTL: Unable to convert function with no 'this' parameter to function 'fn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; diff --git a/test/unit/functions/validation/functionExpressionTypeInference.spec.ts b/test/unit/functions/validation/functionExpressionTypeInference.spec.ts index abac4e483..9220902b2 100644 --- a/test/unit/functions/validation/functionExpressionTypeInference.spec.ts +++ b/test/unit/functions/validation/functionExpressionTypeInference.spec.ts @@ -79,7 +79,7 @@ test.each([ test("Function expression type inference in object literal assigned to narrower type", () => { util.testFunction` - let foo: {} = {bar: s => s}; + let foo: {} = {bar: (s: string) => s}; return (foo as {bar: (a: any) => any}).bar("foobar"); `.expectToMatchJsResult(); }); diff --git a/test/unit/functions/validation/functionPermutations.ts b/test/unit/functions/validation/functionPermutations.ts index 14e478498..4947bc9a4 100644 --- a/test/unit/functions/validation/functionPermutations.ts +++ b/test/unit/functions/validation/functionPermutations.ts @@ -352,20 +352,20 @@ const noSelfInFileTestFunctions: TestFunction[] = [ ]; export const anonTestFunctionExpressions: TestFunction[] = [ - { value: "s => s" }, - { value: "(s => s)" }, - { value: "function(s) { return s; }" }, - { value: "(function(s) { return s; })" }, + { value: "(s: string) => s" }, + { value: "((s: string) => s)" }, + { value: "function(s: string) { return s; }" }, + { value: "(function(s: string) { return s; })" }, ]; export const selfTestFunctionExpressions: TestFunction[] = [ - { value: "function(this: any, s) { return s; }" }, - { value: "(function(this: any, s) { return s; })" }, + { value: "function(this: any, s: string) { return s; }" }, + { value: "(function(this: any, s: string) { return s; })" }, ]; export const noSelfTestFunctionExpressions: TestFunction[] = [ - { value: "function(this: void, s) { return s; }" }, - { value: "(function(this: void, s) { return s; })" }, + { value: "function(this: void, s: string) { return s; }" }, + { value: "(function(this: void, s: string) { return s; })" }, ]; export const anonTestFunctionType = "(s: string) => string"; diff --git a/test/unit/functions/validation/invalidFunctionAssignments.spec.ts b/test/unit/functions/validation/invalidFunctionAssignments.spec.ts index 95b26b7d0..2b9fd61db 100644 --- a/test/unit/functions/validation/invalidFunctionAssignments.spec.ts +++ b/test/unit/functions/validation/invalidFunctionAssignments.spec.ts @@ -96,7 +96,7 @@ test.each(invalidTestFunctionAssignments)( (testFunction, functionType, isSelfConversion) => { util.testModule` ${testFunction.definition ?? ""} - declare function takesFunction(fn: ${functionType}); + declare function takesFunction(fn: ${functionType}): void; takesFunction(${testFunction.value}); `.expectDiagnosticsToMatchSnapshot( [isSelfConversion ? unsupportedSelfFunctionConversion.code : unsupportedNoSelfFunctionConversion.code], @@ -110,7 +110,7 @@ test.each(invalidTestMethodAssignments)( (testFunction, functionType, isSelfConversion) => { util.testModule` ${testFunction.definition ?? ""} - declare function takesObjectWithMethod(obj: { fn: ${functionType} }); + declare function takesObjectWithMethod(obj: { fn: ${functionType} }): void; takesObjectWithMethod({fn: ${testFunction.value}}); `.expectDiagnosticsToMatchSnapshot( [isSelfConversion ? unsupportedSelfFunctionConversion.code : unsupportedNoSelfFunctionConversion.code], @@ -130,7 +130,7 @@ test("Invalid lua lib function argument", () => { test.each(invalidTestFunctionCasts)("Invalid function argument with cast (%p)", (testFunction, castedFunction) => { util.testModule` ${testFunction.definition ?? ""} - declare function takesFunction(fn: typeof ${testFunction.value}); + declare function takesFunction(fn: typeof ${testFunction.value}): void; takesFunction(${castedFunction}); `.expectDiagnosticsToMatchSnapshot( [unsupportedNoSelfFunctionConversion.code, unsupportedSelfFunctionConversion.code], @@ -143,7 +143,7 @@ test.each(invalidTestFunctionAssignments)( (testFunction, functionType, isSelfConversion) => { util.testModule` ${testFunction.definition ?? ""} - declare function takesFunction(fn: T); + declare function takesFunction(fn: T): void; takesFunction(${testFunction.value}); `.expectDiagnosticsToMatchSnapshot( [isSelfConversion ? unsupportedSelfFunctionConversion.code : unsupportedNoSelfFunctionConversion.code], diff --git a/test/unit/hoisting.spec.ts b/test/unit/hoisting.spec.ts index 144460067..e08a7c083 100644 --- a/test/unit/hoisting.spec.ts +++ b/test/unit/hoisting.spec.ts @@ -254,7 +254,7 @@ test("Hoisting variable without initializer", () => { function foo() { return x; } - let x: number; + let x: number | undefined; return foo(); `.expectToMatchJsResult(); }); diff --git a/test/unit/identifiers.spec.ts b/test/unit/identifiers.spec.ts index f148c3c72..ddac778fc 100644 --- a/test/unit/identifiers.spec.ts +++ b/test/unit/identifiers.spec.ts @@ -93,9 +93,8 @@ test.each([ "const foo: any, bar: any, $$$: any;", "class $$$ {}", "namespace $$$ { export const bar: any; }", - "module $$$ { export const bar: any; }", "enum $$$ {}", - "function $$$();", + "function $$$(): void;", ])("ambient identifier must be a valid lua identifier (%p)", statement => { util.testModule` declare ${statement} diff --git a/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap b/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap index a3f6de381..4420b90e0 100644 --- a/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap +++ b/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap @@ -160,22 +160,7 @@ return ____exports" exports[`invalid direct $multi function use (const ar = [1]; const [a] = $multi(...ar)): diagnostics 1`] = `"main.ts(7,37): error TSTL: The $multi function must be called in a return statement."`; -exports[`invalid direct $multi function use (let a; [a] = $multi()): code 1`] = ` -"local ____exports = {} -local function multi(self, ...) - return ... -end -local a -local ____temp_0 = {____(nil)} -a = ____temp_0[1] -____exports.a = a -____exports.a = a -return ____exports" -`; - -exports[`invalid direct $multi function use (let a; [a] = $multi()): diagnostics 1`] = `"main.ts(7,22): error TSTL: The $multi function must be called in a return statement."`; - -exports[`invalid direct $multi function use (let a; for ([a] = $multi(1, 2); false; 1) {}): code 1`] = ` +exports[`invalid direct $multi function use (let a: any; for ([a] = $multi(1, 2); false; 1) {}): code 1`] = ` "local ____exports = {} local function multi(self, ...) return ... @@ -193,9 +178,9 @@ ____exports.a = a return ____exports" `; -exports[`invalid direct $multi function use (let a; for ([a] = $multi(1, 2); false; 1) {}): diagnostics 1`] = `"main.ts(7,27): error TSTL: The $multi function must be called in a return statement."`; +exports[`invalid direct $multi function use (let a: any; for ([a] = $multi(1, 2); false; 1) {}): diagnostics 1`] = `"main.ts(7,32): error TSTL: The $multi function must be called in a return statement."`; -exports[`invalid direct $multi function use (let a; for (const [a] = $multi(1, 2); false; 1) {}): code 1`] = ` +exports[`invalid direct $multi function use (let a: any; for (const [a] = $multi(1, 2); false; 1) {}): code 1`] = ` "local ____exports = {} local function multi(self, ...) return ... @@ -211,9 +196,9 @@ ____exports.a = a return ____exports" `; -exports[`invalid direct $multi function use (let a; for (const [a] = $multi(1, 2); false; 1) {}): diagnostics 1`] = `"main.ts(7,33): error TSTL: The $multi function must be called in a return statement."`; +exports[`invalid direct $multi function use (let a: any; for (const [a] = $multi(1, 2); false; 1) {}): diagnostics 1`] = `"main.ts(7,38): error TSTL: The $multi function must be called in a return statement."`; -exports[`invalid direct $multi function use (let a; if ([a] = $multi(1)) { ++a; }): code 1`] = ` +exports[`invalid direct $multi function use (let a: any; if ([a] = $multi(1)) { ++a; }): code 1`] = ` "local ____exports = {} local function multi(self, ...) return ... @@ -230,4 +215,19 @@ ____exports.a = a return ____exports" `; -exports[`invalid direct $multi function use (let a; if ([a] = $multi(1)) { ++a; }): diagnostics 1`] = `"main.ts(7,26): error TSTL: The $multi function must be called in a return statement."`; +exports[`invalid direct $multi function use (let a: any; if ([a] = $multi(1)) { ++a; }): diagnostics 1`] = `"main.ts(7,31): error TSTL: The $multi function must be called in a return statement."`; + +exports[`invalid direct $multi function use (let a; [a] = $multi()): code 1`] = ` +"local ____exports = {} +local function multi(self, ...) + return ... +end +local a +local ____temp_0 = {____(nil)} +a = ____temp_0[1] +____exports.a = a +____exports.a = a +return ____exports" +`; + +exports[`invalid direct $multi function use (let a; [a] = $multi()): diagnostics 1`] = `"main.ts(7,22): error TSTL: The $multi function must be called in a return statement."`; diff --git a/test/unit/language-extensions/multi.spec.ts b/test/unit/language-extensions/multi.spec.ts index 72b330887..90fc54200 100644 --- a/test/unit/language-extensions/multi.spec.ts +++ b/test/unit/language-extensions/multi.spec.ts @@ -42,6 +42,7 @@ test("Destructuring assignment of LuaMultiReturn returning nil", () => { export {a, b}; ` .withLanguageExtensions() + .setOptions({ strict: false }) .expectToEqual({ a: undefined, b: [] }); }); @@ -58,7 +59,7 @@ test.each<[string, any]>([ }); const multiFunction = ` -function multi(...args) { +function multi(...args: unknown[]) { return $multi(...args); } `; @@ -71,9 +72,9 @@ const createCasesThatCall = (name: string): Array<[string, any]> => [ [`const [a = 1] = ${name}(2)`, 2], [`const ar = [1]; const [a] = ${name}(...ar)`, 1], [`const _ = null, [a] = ${name}(1)`, 1], - [`let a; for (const [a] = ${name}(1, 2); false; 1) {}`, undefined], - [`let a; for ([a] = ${name}(1, 2); false; 1) {}`, 1], - [`let a; if ([a] = ${name}(1)) { ++a; }`, 2], + [`let a: any; for (const [a] = ${name}(1, 2); false; 1) {}`, undefined], + [`let a: any; for ([a] = ${name}(1, 2); false; 1) {}`, 1], + [`let a: any; if ([a] = ${name}(1)) { ++a; }`, 2], ]; test.each<[string, any]>(createCasesThatCall("$multi"))("invalid direct $multi function use (%s)", statement => { @@ -272,7 +273,7 @@ test("return $multi from try", () => { } catch { } } - const [_, a] = multiTest(); + const [_, a] = multiTest()!; return a; ` .withLanguageExtensions() @@ -304,7 +305,7 @@ test("return LuaMultiReturn from try", () => { } catch { } } - const [_, a] = multiTest(); + const [_, a] = multiTest()!; return a; ` .withLanguageExtensions() diff --git a/test/unit/loops.spec.ts b/test/unit/loops.spec.ts index b88e0dbeb..e6ceab5db 100644 --- a/test/unit/loops.spec.ts +++ b/test/unit/loops.spec.ts @@ -208,7 +208,7 @@ test.each([ }, ])("forin[Object] (%p)", ({ inp }) => { util.testFunctionTemplate` - let objTest = ${inp}; + let objTest: Record = ${inp}; for (let key in objTest) { objTest[key] = objTest[key] + 1; } @@ -218,7 +218,7 @@ test.each([ test("forin[Array]", () => { util.testFunction` - const array = []; + const array = [1,2,3]; for (const key in array) {} `.expectDiagnosticsToMatchSnapshot([forbiddenForIn.code]); }); @@ -238,7 +238,7 @@ test.each( ) )("forin with continue (%s %p)", (luaTarget, { inp }) => { util.testFunctionTemplate` - let obj = ${inp}; + let obj: Record = ${inp}; for (let i in obj) { if (obj[i] % 2 == 0) { continue; diff --git a/test/unit/modules/modules.spec.ts b/test/unit/modules/modules.spec.ts index d15f18463..1969806d0 100644 --- a/test/unit/modules/modules.spec.ts +++ b/test/unit/modules/modules.spec.ts @@ -94,8 +94,7 @@ test("Default Import and Export Expression", () => { test("Import and Export Assignment", () => { util.testModule` - // @ts-ignore - import * as m from "./module"; + import m = require("./module"); export const value = m; ` .setOptions({ module: ts.ModuleKind.CommonJS }) diff --git a/test/unit/modules/resolution.spec.ts b/test/unit/modules/resolution.spec.ts index 26c147c68..c6f97d5f9 100644 --- a/test/unit/modules/resolution.spec.ts +++ b/test/unit/modules/resolution.spec.ts @@ -1,5 +1,6 @@ +import * as path from "path"; import * as ts from "typescript"; -import { couldNotResolveRequire } from "../../../src/transpilation/diagnostics"; +import { couldNotResolveRequire, emitPathCollision } from "../../../src/transpilation/diagnostics"; import * as util from "../../util"; const requireRegex = /require\("(.*?)"\)/; @@ -166,6 +167,52 @@ test.each([ .tap(expectToRequire(expectedPath)); }); +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1445 +// Can't test this via execution because the test harness uses package.preload +// instead of real filesystem resolution, so require() always finds the module +// regardless of output path. We check the output path directly instead. +test("dots in directory names are replaced with underscores in output", () => { + const { transpiledFiles } = util.testModule` + import { answer } from "./Foo.Bar"; + export const result = answer; + ` + .addExtraFile("Foo.Bar/index.ts", "export const answer = 42;") + .setOptions({ rootDir: "." }) + .getLuaResult(); + + const dottedFile = transpiledFiles.find(f => f.lua?.includes("answer = 42")); + expect(dottedFile).toBeDefined(); + expect(dottedFile!.outPath).toContain(path.join("Foo_Bar", "index.lua")); + expect(dottedFile!.outPath).not.toContain("Foo.Bar"); +}); + +test("dots in file names are replaced with underscores in output", () => { + const { transpiledFiles } = util.testModule` + import { answer } from "./foo.test"; + export const result = answer; + ` + .addExtraFile("foo.test.ts", "export const answer = 42;") + .setOptions({ rootDir: "." }) + .getLuaResult(); + + const dottedFile = transpiledFiles.find(f => f.lua?.includes("answer = 42")); + expect(dottedFile).toBeDefined(); + expect(dottedFile!.outPath).toContain("foo_test.lua"); + expect(dottedFile!.outPath).not.toContain("foo.test"); +}); + +test("dots in paths that collide with existing paths produce a diagnostic", () => { + util.testModule` + import { a } from "./Foo.Bar"; + import { b } from "./Foo_Bar"; + export const result = a + b; + ` + .addExtraFile("Foo.Bar/index.ts", "export const a = 1;") + .addExtraFile("Foo_Bar/index.ts", "export const b = 2;") + .setOptions({ rootDir: "." }) + .expectToHaveDiagnostics([emitPathCollision.code]); +}); + test("import = require", () => { util.testModule` import foo = require("./foo/bar"); diff --git a/test/unit/namespaces.spec.ts b/test/unit/namespaces.spec.ts index 01b2e3f92..7c98e7ef9 100644 --- a/test/unit/namespaces.spec.ts +++ b/test/unit/namespaces.spec.ts @@ -1,15 +1,5 @@ import * as util from "../util"; -test("legacy internal module syntax", () => { - util.testModule` - module Foo { - export const foo = "bar"; - } - - export const foo = Foo.foo; - `.expectToMatchJsResult(); -}); - test("global scoping", () => { util.testFunction("return a.foo();") .setTsHeader('namespace a { export function foo() { return "bar"; } }') @@ -42,7 +32,7 @@ test("context in namespace function", () => { util.testModule` namespace a { export const foo = "foo"; - export function bar() { return this.foo + "bar"; } + export function bar(this: typeof a) { return this.foo + "bar"; } } export const result = a.bar(); diff --git a/test/unit/optionalChaining.spec.ts b/test/unit/optionalChaining.spec.ts index d99f18be9..10de7e792 100644 --- a/test/unit/optionalChaining.spec.ts +++ b/test/unit/optionalChaining.spec.ts @@ -1,10 +1,9 @@ import { notAllowedOptionalAssignment } from "../../src/transformation/utils/diagnostics"; import * as util from "../util"; -import { ScriptTarget } from "typescript"; test.each(["null", "undefined", '{ foo: "foo" }'])("optional chaining (%p)", value => { util.testFunction` - const obj: {foo: string} | null | undefined = ${value}; + const obj = ${value} as {foo: string} | null | undefined; return obj?.foo; ` .expectToMatchJsResult() @@ -24,7 +23,7 @@ test("long optional chain", () => { test.each(["undefined", "{}", "{ foo: {} }", "{ foo: {bar: 'baz'}}"])("nested optional chaining (%p)", value => { util.testFunction` - const obj: { foo?: { bar?: string } } | undefined = ${value}; + const obj = ${value} as { foo?: { bar?: string } } | undefined; return obj?.foo?.bar; `.expectToMatchJsResult(); }); @@ -33,7 +32,7 @@ test.each(["undefined", "{}", "{ foo: {} }", "{ foo: {bar: 'baz'}}"])( "nested optional chaining combined with coalescing (%p)", value => { util.testFunction` - const obj: { foo?: { bar?: string } } | undefined = ${value}; + const obj = ${value} as { foo?: { bar?: string } } | undefined; return obj?.foo?.bar ?? "not found"; `.expectToMatchJsResult(); } @@ -108,7 +107,7 @@ test("unused call", () => { // should use if statement, as result is not used }); -test.each(["undefined", "{ foo: v=>v }"])("with preceding statements on right side", value => { +test.each(["undefined", "{ foo: (v: any)=>v }"])("with preceding statements on right side", value => { util.testFunction` let i = 0 const obj: any = ${value}; @@ -120,12 +119,12 @@ test.each(["undefined", "{ foo: v=>v }"])("with preceding statements on right si }); // unused, with preceding statements on right side -test.each(["undefined", "{ foo(val) {return val} }"])( +test.each(["undefined", "{ foo(val: any) {return val} }"])( "unused result with preceding statements on right side", value => { util.testFunction` let i = 0 - const obj = ${value}; + const obj = ${value} as any; obj?.foo(i++); return i ` @@ -135,8 +134,10 @@ test.each(["undefined", "{ foo(val) {return val} }"])( } ); -test.each(["undefined", "{ foo(v) { return v} }"])("with preceding statements on right side modifying left", value => { - util.testFunction` +test.each(["undefined", "{ foo(v: any) { return v} }"])( + "with preceding statements on right side modifying left", + value => { + util.testFunction` let i = 0 let obj: any = ${value}; function bar() { @@ -147,10 +148,11 @@ test.each(["undefined", "{ foo(v) { return v} }"])("with preceding statements on return {result: obj?.foo(bar(), i++), obj, i} ` - .expectToMatchJsResult() - .expectLuaToMatchSnapshot(); - // should use if statement, as there are preceding statements -}); + .expectToMatchJsResult() + .expectLuaToMatchSnapshot(); + // should use if statement, as there are preceding statements + } +); test("does not suppress error if left side is false", () => { const result = util.testFunction` @@ -233,7 +235,7 @@ test("does not crash when incorrectly used in assignment (#1044)", () => { describe("optional chaining function calls", () => { test.each(["() => 4", "undefined"])("stand-alone optional function (%p)", value => { util.testFunction` - const f: (() => number) | undefined = ${value}; + const f = (${value}) as (() => number) | undefined; return f?.(); `.expectToMatchJsResult(); }); @@ -255,7 +257,7 @@ describe("optional chaining function calls", () => { test("object with method can be undefined", () => { util.testFunction` - const objWithMethods: { foo: () => number, bar: (this: void) => number } | undefined = undefined; + const objWithMethods = undefined as { foo: () => number, bar: (this: void) => number } | undefined; return [objWithMethods?.foo() ?? "no foo", objWithMethods?.bar() ?? "no bar"]; `.expectToMatchJsResult(); }); @@ -326,8 +328,8 @@ describe("optional chaining function calls", () => { test.each([undefined, "[1, 2, 3, 4]"])("Array: %p", expr => { util.testFunction` - const value: any[] | undefined = ${expr} - return value?.map(x=>x+1) + const value = ${expr} as any[] | undefined; + return value?.map(x=>x+1); `.expectToMatchJsResult(); }); }); @@ -342,7 +344,6 @@ describe("optional chaining function calls", () => { ` .setOptions({ strict, - target: ScriptTarget.ES5, }) .expectToMatchJsResult(); }); @@ -397,7 +398,7 @@ describe("Unsupported optional chains", () => { describe("optional delete", () => { test("successful", () => { util.testFunction` - const table = { + const table: { bar?: number } = { bar: 3 } return [delete table?.bar, table] @@ -415,9 +416,9 @@ describe("optional delete", () => { test("delete on undefined", () => { util.testFunction` - const table : { - bar: number - } | undefined = undefined + const table = undefined as { + bar?: number + } | undefined; return [delete table?.bar, table ?? "nil"] `.expectToMatchJsResult(); }); diff --git a/test/unit/overloads.spec.ts b/test/unit/overloads.spec.ts index d18daa000..85752a64a 100644 --- a/test/unit/overloads.spec.ts +++ b/test/unit/overloads.spec.ts @@ -67,8 +67,8 @@ test("overload method2", () => { test("constructor1", () => { util.testFunction` class myclass { - num: number; - str: string; + num?: number; + str?: string; constructor(def: number); constructor(def: string); @@ -88,8 +88,8 @@ test("constructor1", () => { test("constructor2", () => { util.testFunction` class myclass { - num: number; - str: string; + num?: number; + str?: string; constructor(def: number); constructor(def: string); diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts index 1ce24da19..6dd8c6473 100644 --- a/test/unit/precedingStatements.spec.ts +++ b/test/unit/precedingStatements.spec.ts @@ -101,13 +101,16 @@ describe("execution order", () => { util.testFunction` const o = {a: "A", b: "B", c: "C"}; let i = 0; - const literal = ${literal}; + const literal: Record = ${literal}; const result: Record = {}; (Object.keys(result) as Array).forEach( key => { result[key.toString()] = literal[key]; } ); return result; - `.expectToMatchJsResult(); + ` + // TS2783: duplicate property in spread — intentional, testing execution order + .ignoreDiagnostics([2783]) + .expectToMatchJsResult(); }); test("object literal with computed property names", () => { @@ -516,8 +519,8 @@ describe("assignment execution order", () => { test("function method call", () => { util.testFunction` let o = {val: 3}; - let a = function(x: number) { return this.val + x; }; - let b = function(x: number) { return (this.val + x) * 10; }; + let a = function(this: typeof o, x: number) { return this.val + x; }; + let b = function(this: typeof o, x: number) { return (this.val + x) * 10; }; function foo(x: number) { return (x > 0) ? b : a; } let i = 0; const result = foo(i).call(o, i++); diff --git a/test/unit/printer/deadCodeAfterBreak.spec.ts b/test/unit/printer/deadCodeAfterBreak.spec.ts new file mode 100644 index 000000000..fe1b9073c --- /dev/null +++ b/test/unit/printer/deadCodeAfterBreak.spec.ts @@ -0,0 +1,93 @@ +import * as tstl from "../../../src"; +import * as util from "../../util"; + +// In Lua 5.0, 5.1, and LuaJIT, break must be the last statement in a block. +// Any code after break is a syntax error (e.g. `while true do break; local b = 8 end` +// fails with "'end' expected near 'local'"). Lua 5.2+ relaxed this restriction. +// TSTL should strip dead code after break on all targets to avoid these errors. + +function expectNoDeadCode(builder: util.TestBuilder) { + const lua = builder.getMainLuaCodeChunk(); + expect(lua).not.toContain("local b = 8"); +} + +const affectedVersions: Record void) | boolean> = { + [tstl.LuaTarget.Universal]: false, + [tstl.LuaTarget.Lua50]: builder => builder.tap(expectNoDeadCode).expectToMatchJsResult(), + [tstl.LuaTarget.Lua51]: builder => builder.tap(expectNoDeadCode).expectToMatchJsResult(), + [tstl.LuaTarget.Lua52]: false, + [tstl.LuaTarget.Lua53]: false, + [tstl.LuaTarget.Lua54]: false, + [tstl.LuaTarget.Lua55]: false, + [tstl.LuaTarget.LuaJIT]: builder => builder.tap(expectNoDeadCode), + [tstl.LuaTarget.Luau]: false, +}; + +util.testEachVersion( + "for dead code after break", + () => util.testFunction` + let result = 0; + for (let i = 0; i < 10; i++) { result = i; break; const b = 8; } + return result; + `, + affectedVersions +); + +util.testEachVersion( + "for..in dead code after break", + () => util.testFunction` + let result = ""; + for (let a in {"a": 5, "b": 8}) { result = a; break; const b = 8; } + return result; + `, + affectedVersions +); + +util.testEachVersion( + "for..of dead code after break", + () => util.testFunction` + let result = 0; + for (let a of [1,2,4]) { result = a; break; const b = 8; } + return result; + `, + affectedVersions +); + +util.testEachVersion( + "while dead code after break", + () => util.testFunction` + let result = "done"; + while (true) { break; const b = 8; } + return result; + `, + affectedVersions +); + +util.testEachVersion( + "switch dead code after break", + () => util.testFunction` + let result = "none"; + switch ("abc" as string) { + case "def": + result = "def"; + break; + let abc = 4; + case "abc": + result = "abc"; + break; + let def = 6; + } + return result; + `, + affectedVersions +); + +util.testEachVersion( + "do-while dead code after break", + () => util.testFunction` + let result = "done"; + do { break; const b = 8; } while (true); + return result; + `, + affectedVersions +); diff --git a/test/unit/spread.spec.ts b/test/unit/spread.spec.ts index 53898aae5..93936f29b 100644 --- a/test/unit/spread.spec.ts +++ b/test/unit/spread.spec.ts @@ -116,6 +116,17 @@ describe("in object literal", () => { "{ ...{ x: false }, x: true }", "{ ...{ x: false }, x: false, ...{ x: true } }", ])("of object literal (%p)", expression => { + util.testExpression(expression) + .ignoreDiagnostics([2783]) // duplicate property in spread — intentional, testing spread override behavior + .expectToMatchJsResult(); + }); + + test.each([ + "{ ...((false && { a: 1 }) as any) }", + "{ ...((true && { a: 1 }) as any) }", + "{ a: 1, ...((false && { b: 2 }) as any) }", + "{ ...(null as any), ...(undefined as any) }", + ])("of short-circuited operand (%p)", expression => { util.testExpression(expression).expectToMatchJsResult(); }); diff --git a/test/unit/switch.spec.ts b/test/unit/switch.spec.ts index 4fb72c40c..1b8410a68 100644 --- a/test/unit/switch.spec.ts +++ b/test/unit/switch.spec.ts @@ -137,7 +137,7 @@ test("switch using variable re-declared in cases", () => { let foo: number = 0; switch (foo) { case 0: - let foo = true; + let foo: boolean | undefined = true; case 1: return foo; } diff --git a/test/unit/using.spec.ts b/test/unit/using.spec.ts index bfe5312b0..33749cb37 100644 --- a/test/unit/using.spec.ts +++ b/test/unit/using.spec.ts @@ -116,7 +116,7 @@ test("using disposes even when error happens", () => { test("await using disposes object with await at end of function", () => { util.testModule` - let disposeAsync; + let disposeAsync: (() => void) | undefined; function loggedAsyncDisposable(id: string): AsyncDisposable { logs.push(\`Creating \${id}\`); @@ -145,7 +145,7 @@ test("await using disposes object with await at end of function", () => { logs.push("function returned"); - disposeAsync(); + disposeAsync!(); ` .setTsHeader(usingTestLib) .setOptions({ luaLibImport: LuaLibImportKind.Inline }) @@ -194,10 +194,30 @@ test("await using no extra diagnostics (#1571)", () => { `.expectToHaveNoDiagnostics(); }); +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1622 +test("await-using with nested async arrow that also has await-using (runtime divergence)", () => { + util.testFunction` + const logs: any[] = []; + async function getA(): Promise { + return { [Symbol.asyncDispose]: async () => {} }; + } + async function outer(): Promise { + await using a = await getA(); + const inner = async (): Promise => { + await using b = await getA(); + return 42; + }; + return inner(); + } + outer().then(v => logs.push(v)); + return logs; + `.expectToEqual([42]); +}); + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1584 test("works with disposable classes (#1584)", () => { util.testFunction` - const log = []; + const log: string[] = []; class Scoped { action(): void { diff --git a/test/util.ts b/test/util.ts index 2871f6e5a..1e191fb28 100644 --- a/test/util.ts +++ b/test/util.ts @@ -51,7 +51,7 @@ function getLuaBindingsForVersion(target: tstl.LuaTarget): { lauxlib: LauxLib; l } export function assert(value: any, message?: string | Error): asserts value { - nativeAssert(value, message); + nativeAssert.ok(value, message); } export const formatCode = (...values: unknown[]) => values.map(e => stringify(e)).join(", "); @@ -92,17 +92,15 @@ export function expectEachVersionExceptJit( }; } -const memoize: MethodDecorator = (_target, _propertyKey, descriptor) => { - const originalFunction = descriptor.value as any; +const memoize = (originalFunction: any) => { const memoized = new WeakMap(); - descriptor.value = function (this: any, ...args: any[]): any { + return function (this: any, ...args: any[]): any { if (!memoized.has(this)) { memoized.set(this, originalFunction.apply(this, args)); } return memoized.get(this); - } as any; - return descriptor; + }; }; export class ExecutionError extends Error { @@ -163,7 +161,7 @@ export abstract class TestBuilder { skipLibCheck: true, target: ts.ScriptTarget.ES2017, lib: ["lib.esnext.d.ts"], - moduleResolution: ts.ModuleResolutionKind.Node10, + moduleResolution: ts.ModuleResolutionKind.Bundler, resolveJsonModule: true, sourceMap: true, }; @@ -534,8 +532,9 @@ end)());`; const moduleExports = {}; globalContext.exports = moduleExports; globalContext.module = { exports: moduleExports }; + const baseName = fileName.replace("./", ""); const transpiledExtraFile = transpiledFiles.find(({ sourceFiles }) => - sourceFiles.some(f => f.fileName === fileName.replace("./", "") + ".ts") + sourceFiles.some(f => f.fileName === baseName + ".ts" || f.fileName === baseName + "/index.ts") ); if (transpiledExtraFile?.js) { diff --git a/tsconfig.json b/tsconfig.json index bf121bd20..1fcbb6d79 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,6 @@ "lib": ["es2019"], "types": ["node"], "module": "commonjs", - "experimentalDecorators": true, "rootDir": "src", "outDir": "dist",