diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 904eae1b0..f8146b8cb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,10 @@ on: push: tags: "*" +permissions: + id-token: write + contents: read + jobs: release: name: Release @@ -11,13 +15,11 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Use Node.js 16.14.0 + - name: Use Node.js 24.12.0 uses: actions/setup-node@v1 with: - node-version: 16.14.0 + node-version: 24.12.0 registry-url: "https://registry.npmjs.org" - run: npm ci - run: npm run build - run: npm publish - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index ee190091a..46b2daaed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,72 @@ # Changelog +## 1.37.0 + +- Fixed a bug with where tsconfig paths were not correctly handled +- Improved error message for why >> is not allowed on Lua 5.3 and higher +- Added clearer diganostic for async generators and for-await-of not being supported +- Made @noSelf be respected on interfaces with call signatures +- Fixed promise.finally not being handled according to the javascript spec + +## 1.36.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) +- Fixed a bug where enums in namespaces were not merged correctly +- Fixed an issue with sourcemap traceback that caused lines from anonymous functions not to be translated correctly. +- Fixed a small bug relating to the stacktraces produced by bundled lua + +## 1.33.0 + +- Upgraded TypeScript to 5.9.3 + +## 1.32.0 + +- Fixed a broken `@customName` interation with import statements +- Use `(table.)unpack(expression, from, to)` when using array destructing syntax `const [a,b] = array;` to avoid having to unpack the entire array +- Fixed compiler annotations also considering the next line as part of any possible arguments +- Fixed a bug with unicode classnames not being properly escaped in static initializer blocks +- Fixed a bug where `@noSelf` still was not respected for index signature methods +- Fixed a case where loop variables were incorrectly missing `local` +- Removed dead code that was sometimes generated using `continue` in a loop +- Fixed a bug with tagged template literals when the tag is a function call +- Fixed a bug with class decorators leading to invalid Lua code being generated +- A `-` or `+` prefix now converts expressions to numbers with `Number()` +- Fixed a bug with root level `using` statements not properly disposing objects + +## 1.31.0 + +- Upgraded TypeScript to 5.8.2 +- Changed `currentIndent` from private to protected in the `LuaPrinter` to allow custom printers with alternate indentation +- Added `bit` and `bit32` as reserved Lua keywords to avoid accidental naming clashes. + +## 1.30.0 + +- Allow passing in-memory plugins when using the tstl API, for more flexible integration into scripts +- Changed how stacktraces are handled for `Error` in Lua 5.1 and LuaJIT + +## 1.29.0 + +- Added support for the `Luau` luaTarget. This will use Luau's `continue` statement and ternary conditional expression `if ... then ... else ...` where appropriate. +- Added support for `new Array()` syntax to construct arrays (constructing with a length argument is not allowed). +- Fixed a bug causing arrays to sometimes be indexed with a wrong index. + ## 1.28.0 - Upgraded TypeScript to 5.7.2 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 3772b95e8..31aafbbcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "typescript-to-lua", - "version": "1.28.1", + "version": "1.37.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "typescript-to-lua", - "version": "1.28.1", + "version": "1.37.1", "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.11.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.13.0", - "lua-wasm-bindings": "^0.3.1", + "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.7.2", - "typescript-eslint": "^8.16.0" + "typescript": "6.0.2", + "typescript-eslint": "^8.58.0" }, "engines": { "node": ">=16.10.0" }, "peerDependencies": { - "typescript": "5.7.2" + "typescript": "6.0.2" } }, "node_modules/@ampproject/remapping": { @@ -61,14 +61,15 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -90,6 +91,7 @@ "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -226,9 +228,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -236,9 +238,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -256,121 +258,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", - "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/parser": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", - "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.6" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -619,15 +527,15 @@ } }, "node_modules/@babel/template": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", - "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -663,15 +571,14 @@ } }, "node_modules/@babel/types": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", - "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -709,17 +616,20 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "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": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } @@ -738,9 +648,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", - "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "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": { @@ -748,45 +658,61 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", - "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "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.4", + "@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" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", - "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "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": { @@ -797,19 +723,22 @@ } }, "node_modules/@eslint/js": { - "version": "9.11.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz", - "integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==", + "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": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -817,17 +746,54 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", - "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, + "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -843,11 +809,10 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", - "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -898,9 +863,9 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -1353,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", @@ -1584,6 +1511,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", "dev": true, + "peer": true, "dependencies": { "undici-types": "~6.20.0" } @@ -1637,20 +1565,21 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz", - "integrity": "sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==", - "dev": true, - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.16.0", - "@typescript-eslint/type-utils": "8.16.0", - "@typescript-eslint/utils": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "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.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": "^1.3.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1660,26 +1589,34 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@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": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", - "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", + "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.16.0", - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/typescript-estree": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", - "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" @@ -1689,22 +1626,41 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.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.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.58.0", + "@typescript-eslint/types": "^8.58.0", + "debug": "^4.4.3" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz", - "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==", + "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.16.0", - "@typescript-eslint/visitor-keys": "8.16.0" + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1714,16 +1670,35 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "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": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz", - "integrity": "sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==", + "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/typescript-estree": "8.16.0", - "@typescript-eslint/utils": "8.16.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.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" @@ -1733,19 +1708,16 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz", - "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==", + "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": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1755,19 +1727,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz", - "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==", + "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/types": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", - "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": "^1.3.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" @@ -1776,46 +1750,60 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "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.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "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.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "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.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz", - "integrity": "sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==", + "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.4.0", - "@typescript-eslint/scope-manager": "8.16.0", - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/typescript-estree": "8.16.0" + "@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" @@ -1825,22 +1813,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "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.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz", - "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==", + "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.16.0", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.58.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1850,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", @@ -1857,11 +1855,12 @@ "license": "MIT" }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1893,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": { @@ -1979,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", @@ -2120,9 +2112,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "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": { @@ -2163,6 +2155,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001646", "electron-to-chromium": "^1.5.4", @@ -2408,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": { @@ -2468,9 +2461,9 @@ } }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -2487,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", @@ -2577,32 +2554,33 @@ } }, "node_modules/eslint": { - "version": "9.11.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz", - "integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.11.0", - "@eslint/config-array": "^0.18.0", - "@eslint/core": "^0.6.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.11.1", - "@eslint/plugin-kit": "^0.2.0", + "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.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@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.3.0", - "@nodelib/fs.walk": "^1.2.8", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.0.2", - "eslint-visitor-keys": "^4.0.0", - "espree": "^10.1.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2612,14 +2590,11 @@ "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", "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", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -2640,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": { @@ -2666,9 +2641,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", - "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2683,10 +2658,11 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2695,15 +2671,15 @@ } }, "node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.12.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2906,34 +2882,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "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.4" - }, - "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, - "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", @@ -2948,16 +2896,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "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", @@ -2981,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.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "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", @@ -3059,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" }, @@ -3208,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", @@ -3265,9 +3195,9 @@ } }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3405,16 +3335,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -3516,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", @@ -3548,6 +3449,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -3589,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", @@ -4481,9 +4384,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -4649,16 +4552,16 @@ } }, "node_modules/lua-types": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/lua-types/-/lua-types-2.13.1.tgz", - "integrity": "sha512-rRwtvX6kS+5MpuO3xpvKsnYjdSDDI064Qq1OqX8gY+r+0l7m3dFLiZPDFoHqH22jaBpEvcHcPs6+WD7qkdmFsA==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/lua-types/-/lua-types-2.14.1.tgz", + "integrity": "sha512-+EbnTjF+Bbfp5xmpfT0O9AZgC5J6Oq6jRnZei3HLb0pbWuCRHmdnIVRPIgEpUncWvHq21DHB2qTHhA4xGceHHg==", "dev": true, "license": "MIT" }, "node_modules/lua-wasm-bindings": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/lua-wasm-bindings/-/lua-wasm-bindings-0.3.1.tgz", - "integrity": "sha512-QV5hqeCuBiY8NaIGgoqIXfYDNMLBJz+camshbj24N1etSC9Xl6NYYEcab2eQjoX05xhw09Apmr3C4K2V9QY+Mg==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/lua-wasm-bindings/-/lua-wasm-bindings-0.5.3.tgz", + "integrity": "sha512-GnGSkZmYA2VzkNV/xkwOSoxwNsqEk/EcX6uvRLKQw0pCAxCauTymRO2KmJUIrG/psV3cloaS4Cjsr39iCRkrpQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4706,15 +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, - "engines": { - "node": ">= 8" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -4740,9 +4634,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -4752,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", @@ -4766,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", @@ -4958,16 +4869,16 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "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" @@ -5150,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", @@ -5248,45 +5138,10 @@ "node": ">=10" } }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "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": { @@ -5527,30 +5382,62 @@ "node": ">=8" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "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" + "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/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "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": "BSD-3-Clause" + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "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": ">=4" + "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", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5565,32 +5452,33 @@ } }, "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "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": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "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": { @@ -5601,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": { @@ -5622,14 +5511,31 @@ }, "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", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -5705,10 +5611,12 @@ } }, "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "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, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5718,14 +5626,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.16.0.tgz", - "integrity": "sha512-wDkVmlY6O2do4V+lZd0GtRfbtXbeD0q9WygwXXSJnC1xorE8eqyC2L1tJimqpSeFrOzRlYtWnUp/uzgHQOgfBQ==", + "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.16.0", - "@typescript-eslint/parser": "8.16.0", - "@typescript-eslint/utils": "8.16.0" + "@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" @@ -5735,12 +5645,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.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" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": ">=0.8.0" } }, "node_modules/undici-types": { @@ -5858,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 58b5b17eb..df63dfcf9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "typescript-to-lua", - "version": "1.28.1", + "version": "1.37.1", "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.7.2" + "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.11.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.13.0", - "lua-wasm-bindings": "^0.3.1", + "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.7.2", - "typescript-eslint": "^8.16.0" + "typescript": "6.0.2", + "typescript-eslint": "^8.58.0" } } diff --git a/src/CompilerOptions.ts b/src/CompilerOptions.ts index 5f740123b..1a167e7c3 100644 --- a/src/CompilerOptions.ts +++ b/src/CompilerOptions.ts @@ -1,6 +1,7 @@ import * as ts from "typescript"; import { JsxEmit } from "typescript"; import * as diagnosticFactories from "./transpilation/diagnostics"; +import { Plugin } from "./transpilation/plugins"; type OmitIndexSignature = { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K]; @@ -23,6 +24,11 @@ export interface LuaPluginImport { [option: string]: any; } +export interface InMemoryLuaPlugin { + plugin: Plugin | ((options: Record) => Plugin); + [option: string]: any; +} + export interface TypeScriptToLuaOptions { buildMode?: BuildMode; extension?: string; @@ -30,7 +36,7 @@ export interface TypeScriptToLuaOptions { luaBundleEntry?: string; luaTarget?: LuaTarget; luaLibImport?: LuaLibImportKind; - luaPlugins?: LuaPluginImport[]; + luaPlugins?: Array; noImplicitGlobalVariables?: boolean; noImplicitSelf?: boolean; noHeader?: boolean; @@ -61,7 +67,9 @@ export enum LuaTarget { Lua52 = "5.2", Lua53 = "5.3", Lua54 = "5.4", + Lua55 = "5.5", LuaJIT = "JIT", + Luau = "Luau", } export enum BuildMode { @@ -91,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/LuaAST.ts b/src/LuaAST.ts index 994755617..ad4a7e06b 100644 --- a/src/LuaAST.ts +++ b/src/LuaAST.ts @@ -25,6 +25,7 @@ export enum SyntaxKind { LabelStatement, ReturnStatement, BreakStatement, + ContinueStatement, // Luau only. ExpressionStatement, // Expression @@ -45,6 +46,7 @@ export enum SyntaxKind { Identifier, TableIndexExpression, ParenthesizedExpression, + ConditionalExpression, // Luau only // Operators @@ -488,6 +490,18 @@ export function createBreakStatement(tsOriginal?: ts.Node): BreakStatement { return createNode(SyntaxKind.BreakStatement, tsOriginal) as BreakStatement; } +export interface ContinueStatement extends Statement { + kind: SyntaxKind.ContinueStatement; +} + +export function isContinueStatement(node: Node): node is ContinueStatement { + return node.kind === SyntaxKind.ContinueStatement; +} + +export function createContinueStatement(tsOriginal?: ts.Node): ContinueStatement { + return createNode(SyntaxKind.ContinueStatement, tsOriginal) as ContinueStatement; +} + export interface ExpressionStatement extends Statement { kind: SyntaxKind.ExpressionStatement; expression: Expression; @@ -861,3 +875,26 @@ export function createParenthesizedExpression(expression: Expression, tsOriginal parenthesizedExpression.expression = expression; return parenthesizedExpression; } + +export type ConditionalExpression = Expression & { + condition: Expression; + whenTrue: Expression; + whenFalse: Expression; +}; + +export function isConditionalExpression(node: Node): node is ConditionalExpression { + return node.kind === SyntaxKind.ConditionalExpression; +} + +export function createConditionalExpression( + condition: Expression, + whenTrue: Expression, + whenFalse: Expression, + tsOriginal?: ts.Node +): ConditionalExpression { + const conditionalExpression = createNode(SyntaxKind.ConditionalExpression, tsOriginal) as ConditionalExpression; + conditionalExpression.condition = condition; + conditionalExpression.whenTrue = whenTrue; + conditionalExpression.whenFalse = whenFalse; + return conditionalExpression; +} diff --git a/src/LuaPrinter.ts b/src/LuaPrinter.ts index 1f89dd4ec..26b55421b 100644 --- a/src/LuaPrinter.ts +++ b/src/LuaPrinter.ts @@ -157,7 +157,7 @@ export class LuaPrinter { }; private static rightAssociativeOperators = new Set([lua.SyntaxKind.ConcatOperator, lua.SyntaxKind.PowerOperator]); - private currentIndent = ""; + protected currentIndent = ""; protected luaFile: string; protected relativeSourcePath: string; protected options: CompilerOptions; @@ -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"] : []; @@ -396,6 +396,8 @@ export class LuaPrinter { return this.printReturnStatement(statement as lua.ReturnStatement); case lua.SyntaxKind.BreakStatement: return this.printBreakStatement(statement as lua.BreakStatement); + case lua.SyntaxKind.ContinueStatement: + return this.printContinueStatement(statement as lua.ContinueStatement); case lua.SyntaxKind.ExpressionStatement: return this.printExpressionStatement(statement as lua.ExpressionStatement); default: @@ -574,6 +576,10 @@ export class LuaPrinter { return this.createSourceNode(statement, this.indent("break")); } + public printContinueStatement(statement: lua.ContinueStatement): SourceNode { + return this.createSourceNode(statement, this.indent("continue")); + } + public printExpressionStatement(statement: lua.ExpressionStatement): SourceNode { return this.createSourceNode(statement, [this.indent(), this.printExpression(statement.expression)]); } @@ -614,6 +620,8 @@ export class LuaPrinter { return this.printTableIndexExpression(expression as lua.TableIndexExpression); case lua.SyntaxKind.ParenthesizedExpression: return this.printParenthesizedExpression(expression as lua.ParenthesizedExpression); + case lua.SyntaxKind.ConditionalExpression: + return this.printConditionalExpression(expression as lua.ConditionalExpression); default: throw new Error(`Tried to print unknown statement kind: ${lua.SyntaxKind[expression.kind]}`); } @@ -828,6 +836,17 @@ export class LuaPrinter { return this.createSourceNode(expression, ["(", this.printExpression(expression.expression), ")"]); } + public printConditionalExpression(expression: lua.ConditionalExpression): SourceNode { + return this.createSourceNode(expression, [ + "if ", + this.printExpression(expression.condition), + " then ", + this.printExpression(expression.whenTrue), + " else ", + this.printExpression(expression.whenFalse), + ]); + } + public printOperator(kind: lua.Operator): SourceNode { return new SourceNode(null, null, this.relativeSourcePath, LuaPrinter.operatorMap[kind]); } diff --git a/src/lualib/Error.ts b/src/lualib/Error.ts index ea9cba478..ae5deb05b 100644 --- a/src/lualib/Error.ts +++ b/src/lualib/Error.ts @@ -1,3 +1,5 @@ +import { __TS__New } from "./New"; + interface ErrorType { name: string; new (...args: any[]): Error; @@ -22,6 +24,11 @@ function getErrorStack(constructor: () => any): string | undefined { if (_VERSION.includes("Lua 5.0")) { return debug.traceback(`[Level ${level}]`); + // @ts-ignore Fails when compiled with Lua 5.0 types + } else if (_VERSION === "Lua 5.1") { + // Lua 5.1 and LuaJIT have a bug where it's not possible to specify the level without a message. + // @ts-ignore Fails when compiled with Lua 5.0 types + return string.sub(debug.traceback("", level), 2); } else { // @ts-ignore Fails when compiled with Lua 5.0 types return debug.traceback(undefined, level); @@ -33,7 +40,7 @@ function wrapErrorToString(getDescription: (this: T) => string) const description = getDescription.call(this as T); const caller = debug.getinfo(3, "f"); // @ts-ignore Fails when compiled with Lua 5.0 types - const isClassicLua = _VERSION.includes("Lua 5.0") || _VERSION === "Lua 5.1"; + const isClassicLua = _VERSION.includes("Lua 5.0"); if (isClassicLua || (caller && caller.func !== error)) { return description; } else { @@ -55,7 +62,7 @@ export const Error: ErrorConstructor = initErrorClass( public stack?: string; constructor(public message = "") { - this.stack = getErrorStack((this.constructor as any).new); + this.stack = getErrorStack(__TS__New as any); const metatable = getmetatable(this); if (metatable && !metatable.__errorToStringPatched) { metatable.__errorToStringPatched = true; 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..e8121f1b3 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,20 @@ 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. + // reference: https://github.com/tc39/proposal-promise-finally/blob/fd934c0b42d59bf8d9446e737ba14d50a9067216/polyfill.js#L34-L41 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(); - } + if (typeof onFinally !== "function") { + return this.then(onFinally, onFinally); } - return this; + return this.then( + x => new __TS__Promise(resolve => resolve(onFinally())).then(() => x), + e => + new __TS__Promise(resolve => resolve(onFinally())).then(() => { + throw e; + }) + ); } private resolve(value: T | PromiseLike): void { @@ -168,25 +171,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/lualib/SourceMapTraceBack.ts b/src/lualib/SourceMapTraceBack.ts index 47c2c8739..4597fa3dd 100644 --- a/src/lualib/SourceMapTraceBack.ts +++ b/src/lualib/SourceMapTraceBack.ts @@ -9,7 +9,6 @@ interface SourceMap { declare global { function __TS__originalTraceback(this: void, thread?: LuaThread, message?: string, level?: number): void; - // eslint-disable-next-line no-var var __TS__sourcemap: Record; } @@ -49,7 +48,12 @@ export function __TS__SourceMapTraceBack(this: void, fileName: string, sourceMap return `${file}:${line}`; }; - let [result] = string.gsub(trace, "(%S+)%.lua:(%d+)", (file, line) => + // Rewrite stack trace frames from "{PATH}.lua:{LINE}" to "{PATH}.ts:{ORIGINAL_LINE}". + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1665 + // Avoid matching anonymous function stack entries like `in function <...>` + // by excluding `<` before the file path. + // TODO: This will still fail for paths containing spaces. + let [result] = string.gsub(trace, "([^%s<]+)%.lua:(%d+)", (file, line) => replacer(`${file}.lua`, `${file}.ts`, line) ); diff --git a/src/lualib/StringSplit.ts b/src/lualib/StringSplit.ts index 1d2dd6610..f89ffb57d 100644 --- a/src/lualib/StringSplit.ts +++ b/src/lualib/StringSplit.ts @@ -1,9 +1,7 @@ const sub = string.sub; const find = string.find; export function __TS__StringSplit(this: void, source: string, separator?: string, limit?: number): string[] { - if (limit === undefined) { - limit = 4294967295; - } + limit ??= 4294967295; if (limit === 0) { return []; diff --git a/src/transformation/builtins/index.ts b/src/transformation/builtins/index.ts index 85a4c9802..3eefd64c9 100644 --- a/src/transformation/builtins/index.ts +++ b/src/transformation/builtins/index.ts @@ -221,7 +221,10 @@ export function checkForLuaLibType(context: TransformationContext, type: ts.Type } } -function tryGetStandardLibrarySymbolOfType(context: TransformationContext, type: ts.Type): ts.Symbol | undefined { +export function tryGetStandardLibrarySymbolOfType( + context: TransformationContext, + type: ts.Type +): ts.Symbol | undefined { if (type.isUnionOrIntersection()) { for (const subType of type.types) { const symbol = tryGetStandardLibrarySymbolOfType(context, subType); 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/context/context.ts b/src/transformation/context/context.ts index 79e385b2b..ad40d48c5 100644 --- a/src/transformation/context/context.ts +++ b/src/transformation/context/context.ts @@ -247,8 +247,8 @@ export class TransformationContext { public readonly scopeStack: Scope[] = []; private lastScopeId = 0; - public pushScope(type: ScopeType): Scope { - const scope = { type, id: ++this.lastScopeId }; + public pushScope(type: ScopeType, node: ts.Node): Scope { + const scope: Scope = { type, id: ++this.lastScopeId, node }; this.scopeStack.push(scope); return scope; } diff --git a/src/transformation/pre-transformers/using-transformer.ts b/src/transformation/pre-transformers/using-transformer.ts index d78f6206a..1229b71c2 100644 --- a/src/transformation/pre-transformers/using-transformer.ts +++ b/src/transformation/pre-transformers/using-transformer.ts @@ -5,11 +5,13 @@ import { LuaLibFeature, importLuaLibFeature } from "../utils/lualib"; export function usingTransformer(context: TransformationContext): ts.TransformerFactory { return ctx => sourceFile => { function visit(node: ts.Node): ts.Node { - if (ts.isBlock(node)) { + if (ts.isBlock(node) || ts.isSourceFile(node)) { const [hasUsings, newStatements] = transformBlockWithUsing(context, node.statements, node); if (hasUsings) { // Recurse visitor into updated block to find further usings - const updatedBlock = ts.factory.updateBlock(node, newStatements); + const updatedBlock = ts.isBlock(node) + ? ts.factory.updateBlock(node, newStatements) + : ts.factory.updateSourceFile(node, newStatements); const result = ts.visitEachChild(updatedBlock, visit, ctx); // Set all the synthetic node parents to something that makes sense @@ -29,7 +31,8 @@ export function usingTransformer(context: TransformationContext): ts.Transformer } return ts.visitEachChild(node, visit, ctx); } - return ts.visitEachChild(sourceFile, visit, ctx); + const transformedSourceFile = ts.visitEachChild(sourceFile, visit, ctx); + return visit(transformedSourceFile) as ts.SourceFile; }; } @@ -40,7 +43,7 @@ function isUsingDeclarationList(node: ts.Node): node is ts.VariableStatement { function transformBlockWithUsing( context: TransformationContext, statements: ts.NodeArray | ts.Statement[], - block: ts.Block + block: ts.Block | ts.SourceFile ): [true, ts.Statement[]] | [false] { const newStatements: ts.Statement[] = []; @@ -102,7 +105,14 @@ function transformBlockWithUsing( call = ts.factory.createAwaitExpression(call); } - if (ts.isBlock(block.parent) && block.parent.statements[block.parent.statements.length - 1] !== block) { + if (ts.isSourceFile(block)) { + // If block is a sourcefile, don't insert a return statement into root code + newStatements.push(ts.factory.createExpressionStatement(call)); + } else if ( + block.parent && + ts.isBlock(block.parent) && + block.parent.statements[block.parent.statements.length - 1] !== block + ) { // If this is a free-standing block in a function (not the last statement), dont return the value newStatements.push(ts.factory.createExpressionStatement(call)); } else { diff --git a/src/transformation/utils/annotations.ts b/src/transformation/utils/annotations.ts index 91ef6d531..b66704c97 100644 --- a/src/transformation/utils/annotations.ts +++ b/src/transformation/utils/annotations.ts @@ -105,7 +105,8 @@ export function getFileAnnotations(sourceFile: ts.SourceFile): AnnotationsMap { function getTagArgsFromComment(tag: ts.JSDocTag): string[] { if (tag.comment) { if (typeof tag.comment === "string") { - return tag.comment.split(" "); + const firstLine = tag.comment.split("\n")[0]; + return firstLine.trim().split(" "); } else { return tag.comment.map(part => part.text); } diff --git a/src/transformation/utils/diagnostics.ts b/src/transformation/utils/diagnostics.ts index 137bd15b8..e8d77c169 100644 --- a/src/transformation/utils/diagnostics.ts +++ b/src/transformation/utils/diagnostics.ts @@ -82,7 +82,7 @@ export const unsupportedAccessorInObjectLiteral = createErrorDiagnosticFactory( ); export const unsupportedRightShiftOperator = createErrorDiagnosticFactory( - "Right shift operator is not supported for target Lua 5.3. Use `>>>` instead." + "Signed right shift `>>` is not supported on Lua 5.3+: Lua's native `>>` is logical (zero-fill) on 64-bit integers, with no built-in arithmetic shift. Use `>>>` if you don't need sign extension, or write your own helper." ); const getLuaTargetName = (version: LuaTarget) => (version === LuaTarget.LuaJIT ? "LuaJIT" : `Lua ${version}`); @@ -145,6 +145,10 @@ export const awaitMustBeInAsyncFunction = createErrorDiagnosticFactory( "Await can only be used inside async functions." ); +export const unsupportedAsyncGenerator = createErrorDiagnosticFactory("Async generator functions are not supported."); + +export const unsupportedForAwaitOf = createErrorDiagnosticFactory("'for await...of' loops are not supported."); + export const unsupportedBuiltinOptionalCall = createErrorDiagnosticFactory( "Optional calls are not supported for builtin or language extension functions." ); @@ -172,3 +176,7 @@ export const cannotAssignToNodeOfKind = createErrorDiagnosticFactory( export const incompleteFieldDecoratorWarning = createWarningDiagnosticFactory( "You are using a class field decorator, note that tstl ignores returned value initializers!" ); + +export const unsupportedArrayWithLengthConstructor = createErrorDiagnosticFactory( + `Constructing new Array with length is not supported.` +); diff --git a/src/transformation/utils/export.ts b/src/transformation/utils/export.ts index fa1c03649..f0ef437e5 100644 --- a/src/transformation/utils/export.ts +++ b/src/transformation/utils/export.ts @@ -20,6 +20,14 @@ export function hasExportModifier(node: ts.Node): boolean { ); } +export function shouldBeExported(node: ts.Node): boolean { + if (hasExportModifier(node)) { + // Don't export if we're inside a namespace (module declaration) + return ts.findAncestor(node, ts.isModuleDeclaration) === undefined; + } + return false; +} + export const createDefaultExportStringLiteral = (original?: ts.Node): lua.StringLiteral => lua.createStringLiteral("default", original); diff --git a/src/transformation/utils/function-context.ts b/src/transformation/utils/function-context.ts index 5b2c0cb4e..4fce80f49 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 @@ -124,7 +122,8 @@ function computeDeclarationContextType(context: TransformationContext, signature ts.isConstructSignatureDeclaration(signatureDeclaration) || ts.isConstructorDeclaration(signatureDeclaration) || (signatureDeclaration.parent && ts.isPropertyDeclaration(signatureDeclaration.parent)) || - (signatureDeclaration.parent && ts.isPropertySignature(signatureDeclaration.parent)) + (signatureDeclaration.parent && ts.isPropertySignature(signatureDeclaration.parent)) || + (signatureDeclaration.parent && ts.isIndexSignatureDeclaration(signatureDeclaration.parent)) ) { // Class/interface methods only respect @noSelf on their parent const scopeDeclaration = findFirstNodeAbove( @@ -144,6 +143,19 @@ function computeDeclarationContextType(context: TransformationContext, signature return ContextType.NonVoid; } + // Call signature inside a class or interface respects @noSelf on the enclosing class/interface + if (ts.isCallSignatureDeclaration(signatureDeclaration)) { + const scopeDeclaration = findFirstNodeAbove( + signatureDeclaration, + (n): n is ts.ClassLikeDeclaration | ts.InterfaceDeclaration => + ts.isClassDeclaration(n) || ts.isClassExpression(n) || ts.isInterfaceDeclaration(n) + ); + + if (scopeDeclaration !== undefined && getNodeAnnotations(scopeDeclaration).has(AnnotationKind.NoSelf)) { + return ContextType.Void; + } + } + // When using --noImplicitSelf and the signature is defined in a file targeted by the program apply the @noSelf rule. const program = context.program; const options = program.getCompilerOptions() as CompilerOptions; diff --git a/src/transformation/utils/lua-ast.ts b/src/transformation/utils/lua-ast.ts index b947e0bf6..59f6cffd7 100644 --- a/src/transformation/utils/lua-ast.ts +++ b/src/transformation/utils/lua-ast.ts @@ -81,6 +81,35 @@ export function createUnpackCall( return lua.setNodeFlags(lua.createCallExpression(unpack, [expression], tsOriginal), lua.NodeFlags.TableUnpackCall); } +export function createBoundedUnpackCall( + context: TransformationContext, + expression: lua.Expression, + maxUnpackItem: number, + tsOriginal?: ts.Node +): lua.Expression { + if (context.luaTarget === LuaTarget.Universal) { + return transformLuaLibFunction(context, LuaLibFeature.Unpack, tsOriginal, expression); + } + + // Lua 5.0 does not support this signature, so don't add the arguments there + const extraArgs = + context.luaTarget === LuaTarget.Lua50 + ? [] + : [lua.createNumericLiteral(1), lua.createNumericLiteral(maxUnpackItem)]; + + const unpack = + context.luaTarget === LuaTarget.Lua50 || + context.luaTarget === LuaTarget.Lua51 || + context.luaTarget === LuaTarget.LuaJIT + ? lua.createIdentifier("unpack") + : lua.createTableIndexExpression(lua.createIdentifier("table"), lua.createStringLiteral("unpack")); + + return lua.setNodeFlags( + lua.createCallExpression(unpack, [expression, ...extraArgs], tsOriginal), + lua.NodeFlags.TableUnpackCall + ); +} + export function isUnpackCall(node: lua.Node): boolean { return lua.isCallExpression(node) && (node.flags & lua.NodeFlags.TableUnpackCall) !== 0; } diff --git a/src/transformation/utils/safe-names.ts b/src/transformation/utils/safe-names.ts index 538d77a91..0772d71a8 100644 --- a/src/transformation/utils/safe-names.ts +++ b/src/transformation/utils/safe-names.ts @@ -16,6 +16,8 @@ export const isValidLuaIdentifier = (name: string, options: CompilerOptions) => export const luaKeywords: ReadonlySet = new Set([ "and", + "bit", + "bit32", "break", "do", "else", diff --git a/src/transformation/utils/scope.ts b/src/transformation/utils/scope.ts index 87f431162..314f1d795 100644 --- a/src/transformation/utils/scope.ts +++ b/src/transformation/utils/scope.ts @@ -25,18 +25,22 @@ interface FunctionDefinitionInfo { export enum LoopContinued { WithGoto, WithRepeatBreak, + WithContinue, } export interface Scope { type: ScopeType; id: number; - node?: ts.Node; + node: ts.Node; referencedSymbols?: Map; variableDeclarations?: lua.VariableDeclarationStatement[]; functionDefinitions?: Map; importStatements?: lua.Statement[]; loopContinued?: LoopContinued; functionReturned?: boolean; + asyncTryHasReturn?: boolean; + asyncTryHasBreak?: boolean; + asyncTryHasContinue?: LoopContinued; } export interface HoistingResult { @@ -59,9 +63,7 @@ export function markSymbolAsReferencedInCurrentScopes( identifier: ts.Identifier ): void { for (const scope of context.scopeStack) { - if (!scope.referencedSymbols) { - scope.referencedSymbols = new Map(); - } + scope.referencedSymbols ??= new Map(); const references = getOrUpdate(scope.referencedSymbols, symbolId, () => []); references.push(identifier); @@ -85,10 +87,26 @@ export function findScope(context: TransformationContext, scopeTypes: ScopeType) } } -export function addScopeVariableDeclaration(scope: Scope, declaration: lua.VariableDeclarationStatement) { - if (!scope.variableDeclarations) { - scope.variableDeclarations = []; +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 ??= []; + scope.variableDeclarations.push(declaration); } @@ -101,7 +119,7 @@ function isHoistableFunctionDeclaredInScope(symbol: ts.Symbol, scopeNode: ts.Nod // Checks for references to local functions which haven't been defined yet, // and thus will be hoisted above the current position. export function hasReferencedUndefinedLocalFunction(context: TransformationContext, scope: Scope) { - if (!scope.referencedSymbols || !scope.node) { + if (!scope.referencedSymbols) { return false; } for (const [symbolId, nodes] of scope.referencedSymbols) { @@ -129,10 +147,6 @@ export function hasReferencedSymbol(context: TransformationContext, scope: Scope return false; } -export function isFunctionScopeWithDefinition(scope: Scope): scope is Scope & { node: ts.SignatureDeclaration } { - return scope.node !== undefined && ts.isFunctionLike(scope.node); -} - export function separateHoistedStatements(context: TransformationContext, statements: lua.Statement[]): HoistingResult { const scope = peekScope(context); const allHoistedStatments: lua.Statement[] = []; 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/utils/typescript/types.ts b/src/transformation/utils/typescript/types.ts index 5ba2a013c..4a356b972 100644 --- a/src/transformation/utils/typescript/types.ts +++ b/src/transformation/utils/typescript/types.ts @@ -56,6 +56,20 @@ export function isNumberType(context: TransformationContext, type: ts.Type): boo function isExplicitArrayType(context: TransformationContext, type: ts.Type): boolean { if (context.checker.isArrayType(type) || context.checker.isTupleType(type)) return true; + + if (type.isUnionOrIntersection()) { + if (type.types.some(t => isExplicitArrayType(context, t))) { + return true; + } + } + + const baseTypes = type.getBaseTypes(); + if (baseTypes) { + if (baseTypes.some(t => isExplicitArrayType(context, t))) { + return true; + } + } + if (type.symbol) { const baseConstraint = context.checker.getBaseConstraintOfType(type); if (baseConstraint && baseConstraint !== type) { @@ -63,10 +77,6 @@ function isExplicitArrayType(context: TransformationContext, type: ts.Type): boo } } - if (type.isUnionOrIntersection()) { - return type.types.some(t => isExplicitArrayType(context, t)); - } - return false; } diff --git a/src/transformation/visitors/access.ts b/src/transformation/visitors/access.ts index c97c0f4c7..7c96bdcd8 100644 --- a/src/transformation/visitors/access.ts +++ b/src/transformation/visitors/access.ts @@ -115,7 +115,7 @@ export function transformPropertyAccessExpressionWithCapture( let property = node.name.text; const symbol = context.checker.getSymbolAtLocation(node.name); - const customName = getCustomNameFromSymbol(symbol); + const customName = getCustomNameFromSymbol(context, symbol); if (customName) { property = customName; } diff --git a/src/transformation/visitors/binary-expression/assignments.ts b/src/transformation/visitors/binary-expression/assignments.ts index 3e246c3f8..73e6d9dd1 100644 --- a/src/transformation/visitors/binary-expression/assignments.ts +++ b/src/transformation/visitors/binary-expression/assignments.ts @@ -4,7 +4,7 @@ import * as lua from "../../../LuaAST"; import { TransformationContext } from "../../context"; import { validateAssignment } from "../../utils/assignment-validation"; import { createExportedIdentifier, getDependenciesOfSymbol, isSymbolExported } from "../../utils/export"; -import { createUnpackCall, wrapInTable } from "../../utils/lua-ast"; +import { createBoundedUnpackCall, wrapInTable } from "../../utils/lua-ast"; import { LuaLibFeature, transformLuaLibFunction } from "../../utils/lualib"; import { isArrayType, isDestructuringAssignment } from "../../utils/typescript"; import { isArrayLength, transformDestructuringAssignment } from "./destructuring-assignments"; @@ -252,7 +252,7 @@ export function transformAssignmentStatement( } else { right = context.transformExpression(expression.right); if (!isMultiReturnCall(context, expression.right) && isArrayType(context, rightType)) { - right = createUnpackCall(context, right, expression.right); + right = createBoundedUnpackCall(context, right, expression.left.elements.length, expression.right); } } 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/block.ts b/src/transformation/visitors/block.ts index c0a2ad3a8..4eef4b6f8 100644 --- a/src/transformation/visitors/block.ts +++ b/src/transformation/visitors/block.ts @@ -12,14 +12,14 @@ export function transformScopeBlock( node: ts.Block, scopeType: ScopeType ): [lua.Block, Scope] { - context.pushScope(scopeType); + context.pushScope(scopeType, node); const statements = performHoisting(context, context.transformStatements(node.statements)); const scope = context.popScope(); return [lua.createBlock(statements, node), scope]; } export const transformBlock: FunctionVisitor = (node, context) => { - context.pushScope(ScopeType.Block); + context.pushScope(ScopeType.Block, node); const statements = performHoisting(context, context.transformStatements(node.statements)); context.popScope(); return lua.createDoStatement(statements, node); diff --git a/src/transformation/visitors/break-continue.ts b/src/transformation/visitors/break-continue.ts index cfc411439..e41a7934b 100644 --- a/src/transformation/visitors/break-continue.ts +++ b/src/transformation/visitors/break-continue.ts @@ -2,32 +2,66 @@ 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); }; export const transformContinueStatement: FunctionVisitor = (statement, context) => { const scope = findScope(context, ScopeType.Loop); - const continuedWith = - context.luaTarget === LuaTarget.Universal || - context.luaTarget === LuaTarget.Lua50 || - context.luaTarget === LuaTarget.Lua51 - ? LoopContinued.WithRepeatBreak - : LoopContinued.WithGoto; + + const continuedWith = { + [LuaTarget.Universal]: LoopContinued.WithRepeatBreak, + [LuaTarget.Lua50]: LoopContinued.WithRepeatBreak, + [LuaTarget.Lua51]: LoopContinued.WithRepeatBreak, + [LuaTarget.Lua52]: LoopContinued.WithGoto, + [LuaTarget.Lua53]: LoopContinued.WithGoto, + [LuaTarget.Lua54]: LoopContinued.WithGoto, + [LuaTarget.Lua55]: LoopContinued.WithGoto, + [LuaTarget.LuaJIT]: LoopContinued.WithGoto, + [LuaTarget.Luau]: LoopContinued.WithContinue, + }[context.luaTarget]; if (scope) { 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) { case LoopContinued.WithGoto: return lua.createGotoStatement(label, statement); + case LoopContinued.WithContinue: + return lua.createContinueStatement(statement); + case LoopContinued.WithRepeatBreak: return [ lua.createAssignmentStatement(lua.createIdentifier(label), lua.createBooleanLiteral(true), statement), diff --git a/src/transformation/visitors/call.ts b/src/transformation/visitors/call.ts index 1f9fb521b..c4a74135c 100644 --- a/src/transformation/visitors/call.ts +++ b/src/transformation/visitors/call.ts @@ -138,7 +138,7 @@ export function transformContextualCallExpression( let name = left.name.text; const symbol = context.checker.getSymbolAtLocation(left); - const customName = getCustomNameFromSymbol(symbol); + const customName = getCustomNameFromSymbol(context, symbol); if (customName) { name = customName; @@ -159,7 +159,7 @@ export function transformContextualCallExpression( ); return lua.createCallExpression(expression, transformedArguments, node); } - } else if (ts.isIdentifier(left)) { + } else if (ts.isIdentifier(left) || ts.isCallExpression(left)) { const callContext = context.isStrict ? ts.factory.createNull() : ts.factory.createIdentifier("_G"); let expression: lua.Expression; [expression, transformedArguments] = transformCallWithArguments( diff --git a/src/transformation/visitors/class/index.ts b/src/transformation/visitors/class/index.ts index 0edfce050..f2024babe 100644 --- a/src/transformation/visitors/class/index.ts +++ b/src/transformation/visitors/class/index.ts @@ -5,8 +5,8 @@ import { createDefaultExportExpression, createExportedIdentifier, hasDefaultExportModifier, - hasExportModifier, isSymbolExported, + shouldBeExported, } from "../../utils/export"; import { createSelfIdentifier } from "../../utils/lua-ast"; import { createSafeName, isUnsafeName } from "../../utils/safe-names"; @@ -202,7 +202,7 @@ function transformClassLikeDeclaration( const iif = lua.createFunctionExpression(lua.createBlock(bodyStatements), [ lua.createIdentifier("self"), ]); - const iife = lua.createCallExpression(iif, [className]); + const iife = lua.createCallExpression(iif, [localClassName]); result.push(lua.createExpressionStatement(iife, member)); } } @@ -214,10 +214,10 @@ function transformClassLikeDeclaration( const decoratingStatement = lua.createAssignmentStatement(localClassName, decoratingExpression); result.push(decoratingStatement); - if (hasExportModifier(classDeclaration)) { + if (shouldBeExported(classDeclaration)) { const exportExpression = hasDefaultExportModifier(classDeclaration) ? createDefaultExportExpression(classDeclaration) - : createExportedIdentifier(context, className); + : createExportedIdentifier(context, localClassName); const classAssignment = lua.createAssignmentStatement(exportExpression, localClassName); result.push(classAssignment); @@ -272,10 +272,8 @@ export const transformSuperExpression: FunctionVisitor = (ex } } - if (!baseClassName) { - // Use "className.____super" if the base is not a simple identifier - baseClassName = lua.createTableIndexExpression(className, lua.createStringLiteral("____super"), expression); - } + // Use "className.____super" if the base is not a simple identifier + baseClassName ??= lua.createTableIndexExpression(className, lua.createStringLiteral("____super"), expression); const f = findFirstNodeAbove(expression, ts.isFunctionLike); if (f && ts.canHaveModifiers(f) && isStaticNode(f)) { diff --git a/src/transformation/visitors/class/members/accessors.ts b/src/transformation/visitors/class/members/accessors.ts index 165d3fa88..017100056 100644 --- a/src/transformation/visitors/class/members/accessors.ts +++ b/src/transformation/visitors/class/members/accessors.ts @@ -15,7 +15,7 @@ function transformAccessor( className: lua.Identifier ): lua.Expression { const [params, dot, restParam] = transformParameters(context, node.parameters, createSelfIdentifier()); - const body = node.body ? transformFunctionBody(context, node.parameters, node.body, restParam)[0] : []; + const body = node.body ? transformFunctionBody(context, node.parameters, node.body, node, restParam)[0] : []; const accessorFunction = lua.createFunctionExpression( lua.createBlock(body), params, diff --git a/src/transformation/visitors/class/members/constructor.ts b/src/transformation/visitors/class/members/constructor.ts index 039e705f7..8bff2f4fc 100644 --- a/src/transformation/visitors/class/members/constructor.ts +++ b/src/transformation/visitors/class/members/constructor.ts @@ -28,7 +28,7 @@ export function transformConstructorDeclaration( } // Transform body - const scope = context.pushScope(ScopeType.Function); + const scope = context.pushScope(ScopeType.Function, statement); const body = transformFunctionBodyContent(context, statement.body); const [params, dotsLiteral, restParamName] = transformParameters( diff --git a/src/transformation/visitors/class/new.ts b/src/transformation/visitors/class/new.ts index 7c608bc65..0c5733451 100644 --- a/src/transformation/visitors/class/new.ts +++ b/src/transformation/visitors/class/new.ts @@ -2,16 +2,42 @@ import * as ts from "typescript"; import * as lua from "../../../LuaAST"; import { FunctionVisitor } from "../../context"; import { AnnotationKind, getTypeAnnotations } from "../../utils/annotations"; -import { annotationInvalidArgumentCount } from "../../utils/diagnostics"; +import { annotationInvalidArgumentCount, unsupportedArrayWithLengthConstructor } from "../../utils/diagnostics"; import { LuaLibFeature, transformLuaLibFunction } from "../../utils/lualib"; import { transformArguments, transformCallAndArguments } from "../call"; import { isTableNewCall } from "../language-extensions/table"; +import { tryGetStandardLibrarySymbolOfType } from "../../builtins"; export const transformNewExpression: FunctionVisitor = (node, context) => { if (isTableNewCall(context, node)) { return lua.createTableExpression(undefined, node); } + const constructorType = context.checker.getTypeAtLocation(node.expression); + if (tryGetStandardLibrarySymbolOfType(context, constructorType)?.name === "ArrayConstructor") { + if (node.arguments === undefined || node.arguments.length === 0) { + // turn new Array<>() into a simple {} + return lua.createTableExpression([], node); + } else { + // More than one argument, check if items constructor + const signature = context.checker.getResolvedSignature(node); + const signatureDeclaration = signature?.getDeclaration(); + if ( + signatureDeclaration?.parameters.length === 1 && + signatureDeclaration.parameters[0].dotDotDotToken === undefined + ) { + context.diagnostics.push(unsupportedArrayWithLengthConstructor(node)); + return lua.createTableExpression([], node); + } else { + const callArguments = transformArguments(context, node.arguments, signature); + return lua.createTableExpression( + callArguments.map(e => lua.createTableFieldExpression(e)), + node + ); + } + } + } + const signature = context.checker.getResolvedSignature(node); const [name, params] = transformCallAndArguments(context, node.expression, node.arguments ?? [], signature); diff --git a/src/transformation/visitors/conditional.ts b/src/transformation/visitors/conditional.ts index 178f6ac2d..6285a488d 100644 --- a/src/transformation/visitors/conditional.ts +++ b/src/transformation/visitors/conditional.ts @@ -6,6 +6,7 @@ import { performHoisting, ScopeType } from "../utils/scope"; import { transformBlockOrStatement } from "./block"; import { canBeFalsy } from "../utils/typescript"; import { truthyOnlyConditionalValue } from "../utils/diagnostics"; +import { LuaTarget } from "../../CompilerOptions"; function transformProtectedConditionalExpression( context: TransformationContext, @@ -38,6 +39,16 @@ function transformProtectedConditionalExpression( } export const transformConditionalExpression: FunctionVisitor = (expression, context) => { + if (context.luaTarget === LuaTarget.Luau) { + // Luau's ternary operator doesn't have these issues + return lua.createConditionalExpression( + context.transformExpression(expression.condition), + context.transformExpression(expression.whenTrue), + context.transformExpression(expression.whenFalse), + expression + ); + } + // Check if we need to add diagnostic about Lua truthiness checkOnlyTruthyCondition(expression.condition, context); @@ -65,7 +76,7 @@ export const transformConditionalExpression: FunctionVisitor = (no if (!membersOnly && isFirstDeclaration(context, node)) { const name = transformIdentifier(context, node.name); const table = lua.createBinaryExpression( - lua.cloneIdentifier(name), + addExportToIdentifier(context, name), lua.createTableExpression(), lua.SyntaxKind.OrOperator ); @@ -60,9 +60,7 @@ export const transformEnumDeclaration: FunctionVisitor = (no } } - if (!valueExpression) { - valueExpression = context.transformExpression(member.initializer); - } + valueExpression ??= context.transformExpression(member.initializer); } else { valueExpression = lua.createNilLiteral(); } diff --git a/src/transformation/visitors/errors.ts b/src/transformation/visitors/errors.ts index f81519e4c..a070caac6 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; @@ -94,6 +165,7 @@ export const transformTryStatement: FunctionVisitor = (statemen const tryResultIdentifier = lua.createIdentifier("____try"); const returnValueIdentifier = lua.createIdentifier("____returnValue"); + const rethrowIdentifier = lua.createIdentifier("____rethrow"); const result: lua.Statement[] = []; @@ -120,6 +192,9 @@ export const transformTryStatement: FunctionVisitor = (statemen } } result.push(lua.createVariableDeclarationStatement(tryReturnIdentifiers, tryCall)); + if (statement.finallyBlock) { + result.push(lua.createVariableDeclarationStatement(rethrowIdentifier)); + } const catchCall = lua.createCallExpression( catchIdentifier, @@ -132,8 +207,22 @@ export const transformTryStatement: FunctionVisitor = (statemen ) : lua.createExpressionStatement(catchCall); + const catchCallFunction = lua.createFunctionExpression(lua.createBlock([catchCallStatement])); + + const tryCatchCall = lua.createCallExpression(pCall, [catchCallFunction]); + + const tryCatchCallStatement = lua.createAssignmentStatement( + [lua.cloneIdentifier(tryResultIdentifier), lua.cloneIdentifier(rethrowIdentifier)], + tryCatchCall + ); + const notTryCondition = lua.createUnaryExpression(tryResultIdentifier, lua.SyntaxKind.NotOperator); - result.push(lua.createIfStatement(notTryCondition, lua.createBlock([catchCallStatement]))); + result.push( + lua.createIfStatement( + notTryCondition, + lua.createBlock(statement.finallyBlock ? [tryCatchCallStatement] : [catchCallStatement]) + ) + ); } else if (tryScope.functionReturned) { // try with return, but no catch // returnedIdentifier = lua.createIdentifier("____returned"); @@ -146,6 +235,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 +248,35 @@ export const transformTryStatement: FunctionVisitor = (statemen result.push(...context.transformStatements(statement.finallyBlock)); } + // Re-throw error if try had a finally. + // On pcall failure the error is the second return value, which lands in + // ____hasReturned (when functionReturned) or ____error (otherwise). + if (statement.finallyBlock) { + const notTryCondition = lua.createUnaryExpression( + lua.cloneIdentifier(tryResultIdentifier), + lua.SyntaxKind.NotOperator + ); + + if (!statement.catchClause) { + 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]))); + } else if (statement.catchClause.block.statements.length > 0) { + // Re-throw is possible only from non-empty blocks. + const rethrow = lua.createExpressionStatement( + lua.createCallExpression(lua.createIdentifier("error"), [ + rethrowIdentifier, + lua.createNumericLiteral(0), + ]) + ); + result.push(lua.createIfStatement(notTryCondition, lua.createBlock([rethrow]))); + } + } + if (returnCondition && returnedIdentifier) { const returnValues: lua.Expression[] = []; diff --git a/src/transformation/visitors/function.ts b/src/transformation/visitors/function.ts index 506d4a6f1..115a1cd08 100644 --- a/src/transformation/visitors/function.ts +++ b/src/transformation/visitors/function.ts @@ -16,6 +16,7 @@ import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; import { transformInPrecedingStatementScope } from "../utils/preceding-statements"; import { peekScope, performHoisting, Scope, ScopeType } from "../utils/scope"; import { isFunctionType } from "../utils/typescript"; +import { unsupportedAsyncGenerator } from "../utils/diagnostics"; import { isAsyncFunction, wrapInAsyncAwaiter } from "./async-await"; import { transformIdentifier } from "./identifier"; import { transformExpressionBodyToReturnStatement } from "./return"; @@ -164,11 +165,10 @@ export function transformFunctionBody( context: TransformationContext, parameters: ts.NodeArray, body: ts.ConciseBody, - spreadIdentifier?: lua.Identifier, - node?: ts.FunctionLikeDeclaration + node: ts.FunctionLikeDeclaration, + spreadIdentifier?: lua.Identifier ): [lua.Statement[], Scope] { - const scope = context.pushScope(ScopeType.Function); - scope.node = node; + const scope = context.pushScope(ScopeType.Function, node); let bodyStatements = transformFunctionBodyContent(context, body); if (node && isAsyncFunction(node)) { bodyStatements = [lua.createReturnStatement([wrapInAsyncAwaiter(context, bodyStatements)])]; @@ -225,6 +225,10 @@ export function transformFunctionToExpression( ): [lua.Expression, Scope] { assert(node.body); + if (node.asteriskToken && isAsyncFunction(node)) { + context.diagnostics.push(unsupportedAsyncGenerator(node)); + } + const type = context.checker.getTypeAtLocation(node); let functionContext: lua.Identifier | undefined; @@ -258,8 +262,8 @@ export function transformFunctionToExpression( context, node.parameters, node.body, - spreadIdentifier, - node + node, + spreadIdentifier ); const functionExpression = lua.createFunctionExpression( @@ -343,9 +347,7 @@ export const transformFunctionDeclaration: FunctionVisitor = (node, context) => { + if (node.awaitModifier) { + context.diagnostics.push(unsupportedForAwaitOf(node.awaitModifier)); + } + const body = lua.createBlock(transformLoopBody(context, node)); if (ts.isCallExpression(node.expression) && isRangeFunction(context, node.expression)) { diff --git a/src/transformation/visitors/loops/for.ts b/src/transformation/visitors/loops/for.ts index 3a82b5a24..5ae3bf0ea 100644 --- a/src/transformation/visitors/loops/for.ts +++ b/src/transformation/visitors/loops/for.ts @@ -4,10 +4,13 @@ import { FunctionVisitor } from "../../context"; import { transformInPrecedingStatementScope } from "../../utils/preceding-statements"; import { checkVariableDeclarationList, transformVariableDeclaration } from "../variable-declaration"; import { invertCondition, transformLoopBody } from "./utils"; +import { ScopeType } from "../../utils/scope"; export const transformForStatement: FunctionVisitor = (statement, context) => { const result: lua.Statement[] = []; + context.pushScope(ScopeType.Loop, statement); + if (statement.initializer) { if (ts.isVariableDeclarationList(statement.initializer)) { checkVariableDeclarationList(context, statement.initializer); @@ -61,5 +64,7 @@ export const transformForStatement: FunctionVisitor = (statemen // while (condition) do ... end result.push(lua.createWhileStatement(lua.createBlock(body), condition, statement)); + context.popScope(); + return lua.createDoStatement(result, statement); }; diff --git a/src/transformation/visitors/loops/utils.ts b/src/transformation/visitors/loops/utils.ts index f34b35245..d7e4a3093 100644 --- a/src/transformation/visitors/loops/utils.ts +++ b/src/transformation/visitors/loops/utils.ts @@ -14,13 +14,14 @@ export function transformLoopBody( context: TransformationContext, loop: ts.WhileStatement | ts.DoStatement | ts.ForStatement | ts.ForOfStatement | ts.ForInOrOfStatement ): lua.Statement[] { - context.pushScope(ScopeType.Loop); + context.pushScope(ScopeType.Loop, loop); const body = performHoisting(context, transformBlockOrStatement(context, loop.statement)); const scope = context.popScope(); const scopeId = scope.id; switch (scope.loopContinued) { case undefined: + case LoopContinued.WithContinue: return body; case LoopContinued.WithGoto: @@ -30,13 +31,25 @@ export function transformLoopBody( const identifier = lua.createIdentifier(`__continue${scopeId}`); const literalTrue = lua.createBooleanLiteral(true); + // If there is a break in the body statements, do not include any code afterwards + const transformedBodyStatements = []; + let bodyBroken = false; + for (const statement of body) { + transformedBodyStatements.push(statement); + if (lua.isBreakStatement(statement)) { + bodyBroken = true; + break; + } + } + if (!bodyBroken) { + // Tell loop to continue if not broken + transformedBodyStatements.push(lua.createAssignmentStatement(identifier, literalTrue)); + } + return [ lua.createDoStatement([ lua.createVariableDeclarationStatement(identifier), - lua.createRepeatStatement( - lua.createBlock([...body, lua.createAssignmentStatement(identifier, literalTrue)]), - literalTrue - ), + lua.createRepeatStatement(lua.createBlock(transformedBodyStatements), literalTrue), lua.createIfStatement( lua.createUnaryExpression(identifier, lua.SyntaxKind.NotOperator), lua.createBlock([lua.createBreakStatement()]) @@ -66,7 +79,7 @@ export function transformForInitializer( ): lua.Identifier { const valueVariable = lua.createIdentifier("____value"); - context.pushScope(ScopeType.LoopInitializer); + context.pushScope(ScopeType.LoopInitializer, initializer); if (ts.isVariableDeclarationList(initializer)) { // Declaration of new variable diff --git a/src/transformation/visitors/modules/import.ts b/src/transformation/visitors/modules/import.ts index 66432fb44..7c6a597ac 100644 --- a/src/transformation/visitors/modules/import.ts +++ b/src/transformation/visitors/modules/import.ts @@ -9,7 +9,7 @@ import { createHoistableVariableDeclarationStatement } from "../../utils/lua-ast import { importLuaLibFeature, LuaLibFeature } from "../../utils/lualib"; import { createSafeName } from "../../utils/safe-names"; import { peekScope } from "../../utils/scope"; -import { transformIdentifier } from "../identifier"; +import { getCustomNameFromSymbol, transformIdentifier } from "../identifier"; import { transformPropertyName } from "../literal"; function isNoResolutionPath(context: TransformationContext, moduleSpecifier: ts.Expression): boolean { @@ -46,11 +46,15 @@ function transformImportSpecifier( importSpecifier: ts.ImportSpecifier, moduleTableName: lua.Identifier ): lua.VariableDeclarationStatement { + const type = context.checker.getTypeAtLocation(importSpecifier.name); + const leftIdentifier = transformIdentifier(context, importSpecifier.name); - const propertyName = transformPropertyName( - context, - importSpecifier.propertyName ? importSpecifier.propertyName : importSpecifier.name - ); + + // If imported value has a customName annotation use that, otherwise use regular property + const customName = getCustomNameFromSymbol(context, type.getSymbol()); + const propertyName = customName + ? lua.createStringLiteral(customName, importSpecifier.propertyName ?? importSpecifier.name) + : transformPropertyName(context, importSpecifier.propertyName ?? importSpecifier.name); return lua.createVariableDeclarationStatement( leftIdentifier, @@ -62,9 +66,7 @@ function transformImportSpecifier( export const transformImportDeclaration: FunctionVisitor = (statement, context) => { const scope = peekScope(context); - if (!scope.importStatements) { - scope.importStatements = []; - } + scope.importStatements ??= []; const result: lua.Statement[] = []; const requireCall = createModuleRequire(context, statement.moduleSpecifier); diff --git a/src/transformation/visitors/namespace.ts b/src/transformation/visitors/namespace.ts index 743b52b62..49a3f2289 100644 --- a/src/transformation/visitors/namespace.ts +++ b/src/transformation/visitors/namespace.ts @@ -118,7 +118,7 @@ export const transformModuleDeclaration: FunctionVisitor = // Transform moduleblock to block and visit it if (moduleHasEmittedBody(node)) { - context.pushScope(ScopeType.Block); + context.pushScope(ScopeType.Block, node); const statements = performHoisting( context, context.transformStatements(ts.isModuleBlock(node.body) ? node.body.statements : node.body) 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/sourceFile.ts b/src/transformation/visitors/sourceFile.ts index 4a0c62c6d..2fe860f53 100644 --- a/src/transformation/visitors/sourceFile.ts +++ b/src/transformation/visitors/sourceFile.ts @@ -26,7 +26,7 @@ export const transformSourceFileNode: FunctionVisitor = (node, co statements.push(lua.createExpressionStatement(errorCall)); } } else { - context.pushScope(ScopeType.File); + context.pushScope(ScopeType.File, node); statements = performHoisting(context, context.transformStatements(node.statements)); context.popScope(); diff --git a/src/transformation/visitors/spread.ts b/src/transformation/visitors/spread.ts index 9c8cb6feb..b10a0ae6e 100644 --- a/src/transformation/visitors/spread.ts +++ b/src/transformation/visitors/spread.ts @@ -6,13 +6,7 @@ import { FunctionVisitor, TransformationContext } from "../context"; import { getIterableExtensionKindForNode, IterableExtensionKind } from "../utils/language-extensions"; import { createUnpackCall } from "../utils/lua-ast"; import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; -import { - findScope, - hasReferencedSymbol, - hasReferencedUndefinedLocalFunction, - isFunctionScopeWithDefinition, - ScopeType, -} from "../utils/scope"; +import { findScope, hasReferencedSymbol, hasReferencedUndefinedLocalFunction, ScopeType } from "../utils/scope"; import { findFirstNonOuterParent, isAlwaysArrayType } from "../utils/typescript"; import { isMultiReturnCall } from "./language-extensions/multi"; import { isGlobalVarargConstant } from "./language-extensions/vararg"; @@ -34,7 +28,7 @@ export function isOptimizedVarArgSpread(context: TransformationContext, symbol: } // Scope must be a function scope associated with a real ts function - if (!isFunctionScopeWithDefinition(scope)) { + if (!ts.isFunctionLike(scope.node)) { return false; } diff --git a/src/transformation/visitors/switch.ts b/src/transformation/visitors/switch.ts index 2ceaedb39..cee8b7f57 100644 --- a/src/transformation/visitors/switch.ts +++ b/src/transformation/visitors/switch.ts @@ -73,7 +73,7 @@ const coalesceCondition = ( }; export const transformSwitchStatement: FunctionVisitor = (statement, context) => { - const scope = context.pushScope(ScopeType.Switch); + const scope = context.pushScope(ScopeType.Switch, statement); // Give the switch and condition accumulator a unique name to prevent nested switches from acting up. const switchName = `____switch${scope.id}`; diff --git a/src/transformation/visitors/unary-expression.ts b/src/transformation/visitors/unary-expression.ts index 5960c2200..88b512757 100644 --- a/src/transformation/visitors/unary-expression.ts +++ b/src/transformation/visitors/unary-expression.ts @@ -7,6 +7,8 @@ import { transformCompoundAssignmentExpression, transformCompoundAssignmentStatement, } from "./binary-expression/compound"; +import { isNumberType } from "../utils/typescript"; +import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; export function transformUnaryExpressionStatement( context: TransformationContext, @@ -92,16 +94,29 @@ export const transformPrefixUnaryExpression: FunctionVisitor( + const tableExpression = propertyAccessStack.reduce( (path, property) => lua.createTableIndexExpression(path, transformPropertyName(context, property)), table ); @@ -205,14 +205,15 @@ 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 { - // local vars = this.transpileDestructingAssignmentValue(node.initializer); - const unpackedInitializer = createUnpackCall( + // use unpack(thing, 1, #bindingItems) to unpack + const unpackedInitializer = createBoundedUnpackCall( context, context.transformExpression(initializer), + bindingPattern.elements.length, initializer ); statements.push( diff --git a/src/transpilation/bundle.ts b/src/transpilation/bundle.ts index 1e0436c35..b030bddfe 100644 --- a/src/transpilation/bundle.ts +++ b/src/transpilation/bundle.ts @@ -110,7 +110,9 @@ export function getBundleResult(program: ts.Program, files: ProcessedFile[]): [t // return require("") const args = options.luaTarget === LuaTarget.Lua50 ? "unpack(arg == nil and {} or arg)" : "..."; - const entryPoint = `return require(${createModulePath(entryModuleFilePath ?? entryModule, program)}, ${args})\n`; + // Avoid producing a tail-call (which removes the bundle's stack frame) by assigning the `require` result to a local and returning it. + const entryPath = createModulePath(entryModuleFilePath ?? entryModule, program); + const entryPoint = `local ____entry = require(${entryPath}, ${args})\nreturn ____entry\n`; const footers: string[] = []; if (options.sourceMapTraceback) { 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/plugins.ts b/src/transpilation/plugins.ts index 74e4e542e..f84af2bc9 100644 --- a/src/transpilation/plugins.ts +++ b/src/transpilation/plugins.ts @@ -77,15 +77,23 @@ export function getPlugins(program: ts.Program): { diagnostics: ts.Diagnostic[]; for (const [index, pluginOption] of (options.luaPlugins ?? []).entries()) { const optionName = `tstl.luaPlugins[${index}]`; - const { error: resolveError, result: factory } = resolvePlugin( - "plugin", - `${optionName}.name`, - getConfigDirectory(options), - pluginOption.name, - pluginOption.import - ); - - if (resolveError) diagnostics.push(resolveError); + const factory = (() => { + if ("plugin" in pluginOption) { + return pluginOption.plugin; + } else { + const { error: resolveError, result: factory } = resolvePlugin( + "plugin", + `${optionName}.name`, + getConfigDirectory(options), + pluginOption.name, + pluginOption.import + ); + + if (resolveError) diagnostics.push(resolveError); + return factory; + } + })(); + if (factory === undefined) continue; const plugin = typeof factory === "function" ? factory(pluginOption) : factory; diff --git a/src/transpilation/resolve.ts b/src/transpilation/resolve.ts index bd16773e9..af7981ffc 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.` @@ -210,21 +208,24 @@ class ResolutionContext { if (resolvedNodeModulesFile) return resolvedNodeModulesFile; } + // Bare specifiers: check paths mappings first, matching TypeScript's resolution order. + // TS never applies paths to relative imports, so skip for those. + if (!ts.isExternalModuleNameRelative(dependencyPath) && this.options.paths) { + // 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; + } + } + // Check if file is a file in the project const resolvedPath = this.formatPathToFile(dependencyPath, requiringFile); const fileFromPath = this.getFileFromPath(resolvedPath); if (fileFromPath) return fileFromPath; - if (this.options.paths && this.options.baseUrl) { - // 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; - } - // Not a TS file in our project sources, use resolver to check if we can find dependency try { const resolveResult = resolver.resolveSync({}, fileDirectory, dependencyPath); 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/cli/parse.spec.ts b/test/cli/parse.spec.ts index 283b4095e..c33af1208 100644 --- a/test/cli/parse.spec.ts +++ b/test/cli/parse.spec.ts @@ -247,7 +247,9 @@ describe("tsconfig", () => { ["luaTarget", "5.2", { luaTarget: tstl.LuaTarget.Lua52 }], ["luaTarget", "5.3", { luaTarget: tstl.LuaTarget.Lua53 }], ["luaTarget", "5.4", { luaTarget: tstl.LuaTarget.Lua54 }], + ["luaTarget", "5.5", { luaTarget: tstl.LuaTarget.Lua55 }], ["luaTarget", "jit", { luaTarget: tstl.LuaTarget.LuaJIT }], + ["luaTarget", "luau", { luaTarget: tstl.LuaTarget.Luau }], ["luaBundle", "foo", { luaBundle: "foo" }], ["luaBundleEntry", "bar", { luaBundleEntry: "bar" }], 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/translation/__snapshots__/transformation.spec.ts.snap b/test/translation/__snapshots__/transformation.spec.ts.snap index b0c7eea52..e9fccb4ae 100644 --- a/test/translation/__snapshots__/transformation.spec.ts.snap +++ b/test/translation/__snapshots__/transformation.spec.ts.snap @@ -1,5 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Luau-specific Transformation (luauSpecificTransformations) 1`] = ` +"t = if true then "is true" else "is false" +while false do + continue +end +repeat + do + continue + end +until not false" +`; + exports[`Transformation (blockScopeVariables) 1`] = ` "do local a = 1 @@ -24,6 +36,8 @@ escapedCharsInTemplateString = "\\\\ \\0 \\b \\t \\n \\v \\f \\" ' \`" nonEmptyTemplateString = ("Level 0: \\n\\t " .. ("Level 1: \\n\\t\\t " .. ("Level 3: \\n\\t\\t\\t " .. "Last level \\n --") .. " \\n --") .. " \\n --") .. " \\n --"" `; +exports[`Transformation (customNameWithExtraComment) 1`] = `"TestNamespace.pass()"`; + exports[`Transformation (customNameWithNoSelf) 1`] = `"TestNamespace.pass()"`; exports[`Transformation (exportStatement) 1`] = ` diff --git a/test/translation/transformation.spec.ts b/test/translation/transformation.spec.ts index 42a4b4355..5f9df58fe 100644 --- a/test/translation/transformation.spec.ts +++ b/test/translation/transformation.spec.ts @@ -19,3 +19,18 @@ test.each(fixtures)("Transformation (%s)", (_name, content) => { .disableSemanticCheck() .expectLuaToMatchSnapshot(); }); + +const luauFixturesPath = path.join(fixturesPath, "luau"); +const luauFixtures = fs + .readdirSync(luauFixturesPath) + .filter(f => path.extname(f) === ".ts") + .sort() + .map(f => [path.parse(f).name, fs.readFileSync(path.join(luauFixturesPath, f), "utf8")]); + +test.each(luauFixtures)("Luau-specific Transformation (%s)", (_name, content) => { + util.testModule(content) + .setOptions({ luaLibImport: tstl.LuaLibImportKind.Require, luaTarget: tstl.LuaTarget.Luau }) + .ignoreDiagnostics([annotationDeprecated.code, couldNotResolveRequire.code]) + .disableSemanticCheck() + .expectLuaToMatchSnapshot(); +}); diff --git a/test/translation/transformation/customNameWithExtraComment.ts b/test/translation/transformation/customNameWithExtraComment.ts new file mode 100644 index 000000000..459512ded --- /dev/null +++ b/test/translation/transformation/customNameWithExtraComment.ts @@ -0,0 +1,10 @@ +/** @noSelf */ +declare namespace TestNamespace { + /** + * @customName pass + * The first word should not be included. + **/ + function fail(): void; +} + +TestNamespace.fail(); diff --git a/test/translation/transformation/luau/luauSpecificTransformations.ts b/test/translation/transformation/luau/luauSpecificTransformations.ts new file mode 100644 index 000000000..b4c9fdec4 --- /dev/null +++ b/test/translation/transformation/luau/luauSpecificTransformations.ts @@ -0,0 +1,9 @@ +const t = true ? "is true" : "is false"; + +while (false) { + continue; +} + +do { + continue; +} while (false); 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..35e79ed3d 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,8 +707,16 @@ test("supports complicated paths configuration", () => { .expectToEqual({ foo: 314, bar: 271 }); }); -test("paths without baseUrl is error", () => { - util.testFunction``.setOptions({ paths: {} }).expectToHaveDiagnostics([pathsWithoutBaseUrl.code]); +test("paths mapping wins over sibling project file (TS resolution order)", () => { + const baseProjectPath = path.resolve(__dirname, "module-resolution", "paths-vs-project-file"); + const projectPath = path.join(baseProjectPath, "program"); + const projectTsConfig = path.join(projectPath, "tsconfig.json"); + const mainFile = path.join(projectPath, "main.ts"); + + util.testProject(projectTsConfig) + .setMainFileName(mainFile) + .setOptions({ luaBundle: "bundle.lua", luaBundleEntry: mainFile }) + .expectToEqual({ value: "paths-mapped" }); }); test("module resolution using plugin", () => { 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/paths-vs-project-file/other/alias.ts b/test/transpile/module-resolution/paths-vs-project-file/other/alias.ts new file mode 100644 index 000000000..b09331910 --- /dev/null +++ b/test/transpile/module-resolution/paths-vs-project-file/other/alias.ts @@ -0,0 +1 @@ +export const value = "paths-mapped"; diff --git a/test/transpile/module-resolution/paths-vs-project-file/program/alias.ts b/test/transpile/module-resolution/paths-vs-project-file/program/alias.ts new file mode 100644 index 000000000..565154b68 --- /dev/null +++ b/test/transpile/module-resolution/paths-vs-project-file/program/alias.ts @@ -0,0 +1 @@ +export const value = "project-file"; diff --git a/test/transpile/module-resolution/paths-vs-project-file/program/main.ts b/test/transpile/module-resolution/paths-vs-project-file/program/main.ts new file mode 100644 index 000000000..7b4bc6587 --- /dev/null +++ b/test/transpile/module-resolution/paths-vs-project-file/program/main.ts @@ -0,0 +1,3 @@ +import { value } from "alias"; + +export { value }; diff --git a/test/transpile/module-resolution/paths-vs-project-file/program/tsconfig.json b/test/transpile/module-resolution/paths-vs-project-file/program/tsconfig.json new file mode 100644 index 000000000..26c1a151f --- /dev/null +++ b/test/transpile/module-resolution/paths-vs-project-file/program/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "rootDir": "..", + "outDir": "dist", + "paths": { + "alias": ["../other/alias"] + } + } +} 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/plugins/plugins.spec.ts b/test/transpile/plugins/plugins.spec.ts index 72c9e29a7..ed25ae748 100644 --- a/test/transpile/plugins/plugins.spec.ts +++ b/test/transpile/plugins/plugins.spec.ts @@ -1,5 +1,6 @@ import * as path from "path"; import * as util from "../../util"; +import { Plugin } from "../../../src/transpilation/plugins"; import * as ts from "typescript"; test("printer", () => { @@ -182,3 +183,61 @@ test("afterEmit plugin", () => { expect(diagnostic?.category).toBe(ts.DiagnosticCategory.Message); expect(diagnostic?.messageText).toContain("After emit"); }); + +test("in memory plugin", () => { + const { diagnostics } = util.testModule`` + .setOptions({ + luaPlugins: [ + { + plugin: { + afterEmit(program: ts.Program) { + return [ + { + category: ts.DiagnosticCategory.Message, + messageText: "In memory plugin diagnostic message!", + code: 1234, + file: program.getSourceFiles()[0], + start: undefined, + length: undefined, + } satisfies ts.Diagnostic, + ]; + }, + } satisfies Plugin, + }, + ], + }) + .getLuaResult(); + + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0].code).toBe(1234); +}); + +test("in memory plugin with factory", () => { + const { diagnostics } = util.testModule`` + .setOptions({ + luaPlugins: [ + { + code: 1234, + plugin: options => + ({ + afterEmit(program: ts.Program) { + return [ + { + category: ts.DiagnosticCategory.Message, + messageText: "In memory plugin diagnostic message!", + code: options.code, + file: program.getSourceFiles()[0], + start: undefined, + length: undefined, + } satisfies ts.Diagnostic, + ]; + }, + } satisfies Plugin), + }, + ], + }) + .getLuaResult(); + + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0].code).toBe(1234); +}); 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__/enum.spec.ts.snap b/test/unit/__snapshots__/enum.spec.ts.snap new file mode 100644 index 000000000..181b66a45 --- /dev/null +++ b/test/unit/__snapshots__/enum.spec.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`enum nested in namespace 1`] = ` +"A = A or ({}) +do + A.TestEnum = A.TestEnum or ({}) + A.TestEnum.B = 0 + A.TestEnum[A.TestEnum.B] = "B" + A.TestEnum.C = 1 + A.TestEnum[A.TestEnum.C] = "C" +end" +`; diff --git a/test/unit/__snapshots__/expressions.spec.ts.snap b/test/unit/__snapshots__/expressions.spec.ts.snap index 1e71d2976..7eeb5961a 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" `; @@ -482,6 +482,77 @@ ____exports.__result = a | b return ____exports" `; +exports[`Bitop [5.5] ("~a") 1`] = ` +"local ____exports = {} +____exports.__result = ~a +return ____exports" +`; + +exports[`Bitop [5.5] ("a&=b") 1`] = ` +"local ____exports = {} +a = a & b +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.5] ("a&b") 1`] = ` +"local ____exports = {} +____exports.__result = a & b +return ____exports" +`; + +exports[`Bitop [5.5] ("a<<=b") 1`] = ` +"local ____exports = {} +a = a << b +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.5] ("a<>>=b") 1`] = ` +"local ____exports = {} +a = (a & 4294967295) >> b +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.5] ("a>>>b") 1`] = ` +"local ____exports = {} +____exports.__result = (a & 4294967295) >> b +return ____exports" +`; + +exports[`Bitop [5.5] ("a^=b") 1`] = ` +"local ____exports = {} +a = a ~ b +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.5] ("a^b") 1`] = ` +"local ____exports = {} +____exports.__result = a ~ b +return ____exports" +`; + +exports[`Bitop [5.5] ("a|=b") 1`] = ` +"local ____exports = {} +a = a | b +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.5] ("a|b") 1`] = ` +"local ____exports = {} +____exports.__result = a | b +return ____exports" +`; + exports[`Bitop [JIT] ("~a") 1`] = ` "local ____exports = {} ____exports.__result = bit.bnot(a) @@ -589,9 +660,11 @@ return ____exports" `; exports[`Unary expressions basic ("+a") 1`] = ` -"local ____exports = {} +"local ____lualib = require("lualib_bundle") +local __TS__Number = ____lualib.__TS__Number +local ____exports = {} function ____exports.__main(self) - local ____ = a + __TS__Number(a) end return ____exports" `; @@ -605,9 +678,11 @@ return ____exports" `; exports[`Unary expressions basic ("-a") 1`] = ` -"local ____exports = {} +"local ____lualib = require("lualib_bundle") +local __TS__Number = ____lualib.__TS__Number +local ____exports = {} function ____exports.__main(self) - local ____ = -a + __TS__Number(-a) end return ____exports" `; @@ -681,7 +756,7 @@ ____exports.__result = a return ____exports" `; -exports[`Unsupported bitop 5.3 ("a>>=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Right shift operator is not supported for target Lua 5.3. Use \`>>>\` instead."`; +exports[`Unsupported bitop 5.3 ("a>>=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Signed right shift \`>>\` is not supported on Lua 5.3+: Lua's native \`>>\` is logical (zero-fill) on 64-bit integers, with no built-in arithmetic shift. Use \`>>>\` if you don't need sign extension, or write your own helper."`; exports[`Unsupported bitop 5.3 ("a>>b"): code 1`] = ` "local ____exports = {} @@ -689,7 +764,7 @@ ____exports.__result = a >> b return ____exports" `; -exports[`Unsupported bitop 5.3 ("a>>b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Right shift operator is not supported for target Lua 5.3. Use \`>>>\` instead."`; +exports[`Unsupported bitop 5.3 ("a>>b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Signed right shift \`>>\` is not supported on Lua 5.3+: Lua's native \`>>\` is logical (zero-fill) on 64-bit integers, with no built-in arithmetic shift. Use \`>>>\` if you don't need sign extension, or write your own helper."`; exports[`Unsupported bitop 5.4 ("a>>=b"): code 1`] = ` "local ____exports = {} @@ -698,7 +773,7 @@ ____exports.__result = a return ____exports" `; -exports[`Unsupported bitop 5.4 ("a>>=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Right shift operator is not supported for target Lua 5.3. Use \`>>>\` instead."`; +exports[`Unsupported bitop 5.4 ("a>>=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Signed right shift \`>>\` is not supported on Lua 5.3+: Lua's native \`>>\` is logical (zero-fill) on 64-bit integers, with no built-in arithmetic shift. Use \`>>>\` if you don't need sign extension, or write your own helper."`; exports[`Unsupported bitop 5.4 ("a>>b"): code 1`] = ` "local ____exports = {} @@ -706,4 +781,4 @@ ____exports.__result = a >> b return ____exports" `; -exports[`Unsupported bitop 5.4 ("a>>b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Right shift operator is not supported for target Lua 5.3. Use \`>>>\` instead."`; +exports[`Unsupported bitop 5.4 ("a>>b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Signed right shift \`>>\` is not supported on Lua 5.3+: Lua's native \`>>\` is logical (zero-fill) on 64-bit integers, with no built-in arithmetic shift. Use \`>>>\` if you don't need sign extension, or write your own helper."`; diff --git a/test/unit/__snapshots__/identifiers.spec.ts.snap b/test/unit/__snapshots__/identifiers.spec.ts.snap index 4a8ddc751..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."`; @@ -427,3 +423,18 @@ function ____exports.__main(self) end return ____exports" `; + +exports[`unicode static initialization block (#1645) 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__Class = ____lualib.__TS__Class +local ____exports = {} +____exports.default = __TS__Class() +local _____81EA_5B9A_4E49_5F02_80FD = ____exports.default +_____81EA_5B9A_4E49_5F02_80FD.name = "自定义异能" +function _____81EA_5B9A_4E49_5F02_80FD.prototype.____constructor(self) +end; +(function(self) + local a = 1 +end)(_____81EA_5B9A_4E49_5F02_80FD) +return ____exports" +`; 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 1e952a162..5e3cd8887 100644 --- a/test/unit/builtins/array.spec.ts +++ b/test/unit/builtins/array.spec.ts @@ -1,4 +1,7 @@ -import { undefinedInArrayLiteral } from "../../../src/transformation/utils/diagnostics"; +import { + undefinedInArrayLiteral, + unsupportedArrayWithLengthConstructor, +} from "../../../src/transformation/utils/diagnostics"; import * as util from "../../util"; test("omitted expression", () => { @@ -322,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(); }); @@ -334,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(); }); @@ -418,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(); }); @@ -507,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] }, @@ -568,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(); @@ -577,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(); @@ -590,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(); @@ -598,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(); @@ -656,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", () => { @@ -672,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]); @@ -797,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(); @@ -884,3 +900,43 @@ describe("copying array methods", () => { }); }); }); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1605 +test("array indexing in optional chain (#1605)", () => { + util.testModule` + interface Foo extends Array {} + const b: {arr?: Foo} = {arr:[1,2]}; + export const t = b.arr?.[1]; + + const c: Foo | undefined = b.arr; + export const u = c?.[1]; + ` + .setOptions({ strict: true }) // crucial to reproducing for some reason + .expectToMatchJsResult(); +}); + +test("new Array()", () => { + util.testFunction` + const arr = new Array(); + arr.push(1,2,3); + return arr; + `.expectToMatchJsResult(); +}); + +test("new Array()", () => { + util.testFunction` + const arr = new Array(); + arr.push(1,2,3); + return arr; + `.expectToMatchJsResult(); +}); + +test("new Array(length)", () => { + util.testFunction` + const arr = new Array(10); + `.expectToHaveDiagnostics([unsupportedArrayWithLengthConstructor.code]); +}); + +test("new Array(...items)", () => { + util.testExpression`new Array(1,2,3,4,5) `.expectToMatchJsResult(); +}); diff --git a/test/unit/builtins/async-await.spec.ts b/test/unit/builtins/async-await.spec.ts index 45adb4518..1c5d23018 100644 --- a/test/unit/builtins/async-await.spec.ts +++ b/test/unit/builtins/async-await.spec.ts @@ -1,7 +1,11 @@ import { ModuleKind, ScriptTarget } from "typescript"; import { LuaTarget } from "../../../src"; -import { unsupportedForTargetButOverrideAvailable } from "../../../src/transformation/utils/diagnostics"; -import { awaitMustBeInAsyncFunction } from "../../../src/transformation/utils/diagnostics"; +import { + awaitMustBeInAsyncFunction, + unsupportedAsyncGenerator, + unsupportedForAwaitOf, + unsupportedForTargetButOverrideAvailable, +} from "../../../src/transformation/utils/diagnostics"; import * as util from "../../util"; const promiseTestLib = ` @@ -38,7 +42,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 +55,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 +67,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 +608,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 +679,7 @@ describe("try/catch in async function", () => { } catch { + return "catch"; } } @@ -815,4 +820,433 @@ 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"]); + }); + + test.each([0, 1, 2])("async re-throw (%p)", i => { + util.testModule` + const i: number = ${i}; + async function foo() { + try { + try { + if (i === 0) { throw "z"; } + } catch (e) { + throw "a"; + } finally { + if (i === 1) { throw "b"; } + } + } catch (e) { + throw (e as string).toUpperCase(); + } finally { + throw "C"; + } + } + export let result: string = "x"; + async function run() { + try { + await foo(); + } catch (e) { + result = (e as string)[(e as string).length - 1]; + } + } + run(); + `.expectToEqual({ result: "C" }); + }); + + test("async: catch re-throws, finally still runs", () => { + util.testModule` + const foo = async () => { + throw "original"; + }; + + let finallyCalled = false; + let caughtError: any = false; + + const run = async () => { + try { + await foo(); + } catch (e) { + throw "re-thrown: " + e; + } finally { + finallyCalled = true; + } + }; + + run().catch(e => { caughtError = e; }); + + export const result = { finallyCalled, caughtError }; + `.expectToEqual({ + result: { + finallyCalled: true, + caughtError: "re-thrown: original", + }, + }); + }); + + test("async: finally throw overrides catch throw", () => { + util.testModule` + const run = async () => { + try { + throw "try-error"; + } catch (e) { + throw "catch-error"; + } finally { + throw "finally-error"; + } + }; + + let caughtError: any = false; + run().catch(e => { caughtError = e; }); + + export const result = caughtError; + `.expectToEqual({ result: "finally-error" }); + }); + + test("async: finally return overrides catch throw", () => { + util.testFunction` + async function run() { + try { + throw "try-error"; + } catch (e) { + throw "catch-error"; + } finally { + return "finally-return"; + } + } + + let result: any; + run().then(v => { result = v; }).catch(e => { result = "rejected: " + e; }); + + return result; + `.expectToEqual("finally-return"); + }); +}); + +describe("async generators are unsupported", () => { + test.each([ + "async function* gen() { yield 1; }", + "const gen = async function*() { yield 1; };", + "const gen = { async *m() { yield 1; } };", + "class C { async *m() { yield 1; } }", + ])("diagnoses %p", source => { + util.testModule` + ${source} + export {} + `.expectToHaveDiagnostics([unsupportedAsyncGenerator.code]); + }); +}); + +test("for-await-of is unsupported", () => { + util.testModule` + async function run(items: AsyncIterable) { + for await (const x of items) { + void x; + } + } + export {} + `.expectToHaveDiagnostics([unsupportedForAwaitOf.code]); }); 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 aebaead4c..d09b331cd 100644 --- a/test/unit/builtins/math.spec.ts +++ b/test/unit/builtins/math.spec.ts @@ -95,7 +95,9 @@ 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.Lua54]: builder => builder.tap(expectMathAtan), + [tstl.LuaTarget.Lua55]: builder => builder.tap(expectMathAtan), + [tstl.LuaTarget.Luau]: builder => builder.tap(expectMathAtan2), }); util.testEachVersion( diff --git a/test/unit/builtins/numbers.spec.ts b/test/unit/builtins/numbers.spec.ts index 0a8e0bf38..9570e4af2 100644 --- a/test/unit/builtins/numbers.spec.ts +++ b/test/unit/builtins/numbers.spec.ts @@ -212,15 +212,36 @@ 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 +test("unary + casting to number (#1629)", () => { + util.testFunction` + let abc = "123"; + return [+abc, +"456"]; + `.expectToEqual([123, 456]); +}); + +test("unary - casting to number", () => { + util.testFunction` + let abc = "123"; + return [-abc, -"456"]; + `.expectToEqual([-123, -456]); }); 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..63272c8ab 100644 --- a/test/unit/builtins/promise.spec.ts +++ b/test/unit/builtins/promise.spec.ts @@ -1323,3 +1323,91 @@ 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); + }); + + test("finally throw overrides fulfillment value", () => { + util.testModule` + let result: any; + Promise.resolve("ok") + .finally(() => { throw "finally-error"; }) + .then(v => { result = v; }, e => { result = e; }); + export const output = result; + `.expectToEqual({ output: "finally-error" }); + }); + + test("finally throw overrides rejection reason", () => { + util.testModule` + let result: any; + Promise.reject("original") + .finally(() => { throw "finally-error"; }) + .then(v => { result = v; }, e => { result = e; }); + export const output = result; + `.expectToEqual({ output: "finally-error" }); + }); + + test("finally returning rejected promise overrides fulfillment", () => { + util.testModule` + let result: any; + Promise.resolve("ok") + .finally(() => Promise.reject("finally-rejected") as any) + .then(v => { result = v; }, e => { result = e; }); + export const output = result; + `.expectToEqual({ output: "finally-rejected" }); + }); + + test("finally returning rejected promise overrides rejection", () => { + util.testModule` + let result: any; + Promise.reject("original") + .finally(() => Promise.reject("finally-rejected") as any) + .then(v => { result = v; }, e => { result = e; }); + export const output = result; + `.expectToEqual({ output: "finally-rejected" }); + }); +}); 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 40eccc05a..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() { @@ -909,3 +909,15 @@ test("get inherted __index member from super (DotA 2 inheritance) (#1537)", () = ) .expectToEqual("foo"); }); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1673 +test("vararg spread optimization in class constructor (#1673)", () => { + const lua = util.testModule` + class C { + constructor(...args: any[]) { + console.log(...args); + } + }`.getMainLuaCodeChunk(); + + expect(lua).not.toContain("table.unpack"); +}); diff --git a/test/unit/classes/decorators.spec.ts b/test/unit/classes/decorators.spec.ts index 1923f8b2b..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); @@ -138,7 +138,7 @@ test("Exported class decorator", () => { }); // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1149 -test("exported class with decorator", () => { +test("exported class with decorator (#1149)", () => { util.testModule` import { MyClass } from "./other"; const inst = new MyClass(); @@ -164,6 +164,28 @@ test("exported class with decorator", () => { .expectToEqual({ result: "overridden" }); }); +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1634 +test("namespaced exported class with decorator (#1634)", () => { + util.testModule` + function myDecorator(target: {new(): any}, context: ClassDecoratorContext) { + return class extends target { + foo() { + return "overridden"; + } + } + } + + namespace ns { + @myDecorator + export class MyClass { + foo() { + return "foo"; + } + } + } + `.expectNoExecutionError(); +}); + test("default exported class with decorator", () => { util.testModule` import MyClass from "./other"; @@ -210,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(); }); @@ -221,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; }; @@ -258,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(); }); @@ -273,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; } @@ -288,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(); }); @@ -310,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: { @@ -377,7 +399,7 @@ describe("legacy experimentalDecorators", () => { @setNum(420) class TestClass { - public decoratorNum; + public decoratorNum?: number; } return new TestClass(); @@ -403,8 +425,8 @@ describe("legacy experimentalDecorators", () => { @setTen @setNum class TestClass { - public decoratorTen; - public decoratorNum; + public decoratorTen?: number; + public decoratorNum?: number; } return new TestClass(); @@ -438,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); @@ -487,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 }; }; @@ -526,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 }) @@ -540,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 80d3d482c..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} }; } @@ -195,6 +205,17 @@ describe("array destructuring optimization", () => { .expectToMatchJsResult(); }); + util.testEachVersion( + "array versions", + () => + util.testFunction` + const array = [3, 5, 1]; + const [a, b, c] = array; + return { a, b, c }; + `, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + test("array literal", () => { util.testFunction` const [a, b, c] = [3, 5, 1]; @@ -215,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 f421be2c5..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; })()`; @@ -203,3 +203,14 @@ test("enum merging multiple files", () => { ) .expectToMatchJsResult(); }); + +test("enum nested in namespace", () => { + util.testModule` + namespace A { + export enum TestEnum { + B, + C + } + } + `.expectLuaToMatchSnapshot(); +}); diff --git a/test/unit/error.spec.ts b/test/unit/error.spec.ts index 388fee290..f7056655f 100644 --- a/test/unit/error.spec.ts +++ b/test/unit/error.spec.ts @@ -1,4 +1,5 @@ import * as util from "../util"; +import * as tstl from "../../src"; test("throwString", () => { util.testFunction` @@ -6,31 +7,40 @@ test("throwString", () => { `.expectToEqual(new util.ExecutionError("Some Error")); }); -// TODO: Finally does not behave like it should, see #1137 -// eslint-disable-next-line jest/no-disabled-tests -test.skip.each([0, 1, 2])("re-throw (%p)", i => { +test.each([ + { innerTry: false, innerFinally: false, outerFinally: false }, + { innerTry: false, innerFinally: false, outerFinally: true }, + { innerTry: false, innerFinally: true, outerFinally: false }, + { innerTry: false, innerFinally: true, outerFinally: true }, + { innerTry: true, innerFinally: false, outerFinally: false }, + { innerTry: true, innerFinally: false, outerFinally: true }, + { innerTry: true, innerFinally: true, outerFinally: false }, + { innerTry: true, innerFinally: true, outerFinally: true }, +])("re-throw (%j)", o => { util.testFunction` - const i: number = ${i}; + const innerTry: boolean = ${o.innerTry}; + const innerFinally: boolean = ${o.innerFinally}; + const outerFinally: boolean = ${o.outerFinally}; function foo() { try { try { - if (i === 0) { throw "z"; } + if (innerTry) { throw "inner.try"; } } catch (e) { - throw "a"; + throw (e as string) + "->" + "inner.catch"; } finally { - if (i === 1) { throw "b"; } + if (innerFinally) { throw "inner.finally"; } } } catch (e) { - throw (e as string).toUpperCase(); + throw (e as string) + "->" + "outer.catch"; } finally { - throw "C"; + if (outerFinally) { throw "outer.finally"; } } } let result: string = "x"; try { foo(); } catch (e) { - result = (e as string)[(e as string).length - 1]; + result = e as string; } return result; `.expectToMatchJsResult(); @@ -87,7 +97,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"); @@ -128,7 +138,7 @@ test("multi return from catch", () => { function foobar(): LuaMultiReturn<[string, string]> { try { throw "foobar"; - } catch (e) { + } catch (e: any) { return $multi(e.toString(), " catch"); } } @@ -242,7 +252,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"); @@ -255,6 +265,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 = ""; @@ -343,3 +375,153 @@ test("still works without debug module", () => { stack: undefined, }); }); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1665 +test("sourceMapTraceback maps anonymous function locations in .lua files (#1665)", () => { + // Nested IIFEs produce anonymous function notation in traceback. + // Old pattern (%S+)%.lua:(%d+) captures "", + // failing the sourcemap lookup. Fix: ([^%s<]+) excludes "<". + + // mapping is copied from the emitted Lua, not invented. + const mapping = `{["5"] = 1,["6"] = 2,["7"] = 3,["8"] = 4,["9"] = 3,["10"] = 2,["11"] = 1}`; + + // Test harness executes via luaL_dostring (chunk names are [string "..."]), so we mock a file-based traceback. + const fakeTraceback = [ + "stack traceback:", + "\tmain.lua:8: in function ", + "\tmain.lua:7: in function ", + "\t[C]: in ?", + ].join("\n"); + + // Inject sourcemap for "main.lua" and mock debug.traceback to return file-based frames. + const header = ` + __TS__sourcemap = { ["main.lua"] = ${mapping} }; + local __real_tb = debug.traceback + debug.traceback = function() return ${JSON.stringify(fakeTraceback)} end + `; + + const builder = util.testFunction` + return (() => { + return (() => { + return (debug.traceback as (this: void) => string)(); + })(); + })(); + ` + .setLuaHeader(header) + .setOptions({ sourceMapTraceback: true }); + + const lua = builder.getMainLuaCodeChunk(); + // Sanity check: emitted code registers the same mapping literal we inject above. + expect(lua).toContain(`__TS__SourceMapTraceBack(debug.getinfo(1).short_src, ${mapping});`); + + const result = builder.getLuaExecutionResult(); + expect(result).toEqual(expect.any(String)); + // Both `main.lua:N` and `` frames should be rewritten using the sourcemap. + expect(result).not.toContain("main.lua"); + // Regular line references + expect(result).toContain("\tmain.ts:4:"); + expect(result).toContain("\tmain.ts:3:"); + // Anonymous function references must keep <> format + expect(result).toContain(""); + 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` + const e = new Error(); + return e.stack; + `, + { + ...util.expectEachVersionExceptJit(builder => { + builder.expectToHaveNoDiagnostics(); + const luaResult = builder.getLuaExecutionResult(); + // eslint-disable-next-line jest/no-standalone-expect + expect(luaResult.split("\n")).toHaveLength(4); + }), + + // 5.0 debug.traceback doesn't support levels + [tstl.LuaTarget.Lua50](builder) { + builder.expectToHaveNoDiagnostics(); + const luaResult = builder.getLuaExecutionResult(); + // eslint-disable-next-line jest/no-standalone-expect + expect(luaResult).toContain("Level 4"); + }, + } +); diff --git a/test/unit/expressions.spec.ts b/test/unit/expressions.spec.ts index 46ca2f7bd..12fd85be1 100644 --- a/test/unit/expressions.spec.ts +++ b/test/unit/expressions.spec.ts @@ -96,6 +96,13 @@ test.each(supportedInAll)("Bitop [5.4] (%p)", input => { .expectLuaToMatchSnapshot(); }); +test.each(supportedInAll)("Bitop [5.5] (%p)", input => { + util.testExpression(input) + .setOptions({ luaTarget: tstl.LuaTarget.Lua55, luaLibImport: tstl.LuaLibImportKind.None }) + .disableSemanticCheck() + .expectLuaToMatchSnapshot(); +}); + test.each(unsupportedIn53And54)("Unsupported bitop 5.3 (%p)", input => { util.testExpression(input) .setOptions({ luaTarget: tstl.LuaTarget.Lua53, luaLibImport: tstl.LuaLibImportKind.None }) @@ -110,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/__snapshots__/noSelfAnnotation.spec.ts.snap b/test/unit/functions/__snapshots__/noSelfAnnotation.spec.ts.snap index f4e5fdd6d..64eb65457 100644 --- a/test/unit/functions/__snapshots__/noSelfAnnotation.spec.ts.snap +++ b/test/unit/functions/__snapshots__/noSelfAnnotation.spec.ts.snap @@ -13,3 +13,8 @@ exports[`@noSelf on parent class declaration removes context argument 1`] = `"ho exports[`@noSelf on parent interface declaration removes context argument 1`] = `"holder.myMethod()"`; exports[`@noSelf on parent namespace declaration removes context argument 1`] = `"MyNamespace.myMethod()"`; + +exports[`@noSelf on static class methods with string key access 1`] = ` +"TestClass.myMethod() +TestClass.myKey()" +`; 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/noSelfAnnotation.spec.ts b/test/unit/functions/noSelfAnnotation.spec.ts index b25742a11..e9fbb853d 100644 --- a/test/unit/functions/noSelfAnnotation.spec.ts +++ b/test/unit/functions/noSelfAnnotation.spec.ts @@ -52,6 +52,63 @@ test("@noSelf on parent namespace declaration removes context argument", () => { `.expectLuaToMatchSnapshot(); }); +test("@noSelf on static class methods with string key access", () => { + util.testModule` + /** @noSelf */ + declare class TestClass { + static [key: string]: () => void; + static myMethod(): void; + } + + TestClass.myMethod(); + TestClass.myKey(); + `.expectLuaToMatchSnapshot(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1661 +// A Lua-side function observes the actual argc, so a missing @noSelf would +// surface as a phantom leading nil (argc 2 instead of 1). +const argcProbeHeader = ` + function probe(...) + return select("#", ...) + end +`; + +test("@noSelf on interface call signature: Lua probe sees correct argc", () => { + util.testModule` + /** @noSelf */ + interface Probe { (a: string): number; } + declare const probe: Probe; + export const result = probe("hi"); + ` + .setLuaHeader(argcProbeHeader) + .expectToEqual({ result: 1 }); +}); + +test("@noSelf parent interface, property typed by call-signature interface: Lua probe sees correct argc", () => { + util.testModule` + /** @noSelf */ + interface CallSignature { (a: string): number; } + /** @noSelf */ + interface Holder { fn: CallSignature; } + declare const holder: Holder; + export const result = holder.fn("hi"); + ` + .setLuaHeader(`${argcProbeHeader}\nholder = { fn = probe }`) + .expectToEqual({ result: 1 }); +}); + +test("@noSelf parent interface, property typed by type-literal call signature: Lua probe sees correct argc", () => { + util.testModule` + /** @noSelf */ + interface Holder { fn: { (a: string): number }; } + declare const holder: Holder; + export const result = holder.fn("hi"); + ` + .setLuaHeader(`${argcProbeHeader}\nholder = { fn = probe }`) + .expectToEqual({ result: 1 }); +}); + // additional coverage for https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1292 test("explicit this parameter respected over @noSelf", () => { util.testModule` 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 14f86fff8..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} @@ -317,6 +316,17 @@ test("unicode export default class", () => { .expectToEqual({ result: "你好" }); }); +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1645 +test("unicode static initialization block (#1645)", () => { + util.testModule` + export default class 自定义异能 { + static { + let a = 1; + } + } + `.expectLuaToMatchSnapshot(); +}); + describe("lua keyword as identifier doesn't interfere with lua's value", () => { test("variable (nil)", () => { util.testFunction` @@ -577,6 +587,15 @@ describe("lua keyword as identifier doesn't interfere with lua's value", () => { expect(luaResult).toBe("foobar"); }); + test("variable (bit32)", () => { + util.testFunction` + const bit32 = 1; + return bit32 << 1; + ` + .setOptions({ luaTarget: LuaTarget.Lua52 }) + .expectToMatchJsResult(); + }); + test("variable (_G)", () => { util.testFunction` const _G = "bar"; @@ -971,3 +990,67 @@ test("customName rename declared function", () => { expect(mainFile.lua).toContain("Test2("); expect(mainFile.lua).not.toContain("Test("); }); + +test("customName rename import specifier", () => { + const testModule = util.testModule` + import { Test } from "./myimport"; + import { Test as Aliased } from "./myimport"; + Test(); + Aliased(); + `.addExtraFile( + "myimport.ts", + ` + /** @customName Test2 **/ + export function Test(this: void): void {} + ` + ); + + testModule.expectToHaveNoDiagnostics(); + const result = testModule.getLuaResult(); + expect(result.transpiledFiles).not.toHaveLength(0); + + const mainFile = result.transpiledFiles.find(f => f.outPath === "main.lua"); + expect(mainFile).toBeDefined(); + + // avoid ts error "not defined", even though toBeDefined is being checked above + if (!mainFile) return; + + expect(mainFile.lua).toBeDefined(); + expect(mainFile.lua).toContain("Test2("); + expect(mainFile.lua).toContain("myimport.Test2"); + expect(mainFile.lua).not.toContain("Test("); + + testModule.expectNoExecutionError(); +}); + +test("customName import specifier from declarations", () => { + const testModule = util.testModule` + import { Test } from "./myimport"; + import { Test as Aliased } from "./myimport"; + Test(); + Aliased(); + ` + .addExtraFile( + "myimport.d.ts", + ` + /** @customName Test2 **/ + export declare function Test(this: void): void; + ` + ) + .setOptions({ noResolvePaths: ["./myimport"] }); + + testModule.expectToHaveNoDiagnostics(); + const result = testModule.getLuaResult(); + expect(result.transpiledFiles).not.toHaveLength(0); + + const mainFile = result.transpiledFiles.find(f => f.outPath === "main.lua"); + expect(mainFile).toBeDefined(); + + // avoid ts error "not defined", even though toBeDefined is being checked above + if (!mainFile) return; + + expect(mainFile.lua).toBeDefined(); + expect(mainFile.lua).toContain("Test2("); + expect(mainFile.lua).toContain("myimport.Test2"); + expect(mainFile.lua).not.toContain("Test("); +}); 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 f2225c7f5..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; @@ -540,10 +540,13 @@ for (const testCase of [ "for (const a of []) { continue; }", ]) { const expectContinueVariable: util.TapCallback = builder => - expect(builder.getMainLuaCodeChunk()).toMatch("local __continue2"); + expect(builder.getMainLuaCodeChunk()).toMatch(/local __continue\d+/); const expectContinueGotoLabel: util.TapCallback = builder => - expect(builder.getMainLuaCodeChunk()).toMatch("::__continue2::"); + expect(builder.getMainLuaCodeChunk()).toMatch(/::__continue\d+::/); + + const expectContinueStatement: util.TapCallback = builder => + expect(builder.getMainLuaCodeChunk()).toMatch("continue;"); util.testEachVersion(`loop continue (${testCase})`, () => util.testModule(testCase), { [tstl.LuaTarget.Universal]: expectContinueVariable, @@ -552,10 +555,26 @@ for (const testCase of [ [tstl.LuaTarget.Lua52]: expectContinueGotoLabel, [tstl.LuaTarget.Lua53]: expectContinueGotoLabel, [tstl.LuaTarget.Lua54]: expectContinueGotoLabel, + [tstl.LuaTarget.Lua55]: expectContinueGotoLabel, [tstl.LuaTarget.LuaJIT]: expectContinueGotoLabel, + [tstl.LuaTarget.Luau]: () => expectContinueStatement, }); } +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1638 +test.each([tstl.LuaTarget.Universal, tstl.LuaTarget.Lua50, tstl.LuaTarget.Lua51])( + "no unreachable code when using continue for target %s (#1638)", + target => { + util.testFunction` + let i = 0; + while(++i < 10) continue; + return i; + ` + .setOptions({ luaTarget: target }) + .expectToMatchJsResult(); + } +); + test("do...while", () => { util.testFunction` let result = 0; @@ -620,3 +639,12 @@ test("for...in with pre-defined variable keeps last value", () => { // Need custom matcher because order is not guaranteed in neither JS nor Lua expect([keyX, keyFoo]).toContain(result); }); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1631 +test("loop variables should not be global (#1631)", () => { + const code = util.testModule` + for (let val = 0; val < 2; ++val) {} + `.getMainLuaCodeChunk(); + + expect(code).toContain("local val"); +}); 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 04b1bd726..93936f29b 100644 --- a/test/unit/spread.spec.ts +++ b/test/unit/spread.spec.ts @@ -81,6 +81,8 @@ describe("in function call", () => { [tstl.LuaTarget.Lua52]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), [tstl.LuaTarget.Lua53]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), [tstl.LuaTarget.Lua54]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), + [tstl.LuaTarget.Lua55]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), + [tstl.LuaTarget.Luau]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), } ); }); @@ -94,6 +96,8 @@ describe("in array literal", () => { [tstl.LuaTarget.Lua52]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), [tstl.LuaTarget.Lua53]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), [tstl.LuaTarget.Lua54]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), + [tstl.LuaTarget.Lua55]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), + [tstl.LuaTarget.Luau]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), }); test("of array literal /w OmittedExpression", () => { @@ -112,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/templateLiterals.spec.ts b/test/unit/templateLiterals.spec.ts index d5dc519a5..318804ee9 100644 --- a/test/unit/templateLiterals.spec.ts +++ b/test/unit/templateLiterals.spec.ts @@ -83,3 +83,26 @@ test.each(["string", "'string literal type'", "string & unknown"])( .expectToMatchJsResult(); } ); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1637 +test("tagged template literal returned from function call (#1637)", () => { + util.testModule` + function templateFactory() { + return (template: TemplateStringsArray) => { + return "bar"; + } + } + export let result = templateFactory()\`foo\`; + `.expectToEqual({ result: "bar" }); +}); + +test("tagged template literal returned from function call, explicit no context (#1637)", () => { + util.testModule` + function templateFactory() { + return function(this: void, template: TemplateStringsArray) { + return "bar"; + } + } + export let result = templateFactory()\`foo\`; + `.expectToEqual({ result: "bar" }); +}); diff --git a/test/unit/using.spec.ts b/test/unit/using.spec.ts index 34b4fef67..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 { @@ -217,3 +237,19 @@ test("works with disposable classes (#1584)", () => { return log; `.expectToEqual(["action", "cleanup"]); }); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1632 +test("works on root level (#1632)", () => { + util.testModule` + export let disposed = false; + + class A { + [Symbol.dispose] = function (this: A) { + disposed = true; + } + } + using a = new A(); + `.expectToEqual({ + disposed: true, + }); +}); diff --git a/test/util.ts b/test/util.ts index 73942a473..1e191fb28 100644 --- a/test/util.ts +++ b/test/util.ts @@ -43,7 +43,7 @@ function getLuaBindingsForVersion(target: tstl.LuaTarget): { lauxlib: LauxLib; l return { lauxlib, lua, lualib }; } if (target === tstl.LuaTarget.LuaJIT) { - throw Error("Can't use executeLua() or expectToMatchJsResult() wit LuaJIT as target!"); + throw Error("Can't use executeLua() or expectToMatchJsResult() with LuaJIT as target!"); } const { lauxlib, lua, lualib } = require("lua-wasm-bindings/dist/lua.54"); @@ -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(", "); @@ -63,7 +63,7 @@ export function testEachVersion( ): void { for (const version of Object.values(tstl.LuaTarget) as tstl.LuaTarget[]) { const specialBuilder = special?.[version]; - if (specialBuilder === false) return; + if (specialBuilder === false) continue; const testName = name === undefined ? version : `${name} [${version}]`; defineTest(testName, () => { @@ -86,21 +86,21 @@ export function expectEachVersionExceptJit( [tstl.LuaTarget.Lua52]: expectation, [tstl.LuaTarget.Lua53]: expectation, [tstl.LuaTarget.Lua54]: expectation, + [tstl.LuaTarget.Lua55]: expectation, [tstl.LuaTarget.LuaJIT]: false, // Exclude JIT + [tstl.LuaTarget.Luau]: false, }; } -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 { @@ -156,12 +156,12 @@ export abstract class TestBuilder { } protected options: tstl.CompilerOptions = { - luaTarget: tstl.LuaTarget.Lua54, + luaTarget: tstl.LuaTarget.Lua55, noHeader: true, skipLibCheck: true, target: ts.ScriptTarget.ES2017, lib: ["lib.esnext.d.ts"], - moduleResolution: ts.ModuleResolutionKind.Node10, + moduleResolution: ts.ModuleResolutionKind.Bundler, resolveJsonModule: true, sourceMap: true, }; @@ -443,7 +443,7 @@ export abstract class TestBuilder { // Main file const mainFile = this.getMainLuaCodeChunk(); - const luaTarget = this.options.luaTarget ?? tstl.LuaTarget.Lua54; + const luaTarget = this.options.luaTarget ?? tstl.LuaTarget.Lua55; const { lauxlib, lua, lualib } = getLuaBindingsForVersion(luaTarget); const L = lauxlib.luaL_newstate(); @@ -532,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-schema.json b/tsconfig-schema.json index 5af971b49..6bffd5e46 100644 --- a/tsconfig-schema.json +++ b/tsconfig-schema.json @@ -50,7 +50,7 @@ "description": "Specifies the Lua version you want to generate code for.", "type": "string", "default": "universal", - "enum": ["5.0", "universal", "5.1", "5.2", "5.3", "5.4", "JIT"] + "enum": ["5.0", "universal", "5.1", "5.2", "5.3", "5.4", "5.5", "JIT", "Luau"] }, "noImplicitGlobalVariables": { "description": "Always declare all root-level variables as local, even if the file is not a module and they would be global in TypeScript.", 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",